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

来源:互联网 发布:淘宝海外代购是真的吗 编辑:程序博客网 时间:2024/05/19 11:48

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

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

目录(上)

      • 目录上
    • 一功能需求及技术可行性分析
    • 二创建数据库和表
    • 三遍历全国省市县数据

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

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

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

2、考虑数据接口问题

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

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

  • 首推郭霖大神架设的供学习使用的服务器(返回JSON数据格式):http://guolin.tech/api/china
  • 和风天气国内城市ID:https://cdn.heweather.com/china-city-list.txt
  • 中国天气城市ID列表:http://cj.weather.com.cn/support/Detail.aspx?id=51837fba1b35fe0f8411b6df
    注:除第一个外,其余都是可获取的原始数据,需要进行数据预处理。(数据预处理部分后期文章补上)

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

  • 和风天气API:普通用户每天1000次访问量 https://www.heweather.com/documents
  • 彩云天气API:个人开发者免费10000次访问量 http://wiki.swarma.net/index.php/彩云天气API/v2
  • 聚合数据天气API:2元/1000次访问量 https://www.juhe.cn/docs/api/id/39
  • YY天气API:免费会员30次/小时 http://www.yytianqi.com/api.html
  • 心知天气API:个人免费版400次/小时 https://www.seniverse.com/doc
  • 中国气象数据网API:气象大数据交易平台 http://data.cma.cn/market/marketList.html
  • 气象大数据应用产创平台:http://www.weatherdt.com/api.html

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


阅读全文
0 0
原创粉丝点击