利用百度API Store接口进行火车票查询

火车票查询

 

  项目源码下载链接:

  Github:https://github.com/VincentWYJ/TrainTicketQuery  

  博客文件:http://files.cnblogs.com/files/tgyf/TrainTicketQuery.rar

 

1. 获取api key

  API Store链接地址:http://apistore.baidu.com/

  1.1 通过上述链接进入百度API Store主页之后,左下角有一个“旅游票务”项,选择其中的“去哪儿网火车票”(目前该项中只有这么一个选择,没有汽车、飞机等票务信息);

  1.2 一般我们查询的是站与站之间的余票情况,比如“杭州”—“北京”,本文中的案例就是以“站站搜索接口”来进行实现(其他接口使用方法类似);

  1.3 点击“站站搜索接口”项,页面右边是对应的使用方法与参数信息,包括调试、SDK下载链接,请求参数、请求示例、返回结果说明;

  1.4 该接口套餐为免费,而且申请权限与访问峰值都是没有限制的(相信大家都喜欢这点);

  1.5 使用该api唯一的条件是——获得api key,点击页面上的“获取apikey”,会跳转到相应页面,其实也不难,只要用百度账号登录后完善个人信息并提交就可以了(如果没有百度账号马上花一点点时间申请一个);

 

  到此,就能够获取到api key与相应的sdk了。不过如果采用Android来进行开发,可以利用Java语言栏的示例代码直接用网络资源请求类HttpURLConnection来完成火车票信息的查询,而不需要像Android sdk压缩包中给出的例子那样添加额外的jar包(“ApiStoreSDK1.0.4.jar”),大家根据需求自行选择,只要能得到想要的结果就OK。

  说实话,相比于其他api接口申请步骤与套餐策略,去“哪儿网火车票”这个算是简单使用了(绝非打广告,谁用谁知道,只希望不要哪一天突然不支持)。

  如果有朋友想做测试,但懒得自己动手获取的,可以直接用实例代码中的api key(在文件MainActivity.java开头部分,项目源码链接在文章开头已给出)。还有一点需要知道的是:获取过一次api key,那么之后再申请调用百度API Store中的其他接口是通过的,至于套餐是不是免费就是另外一回事了。

 

2. 火车票查询实现

  整个查询流程大概是这样的:

  a. 输入始发地、目的地,选好出发日期,点击“查询”按键开始尝试火车票的查询;

  b. 先判断网络是否连接,若没有,则进入网络设置选择页面(用户自己选择是设置“移动数据”或“无线网络”);

  c. 若用户没有设置好网络就返回到查询主页面,那么查询过程终止,除非再次点击“查询”按键;

  d. 若已经联网或网络设置好返回查询页面,那么还会判断一次始发地与目的地的输入字串是否为空,若有一个为空则给出Toast提示;

  e. 若都不为空,则开始查询,如果站--站名称无误,就会将查询结果显示在列表中(结果是以出发时间为升序排列);

  项目源码通过文章开头链接可以下载到,所以接下来只针对部分重要或者需要注意的代码进行分析,欢迎大家指正与讨论。先上两张效果图:

               

  2.1 AndroidManifest.xml文件中添加网络访问与状态获取的权限申请:

1 <!-- 查询火车票需要联网 -->
2 <uses-permission android:name="android.permission.INTERNET" />
3 <!-- 查询前进行网络判断, 需要获取网络状态 -->
4 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

  注意:网络相关权限不管android sdk版本是L还是M(最新的已经为N--7.0),都是应用安装好后自动打开的。不像文件写权限“WRITE_EXTERNAL_STORAGE”等被Google定义为危险的权限类型,需要在应用使用过程中弹出自定义的页面让用户选择是否允许打开某权限的申请。

  2.2 抽象出来一个简单实用的类Utils:

 1 private static final String TAG = "MainActivity";
 2 
 3 public static Context mContext;
 4 
 5 public static boolean mIsBackFromSetNetwork;
 6 
 7 /*
 8  * 判断网络连接情况
 9  */
10 public static boolean isNetWorkConnected(){
11     ConnectivityManager connectivityManager = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
12     NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
13 
14     return networkInfo != null && networkInfo.isConnected();
15 }
16 
17 /*
18  * Toast
19  */
20 public static void showToast(String message) {
21     Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
22 }
23 
24 /*
25  * Log
26  */
27 public static void showLog(Object message) {
28     Log.i(TAG, ""+message);
29 }

  方法isNetworkConnected()判断设备是否联网,showToast(String message)和showLog(String message)则分别对Toast和Log信息的显示进行了封装(使用时传入要显示的信息即可,不用每次都繁琐地去指定那些固定的参数值)。

  说明:Utils类中大部分变量与方法都是public static类型,需要特别注意mContext成员的赋值方式——最好不要将Activity的上下文环境(Context)赋给全局的Context类对象,如:

1 Utils.mContext = MainActivity.this

  因为当应用程序完全退出之前全局变量是一直存在的,而如果那样赋值之后,当Activity想destroy时,由于还有其他对象在引用其环境,可能出现内存泄漏。可行的方式之一:

1 Utils.mContext = getApplicationContext()

  2.3 输入始发地与目的地并选择好时间后,点击界面上的“查询”按钮,调用方法startTicketInquireThread():

 1 private void startTicketInquireThread() {
 2     if(Utils.isNetWorkConnected()) {
 3         new Thread(new Runnable() {
 4 
 5             @Override
 6             public void run() {
 7                 inquireTrainTickets();
 8             }
 9         }).start();
10     } else {
11         Intent intent = new Intent(this, AccessNetworkActivity.class);
12         startActivity(intent);
13     }
14 }

  2.3.1 首先判断设备是否联网(包括移动与无线,具体见Utils类),若检测到网络不通,则跳转到网络设置的选择页面AccessNetworkActivity,界面如下:

  有两个选择:移动网络和无线网络,点击后分别进入相应的系统网络设置页面:


         

  而这两个页面也是Activity,对应的Intent建立代码分别如下:

1 Intent intent = null;
2 //移动数据
3 intent =  new Intent(Settings.ACTION_DATA_ROAMING_SETTINGS);
4 //无线网络
5 intent =  new Intent(Settings.ACTION_WIFI_SETTINGS);

  以打开移动数据为例,如图:

  连接网络后点击back键返回到查询主界面(或没有打开直接返回),主程序MainActivity类会在onResume()方法中会紧接着做网络连接判断及相应的处理:

 1 public void onResume() {
 2     super.onResume();
 3 
 4     Utils.showLog("MainActivity Resume");
 5 
 6     if(Utils.mIsBackFromSetNetwork) {
 7         if (Utils.isNetWorkConnected()) {
 8             startTicketInquireThread();
 9         } else {
10             mResultMapList.clear();
11             mResultListAdapter.notifyDataSetChanged();
12         }
13 
14         Utils.mIsBackFromSetNetwork = false;
15     }
16 }

  这里该解释下Utils类中声明的变量mIsBackFromSetNetwork了,用于标记应用返回MainActivity页面时是因为网络设置还是其他原因(如从home界面重新回到本应用等)。如果该标记为true,那么进一步判断网络设置是否成功,如果成功则重新调用startTicketInquireThread()尝试火车票查询;如果不成功则将结果列表数据清空(不管之前列表中有没有结果),真正做到界面上的列表清空代码是第11行,由SimpleAdapter类对象mResultListAdapter调用方法notifyDataSetChanged(),该对象为ListView对象mResultMapList的数据适配器。

   2.3.2 若一开始或者从网络设置返回到查询页面时网络连接正常,那么开始真正的查询过程,调用方法inquireTrainTickets(),该方法开始时会先判断始发地与目的地是否都有输入:

1 String startPlace = mStartPlaceET.getText().toString();
2 String endPlace = mEndPlaceET.getText().toString();
3 if(TextUtils.isEmpty(startPlace) || TextUtils.isEmpty(endPlace)) {
4     mHandler.sendEmptyMessage(ERROR_WHAT);
5 }

  TextUtils是很好用的一个类,其中有不少关于字符串的方法。代码判断如果始发地与目的地只要有一个为空,就通过Handler类的消息机制给出提示信息。其实这里完全可以直接调用Utils.showToast(String message)方法,但是下面会给出mHandler对象的定义,把查询成功与失败的处理放在了一起,便于统一管理。

 1 private Handler mHandler = new Handler() {
 2 
 3     @Override
 4     public void handleMessage(Message msg) {
 5         super.handleMessage(msg);
 6 
 7         int what = msg.what;
 8         if(what == SUCCESS_WHAT) {
 9             mResultListAdapter.notifyDataSetChanged();
10         }else if(what == ERROR_WHAT) {
11             Utils.showToast(getString(R.string.please_input_place));
12         }
13     }
14 };

  即地点输入不全时,利用Toast提示用户先输入再查询,至于字串的定义在res/values/strings.xml文件中,这里不进行描述。效果图:

  2.3.3 若从网络设置返回到查询页面,网络连接还是失败,则清空列表数据,这点在上面onResume()方法的描述中已经提及了。

  2.4 在讲解火车票数据的获取及处理之前,还要补充一点,观察方法startTicketInquireThread()判断网络连接成功后,调用方法inquireTrainTickets()是在新开的一个线程中。这样做可以说是普遍的处理方式或是良好的习惯,因为像获取网络数据这种操作一般都比较费时,放在主UI线程中肯定是不妥的(哪怕某次操作非常快,利用另一个线程处理总是保险的做法)。其实说到这,大家已经知道为什么要用Handler类机制了,它很重要的作用就是帮助子线程来在主UI线程中更新组件信息,本案例中是更新利用ListView显示的火车票信息。

  2.4.1 搜索站--站火车票的关键代码定义在inquireTrainTickets()方法中:

 1 private final String API_KEY = "361cf2a2459552575b0e86e0f62302bc";
 2 private final String HTTP_URL = "http://apis.baidu.com/qunar/qunar_train_service/s2ssearch";
 3 private final String HTTP_ARG = "version=1.0&from=START&to=END&date=YEAR-MOUTH-DAY";
 4 
 5 String httpUrl;
 6 String httpArg = HTTP_ARG.replace("START", startPlace)
 7         .replace("END", endPlace)
 8         .replace("YEAR", ""+mYear)
 9         .replace("MOUTH", mMouth>9?""+mMouth:"0"+mMouth)
10         .replace("DAY", mDayOfMouth>9?""+mDayOfMouth:"0"+mDayOfMouth);
11 
12 Utils.showLog(httpArg);
13 
14 BufferedReader reader = null;
15 String result = null;
16 StringBuffer sbf = new StringBuffer();
17 httpUrl = HTTP_URL + "?" + httpArg;
18 try {
19     URL url = new URL(httpUrl);
20     HttpURLConnection connection = (HttpURLConnection) url
21             .openConnection();
22     connection.setRequestMethod("GET");
23     connection.setRequestProperty("apikey",  API_KEY);
24     connection.connect();
25     InputStream is = connection.getInputStream();
26     reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
27     String strRead = null;
28     while ((strRead = reader.readLine()) != null) {
29         sbf.append(strRead);
30         sbf.append("
");
31     }
32     reader.close();
33     result = sbf.toString();
34 } catch (Exception e) {
35     e.printStackTrace();
36 }

  注意开头的1-3行代码需要放在方法体之外,这里放在一起只是方便说明,分别定义了当前api的key、站--站搜索接口链接、以及版本+地址+时间的格式字串。

  可以看到在对时间的月和日设置时,对值小于10的数据做了加“0”处理。这是因为api的规定,接口主页上明确给出了格式为“出发日期:格式 YYYY-MM-DD”,也就是说年为4位,月和日均为2位(等到年为5位的时候估计不坐火车了)。当然字串HTTP_ARG的定义可以将待替换的部分用占位符声明,这样就不用多次调用replace(String src, String dst)方法了。

1 String HTTP_ARG = "version=1.0&from=%1$s&to=%2$s&date=%3$s-%4$s-%5$s";
2 String string2 = String.format(HTTP_ARG, startPlace, endPlace, ""+mYear, mMouth>9?""+mMouth:"0"+mMouth, mDayOfMouth>9?""+mDayOfMouth:"0"+mDayOfMouth);

  代码是不是简洁多了,占位符序号从1开始,传入参数时也必须按照声明的顺序。

  2.4.2 获取的结果存在字串result中,这里有两种情况:第一种是地址错误,那么得到的结果为空;第二种就是成功获取到火车票信息,需要在列表中进行显示。不管哪种情况,都会在方法inquireTrainTickets()末尾调用了方法setTrainTicketList(String result),这里有必要贴一下代码:

 1 private void setTrainTicketList(String result) {
 2     mResultMapList.clear();
 3     try {
 4         JSONArray jsonArray1 = new JSONObject(result).getJSONObject("data").getJSONArray("trainList");
 5         mJsonArray = jsonArray1;
 6         for(int i=0; i<jsonArray1.length(); ++i) {
 7             JSONObject jsonObject1 = jsonArray1.getJSONObject(i);
 8             Map<String, Object> map = new HashMap<String, Object>();
 9             map.put(START_TIME, jsonObject1.get(START_TIME));
10             map.put(END_TIME, jsonObject1.get(END_TIME));
11             map.put(FROM, jsonObject1.get(FROM));
12             map.put(TO, jsonObject1.get(TO));
13             map.put(TRAIN_NO, jsonObject1.get(TRAIN_NO));
14             map.put(DURATION, jsonObject1.get(DURATION));
15             JSONArray jsonArray2 = jsonObject1.getJSONArray(SEAT_INFOS);
16             JSONObject jsonObject2 = jsonArray2.getJSONObject(0);
17             map.put(SEAT, jsonObject2.get(SEAT));
18             map.put(SEAT_PRICE, jsonObject2.get(SEAT_PRICE)+getString(R.string.train_price_unit));
19             map.put(REMAIN_NUM, jsonObject2.get(REMAIN_NUM)+getString(R.string.train_ticket_unit));
20             mResultMapList.add(map);
21         }
22     } catch (JSONException e) {
23         e.printStackTrace();
24     }
25     Collections.sort(mResultMapList, mComparatorResultMap);
26     mHandler.sendEmptyMessage(SUCCESS_WHAT);
27 }

  进入方法后,代码第2行将列表数据清空,第26行则发送消息进而更新列表。由于网络请求结果一般为JSON格式字串,所以往往需要将String对象转化为JSONObject对象再进行具体数据的提取。和之前网络请求过程类似,本案例中JSON字串处理的代码较基础,不在展开详述了,如果对这块不熟悉的朋友可以自学习下相关知识,入门还是不难的。

  补充:关于JSON字串的处理,上面代码虽然不算复杂也勉强能够达到目的,但从实用、简洁与面向对象的角度来说是落伍了。之后有时间打算用GSon+GsonFormat进行实现,GsonFormat作为一款插件,可以快速建立JSON字串对应的JavaBean类,然后利用GSON对象操作数据,非常好用,可以说它们将眼花缭乱的字串变成了我们熟悉的类。

  2.4.3 前面提到列表中显示的火车票信息是按照出发时间升序排列的,即将时间早的排在前面,这样比较符合用户的查询习惯。而进列表数据进行排序的代码为:

1 Collections.sort(mResultMapList, mComparatorResultMap);

  这行代码在上面setTrainTicketList(String result)方法的第25行,先看一下Collections中关于该方法的源码:

1 public static void sort(List list, Comparator comparator) {
2     throw new RuntimeException("Stub!");
3 }

  第一个参数为List对象,第二个参数为Comparator对象。接着给出接口Comparator的源码:

1 public interface Comparator {
2     public abstract int compare(Object obj, Object obj1);
3     public abstract boolean equals(Object obj);
4 }

  我们的列表对象mResultMapList的类型为List<Map<String, Object>>,满足第一个参数的List类型要求,但是其内部数据类型为Map<String, Object>,既不是String也不是Object,这时候就需要自己实现compare(Obejct obj, Object obj1)方法了。

1 private Collator mCollator = Collator.getInstance(Locale.CHINA);
2 private Comparator<Map<String, Object>> mComparatorResultMap = new Comparator<Map<String, Object>>() {
3 
4     @Override
5     public int compare(Map<String, Object> lhs, Map<String, Object> rhs) {
6         return mCollator.compare(lhs.get(START_TIME), rhs.get(START_TIME));
7     }
8 };

  首先声明一个Collator类对象,如果在中文环境中使用,在获取实例时必须传入Locale.CHINA类型,否则对中文字串的排序不准确或无效。其实重载该方法的关键就是在返回值部分,调用了Collator类的compare(Object object1, Object object2),源码如下:

1 public int compare(Object object1, Object object2) {
2     return compare((String) object1, (String) object2);
3 }

  进而调用了其自身的抽象方法compare(String string1, String string2):

1 public abstract int compare(String string1, String string2);

  所以,最终进行比较的是String类型值,只要将Map<String, Object>对象中字段START_TIME对应的字串值当做参数传入即可。顺便将START_TIME等变量定义给出:

 1 private final String START_TIME = "startTime";
 2 private final String END_TIME = "endTime";
 3 private final String FROM = "from";
 4 private final String TO = "to";
 5 private final String TRAIN_NO = "trainNo";
 6 private final String DURATION = "duration";
 7 private final String SEAT_INFOS = "seatInfos";
 8 private final String SEAT = "seat";
 9 private final String SEAT_PRICE = "seatPrice";
10 private final String REMAIN_NUM = "remainNum";

  很简单的定义,这里想说明的是:如果采用本案例中的常规方式进行JSON字串的处理(不用其他库或插件),那么就好将变量的值设置为与JSON字串中key-value部分的key值一致,这样在以后的处理过程中比较直观,不易出错。

  2.4.4 运行过程序的朋友就会知道,点击列表中的某一车次信息后,会弹出一个对话框,显示具体的座位与余票信息,这个和前一步显示所有查询结果类似,不再进行讲解了。感兴趣的可以阅读相关代码中方法showTicketsInfoDialog(String trainNo),通过列车号作为索引来获取具体信息,效果图如下:

  需要注意的是:从获取网络请求结果到显示在列表经历了网络请求->结果返回->String到JSONObject类型转换->数据提取成Map对象添加到List列表->列表信息排序->显示,经过排序之后列表中的火车票信息与网络请求返回的顺序很大可能是不一致的,所以在点击某一车次获取具体信息时不能够以列表元素下标作为索引,而是要以列车号为索引(传给showTicketsInfoDialog(String trainNo)方法)。还有就是对话框布局是自定义的,有三部分组成:标题(包含当前车次号)、余票(以座位类型区分)、确认按钮。

  2.4.5 始发地与目的地的互换,就是将两个EditText组件的值进行调换,给出代码:

1 String startPlace = mStartPlaceET.getText().toString();
2 String endPlace = mEndPlaceET.getText().toString();
3 mStartPlaceET.setText(endPlace);
4 mEndPlaceET.setText(startPlace);

  2.4.6 关于时间的选择,要好好地说一说。目前我国的购票政策是:从今天算起,能够买到60天之内的票。那么我们在实现时间的选择器的时候,就要规定好可选时间的区间(总不能老提示用户您选的时间不对吧),获取当前时间与计算时间上限的代码:

1 mCalendar = Calendar.getInstance();
2 mMinTimeMills = mCalendar.getTimeInMillis();
3 mMaxTimeMills = mMinTimeMills+59l*24*3600*1000;

  时间上下限的单位为毫秒,变量类型为long。顺便提一下从Calendar实例中获取年月日的代码:

1 mYear = mCalendar.get(Calendar.YEAR);
2 mMouth = mCalendar.get(Calendar.MONTH)+1;
3 mDayOfMouth = mCalendar.get(Calendar.DAY_OF_MONTH);

  月份需要进行+1处理,即如果这个月为8月,实际获取的值为7。还有获取星期几的情况也是如此,

1 mDayOfWeek = mCalendar.get(Calendar.DAY_OF_WEEK);

  获取的值可能是1,2,3,4,5,6,7之一,对应的星期其实是日,一,二,三,四,五,六,即也存在差一的情况。

   下面贴出日期选择对话框的布局与实现代码:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5     android:orientation="vertical"
 6     android:gravity="center"
 7     android:background="#ffffffff" >
 8     
 9     <DatePicker
10         android:id="@+id/train_datepicker"
11         android:layout_width="wrap_content"
12         android:layout_height="wrap_content"
13         android:calendarViewShown="false"
14         android:layout_marginTop="0dp"
15         android:layout_marginBottom="10dp" />
16     
17     <View 
18         style="@style/TrainLine" />
19     
20     <LinearLayout
21         android:layout_width="match_parent"
22         android:layout_height="60dp"
23         android:padding="0dp"
24         android:orientation="horizontal"
25         android:gravity="center" >
26         
27         <TextView
28             android:id="@+id/date_cancel"
29             android:layout_width="0dp"
30             android:layout_height="wrap_content"
31             android:layout_weight="1"
32             android:text="@string/cancel"
33             android:textColor="#ff000000"
34             android:gravity="center" />
35         
36         <View 
37             android:layout_width="0.5dp"
38             android:layout_height="match_parent"
39             android:background="#ffff0000" />
40         
41         <TextView
42             android:id="@+id/date_confirm"
43             android:layout_width="0dp"
44             android:layout_height="wrap_content"
45             android:layout_weight="1"
46             android:text="@string/confirm"
47             android:textColor="#ff000000"
48             android:gravity="center" />
49         
50     </LinearLayout>
51 
52 </LinearLayout>
 1 private void showDateSelectDialog() {
 2     final Dialog dateDialog = new Dialog(this, R.style.DialogFixTitle);
 3     dateDialog.setContentView(R.layout.train_datepicker);
 4     dateDialog.setCanceledOnTouchOutside(true);
 5     TextView cancel = (TextView) dateDialog.findViewById(R.id.date_cancel);
 6     cancel.setOnClickListener(new OnClickListener() {
 7 
 8         @Override
 9         public void onClick(View v) {
10             dateDialog.cancel();
11         }
12     });
13     TextView confirm = (TextView) dateDialog.findViewById(R.id.date_confirm);
14     confirm.setOnClickListener(new OnClickListener() {
15 
16         @Override
17         public void onClick(View v) {
18             dateDialog.cancel();
19             mTargetDayTV.setText(tempTargetDay);
20             getDateParams();
21             startTicketInquireThread();
22         }
23     });
24     DatePicker datePicker = (DatePicker) dateDialog.findViewById(R.id.train_datepicker);
25     datePicker.setMinDate(mMinTimeMills);
26     datePicker.setMaxDate(mMaxTimeMills);
27     datePicker.init(mYear, mMouth-1, mDayOfMouth, new OnDateChangedListener() {
28 
29         @Override
30         public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
31             mCalendar.set(year, monthOfYear, dayOfMonth);
32             tempTargetDay = getTempTargetDay(year, monthOfYear+1, dayOfMonth, mCalendar.get(Calendar.DAY_OF_WEEK));
33 
34             Utils.showLog(tempTargetDay);
35         }
36     });
37     dateDialog.show();
38 }

  布局文件没什么好说的,除了两个按钮,较关键的就是DatePicker,是Android自带的时间选择组件组件。效果图:

            

  首先从布局中获取组件实例,进行时间区间上下限的设置;接着调用init(mYear, mMouth-1, mDayOfMouth, new OnDateChangedListener())方法对DatePicker类对象dataPicker进行初始化,同样地,传入月份时需要做-1处理;然后在其中重载监听器OnDateChangedListener接口的onDateChanged()方法,可以获取到DatePicker组件选择后的结果。

  时间选择好后,点击“确认”按钮会调用做两件事情:1、更新时间选择按钮的信息,显示为最新选择的日期;2、调用方法startTicketInquireThread()开始尝试对应时间的火车票查询,就不用再次点击“查询”按键了。

  不过这个时间选择组件在不同机器上的显示形式会不一样,感觉上面最左图好丑有没有。如果想做到像去哪儿app(上面最右图)那样统一的效果,还得想其他方法(比如万能的自定义)。

  2.4.7 最后贴一下网络请求返回数据,当地址错误等原因引起查询结果为空时:

  {

    "ret":true,

    "data":{

      "trainList":null

    }

  }

  正常的火车票信息,时间为2016年9月13日 周二,杭州到北京:

{
  "ret":true,
  "data":{
    "trainList":[
      {

        "trainType":"直达特快","trainNo":"Z10","from":"杭州","to":"北京",

        "startTime":"17:17","endTime":"07:34","duration":"14时17分",

        "seatInfos":[

              {"seat":"无座","seatPrice":"192","remainNum":278},

              {"seat":"硬座","seatPrice":"192","remainNum":526},

              {"seat":"硬卧","seatPrice":"328","remainNum":365},

              {"seat":"软卧","seatPrice":"515","remainNum":4}]

      },
      {

        "trainType":"空调特快","trainNo":"T32","from":"杭州","to":"北京",

        "startTime":"18:20","endTime":"10:26","duration":"16时6分",

        "seatInfos":[

              {"seat":"高级软卧","seatPrice":"949","remainNum":4},

              {"seat":"无座","seatPrice":"192","remainNum":236},

              {"seat":"硬座","seatPrice":"192","remainNum":365},

              {"seat":"硬卧","seatPrice":"328","remainNum":164},

              {"seat":"软卧","seatPrice":"515","remainNum":28},

              {"seat":"一人软包","seatPrice":"1342","remainNum":2}]

      }
    ]
  }
}

3. 总结

  本文利用百度API Store中去哪儿网提供火车票查询接口,实现了国内简单的站--站火车票搜索功能。整个过程中,网络数据请求其实比较简单,难的是对获取到的数据进行处理,以及做出美观、交互性强的界面。所以,后续有时间会对案例进行优化,目前想到的可以做的事情包括:

  a. 显示火车票信息的列表可以用RecyclerView替代ListView,添加下拉刷新等功能;

  b. 时间选择组件的统一化,让不同设备显示出的画面一致;

  c. 始发地与目的地的设置,不再利用EditText组件进行文本输入,而是像一般上线的app那样直接提供地区选择列表;

  d. 将项目改用Android Studio实现,添加引用库或者插件简直so easy;

  对于火车票的预订暂时没什么想法,毕竟涉及到支付了,不过关于这块内容希望有开发经验的前辈可以指点一二,没经验的也欢迎一起学习与讨论,谢谢。

原文地址:https://www.cnblogs.com/tgyf/p/5799998.html