首页 > 系统 > Android > 正文

Android项目:Cool Weather酷欧天气(附源码)

2019-11-07 23:46:39
字体:
来源:转载
供稿:网友

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

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

逻辑部分

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

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. 部分界面效果调优。

效果图:

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


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表