写这个系列的博客的初衷主要是想记录自己这次的开发过程,也顺便复习一下之前学过的技术点。好啦,废话不多说,直接步入正题~
一、贝言天气功能需求和技术可行性分析
在开始编码前,先对程序进行需求分析,清楚程序应该具备哪些功能。
1、具备功能
- 包含全国所有的省、市、县;
- 可以查看全国任意城市的天气信息;
- 可以自由切换城市,去查看其它城市的天气;
- 提供手动更新以及后台自动更新天气的功能。
想要实现以上4个功能,需要用到UI、网络、数据存储、服务等技术。
2、技术可行性分析
关于天气数据问题,现在网上免费的天气预报接口越来越少了,很多之前可以用的接口都慢慢关闭掉了。
因为这次只是想学习开发安卓app,所以并不具备真实的天气数据,用了一款模拟的天气数据的接口,用到了郭老师提供的全国所有省市县的数据信息以及模拟的天气数据信息。如果想做一款有真实天气数据的软件,可以考虑使用一些付费的天气预报接口。
二、创建数据库和表
1、在com.beiyanweather.android包下面创建4个包。
4个包用途:
db包:存放数据库相关代码;
gson包:存放GSON模型相关代码;
service包:存放服务相关代码;
util包:存放工具相关代码。
如图:
2、创建好后,对该项目所需要用的依赖库进行声明
打开app包中的build.gradle文件,在dependencies闭包中添加如下内容:
dependencies {
......
/*
* 添加下面这5行
*/
// 对数据库进行操作 ↓
implementation 'org.litepal.android:java:3.0.0'
// 进行网络请求 ↓
implementation("com.squareup.okhttp3:okhttp:4.7.2")
// 解析JSON数据 ↓
implementation 'com.google.code.gson:gson:2.8.6'
// 加载和展示图片 ↓
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
}
3、设计数据库的表结构
需要实现:
- 建立3张表:province、city、county
- 分别存放省、市、县的数据信息
- 对应到实体类中,需要在db包建立Province、City、County,3个类
- 声明一些需要的字段,并生成对应的getter和setter方法
(1)Province类:
public class Province extends LitePalSupport {
private int 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;
}
}
(2)City类:
public class City extends LitePalSupport {
private int id;
private String cityName; // 记录市的名字
private int cityCode; // 记录市的代号
private int provinceId; // 记录当前市所属省的id值
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;
}
}
(3)County类:
public class County extends LitePalSupport {
private int id;
private String countyName; // 记录县的名字
private String weatherId; // 记录县对应的天气id
private int cityId; // 记录当前县所属市的id值
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;
}
}
4、配置litepal.xml文件
(1)右击app/src/main目录,右击选择New,点击Directory创建一个assets目录
(2)在assets目录下新建一个litepal.xml文件(File),然后编辑litepal.xml中的内容:
<litepal>
// 数据库名指定为:beiyan_weather
<dbname value="beiyan_weather" />
// 数据库版本号指定为:1
<version value="1" />
//将刚刚创建的3个实体类添加到映射列表中
<list>
<mapping class="com.beiyanweather.android.db.Province" />
<mapping class="com.beiyanweather.android.db.City" />
<mapping class="com.beiyanweather.android.db.County" />
</list>
</litepal>
5、配置LitePalApplication
修改AndroidManifest.xml中的代码
android:name="org.litepal.LitePalApplication"
这样就将所有的配置都完成了,数据库和表会在首次执行任意数据库的时候自动创建。
三、遍历全国省市县数据
需要实现:把遍历全国省市县的功能加入。
1、因为数据都是从服务器端获取到的,这里与服务器交互必不可少,所以需要在util包中新建一个HttpUtil类
HttpUtil类的代码:
public class HttpUtil {
/*
* 发起一条HTTP请求只需要调用sendOkHttpRequest()方法;
* 传入请求地址;
* 注册一个回调来处理服务器响应。
*/
public static void sendOkHttpRequest(String address, okhttp3.Callback callback)
{
// 创建OkHttpClient实例
OkHttpClient client = new OkHttpClient();
// 想要发起请求就需要创建一个Request对象,可以通过url()方法设置目标的网络地址
Request request = new Request.Builder().url(address).build();
// 调用OkHttpClient的newCall()方法创建一个Call对象,调用它的enqueue方法来发送请求并获取服务器返回的数据
client.newCall(request).enqueue(callback);
}
}
2、由于服务器返回的省市县数据都是JSON格式的,所以我们提供一个工具类来解析和处理这种数据,在util包中新建一个Utility类
Utility类的代码:
public class Utility {
/**
* 解析和处理服务器返回的省级数据
*/
public static boolean handleProvinceResponse(String response)
{
if (!TextUtils.isEmpty(response))
{
try
{
// 先使用JSONArray和JSONObject将数据解析出来,组装成实体类对象
// 将服务器返回的数组传入到一个JSONArray对象中
JSONArray allProvinces = new JSONArray(response);
// 循环遍历这个JSONArray,从中取出的每一个元素都是一个JSONObject对象
for (int i = 0; i < allProvinces.length(); i++)
{
JSONObject provinceObject = allProvinces.getJSONObject(i);
Province province = new Province();
// 每个JSONObject对象中包含name和id数据,调用getString()方法将这些数据取出并打印
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;
}
}
3、由于遍历全国省市县的功能在后面会复用,所以就不写到活动里了,这次写到碎片里面,方便复用,可以在复用的时候直接在布局里面引用碎片即可。
在res/layout目录中新建choose_area.xml布局
choose_area.xml代码:
<!--定义一个头布局来作为标题栏-->
<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_black_24dp"/>
</RelativeLayout>
<!--省市县的数据将显示在这里-->
<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!--这里之所以选择ListView是因为它会自动给每个子项之间添加一条分隔线,
而如果使用RecyclerView想实现同样的功能会比较麻烦,选择最优的实现方案-->
4、编写用于遍历省市县数据的碎片。
新建ChooseAreaFragment,继承自Fragment
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<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,并将它设置为ListView的适配器
adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, dataList);
listView.setAdapter(adapter);
return view;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// ListView点击事件
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
/*
* 当点击了某个省的时候会进入到ListView的onItemClick()方法中,
* 这时,会根据当前的级别来判断是去调用queryCities()方法还是queryCounties()方法,
*/
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (currentLevel == LEVEL_PROVINCE)
{
// queryCities()方法是查询市级数据
selectedProvince = provinceList.get(position);
queryCities();
}
else if (currentLevel == LEVEL_CITY)
{
// queryCounties()方法是查询县级数据
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()方法,从这里开始加载省级数据
queryProvinces();
}
/**
* 查询全国所有的省,优先从数据库查询,如果没有查询到再去服务器上查询
*/
private void queryProvinces() {
// 将头布局的标题设置为中国
titleText.setText("中国");
// 因为省级列表不能再返回了,所以将返回按钮隐藏起来
backButton.setVisibility(View.GONE);
// 调用LitePal的查询接口来从数据库中读取省级数据
provinceList = LitePal.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()方法从服务器查询数据
queryFromServer(address, "province");
}
}
/**
* 查询选中省内所有的市,优先从数据库查询,如果没有查询到再去服务器上查询
*/
private void queryCities() {
titleText.setText(selectedProvince.getProvinceName());
backButton.setVisibility(View.VISIBLE);
cityList = LitePal.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 = LitePal.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();
// 调用HttpUtil的sendOkHttpRequest()方法向服务器发送请求
HttpUtil.sendOkHttpRequest(address, new Callback() {
// 响应的数据会回调到onResponse()方法中
@Override
public void onResponse(Call call, Response response) throws IOException {
String responseText = response.body().string();
boolean result = false;
if ("province".equals(type))
{
// 调用Utility的handleProvinceResponse()方法来解析和处理服务器返回的数据,并存储到数据库中
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)
{
/**
* 再次调用queryProvinces()方法重新加载省级数据。
* 由于queryProvinces()方法牵扯到了UI操作,所以必须要在主线程中调用,
* 这里借助了runOnUiThread()方法来实现从子线程切换到主线程。
*/
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
closeProgressDialog();
if ("province".equals(type)){
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();
}
}
}
5、碎片是不能直接显示在界面上的,所以需要把它添加到活动里。
修改activity_main.xml中的代码:
<!--定义一个FrameLayout,将ChooseAreaFragment添加进来,让它充满整个布局-->
<fragment
android:id="@+id/choose_area_fragment"
android:name="com.beiyanweather.android.ChooseAreaFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
6、刚刚自定义了一个标题栏,所以就不需要原生的ActivityBar了。
修改res/values/styles.xml的代码:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
7、声明程序需要的权限
修改AndroidManifest.xml中的代码:
// 新增访问网络的权限
<uses-permission android:name="android.permission.INTERNET"/>
点击运行,效果如图:
省级数据:
市级数据:
县级数据: