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

最近看了郭霖大神的《第一行代码(第二版)》(第二行代码?),决定照着书中的样例做了一个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类,分别用于保存省市县的数据。

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地址 仅供学习交流。


  • 9
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 14
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值