当前位置:   article > 正文

Android项目:Cool Weather酷欧天气(附源码)_android第一行代码第二版天气

android第一行代码第二版天气

最近看了郭霖大神的《第一行代码(第二版)》(第二行代码?),决定照着书中的样例做了一个Cool Weather的客户端,并进行了优化。


整理一下完成的思路,并附上部分代码和注释以及自己的理解。

(看到有同学问,附上项目地址:https://github.com/LittleFogCat/coolweather

逻辑部分

一、首先通过网络接口获得全国省市县的列表。

1. 新建一个HttpUtil类,在其中创建一个sendOkHttpRequest()方法:

  1. public static void sendOkHttpRequest(String url, Callback callback) {
  2. OkHttpClient client = new OkHttpClient();
  3. Request request = new Request.Builder().url(url).build();
  4. client.newCall(request).enqueue(callback);
  5. }

传入一个一个url字符串,以及一个回调接口。


2. 新建Province, City, County类,分别用于保存省市县的数据。

  1. public class Province extends DataSupport {
  2. private int id;
  3. private String provinceName;
  4. private int provinceCode;
  5. getter & setter
  6. }
  1. public class City extends DataSupport {
  2. private int id;
  3. private String cityName;
  4. private int cityCode;
  5. private int provinceId;
  6. getter & setter
  7. }

  1. public class County extends DataSupport {
  2. private int id;
  3. private String countyName;
  4. private String weatherId;
  5. private int cityId;
  6. getter & setter
  7. }

其中provinceCode用于请求天气数据。使用LitePal库进行数据库操作,所以三个类都要继承DataSupport类。

3. 配置litepal

配置assets/litepal.xml

配置Manifest-Application

4. 新建Utility类,用于处理返回的json数据

  1. public class Utility { // 解析省市县的json数据,并保存在数据库中
  2. public static boolean handleProvinceResponse(String response) {
  3. if (!TextUtils.isEmpty(response)) {
  4. try {
  5. JSONArray allProvinces = new JSONArray(response);
  6. for (int i = 0; i < allProvinces.length(); i++) {
  7. JSONObject object = allProvinces.getJSONObject(i);
  8. Province province = new Province();
  9. province.setProvinceCode(object.getInt("id"));
  10. province.setProvinceName(object.getString("name"));
  11. province.save();
  12. }
  13. return true;
  14. } catch (JSONException e) {
  15. e.printStackTrace();
  16. return false;
  17. }
  18. } else return false;
  19. }
  20. public static boolean handleCityResponse(String response, int provinceId) {
  21. if (!TextUtils.isEmpty(response)) {
  22. try {
  23. JSONArray allCity = new JSONArray(response);
  24. for (int i = 0; i < allCity.length(); i++) {
  25. JSONObject object = allCity.getJSONObject(i);
  26. City city = new City();
  27. city.setCityName(object.getString("name"));
  28. city.setCityCode(object.getInt("id"));
  29. city.setProvinceId(provinceId);
  30. city.save();
  31. }
  32. return true;
  33. } catch (JSONException e) {
  34. e.printStackTrace();
  35. return false;
  36. }
  37. } else return false;
  38. }

很简单,利用JSONObject处理json数据,并调用save()方法保存入数据库中。

5. 新建遍历省市县的Fragment

  1. public class ChooseAreaFragment extends Fragment {
  2. // vars
  3. final int LEVEL_PROVINCE = 0;
  4. final int LEVEL_CITY = 1;
  5. final int LEVEL_COUNTY = 2;
  6. private ProgressDialog progressDialog;
  7. private TextView txtTitle;
  8. private Button btnBack;
  9. private ListView listView;
  10. private ArrayAdapter<String> adapter;
  11. private List<String> dataList = new ArrayList<>();
  12. private List<Province> provinceList;
  13. private List<City> cityList;
  14. private List<County> countyList;
  15. private Province selectedProvince;
  16. private City selectedCity;
  17. private int currentLevel;
  18. // methods
  19. }
△在dataList数组中保存当前显示在屏幕上的内容;

△通过三个常量LEVEL_PROVINCE, LEVEL_CITY, LEVEL_COUTY来判断当前显示是省市还是县。

methods:

△在onCreateView中实例化Fragment的布局并传回。

△在onActivityCreated中设置列表和返回键的点击事件。

△新建queryProvinces()、queryCities()、queryCounties()方法查询省市县的数据。

  1. /**
  2. * 标题改为"China",隐去back键
  3. * <p>
  4. * 从数据库中查找Province数据,如果存在则:
  5. * 1. 赋值到provinceList中;
  6. * 2. 将provinceList的成员name添加到dataList中
  7. * 3. 使用adapter.notifyDataSetChanged()方法更新列表,并使用adapter.setSelection(0)将选中行设为第一行
  8. * <p>
  9. * 如果不存在则调用queryFromServer从网络查找
  10. */
  11. private void queryProvinces() {
  12. txtTitle.setText("China");
  13. btnBack.setVisibility(View.GONE);
  14. provinceList = DataSupport.findAll(Province.class);
  15. if (provinceList.size() > 0) {
  16. dataList.clear();
  17. for (Province p : provinceList) {
  18. dataList.add(p.getProvinceName());
  19. }
  20. adapter.notifyDataSetChanged();
  21. listView.setSelection(0);
  22. currentLevel = LEVEL_PROVINCE;
  23. } else {
  24. String url = getResources().getString(R.string.url_query_province);
  25. queryFromServer(url, "province");
  26. }
  27. }

第一次运行时,由于本地没有数据,所以调用queryFromServer()方法在服务器查询:

  1. private void queryFromServer(String url, final String type) {
  2. showProgressDialog();
  3. HttpUtil.sendOkHttpRequest(url, new Callback() {
  4. ...
  5. } }); }

由于调用了sendOkHttpRequest,所以要实现它的回调接口中的onResponse和onFailure方法:

如果得到返回数据,则调用Utility.handleXresponse(response.body().string())处理传回的json数据,X即是传入的第二个变量type。

如果查询失败,则显示失败的Toast。

至此,遍历全国省市县基本完成。

二、通过查询到结果获得天气数据:

0. 由于传回的JSON数据较为复杂,故使用Gson来解析传回的数据。

1. 定义Gson实体类:

由于返回的数据格式大致为:

  1. {"HeWeather": [
  2. {
  3. "now":{}
  4. "aqi":{},
  5. "basic":{},
  6. "daily_forecast":[],
  7. "hourly_forecast":[],
  8. "status":"ok",
  9. "suggestion":{}
  10. ]}

故定义Weather实体类为(无视小时预报):

  1. public class Weather {
  2. public String status;
  3. public Basic basic;
  4. public AQI aqi;
  5. public Now now;
  6. public Suggestion suggestion;
  7. @SerializedName("daily_forecast")
  8. public List<Forcast> forcastList;
  9. }
注意使用@SerializedName来对java字段和Json字段建立映射。


2. 显示查询到的天气

在Utility中新建一个用于解析传回天气数据的方法:

  1. /**
  2. * 传入json数据,返回实例化后的Weather对象
  3. *
  4. * @param responseData 传入的json数据
  5. * @return 实例化后的Weather对象
  6. */
  7. public static Weather handleWeatherResponse(String responseData) {
  8. try {
  9. // 将整个json实例化保存在jsonObject中
  10. JSONObject jsonObject = new JSONObject(responseData);
  11. // 从jsonObject中取出键为"HeWeather"的数据,并保存在数组中
  12. JSONArray jsonArray = jsonObject.getJSONArray("HeWeather");
  13. // 取出数组中的第一项,并以字符串形式保存
  14. String weatherContent = jsonArray.getJSONObject(0).toString();
  15. // 返回通过Gson解析后的Weather对象
  16. return new Gson().fromJson(weatherContent, Weather.class);
  17. } catch (JSONException e) {
  18. e.printStackTrace();
  19. }
  20. return null;
  21. }
该方法传入需要解析的天气数据,返回一个Weather对象。通过weather对象即可得到具体的天气情况,然后再将其显示到界面上,天气查询的功能就基本完成了。


界面部分

activity_main.xml:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent">
  5. <fragment
  6. android:id="@+id/choose_area_fragment"
  7. android:name="com.lfc.coolweather2.ChooseAreaFragment"
  8. android:layout_width="match_parent"
  9. android:layout_height="match_parent" />
  10. </FrameLayout>
只有一个Fragment,用于第一次启动时选择地区。


weather_layout.xml:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3. xmlns:tools="http://schemas.android.com/tools"
  4. android:id="@+id/activity_weather"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. android:background="@color/colorPrimary"
  8. android:padding="10dp"
  9. tools:context="com.lfc.coolweather2.WeatherActivity">
  10. <ImageView
  11. android:id="@+id/img_bing"
  12. android:layout_width="match_parent"
  13. android:layout_height="match_parent"
  14. android:scaleType="centerCrop" />
  15. <android.support.v4.widget.DrawerLayout
  16. android:id="@+id/drawer_layout"
  17. android:layout_width="match_parent"
  18. android:layout_height="match_parent">
  19. <android.support.v4.widget.SwipeRefreshLayout
  20. android:id="@+id/swipe_refresh_layout"
  21. android:layout_width="match_parent"
  22. android:layout_height="match_parent">
  23. <ScrollView
  24. android:id="@+id/weather_layout"
  25. android:layout_width="match_parent"
  26. android:layout_height="match_parent"
  27. android:overScrollMode="never"
  28. android:scrollbars="none">
  29. <LinearLayout
  30. android:layout_width="match_parent"
  31. android:layout_height="wrap_content"
  32. android:orientation="vertical">
  33. <LinearLayout
  34. android:layout_width="match_parent"
  35. android:layout_height="wrap_content"
  36. android:background="@drawable/shape_corner"
  37. android:orientation="vertical"
  38. android:padding="15dp">
  39. <include layout="@layout/title" />
  40. <include layout="@layout/now" />
  41. </LinearLayout>
  42. <include layout="@layout/aqi" />
  43. </LinearLayout>
  44. </ScrollView>
  45. </android.support.v4.widget.SwipeRefreshLayout>
  46. <fragment
  47. android:id="@+id/frag_choose_area"
  48. android:name="com.lfc.coolweather2.ChooseAreaFragment"
  49. android:layout_width="match_parent"
  50. android:layout_height="match_parent"
  51. android:layout_gravity="start"
  52. tools:layout="@layout/choose_area" />
  53. </android.support.v4.widget.DrawerLayout>
  54. </FrameLayout>
主要显示的布局,最外层使用一个FramLayout,便于背景图片的显示。

天气显示部分使用一个DrawerLayout,其中drawer中放了一个选择地区的Fragment,主要部分则是各种显示天气信息的TextView嵌套在一个SwipeRefreshLayout中,用于下拉刷新的实现。

反思部分

原程序暂时遇到几个地方是有缺陷的:

1. 在获取省市区数据的时候,如果第一次从服务器没有获得正确、完整的数据,那么之后程序在查询的时候,虽然数据不完整,但是数据库并不为空,依然会通过本地查询,这样就会因为得不到需要的数据造成空指针异常。可捕获此异常,并删除数据库中数据,重新从服务器查询。

2. 如果服务器返回的天气数据不是正确、完整的,在通过weather取天气数据的时候则会得到一个null对象而不是字符串。这里不能用于显示,可加一个判断,不为null再赋值。

3. 在第一次选择城市之后,之后不管选择哪个城市,刷新之后都会显示第一次选择城市的天气。可通过改变传入参数来调整。


更新部分

1. 优化部分逻辑,使运行更加稳定可靠,减少了出错崩溃的可能性:

增加了数据完整性判断

  1. private void queryCities() {
  2. txtTitle.setText(selectedProvince.getProvinceName());
  3. btnBack.setVisibility(View.VISIBLE);
  4. cityList = DataSupport.where("provinceid = ?", String.valueOf(selectedProvince.getId()))
  5. .find(City.class);
  6. if (cityList.size() > 0) {
  7. try {
  8. dataList.clear();
  9. for (City c : cityList) {
  10. dataList.add(c.getCityName());
  11. }
  12. adapter.notifyDataSetChanged();
  13. listView.setSelection(0);
  14. currentLevel = LEVEL_CITY;
  15. } catch (NullPointerException e) {
  16. String url = getResources().getString(R.string.url_query_province);
  17. queryFromServer(url, "province");
  18. int provinceCode = selectedProvince.getProvinceCode();
  19. url = getResources().getString(R.string.url_query_province) + provinceCode;
  20. queryFromServer(url, "city");
  21. }
  22. } else {
  23. int provinceCode = selectedProvince.getProvinceCode();
  24. String url = getResources().getString(R.string.url_query_province) + provinceCode;
  25. queryFromServer(url, "city");
  26. }
  27. }


及时更新weatherId,使刷新后显示的是新地点而不是老地点:
  1. public void onActivityCreated(@Nullable Bundle savedInstanceState) {
  2. ...
  3. listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
  4. @Override
  5. public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
  6. switch (currentLevel) {
  7. ...
  8. case LEVEL_COUNTY:
  9. String weatherId = countyList.get(position).getWeatherId();
  10. SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
  11. SharedPreferences.Editor editor = sharedPreferences.edit();
  12. editor.putString("weather_id", weatherId);
  13. editor.apply();
  14. if (getActivity() instanceof MainActivity) {
  15. Intent intent = new Intent(getActivity(), WeatherActivity.class);
  16. startActivity(intent);
  17. getActivity().finish();
  18. } else if (getActivity() instanceof WeatherActivity) {
  19. WeatherActivity activity = (WeatherActivity) getActivity();
  20. activity.refresh(weatherId);
  21. }
  22. break;
  23. default:
  24. }
  25. }
  26. });
  27. ...
  28. }

2. 增加了空气质量指数的分级,并用不同的颜色划分;

  1. void setAqiAndPm25(Weather weather) {
  2. if (weather.aqi != null) {
  3. int aqi = 0, pm25 = 0;
  4. try {
  5. aqi = Integer.parseInt(weather.aqi.city.aqi);
  6. pm25 = Integer.parseInt(weather.aqi.city.pm25);
  7. } catch (Exception e) {
  8. e.printStackTrace();
  9. }
  10. txtAqi.setText(weather.aqi.city.aqi);
  11. txtPm25.setText(weather.aqi.city.pm25);
  12. txtAqi.setTextSize(40);
  13. txtPm25.setTextSize(40);
  14. if (aqi == 0) txtAqi.setTextColor(Color.WHITE);
  15. else if (aqi < 50) txtAqi.setTextColor(getResources().getColor(R.color.a50));
  16. else if (aqi < 100) txtAqi.setTextColor(getResources().getColor(R.color.a100));
  17. else if (aqi < 150) txtAqi.setTextColor(getResources().getColor(R.color.a150));
  18. else if (aqi < 200) txtAqi.setTextColor(getResources().getColor(R.color.a200));
  19. else if (aqi < 300) txtAqi.setTextColor(getResources().getColor(R.color.a300));
  20. else if (aqi > 300) txtAqi.setTextColor(getResources().getColor(R.color.a300up));
  21. if (pm25 == 0) txtPm25.setTextColor(Color.WHITE);
  22. else if (pm25 < 35) txtPm25.setTextColor(getResources().getColor(R.color.a50));
  23. else if (pm25 < 75) txtPm25.setTextColor(getResources().getColor(R.color.a100));
  24. else if (pm25 < 115) txtPm25.setTextColor(getResources().getColor(R.color.a150));
  25. else if (pm25 < 150) txtPm25.setTextColor(getResources().getColor(R.color.a200));
  26. else if (pm25 < 250) txtPm25.setTextColor(getResources().getColor(R.color.a300));
  27. else if (pm25 > 250) txtPm25.setTextColor(getResources().getColor(R.color.a300up));
  28. } else {
  29. txtAqi.setTextColor(Color.WHITE);
  30. txtPm25.setTextColor(Color.WHITE);
  31. txtAqi.setText("暂无数据");
  32. txtPm25.setText("暂无数据");
  33. txtAqi.setTextSize(25);
  34. txtPm25.setTextSize(25);
  35. txtAqi.setSingleLine();
  36. txtPm25.setSingleLine();
  37. }
  38. }

3. 改变了自动更新的方式,减少电量和流量消耗;

  1. /**
  2. * 启动时首先判断缓存是否有天气数据:
  3. * 如果没有,则请求服务器数据;
  4. * 如果有的话,则判断数据距离现在时间:
  5. * 若超过8小时,则请求服务器数据;
  6. * 若不超过8小时,则取出缓存数据。
  7. */
  8. if (weatherData != null) {
  9. Weather weather = Utility.handleWeatherResponse(weatherData);
  10. long currentMillis = System.currentTimeMillis();
  11. if (currentMillis - sharedPreferences.getLong("last_request", 0) > 28800000) {
  12. requestWeather(weatherId);
  13. } else {
  14. showWeatherInfo(weather);
  15. }
  16. } else {
  17. weatherLayout.setVisibility(View.INVISIBLE);
  18. requestWeather(weatherId);
  19. }

4. 部分界面效果调优。


效果图:



酷欧天气github源码 项目github地址 仅供学习交流。


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/210498
推荐阅读
相关标签
  

闽ICP备14008679号