1. 程式人生 > >Android實戰:CoolWeather酷歐天氣(加強版資料介面)程式碼詳解(上)

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、宣告許可權

因為需要從伺服器中呼叫資料,則需要宣告網路許可權。

這裡寫圖片描述

執行程式,就可以看到全國所有的省市縣資料啦。如下圖所示(右上角小人為截圖軟體,請忽略):

這裡寫圖片描述