整理一下完成的思路,并附上部分代码和注释以及自己的理解。
(看到有同学问,附上项目地址:https://github.com/LittleFogCat/coolweather)
逻辑部分
一、首先通过网络接口获得全国省市县的列表。
1. 新建一个HttpUtil类,在其中创建一个sendOkHttpRequest()方法:
public static void sendOkHttpRequest(String url, Callback callback) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
client.newCall(request).enqueue(callback);
}
传入一个一个url字符串,以及一个回调接口。
2. 新建Province, City, County类,分别用于保存省市县的数据。
public class Province extends DataSupport {
private int id;
private String provinceName;
private int provinceCode;
getter & setter
}public class City extends DataSupport {
private int id;
private String cityName;
private int cityCode;
private int provinceId;
getter & setter
}
public class County extends DataSupport {
private int id;
private String countyName;
private String weatherId;
private int cityId;
getter & setter
}
其中provinceCode用于请求天气数据。使用LitePal库进行数据库操作,所以三个类都要继承DataSupport类。
3. 配置litepal
配置assets/litepal.xml
配置Manifest-Application
4. 新建Utility类,用于处理返回的json数据
public class Utility { // 解析省市县的json数据,并保存在数据库中
public static boolean handleProvinceResponse(String response) {
if (!TextUtils.isEmpty(response)) {
try {
JSONArray allProvinces = new JSONArray(response);
for (int i = 0; i < allProvinces.length(); i++) {
JSONObject object = allProvinces.getJSONObject(i);
Province province = new Province();
province.setProvinceCode(object.getInt("id"));
province.setProvinceName(object.getString("name"));
province.save();
}
return true;
} catch (JSONException e) {
e.printStackTrace();
return false;
}
} else return false;
}
public static boolean handleCityResponse(String response, int provinceId) {
if (!TextUtils.isEmpty(response)) {
try {
JSONArray allCity = new JSONArray(response);
for (int i = 0; i < allCity.length(); i++) {
JSONObject object = allCity.getJSONObject(i);
City city = new City();
city.setCityName(object.getString("name"));
city.setCityCode(object.getInt("id"));
city.setProvinceId(provinceId);
city.save();
}
return true;
} catch (JSONException e) {
e.printStackTrace();
return false;
}
} else return false;
}
很简单,利用JSONObject处理json数据,并调用save()方法保存入数据库中。
5. 新建遍历省市县的Fragment
public class ChooseAreaFragment extends Fragment {
// vars
final int LEVEL_PROVINCE = 0;
final int LEVEL_CITY = 1;
final int LEVEL_COUNTY = 2;
private ProgressDialog progressDialog;
private TextView txtTitle;
private Button btnBack;
private ListView listView;
private ArrayAdapter<String> adapter;
private List<String> dataList = new ArrayList<>();
private List<Province> provinceList;
private List<City> cityList;
private List<County> countyList;
private Province selectedProvince;
private City selectedCity;
private int currentLevel;
// methods
}△在dataList数组中保存当前显示在屏幕上的内容;
△通过三个常量LEVEL_PROVINCE, LEVEL_CITY, LEVEL_COUTY来判断当前显示是省市还是县。
methods:
△在onCreateView中实例化Fragment的布局并传回。
△在onActivityCreated中设置列表和返回键的点击事件。
△新建queryProvinces()、queryCities()、queryCounties()方法查询省市县的数据。
/**
* 标题改为"China",隐去back键
* <p>
* 从数据库中查找Province数据,如果存在则:
* 1. 赋值到provinceList中;
* 2. 将provinceList的成员name添加到dataList中
* 3. 使用adapter.notifyDataSetChanged()方法更新列表,并使用adapter.setSelection(0)将选中行设为第一行
* <p>
* 如果不存在则调用queryFromServer从网络查找
*/
private void queryProvinces() {
txtTitle.setText("China");
btnBack.setVisibility(View.GONE);
provinceList = DataSupport.findAll(Province.class);
if (provinceList.size() > 0) {
dataList.clear();
for (Province p : provinceList) {
dataList.add(p.getProvinceName());
}
adapter.notifyDataSetChanged();
listView.setSelection(0);
currentLevel = LEVEL_PROVINCE;
} else {
String url = getResources().getString(R.string.url_query_province);
queryFromServer(url, "province");
}
}
第一次运行时,由于本地没有数据,所以调用queryFromServer()方法在服务器查询:
private void queryFromServer(String url, final String type) {
showProgressDialog();
HttpUtil.sendOkHttpRequest(url, new Callback() {
...
} }); }
由于调用了sendOkHttpRequest,所以要实现它的回调接口中的onResponse和onFailure方法:
如果得到返回数据,则调用Utility.handleXresponse(response.body().string())处理传回的json数据,X即是传入的第二个变量type。
如果查询失败,则显示失败的Toast。
至此,遍历全国省市县基本完成。
二、通过查询到结果获得天气数据:
0. 由于传回的JSON数据较为复杂,故使用Gson来解析传回的数据。
1. 定义Gson实体类:
由于返回的数据格式大致为:
{"HeWeather": [
{
"now":{}
"aqi":{},
"basic":{},
"daily_forecast":[],
"hourly_forecast":[],
"status":"ok",
"suggestion":{}
]}
故定义Weather实体类为(无视小时预报):
public class Weather {
public String status;
public Basic basic;
public AQI aqi;
public Now now;
public Suggestion suggestion;
@SerializedName("daily_forecast")
public List<Forcast> forcastList;
}注意使用@SerializedName来对java字段和Json字段建立映射。
2. 显示查询到的天气
在Utility中新建一个用于解析传回天气数据的方法:
/**
* 传入json数据,返回实例化后的Weather对象
*
* @param responseData 传入的json数据
* @return 实例化后的Weather对象
*/
public static Weather handleWeatherResponse(String responseData) {
try {
// 将整个json实例化保存在jsonObject中
JSONObject jsonObject = new JSONObject(responseData);
// 从jsonObject中取出键为"HeWeather"的数据,并保存在数组中
JSONArray jsonArray = jsonObject.getJSONArray("HeWeather");
// 取出数组中的第一项,并以字符串形式保存
String weatherContent = jsonArray.getJSONObject(0).toString();
// 返回通过Gson解析后的Weather对象
return new Gson().fromJson(weatherContent, Weather.class);
} catch (JSONException e) {
e.printStackTrace();
}
return null;
}该方法传入需要解析的天气数据,返回一个Weather对象。通过weather对象即可得到具体的天气情况,然后再将其显示到界面上,天气查询的功能就基本完成了。
界面部分
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="@+id/choose_area_fragment"
android:name="com.lfc.coolweather2.ChooseAreaFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>只有一个Fragment,用于第一次启动时选择地区。
weather_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_weather"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorPrimary"
android:padding="10dp"
tools:context="com.lfc.coolweather2.WeatherActivity">
<ImageView
android:id="@+id/img_bing"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
<android.support.v4.widget.DrawerLayout
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:id="@+id/weather_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_corner"
android:orientation="vertical"
android:padding="15dp">
<include layout="@layout/title" />
<include layout="@layout/now" />
</LinearLayout>
<include layout="@layout/aqi" />
</LinearLayout>
</ScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
<fragment
android:id="@+id/frag_choose_area"
android:name="com.lfc.coolweather2.ChooseAreaFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
tools:layout="@layout/choose_area" />
</android.support.v4.widget.DrawerLayout>
</FrameLayout>主要显示的布局,最外层使用一个FramLayout,便于背景图片的显示。
天气显示部分使用一个DrawerLayout,其中drawer中放了一个选择地区的Fragment,主要部分则是各种显示天气信息的TextView嵌套在一个SwipeRefreshLayout中,用于下拉刷新的实现。
反思部分
原程序暂时遇到几个地方是有缺陷的:
1. 在获取省市区数据的时候,如果第一次从服务器没有获得正确、完整的数据,那么之后程序在查询的时候,虽然数据不完整,但是数据库并不为空,依然会通过本地查询,这样就会因为得不到需要的数据造成空指针异常。可捕获此异常,并删除数据库中数据,重新从服务器查询。
2. 如果服务器返回的天气数据不是正确、完整的,在通过weather取天气数据的时候则会得到一个null对象而不是字符串。这里不能用于显示,可加一个判断,不为null再赋值。
3. 在第一次选择城市之后,之后不管选择哪个城市,刷新之后都会显示第一次选择城市的天气。可通过改变传入参数来调整。
更新部分
1. 优化部分逻辑,使运行更加稳定可靠,减少了出错崩溃的可能性:
增加了数据完整性判断
private void queryCities() {
txtTitle.setText(selectedProvince.getProvinceName());
btnBack.setVisibility(View.VISIBLE);
cityList = DataSupport.where("provinceid = ?", String.valueOf(selectedProvince.getId()))
.find(City.class);
if (cityList.size() > 0) {
try {
dataList.clear();
for (City c : cityList) {
dataList.add(c.getCityName());
}
adapter.notifyDataSetChanged();
listView.setSelection(0);
currentLevel = LEVEL_CITY;
} catch (NullPointerException e) {
String url = getResources().getString(R.string.url_query_province);
queryFromServer(url, "province");
int provinceCode = selectedProvince.getProvinceCode();
url = getResources().getString(R.string.url_query_province) + provinceCode;
queryFromServer(url, "city");
}
} else {
int provinceCode = selectedProvince.getProvinceCode();
String url = getResources().getString(R.string.url_query_province) + provinceCode;
queryFromServer(url, "city");
}
}
及时更新weatherId,使刷新后显示的是新地点而不是老地点:
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
...
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
switch (currentLevel) {
...
case LEVEL_COUNTY:
String weatherId = countyList.get(position).getWeatherId();
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("weather_id", weatherId);
editor.apply();
if (getActivity() instanceof MainActivity) {
Intent intent = new Intent(getActivity(), WeatherActivity.class);
startActivity(intent);
getActivity().finish();
} else if (getActivity() instanceof WeatherActivity) {
WeatherActivity activity = (WeatherActivity) getActivity();
activity.refresh(weatherId);
}
break;
default:
}
}
});
...
}
2. 增加了空气质量指数的分级,并用不同的颜色划分;
void setAqiAndPm25(Weather weather) {
if (weather.aqi != null) {
int aqi = 0, pm25 = 0;
try {
aqi = Integer.parseInt(weather.aqi.city.aqi);
pm25 = Integer.parseInt(weather.aqi.city.pm25);
} catch (Exception e) {
e.printStackTrace();
}
txtAqi.setText(weather.aqi.city.aqi);
txtPm25.setText(weather.aqi.city.pm25);
txtAqi.setTextSize(40);
txtPm25.setTextSize(40);
if (aqi == 0) txtAqi.setTextColor(Color.WHITE);
else if (aqi < 50) txtAqi.setTextColor(getResources().getColor(R.color.a50));
else if (aqi < 100) txtAqi.setTextColor(getResources().getColor(R.color.a100));
else if (aqi < 150) txtAqi.setTextColor(getResources().getColor(R.color.a150));
else if (aqi < 200) txtAqi.setTextColor(getResources().getColor(R.color.a200));
else if (aqi < 300) txtAqi.setTextColor(getResources().getColor(R.color.a300));
else if (aqi > 300) txtAqi.setTextColor(getResources().getColor(R.color.a300up));
if (pm25 == 0) txtPm25.setTextColor(Color.WHITE);
else if (pm25 < 35) txtPm25.setTextColor(getResources().getColor(R.color.a50));
else if (pm25 < 75) txtPm25.setTextColor(getResources().getColor(R.color.a100));
else if (pm25 < 115) txtPm25.setTextColor(getResources().getColor(R.color.a150));
else if (pm25 < 150) txtPm25.setTextColor(getResources().getColor(R.color.a200));
else if (pm25 < 250) txtPm25.setTextColor(getResources().getColor(R.color.a300));
else if (pm25 > 250) txtPm25.setTextColor(getResources().getColor(R.color.a300up));
} else {
txtAqi.setTextColor(Color.WHITE);
txtPm25.setTextColor(Color.WHITE);
txtAqi.setText("暂无数据");
txtPm25.setText("暂无数据");
txtAqi.setTextSize(25);
txtPm25.setTextSize(25);
txtAqi.setSingleLine();
txtPm25.setSingleLine();
}
}
3. 改变了自动更新的方式,减少电量和流量消耗;
/**
* 启动时首先判断缓存是否有天气数据:
* 如果没有,则请求服务器数据;
* 如果有的话,则判断数据距离现在时间:
* 若超过8小时,则请求服务器数据;
* 若不超过8小时,则取出缓存数据。
*/
if (weatherData != null) {
Weather weather = Utility.handleWeatherResponse(weatherData);
long currentMillis = System.currentTimeMillis();
if (currentMillis - sharedPreferences.getLong("last_request", 0) > 28800000) {
requestWeather(weatherId);
} else {
showWeatherInfo(weather);
}
} else {
weatherLayout.setVisibility(View.INVISIBLE);
requestWeather(weatherId);
}
4. 部分界面效果调优。
效果图: