郭神提供的数据接口,包含全国省市县名称和编号信息:
省级单位:
http://guolin.tech/api/china
服务器会返回JSON格式数据
市级单位:在后面加上具体省份id即可
http://guolin.tech/api/china/16
县级单位以此类推:
http://guolin.tech/api/china/16/116
接着为了获取每个地区具体的天气情况需要注册和风天气的接口:
拿到自己App的API KEY
之后配合每个具体地区的weather_id即可查看天气信息,如:
http://guolin.tech/api/weathercityid=cn101190401&key=bc0418b57b2d4918819d3974ac1285d9
返回的数据如:
数据获取后接着做JSON解析工作即可。
第一阶段要做的就是创建好数据库和表,从而将服务器获取到的数据存储到本地。这里使用 LitePal来管理数据库。
首先创建目录结构 ,其中db包用于存放数据库模型相关的代码
gson包用于存放GSON模型相关的代码,
service包用于存放服务相关的代码
util包用于存放工具相关的代码。
添加相关依赖:
使用 LiteRal,可以用面向对象的思维来实现数据库相关操作,比如定义一个 Java bean,在Book类中我们定义了id、 author、 price、 pages、name这几个字段,并生成了相应的 getter和 setter方法。Book类就会对应数据库中的Book表,而类中的每一个字段分别对应了表中的每一个列,这就是对象关系映射最直观的体验。
在db下新建省市县三个bean来对应三张表,具体代码如下:
/** *省信息表 */ public class Province extends DataSupport { private int id;//代号 private String provinceName;//省名 private int provinceCode;//省编号 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getProvinceName() { return provinceName; } public void setProvinceName(String provinceName) { this.provinceName = provinceName; } public int getProvinceCode() { return provinceCode; } public void setProvinceCode(int provinceCode) { this.provinceCode = provinceCode; } }LiteRal进行表管理操作时不需要模型类有任何的继承结构,但是进行CRUD操作时就不行了, 必须要继承自 DataSupport类才行,因此这里我们需要把继承结构给加上。
/** * 城市信息表 */ public class City extends DataSupport { private int id; //字段 private String cityName; //城市名称 private int cityCode; //城市代码 private int provinceId;//城市所属省份编号 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getCityName() { return cityName; } public void setCityName(String cityName) { this.cityName = cityName; } public int getCityCode() { return cityCode; } public void setCityCode(int cityCode) { this.cityCode = cityCode; } public int getProvinceId() { return provinceId; } public void setProvinceId(int provinceId) { this.provinceId = provinceId; } } /** * 地区/县信息表 */ public class County extends DataSupport { private int id; private String countyName;//县名 private String weatherId;//天气id private int cityId;//所属县ID public int getId() { return id; } public void setId(int id) { this.id = id; } public String getCountyName() { return countyName; } public void setCountyName(String countyName) { this.countyName = countyName; } public String getWeatherId() { return weatherId; } public void setWeatherId(String weatherId) { this.weatherId = weatherId; } public int getCityId() { return cityId; } public void setCityId(int cityId) { this.cityId = cityId; } }接下来需要配置``litepal.xml`
<?xml version="1.0" encoding="utf-8"?> <litepal> <dbname value ="MyWeatherApp"/> <version value="1"/> <list> <mapping class="com.wz.myweatherapp.db.County"/> <mapping class="com.wz.myweatherapp.db.City"/> <mapping class="com.wz.myweatherapp.db.Province"/> </list> </litepal>其中,< dbname>标签用于指定数据库名,< version>标签用于指定数据库版本号, 标签用于指定所有的映射模型,我们稍后就会用到。
最后还需要再配置一下 LitePalApplication,修改 Androidmanifest xml中的代码,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.wz.myweatherapp"> <uses-permission android:name="android.permission.INTERNET"/> <application android:name="org.litepal.LitePalApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" ......由于数据是从网络服务端获取的,故可以创建工具类
public class HttpUtil { /** * 传入请求地址 注册一个回调来处理服务器响应 * @param address * @param callback */ public static void sendOkHttpRequest(String address,Callback callback){ //创建一个OkHttpClient实例 OkHttpClient client = new OkHttpClient(); //创建Request来发起HTTP请求 Request request = new Request.Builder().url(address).build(); //之后调用OkhttpClient的newCall()方法来创建一个CalL对象,并调用它的execute()方 //法来发送请求并获取服务器返回的数据,写法如下 client.newCall(request).enqueue(callback); } }另外,由于服务器返回的省市县数据都是JSON格式的,所以最好再提供一个工具类来解析和处理这种数据。在util包下新建一个 Utility类,代码如下所示
public class Utility { /** * 解析处理服务器返回的省级数据 * */ public static boolean handleProvinceResponse(String response){ if (!TextUtils.isEmpty(response)) { try { //可以看到,解析JSON的代码非常简单,由于在服务器中定义的是一个JSON数组, //因此这里首先是将服务器返回的数据传入到了一个JSONArray对象中。然后循环遍历这个JSONArray, // 从中取出的每一个元素都是一个JSONArray对象,每个JSONArray对象中又会包含id、name和 version这些数据。 //接下来只需要调用 getstring()方法将这些数据取出,并打印出来即可。 //先使用JSONArray 和 JSONObject将数据解析 JSONArray allProvince = new JSONArray(response); for (int i = 0; i < allProvince.length(); i++) { JSONObject provinceObject = allProvince.getJSONObject(i); //装入实体对象 Province province = new Province(); province.setProvinceName(provinceObject.getString("name")); province.setProvinceCode(provinceObject.getInt("id")); //由于province 继承了litepal特性 故使用save存储进数据库 province.save(); } return true; } catch (JSONException e) { e.printStackTrace(); } } return false; } /** * 解析处理服务器返回的市级数据 */ public static boolean handleCityResponse(String response,int provinceId){ try { if (!TextUtils.isEmpty(response)) { JSONArray allCities = new JSONArray(response); for (int i = 0; i < allCities.length(); i++) { JSONObject cityObject = allCities.getJSONObject(i); City city = new City(); city.setCityName(cityObject.getString("name")); city.setCityCode(cityObject.getInt("id")); city.setProvinceId(provinceId); city.save(); } return true; } } catch (JSONException e) { e.printStackTrace(); } return false; } /** * 解析处理县级数据 * */ public static boolean handleCountyResponse(String response ,int cityId){ try { if (!TextUtils.isEmpty(response)) { JSONArray allCounties = new JSONArray(response); for (int i = 0; i < allCounties.length(); i++) { JSONObject countyObject = allCounties.getJSONObject(i); County county = new County(); county.setCityId(cityId); county.setCountyName(countyObject.getString("name")); county.setWeatherId(countyObject.getString("weather_id")); county.save(); } return true; } } catch (JSONException e) { e.printStackTrace(); } return false; } }需要准备的工具类就这么多,现在可以开始写界面了。由于遍历全国省市县的功能我们在后面还会复用,因此就不写在活动里面了,而是写在碎片里面,这样需要复用的时候直接在布局里面引用碎片就可以了。
接下来也是最关键的一步,需要编写用于加载省市县数据的碎片了。新建 ChooseAreaFragment继承自 Fragment这个逻辑却不复杂,需要慢慢理一下。在 onCreateview()方法中先是获取到了一些控件的实例,然后去初始化了 Array Adapter,并将它设置为 List view的适配器。接着在 onActivityCreated()方法中给 Listview和Button设置了点击事件,到这里初始化工作就算是完成了。
@Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.choose_area, container, false); mTitleText = view.findViewById(R.id.title_text); mButton = view.findViewById(R.id.back_button); mListView = view.findViewById(R.id.list_view); //不过,数组中的数据是无法直接传递给 Listview的,我们还需要借助适配器来完成。 Android //中提供了很多适配器的实现类,其中我认为最好用的就是 Array Adapter。它可以通过泛型来指定 //要适配的数据类型,然后在构造函数中把要适配的数据传入。 Array Adapter有多个构造函数的重 //载,你应该根据实际情况选择最合适的一种。这里如果提供的数据都是字符串,可以将 //ArrayAdapter的泛型指定为 String // 然后在ArrayAdapter的构造函数中依次传入当前上下文,List view子项布局的id,以及要适配的数据。 // 注意,使用了simple_list_item_1作为 List view子项布局的id,这是一个 Android内置的布局文件,里面只有一个 //Text View,可用于简单地显示一段文本。这样适配器对象就构建好了 //最后,还需要调用 List View的 setAdapter()方法,将构建好的适配器对象传递进去,这样 //List view和数据之间的关联就建立完成了。 adapter = new ArrayAdapter<>(getContext(),android.R.layout.simple_list_item_1,dataList); mListView.setAdapter(adapter); return view; } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); /* 列表点击事件 */ mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { //可以看到,我们使用 setonItemClicklistener()方法为 Listview注册了一个监听器,当 //用户点击了 Listview中的任何一个子项时,就会回调 onItemclick()方法。在这个方法中可以 //通过 position参数判断出用户点击的是哪一个子项,然后获取到相应的类信息,并通过Toast显示 @Override public void onItemClick(AdapterView<?> adapterView, View view, int pos, long idl) { if (currentLevel == LEVEL_PROVINCE){ selectedProvince = provinceList.get(pos); queryCity(); }else if (currentLevel == LEVEL_CITY){ selectedCity = cityList.get(pos); queryCounty(); } } }); /* 返回按钮 点击事件 */ mButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (currentLevel == LEVEL_COUNTY){ /* 如果是在县页面返回 则获取市信息 */ queryCity(); }else if (currentLevel == LEVEL_CITY){ /* 如果实在市级别返回 则获取省信息 */ queryProvince(); } } }); /* 创建时默认获取省信息 */ queryProvince(); } /* 查询省内所有市 */ private void queryCity() { mTitleText.setText(selectedProvince.getProvinceName()); mButton.setVisibility(View.VISIBLE); cityList = DataSupport.where("provinceid = ?",String.valueOf(selectedProvince.getId())).find(City.class); if (cityList.size()>0) { dataList.clear(); for (City city:cityList){ dataList.add(city.getCityName()); } adapter.notifyDataSetChanged(); mListView.setSelection(0); currentLevel = LEVEL_CITY; }else { int provinceCode = selectedProvince.getProvinceCode(); String address ="http://guolin.tech/api/china/"+provinceCode; queryFromServer(address,"city"); } } /* 查询全国所有省 优先从数据可查询 若无再去服务器查询 query Provinces()方法中首先会将头布局的标题设置成中国,将返回按钮 隐藏起来,因为省级列表已经不能再返回了。然后调用 LiteRal的查询接口来从数据库中读取省 级数据,如果读取到了就直接将数据显示到界面上,如果没有读取到组装出一个请求地址, 然后调用 queryFromServer()方法来从服务器上查询数据。 */ private void queryProvince() { mTitleText.setText("中国"); //另外还有一点需要注意,在返回按钮的点击事件里,会对当前 List view的列表级别进行判断。 //如果当前是县级列表,那么就返回到市级列表,如果当前是市级列表,那么就返回到省级表列表。 //当返回到省级列表时,返回按钮会自动隐藏,从而也就不需要再做进一步的处理了。 mButton.setVisibility(View.GONE); provinceList = DataSupport.findAll(Province.class); if (provinceList.size() > 0) { dataList.clear(); for (Province province : provinceList) { dataList.add(province.getProvinceName()); } adapter.notifyDataSetChanged(); mListView.setSelection(0); currentLevel = LEVEL_PROVINCE; }else { String address = "http://guolin.tech/api/china"; queryFromServer(address, "province"); } } /** * query Fromserver()方法中会调用 HttpUtil的send0httPrequest()方法来向服务器发送 * 请求,响应的数据会回调到 onResponse()方法中,然后我们在这里去调用 Utility的 * handleprovincesresponse()方法来解析和处理服务器返回的数据,并存储到数据库中。接下 * 来的一步很关键,在解析和处理完数据之后,再次调用了 queryProvinces()方法来重新加 * 载省级数据,由于 queryProvinces()方法牵扯到了U操作,因此必须要在主线程中调用,这 * 里借助了 runonuiThread()方法来实现从子线程切换到主线程。现在数据库中已经存在了数据 * 因此调用 queryProvinces()就会直接将数据显示到界面上了 * @param address * @param type */ private void queryFromServer(String address, final String type) { showProgressDialog(); HttpUtil.sendOkHttpRequest(address, new Callback() { @Override public void onFailure(Call call, IOException e) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { closeProgressDialog(); Toast.makeText(getContext(), "加载失败", Toast.LENGTH_SHORT).show(); } }); } @Override public void onResponse(Call call, Response response) throws IOException { //我们可以使用如下写法来得到返回的具体内容: String responseText = response.body().string(); boolean result = false; if ("province".equals(type)) { result = Utility.handleProvinceResponse(responseText); }else if ("city".equals(type)){ result = Utility.handleCityResponse(responseText,selectedProvince.getId()); }else if ("county".equals(type)){ result = Utility.handleCountyResponse(responseText,selectedCity.getId()); } if (result){ getActivity().runOnUiThread(new Runnable() { @Override public void run() { closeProgressDialog(); if ("province".equals(type)){ queryProvince(); }else if ("city".equals(type)){ queryCity(); }else if ("county".equals(type)){ queryCounty(); } } }); } } }); } private void closeProgressDialog() { if (mProgressDialog != null) { mProgressDialog.dismiss(); } } private void showProgressDialog() { if (mProgressDialog == null) { mProgressDialog = new ProgressDialog(getActivity()); mProgressDialog.setMessage("正在加载"); mProgressDialog.setCanceledOnTouchOutside(false); } mProgressDialog.show(); } /* 查询市内所有区/县 */ private void queryCounty() { mTitleText.setText(selectedCity.getCityName()); mButton.setVisibility(View.VISIBLE); countyList = DataSupport.where("cityid=?",String.valueOf(selectedCity.getId())).find(County.class); if (countyList.size()>0) { dataList.clear(); for (County county:countyList ) { dataList.add(county.getCountyName()); } adapter.notifyDataSetChanged(); mListView.setSelection(0); currentLevel =LEVEL_COUNTY; }else { int provinceCode = selectedProvince.getProvinceCode(); int cityCode = selectedCity.getCityCode(); String address = "http://guolin.tech/api/china/"+provinceCode +"/"+cityCode; queryFromServer(address,"county"); } }这样我们就把加载全国省市县的功能完成了,可是碎片是不能直接显示在界面上的,因此我们还需要把它添加到活动里才行。修改 activity main.xml中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <fragment android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/choose_area_fragment" android:name="com.wz.myweatherapp.ChooseAreaFragment" /> </FrameLayout>另外,我们刚才在碎片的布局里面已经自定义了一个标题栏,因此就不再需要原生的Action bar了,修改res/ values/ styles.xml中的代码,如下所示
<resources> <!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style> </resources>声明所需网络权限
<uses-permission android:name="android.permission.INTERNET"/>现在可以运行一下程序进行测试了,效果如下。