郭神提供的数据接口,包含全国省市县名称和编号信息:
省级单位:
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包用于存放服务相关的代码
使用 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`
<litepal>
<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"/>
由于数据是从网络服务端获取的,故可以创建工具类
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;
}
}
需要准备的工具类就这么多,现在可以开始写界面了。由于遍历全国省市县的功能我们在后面还会复用,因此就不写在活动里面了,而是写在碎片里面,这样需要复用的时候直接在布局里面引用碎片就可以了。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fff">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary">
<TextView
android:id="@+id/title_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:textSize="20sp"
android:textColor="#fff"
/>
<Button
android:id="@+id/back_button"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginLeft="10dp"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:background="@drawable/ic_back"/>
RelativeLayout>
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
LinearLayout>
接下来也是最关键的一步,需要编写用于加载省市县数据的碎片了。新建 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中的代码,如下所示:
<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>
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
- "colorPrimary"
>@color/colorPrimary
- "colorPrimaryDark">@color/colorPrimaryDark
- "colorAccent">@color/colorAccent
style>
resources>
声明所需网络权限
<uses-permission android:name="android.permission.INTERNET"/>