-----------------------------------该文章代码已停更,可参考浩比天气(更新于2019/6/25)-----------------------------------
拜读了郭霖大神的《第一行代码(第二版)》后,决定对其文末的酷欧天气实战项目进行数据扩充以及代码详解,完整文件请从我的GitHub中下载,想学习更多Android知识在看完本篇文章后请出门右转:京东、当当、亚马逊、天猫、PDF、Kindle、豆瓣、多看。
具体步骤还是按照郭霖大神的分析思路来,外加一点点个人的认知。
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文件中的内容,如下所示:
我们将数据库名指定为cool_weather,数据库版本指定为1(注:使用LitePal来升级数据库非常简单,只需要修改你想改的内容,然后将版本号加1即可),并将Province、City和County这3个实体类添加到映射列表当中。最后还需要配置一下LitePalApplication,修改AndroidManifest.xml中的代码,如下所示:
这样我们就将所有的配置写完了,数据库和表会在首次执行任意数据库操作的时候自动创建。
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
在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 adapter;//适配器
private List dataList = new ArrayList<>();//泛型
/**
* 省列表
*/
private List provinceList;
/**
* 市列表
*/
private List cityList;
/**
* 县列表
*/
private List 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、将碎片添加在活动里
由于碎片不能直接显示,需要将其添加到活动里才能将其正常显示在界面上。
6、移除原生ActionBar
由于在碎片的布局里面已经自定义了一个RelativeLayout标题栏,因此就不需要原生的ActionBar了,修改res/values/styles.xml中的代码如下:
7、声明权限
因为需要从服务器中调用数据,则需要声明网络权限。
运行程序,就可以看到全国所有的省市县数据啦。如下图所示(右上角小人为截屏软件,请忽略):
上篇到此结束,剩余项目请关注下篇。完整代码文件:https://github.com/ambition-hb/CoolWeather