Android实战:CoolWeather酷欧天气(加强版数据接口)代码详解(上)

拜读了郭霖大神的《第一行代码(第二版)》后,决定对其文末的酷欧天气实战项目进行数据扩充以及代码详解,完整文件请从我的GitHub中下载,想学习更多Android知识在看完本篇文章后请出门右转:京东当当亚马逊天猫PDFKindle豆瓣多看

具体步骤还是按照郭霖大神的分析思路来,外加一点点个人的认知。

目录(上)

一、功能需求及技术可行性分析

1、确定APP应该具有的功能

  • 可以查询全国所有省、市、县的数据(列表)
  • 可以查询全国任意城市的天气
  • 可以切换城市查询天气
  • 可以手动更新以及后台自动更新天气

2、考虑数据接口问题

  • 如何得到全国省市县的数据信息
  • 如何获取每个城市的天气信息

3、获取全国省市县数据信息

4、获取每个城市的天气信息

5、解析数据

以和风天气为例(其他API接口的使用后期文章更新),获取和风天气返回的JSON格式的城市详细天气数据。取苏州的详细天气信息,如下图:

这里写图片描述

并对其进行分析:(选择你所需要的数据)

这里写图片描述

其中,aqi包含当前空气质量的情况。basic中包含城市的一些具体信息。daily_forecast中包含未来3天的天气信息。now表示当前的天气信息。status表示接口状态,“ok”表示数据正常,具体含义请参考接口状态码及错误码。suggestion中包含一些天气相关的生活建议。


二、创建数据库和表

1、建立新的项目结构

在Android Studio中新建一个Android项目,项目名叫CoolWeather,包名叫做com.coolweather.android,之后一路Next,所有选项都使用默认就可以完成项目的创建。

这里写图片描述

为了让项目能有更好的结构,在com.coolweather.android包下再新建四个包。其中,db包用于存放数据库模型相关代码。gson包用于存放GSON模型相关的代码,service包用于存放服务相关代码,util包用于存放工具相关的代码。

2、将项目中所需的各种依赖库进行声明,编辑app/build.gradle文件,在dependencies闭包中添加如下内容:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.0-alpha7'
    testCompile 'junit:junit:4.12'
    compile 'org.litepal.android:core:1.6.0'
    compile 'com.squareup.okhttp3:okhttp:3.9.0'
    compile 'com.google.code.gson:gson:2.8.2'
    compile 'com.github.bumptech.glide:glide:4.3.1'
}

为了简化数据库的操作,我们使用LitePal来管理数据库。在dependencies闭包中,最后四行为新添加的声明,都更新为最新的版本号。其中,LitePal用于对数据库进行操作,OkHttp用于进行网络请求,GSON用于解析JSON数据,Glide用于加载和展示图片,以上四种声明都附有GitHub超链,可以点击进行深入了解。

3、设计数据库表结构

准备建立3张表:province、city、county,分别用于存放省、市、县的数据信息。对应到实体类中就是建立Province、City、County这三个类。由于LitePal要求所有的实体类都要继承自DataSupport这个类,所以三个类都要继承DataSupport类。

在db包下新建一个Province类,代码如下:

public class Province extends DataSupport{

    private int id;//实体类的id

    private String provinceName;//省的名字

    private int provinceCode;//省的代号

    //getter和setter方法
    public int getId(){
        return id;
    }

    public void setId(int id){
        this.id = id;
    }

    public String getProvinceName() {
        return provinceName;
    }

    public void setProvinceName(String provinceName) {
        this.provinceName = provinceName;
    }

    public int getProvinceCode() {
        return provinceCode;
    }

    public void setProvinceCode(int provinceCode) {
        this.provinceCode = provinceCode;
    }
}

接着在db包下新建一个City类,代码如下:

public class City extends DataSupport{

    private int id;//实体类的id

    private String cityName;//城市名

    private int cityCode;//城市的代号

    private int provinceId;//当前市所属省的id值

    //getter和setter方法
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCityName() {
        return cityName;
    }

    public void setCityName(String cityName) {
        this.cityName = cityName;
    }

    public int getCityCode() {
        return cityCode;
    }

    public void setCityCode(int cityCode) {
        this.cityCode = cityCode;
    }

    public int getProvinceId() {
        return provinceId;
    }

    public void setProvinceId(int provinceId) {
        this.provinceId = provinceId;
    }
}

然后在db包下新建一个County类,代码如下:

public class County extends DataSupport{

    private int id;//实体类的id

    private String countyName;//县的名字

    private String weatherId;//县所对应天气的id值

    private int cityId;//当前县所属市的id值

    //getter和setter方法
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getCountyName() {
        return countyName;
    }

    public void setCountyName(String countyName) {
        this.countyName = countyName;
    }

    public String getWeatherId() {
        return weatherId;
    }

    public void setWeatherId(String weatherId) {
        this.weatherId = weatherId;
    }

    public int getCityId() {
        return cityId;
    }

    public void setCityId(int cityId) {
        this.cityId = cityId;
    }
}

实体类内容很简单,就是声明一些用到的字段,并生成相应的getter和setter方法。接下来需要配置litepal.xml文件,切换左上角下拉菜单到project模式,右击app/src/main目录->New->Directory,创建一个assets目录,然后在assets目录下再创建一个litepal.xml文件(新建.xml时文件可能会跑到/app目录下,用鼠标托回到assets目录下即可),编辑litepal.xml文件中的内容,如下所示:

<litepal>

    <dbname value="cool_weather"/>

    <version value="1"/>

    <list>
        <mapping class="com.coolweather.android.db.Province"/>
        <mapping class="com.coolweather.android.db.City"/>
        <mapping class="com.coolweather.android.db.County"/>
    </list>

</litepal>

我们将数据库名指定为cool_weather,数据库版本指定为1(注:使用LitePal来升级数据库非常简单,只需要修改你想改的内容,然后将版本号加1即可),并将Province、City和County这3个实体类添加到映射列表当中。最后还需要配置一下LitePalApplication,修改AndroidManifest.xml中的代码,如下所示:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.coolweather.android" >

    <application
        android:name="org.litepal.LitePalApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme" >
        <activity android:name=".MainActivity" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

这样我们就将所有的配置写完了,数据库和表会在首次执行任意数据库操作的时候自动创建。


三、遍历全国省市县数据


1、与服务器进行数据交互

全国省市县的数据都是从服务器端获取的,因此需要与服务器端进行数据的交互。我们在util包下增加一个HttpUtil类,代码如下:

public class HttpUtil {
    /**
     * 和服务器进行交互,获取从服务器返回的数据
     */
    public static void sendOkHttpRequest(String address, okhttp3.Callback callback){
        //创建一个OkHttpClient的实例
        OkHttpClient client = new OkHttpClient();
        //创建一个Request对象,发起一条HTTP请求,通过url()方法来设置目标的网络地址
        Request request = new Request.Builder().url(address).build();
        //调用OkHttpClient的newCall()方法来创建一个Call对象,
        // 并调用它的enqueue()方法将call加入调度队列,然后等待任务执行完成
        client.newCall(request).enqueue(callback);
    }
}

由于OkHttp的出色封装,仅用3行代码即完成与服务器进行交互功能,有了该功能后我们发起一条HTTP请求只需要调用sendOkHttpRequest()方法,传入请求地址,并注册一个回调来处理服务器响应就可以了。

2、解析和处理JSON格式数据

由于服务器返回的省市县的数据都是JSON格式,所以我们再构建一个工具用于解析和处理JSON数据。在util包下新建一个Utility类,代码如下所示:

public class Utility {
    /**
     * 解析和处理服务器返回的省级数据
     */
    public static boolean handleProvinceResponse(String response){
        if(!TextUtils.isEmpty(response)){
            try{
                //将服务器返回的数据传入到JSONArray对象allProvinces中
                JSONArray allProvinces = new JSONArray(response);
                //循环遍历JSONAray
                for(int i=0;i<allProvinces.length();i++){
                    //从中取出的每一个元素都是一个JSONObject对象
                    JSONObject provinceObject = allProvinces.getJSONObject(i);
                    //每个JSONObject对象包含name、code等信息,调用getString()方法将数据取出
                    // 将数据组装成实体类对象
                    Province province = new Province();
                    province.setProvinceName(provinceObject.getString("name"));
                    province.setProvinceCode(provinceObject.getInt("id"));
                    //调用save()方法将数据存储到数据库当中
                    province.save();
                }
                return true;
            }catch(JSONException e){
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 解析和处理服务器返回的市级数据
     */
    public static boolean handleCityResponse(String response,int provinceId){
        if(!TextUtils.isEmpty(response)){
            try{
                JSONArray allCities = new JSONArray(response);
                for(int i=0;i<allCities.length();i++){
                    JSONObject cityObject = allCities.getJSONObject(i);
                    City city = new City();
                    city.setCityName(cityObject.getString("name"));
                    city.setCityCode(cityObject.getInt("id"));
                    city.setProvinceId(provinceId);
                    city.save();
                }
                return true;
            }catch (JSONException e){
                e.printStackTrace();
            }
        }
        return false;
    }

    /**
     * 解析和处理服务器返回的县级数据
     */
    public static boolean handleCountyResponse(String response,int cityId){
        if(!TextUtils.isEmpty(response)){
            try{
                JSONArray allCounties = new JSONArray(response);
                for(int i=0;i<allCounties.length();i++){
                    JSONObject countyObject = allCounties.getJSONObject(i);
                    County county = new County();
                    county.setCountyName(countyObject.getString("name"));
                    county.setWeatherId(countyObject.getString("weather_id"));
                    county.setCityId(cityId);
                    county.save();
                }
                return true;
            }catch (JSONException e){
                e.printStackTrace();
            }
        }
        return false;
    }

}

在Utility类中,分别提供了handleProvinceResponse()、handleCityResponse()、handleCountyResponse()这三个方法,分别用于解析和处理从服务器返回的各级数据。

3、左边栏碎片布局

将左边栏的内容写在碎片里,使用的时候直接在布局里面引用碎片即可。在res/layout目录中新建choose_area.xml布局,代码如下所示:

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#fff">

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary">

        <TextView
            android:id="@+id/title_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:textColor="#fff"
            android:textSize="20sp"/>

        <Button
            android:id="@+id/back_button"
            android:layout_width="25dp"
            android:layout_height="25dp"
            android:layout_marginLeft="10dp"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:background="@drawable/ic_back"/>

    </RelativeLayout>

    <ListView
        android:id="@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>

以线性布局做为主体,里面嵌套了一个相对布局和ListView,其中相对布局作为头布局,其中的TextView用于显示标题内容,Button用于执行返回操作(注:需要提前准备好一张ic_back.png图片作为返回按钮的图片)。省市县的数据信息则会显示在ListView中,其中每个子项之间会有一条分割线。

4、遍历省市县数据的碎片

在com.coolweather.android包下新建ChooseAreaFragment类继承自Fragment(注:在引入Fragment包的时候,建议使用support-v4库中的Fragment,因为它可以让碎片在所有的Android版本中保持功能一致性),代码如下:

public class ChooseAreaFragment extends Fragment {

    public static final int LEVEL_PROVINCE = 0;

    public static final int LEVEL_CITY = 1;

    public static final int LEVEL_COUNTY = 2;

    private ProgressDialog progressDialog;//进度条(加载省市县信息时会出现)

    private TextView titleText;//标题

    private Button backButton;//返回键

    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;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        //获取控件实例
        View view = inflater.inflate(R.layout.choose_area, container, false);
        titleText = (TextView) view.findViewById(R.id.title_text);
        backButton = (Button) view.findViewById(R.id.back_button);
        listView = (ListView) view.findViewById(R.id.list_view);
        //初始化ArrayAdapter
        adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, dataList);
        //将adapter设置为ListView的适配器
        listView.setAdapter(adapter);
        return view;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //ListView的点击事件
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener(){
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if(currentLevel == LEVEL_PROVINCE){//在省级列表
                    selectedProvince = provinceList.get(position);//选择省
                    queryCities();//查找城市
                }else if(currentLevel == LEVEL_CITY){
                    selectedCity = cityList.get(position);
                    queryCounties();
                }
            }
        });
        //Button的点击事件
        backButton.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View v) {
                if(currentLevel == LEVEL_COUNTY){
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    queryProvinces();
                }
            }
        });
        queryProvinces();//加载省级数据
    }

    /**
     * 查询全国所有的省,优先从数据库查,如果没有查询到再去服务器上查询
     */
    private void queryProvinces(){
        titleText.setText("中国");//头标题
        backButton.setVisibility(View.GONE);//当处于省级列表时,返回按键隐藏
        //从数据库中读取省级数据
        provinceList = DataSupport.findAll(Province.class);
        //如果读到数据,则直接显示到界面上
        if(provinceList.size() > 0){
            dataList.clear();
            for(Province province : provinceList){
                dataList.add(province.getProvinceName());
            }
            adapter.notifyDataSetChanged();
            listView.setSelection(0);
            currentLevel = LEVEL_PROVINCE;
        }else{
            //如果没有读到数据,则组装出一个请求地址,调用queryFromServer()方法从服务器上查询数据
            String address = "http://guolin.tech/api/china";//郭霖地址服务器
            queryFromServer(address, "province");
        }
    }

    /**
     * 查询选中省内所有的市,优先从数据库查询,如果没有查到再去服务器上查询
     */
    private void queryCities(){
        titleText.setText(selectedProvince.getProvinceName());
        backButton.setVisibility(View.VISIBLE);//当处于市级列表时,返回按键显示
        cityList = DataSupport.where("provinceid = ?",String.valueOf(selectedProvince.getId())).find(City.class);
        if(cityList.size() > 0){
            dataList.clear();
            for(City city : cityList){
                dataList.add(city.getCityName());
            }
            adapter.notifyDataSetChanged();
            listView.setSelection(0);
            currentLevel = LEVEL_CITY;
        }else{
            int provinceCode = selectedProvince.getProvinceCode();
            String address = "http://guolin.tech/api/china/"+provinceCode;
            queryFromServer(address, "city");
        }
    }

    /**
     * 查询选中市内所有的县,优先从数据库查询,如果没有查询到再去服务器上查询
     */
    private void queryCounties(){
        titleText.setText(selectedCity.getCityName());
        backButton.setVisibility(View.VISIBLE);//当处于县级列表时,返回按键显示
        countyList = DataSupport.where("cityid = ?",String.valueOf(selectedCity.getId())).find(County.class);
        if(countyList.size() > 0){
            dataList.clear();
            for(County county : countyList){
                dataList.add(county.getCountyName());
            }
            adapter.notifyDataSetChanged();
            listView.setSelection(0);
            currentLevel = LEVEL_COUNTY;
        }else{
            int provinceCode = selectedProvince.getProvinceCode();
            int cityCode = selectedCity.getCityCode();
            String address = "http://guolin.tech/api/china/"+provinceCode+"/"+cityCode;
            queryFromServer(address, "county");
        }
    }

    /**
     * 根据传入的地址和类型从服务器上查询省市县数据
     */
    private void queryFromServer(String address, final String type){
        showProgressDialog();
        //向服务器发生请求,响应的数据会回调到onResponse()方法中
        HttpUtil.sendOkHttpRequest(address, new Callback() {
            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String responseText = response.body().string();
                boolean result = false;
                if("province".equals(type)){
                    //解析和处理从服务器返回的数据,并存储到数据库中
                    result = Utility.handleProvinceResponse(responseText);
                }else if("city".equals(type)){
                    result = Utility.handleCityResponse(responseText,selectedProvince.getId());
                }else if("county".equals(type)){
                    result = Utility.handleCountyResponse(responseText,selectedCity.getId());
                }
                if(result){
                    //由于query方法用到UI操作,必须要在主线程中调用。
                    // 借助runOnUiThread()方法实现从子线程切换到主线程
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            closeProgressDialog();
                            if("province".equals(type)){
                                //数据库已经存在数据,调用queryProvinces直接将数据显示到界面上
                                queryProvinces();
                            }else if("city".equals(type)){
                                queryCities();
                            }else if("county".equals(type)){
                                queryCounties();
                            }
                        }
                    });
                }
            }

            @Override
            public void onFailure(Call call, IOException e) {
                //通过runOnUiThread()方法回到主线程处理逻辑
                getActivity().runOnUiThread( new Runnable() {
                    @Override
                    public void run() {
                        closeProgressDialog();
                        Toast.makeText(getContext(),"加载失败",Toast.LENGTH_SHORT).show();
                    }
                });
            }
        });
    }

    /**
     * 显示进度对话框
     */
    private void showProgressDialog(){
        if(progressDialog == null){
            progressDialog = new ProgressDialog(getActivity());
            progressDialog.setMessage("正在加载...");
            progressDialog.setCanceledOnTouchOutside(false);
        }
        progressDialog.show();
    }

    /**
     * 关闭进度对话框
     */
    private void closeProgressDialog(){
        if(progressDialog != null){
            progressDialog.dismiss();
        }
    }
}

在这个类中,具体代码的功能在代码里注释的很详细。其中,onCreateView()方法和onActivityCreated()方法进行初始化操作,queryProvinces()方法、queryCities()方法和queryCounties()方法分别提供省、市、县数据的查询功能。queryFromServer()方法根据传入的参数从服务器上读取省市县的数据。

5、将碎片添加在活动里

由于碎片不能直接显示,需要将其添加到活动里才能将其正常显示在界面上。

<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.coolweather.android.ChooseAreaFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</FrameLayout>

6、移除原生ActionBar

由于在碎片的布局里面已经自定义了一个RelativeLayout标题栏,因此就不需要原生的ActionBar了,修改res/values/styles.xml中的代码如下:

这里写图片描述

7、声明权限

因为需要从服务器中调用数据,则需要声明网络权限。

这里写图片描述

运行程序,就可以看到全国所有的省市县数据啦。如下图所示(右上角小人为截屏软件,请忽略):

这里写图片描述

上篇到此结束,剩余项目请关注下篇。完整代码文件:https://github.com/ambition-hb/CoolWeather


猜你喜欢

转载自blog.csdn.net/maybeforever/article/details/78497806
今日推荐