酷欧天气的开发

简介

参考《第一行代码》,开发出一款全国省市县的天气预报app.

创建数据库和表

使用LitePal对数据库进行操作,创建三个实体类分别是Province、City和County。

1. 添加依赖项
compile 'org.litepal.android:core:1.3.2'
2. 创建实体类
package com.example.stardream.coolweather.db;

import org.litepal.crud.DataSupport;

/**
 * Created by StarDream on 2018/8/22.
 */
//LitePal中的每一个实体类都应该继承DataSupport
public class Province extends DataSupport {
    private int id;  //实体类具有的id
    private String provinceName;  //省份的名字
    private int provinceCode;  //省的代号

    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;
    }
}

City实体类和County实体类同理。每个实体类代表一张表,实体类中的属性代表表中的每一列。

3. 配置litepal.xml文件

    
    
    
    
    
    
        
        
        
    

4. 修改AndroidManifest.xml文件

将项目的application配置为org.litepal.LitePalApplication

android:name="org.litepal.LitePalApplication"

关于LitePal的具体内容详见LitePal详解

遍历全国省市县数据

1. 客户端与服务器的交互

package com.example.stardream.coolweather.util;

import okhttp3.OkHttpClient;
import okhttp3.Request;

/**
 * Created by StarDream on 2018/8/22.
 */
//采用OkHttp与服务器进行通信
public class HttpUtil {
    //与服务器进行交互发起一条http请求只需要调用sendOkHttpRequest()即可
    //传入要请求的地址,注册一个回调来处理服务器的响应
    public static void sendOkHttpRequest(String address,okhttp3.Callback callback){
        OkHttpClient client = new OkHttpClient();
        Request request =  new Request.Builder().url(address).build();
        client.newCall(request).enqueue(callback);
    }
}

发起http请求只需调用sendOkHttprequest()这个方法,传入一个地址,并且注册一个回调来处理服务器的响应。

2. 处理服务器返回的Json格式的数据

新建一个Utility类处理和解析Json数据。

package com.example.stardream.coolweather.util;

import android.text.TextUtils;

import com.example.stardream.coolweather.db.City;
import com.example.stardream.coolweather.db.County;
import com.example.stardream.coolweather.db.Province;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * Created by StarDream on 2018/8/22.
 */

public class Utility {
    //处理和解析省份的数据
    public static boolean hanldeProvinceResponse(String response){
        if(!TextUtils.isEmpty(response)){
            try{
                //json的对象的数组,用来接收传回的多个省份的数据
                JSONArray allProvinces = new JSONArray(response);
                for(int i=0;i

省级、市级和县级对服务器发来的数据的处理解析方式都是类似的,使用JSONArrayJSONObject进行解析,然后组装成实体类对象,再调用save()方法存储到数据库中。

3. 遍历省市县

遍历省市县功能.xml

因为遍历省市县的功能会用到多次,因此将其写为碎片的形式而不是写在活动里面,这样复用的时候可以直接在布局文件中调用碎片。




    
    
   
        
        
        

一般情况下标题栏可以采用ActionBar,但是碎片中最好不用ActionBar或Toolbar,否则会有问题。

遍历省市县碎片

1. 新建一个ChooseAreaFragment用于展示查询界面和实现基本查询功能,定义一些量值。

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 adapter;//适配器,与ListView配合使用
    private List dataList = new ArrayList<>();//动态数组
    //省列表
    private List provinceList;
    //市列表
    private List cityList;
    //县列表
    private List countyList;
    //选中的省份
    private Province selectedProvince;
    //选中的城市
    private City selectedCity;
    //当前选中的级别
    private int currentLevel;
}

2. onCreateView()方法中获取到一些控件的实例,初始化了ArrayAdapter,将其设置为ListView的适配器。

   @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        /*
        * 【LayoutInflater】其实是在res/layout/下找到xml布局文件,并且将其实例化,
        * 对于一个没有被载入或者想要动态载入的界面,都需要使用LayoutInflater.inflate()来载入;
        * */
        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);
        adapter = new ArrayAdapter<>(getContext(),android.R.layout.simple_list_item_1,dataList);
        //载入listView
        listView.setAdapter(adapter);
        return view;
    }

3. 在onActivityCreated()方法中设置ListView和Button的点击事件,在这里完成了基本的初始化操作,调用queryProvinces()方法,加载省级数据。

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //对列表设置监听事件
        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);
                    //显示出省份对应下city的界面
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //记住选中的City
                    selectedCity = cityList.get(position);
                    //切换到相应的county界面
                    queryCounties();
                }
            }
        });
        //为返回按钮注册监听事件
        backButton.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                //若在county切换到City
                if(currentLevel == LEVEL_COUNTY){
                    queryCities();
                }else if(currentLevel == LEVEL_CITY){
                    //若在City切换到province
                    queryProvinces();
                }
            }
        });
        //初始状态下显示province
        queryProvinces();
    }

4. 在queryProvinces()方法中,设置头布局的标题,返回按钮。调用LitePal查询结构读取省级数据,若读取到则将其显示在界面上,若没有则调用queryServer()方法从服务器查询数据。queryCities()方法和queryCounty()方法同理。

  • 查询省级数据
/*查询全国所有的省,先从数据库查,没有的话去服务器查询
    * */
    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{
            //从服务器中查询
            String address = "http://guolin.tech/api/china";
            queryFromServer(address,"province");
        }
    }
  • 查询市级数据
/*查询对应省的City数据,优先从数据库查,若没有,则去服务器查询
    * */
    private void queryCities(){
        //设置标题栏
        titleText.setText(selectedProvince.getProvinceName());
        //设置返回按钮可见
        backButton.setVisibility(View.VISIBLE);
        //在数据库中查询对应的City数据
        cityList = DataSupport.findAll(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{
            String address = "http://guolin.tech/api/china/"+selectedProvince.getProvinceCode();
            queryFromServer(address,"city");
        }
    }
  • 查询县级数据
 /*查询选中的市内的所有县,优先从数据库查,若没有则去服务器查询
    * */
    private void queryCounties(){
        titleText.setText(selectedCity.getCityName());
        backButton.setVisibility(View.VISIBLE);
        countyList = DataSupport.findAll(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{
            String address = "http://guolin.tech/api/china/"+
                    selectedProvince.getProvinceCode()+"/"+selectedCity.getCityCode();
            queryFromServer(address,"county");
        }
    }

5. 在queryFromServer()方法中,调用HttpUtil中的sendOkHttpRequest()方法向服务器发送请求,响应的数据回调到onResponse()方法中,然后调用Utility中的handleProvincesResponse()方法解析和处理服务器返回的数据,然后将其存储到数据库中。最后再次调用queryProvinces()方法重新加载省级数据,因为queryProvinces()涉及UI操作,则须在主线程中调用,因此借助runOnUiThread()方法从子线程切换到主线程

  • 问题:queryFromServer()在哪里说明是是在子线程中执行的???
   /*根据传入的地址和类型从服务器上获取数据
    * */
    private void queryFromServer(String address,final String type){
        //未查出之前显示出进度条框
        showProgressDialog();
        HttpUtil.sendOkHttpRequest(address, new Callback() {
            @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();
                    }
                });

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                String responseText = response.body().string();
                boolean result = false;
                if(type.equals("province")){
                    result = Utility.hanldeProvinceResponse(responseText);
                }else if(type.equals("city")){
                    result = Utility.handleCityResponse(responseText,selectedProvince.getId());
                }else if(type.equals("county")){
                    result = Utility.handleCountyResponse(responseText,selectedCity.getId());
                }
                if(result){
                    getActivity().runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            closeProgressDialog();
                            if(type.equals("province")){
                                queryProvinces();
                            }else if(type.equals("city")){
                                queryCities();
                            }else if(type.equals("county")){
                                queryCounties();
                            }
                        }
                    });
                }


            }
        });
    }

6. 涉及到的进度条框

  • 进度条框的显示
    //显示进度条框
    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();
        }
    }

将碎片添加到活动

1. 修改activity_main.xml中的代码

定义一个FrameLayout,将ChooseAreaFragment加入,并让其充满整个布局。

 
        
    

2. 删除原生的ActionBar

在style.xml中,