最近看了郭霖大神的《第一行代码(第二版)》(第二行代码?),决定照着书中的样例做了一个Cool Weather的客户端,并进行了优化。
整理一下完成的思路,并附上部分代码和注释以及自己的理解。
(看到有同学问,附上项目地址: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类,分别用于保存省市县的数据。
3. 配置litepalpublic 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类。
配置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
△在dataList数组中保存当前显示在屏幕上的内容;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
adapter; private List dataList = new ArrayList<>(); private List provinceList; private List cityList; private List countyList; private Province selectedProvince; private City selectedCity; private int currentLevel; // methods } △通过三个常量LEVEL_PROVINCE, LEVEL_CITY, LEVEL_COUTY来判断当前显示是省市还是县。
methods:
△在onCreateView中实例化Fragment的布局并传回。
△在onActivityCreated中设置列表和返回键的点击事件。
△新建queryProvinces()、queryCities()、queryCounties()方法查询省市县的数据。
/** * 标题改为"China",隐去back键 *
* 从数据库中查找Province数据,如果存在则: * 1. 赋值到provinceList中; * 2. 将provinceList的成员name添加到dataList中 * 3. 使用adapter.notifyDataSetChanged()方法更新列表,并使用adapter.setSelection(0)将选中行设为第一行 *
* 如果不存在则调用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实体类为(无视小时预报):
注意使用@SerializedName来对java字段和Json字段建立映射。public class Weather { public String status; public Basic basic; public AQI aqi; public Now now; public Suggestion suggestion; @SerializedName("daily_forecast") public List
forcastList; }
2. 显示查询到的天气
界面部分在Utility中新建一个用于解析传回天气数据的方法:
该方法传入需要解析的天气数据,返回一个Weather对象。通过weather对象即可得到具体的天气情况,然后再将其显示到界面上,天气查询的功能就基本完成了。/** * 传入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; }
反思部分activity_main.xml:
只有一个Fragment,用于第一次启动时选择地区。
weather_layout.xml:
主要显示的布局,最外层使用一个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. 部分界面效果调优。
效果图:
酷欧天气github源码 项目github地址 仅供学习交流。