14、进入实战——开发酷欧天气
我们将要在本章编写一个功能较为完整的天气预报程序,学习了这么久的Android开发,我们给这个软件起个名字叫酷欧天气,英文名叫作Cool Weather。下面就可以开始动手了。
14.1 功能需求及技术可行性分析
在编码之前先对程序进行需求分析,想一想酷欧天气中应该具备哪些功能。将这些功能全部整理出来之后,我们才好动手去一一实现。这里我认为酷欧天气中至少应该具备以下功能:
可以罗列出全国所有的省、市、县;
可以查看全国任意城市的天气信息;
可以自由地切换城市,去查看其他城市的天气;
提供手动更新以及后台自动更新天气的功能。
虽然看上去只有4个主要的功能点,但如果想要全部实现这些功能却需要用到UI、网络、数据存储、服务等技术,因此还是非常考验你的综合应用能力的。不过好在这些技术在前面的章节中我们全部都学习过了,只要你学得用心,相信完成这些功能对你来说并不难。
分析完了需求之后,接下来就要进行技术可行性分析了。首先需要考虑的一个问题就是,我们如何才能得到全国省市县的数据信息,以及如何才能获取到每个城市的天气信息。比较遗憾的是,现在网上免费的天气预报接口已经越来越少,很多之前可以使用的接口都慢慢关闭掉了,包括本书第1版中使用的中国天气网的接口。因此,这次我也是特意用心去找了一些更加稳定的天气预报服务,比如彩云天气以及和风天气都非常不错。这两个天气预报服务虽说都是收费的,但它们每天都提供了一定次数的免费天气预报请求。其中彩云天气的数据更加实时和专业,可以将天气预报精确到分钟级,每天提供1000次免费请求;和风天气的数据相对简单一些,比较适合新手学习,每天提供3000次免费请求。那么简单起见,这里我们就使用和风天气来作为天气预报的数据来源,每天3000次的免费请求对于学习而言已经是相当充足了。
解决了天气数据的问题,接下来还需要解决全国省市县数据的问题。同样,现在网上也没有一个稳定的接口可以使用,那么为了方便你的学习,我专门架设了一台服务器用于提供全国所有省市县的数据信息,从而帮你把道路都铺平了。
那么下面我们来看一下这些接口的具体用法。比如要想罗列出中国所有的省份,只需访问如下地址:
http://guolin.tech/api/china
服务器会返回我们一段JSON格式的数据,其中包含了中国所有的省份名称以及省份id,如下所示:
[{"id":1,"name":"北京"},{"id":2,"name":"上海"},{"id":3,"name":"天津"}, {"id":4,"name":"重庆"},{"id":5,"name":"香港"},{"id":6,"name":"澳门"}, {"id":7,"name":"台湾"},{"id":8,"name":"黑龙江"},{"id":9,"name":"吉林"}, {"id":10,"name":"辽宁"},{"id":11,"name":"内蒙古"},{"id":12,"name":"河北"}, {"id":13,"name":"河南"},{"id":14,"name":"山西"},{"id":15,"name":"山东"}, {"id":16,"name":"江苏"},{"id":17,"name":"浙江"},{"id":18,"name":"福建"}, {"id":19,"name":"江西"},{"id":20,"name":"安徽"},{"id":21,"name":"湖北"},{"id":22,"name":"湖南"},{"id":23,"name":"广东"},{"id":24,"name":"广西"}, {"id":25,"name":"海南"},{"id":26,"name":"贵州"},{"id":27,"name":"云南"}, {"id":28,"name":"四川"},{"id":29,"name":"西藏"},{"id":30,"name":"陕西"}, {"id":31,"name":"宁夏"},{"id":32,"name":"甘肃"},{"id":33,"name":"青海"}, {"id":34,"name":"新疆"}]
可以看到,这是一个JSON数组,数组中的每一个元素都代表着一个省份。其中,北京的id是1,上海的id是2。那么如何才能知道某个省内有哪些城市呢?其实也很简单,比如江苏的id是16,访问如下地址即可:
http://guolin.tech/api/china/16
也就是说,只需要将省份id添加到url地址的最后面就可以了,现在服务器返回的数据如下:
[{"id":113,"name":"南京"},{"id":114,"name":"无锡"},{"id":115,"name":"镇江"}, {"id":116,"name":"苏州"},{"id":117,"name":"南通"},{"id":118,"name":"扬州"}, {"id":119,"name":"盐城"},{"id":120,"name":"徐州"},{"id":121,"name":"淮安"}, {"id":122,"name":"连云港"},{"id":123,"name":"常州"},{"id":124,"name":"泰州"}, {"id":125,"name":"宿迁"}]
这样我们就得到江苏省内所有城市的信息了,可以看到,现在返回的数据格式和刚才查看省份信息时返回的数据格式是一样的。相信此时你已经可以举一反三了,比如说苏州的id是116,那么想要知道苏州市下又有哪些县和区的时候,只需访问如下地址:
http://guolin.tech/api/china/16/116
这次服务器返回的数据如下:
[{"id":937,"name":"苏州","weather_id":"CN101190401"},
{"id":938,"name":"常熟","weather_id":"CN101190402"},
{"id":939,"name":"张家港","weather_id":"CN101190403"},
{"id":940,"name":"昆山","weather_id":"CN101190404"},
{"id":941,"name":"吴中","weather_id":"CN101190405"},
{"id":942,"name":"吴江","weather_id":"CN101190407"},
{"id":943,"name":"太仓","weather_id":"CN101190408"}]
通过这种方式,我们就能把全国所有的省、市、县都罗列出来了。那么解决了省市县数据的获取,我们又怎样才能查看到具体的天气信息呢?这就必须要用到每个地区对应的天气id了。观察上面返回的数据,你会发现每个县或区都会有一个weather_id,拿着这个id再去访问和风天气的接口,就能够获取到该地区具体的天气信息了。
下面我们来看一下和风天气的接口该如何使用。首先你需要注册一个自己的账号,注册地址是http://guolin.tech/api/weather/register 。注册好了之后使用这个账号登录,就能看到自己的API Key,以及每天剩余的访问次数了,如图14.1所示。
图 14.1 API Key和每天剩余访问次数
有了API Key,再配合刚才的weather_id,我们就能获取到任意城市的天气信息了。比如说苏州的weather_id是CN101190401,那么访问如下接口即可查看苏州的天气信息:
http://guolin.tech/api/weather?cityid=CN101190401&key=bc0418b57b2d4918819d3974ac1285d9
其中,cityid部分填入的就是待查看城市的weather_id,key部分填入的就是我们申请到的API Key。这样,服务器就会把苏州详细的天气信息以JSON格式返回给我们了。不过,由于返回的数据过于复杂,这里我做了一下精简处理,如下所示:
{
"HeWeather": [
{
"status": "ok",
"basic": {},
"aqi": {},
"now": {},
"suggestion": {},
"daily_forecast": []
}
]
}
返回数据的格式大体上就是这个样子了,其中status 代表请求的状态,ok 表示成功。basic 中会包含城市的一些基本信息,aqi 中会包含当前空气质量的情况,now 中会包含当前的天气信息,suggestion 中会包含一些天气相关的生活建议,daily_forecast 中会包含未来几天的天气信息。访问http://guolin.tech/api/weather/doc 这个网址可以查看更加详细的文档说明。
数据都能获取到了之后,接下来就是JSON解析的工作了,这对于你来说应该很轻松了吧?
确定了技术完全可行之后,接下来就可以开始编码了。不过别着急,我们准备让酷欧天气成为一个开源软件,并使用GitHub来进行代码托管,因此先让我们进入到本书最后一次的Git时间。
14.2 Git时间——将代码托管到GitHub上
经过前面几章的学习,相信你已经可以非常熟练地使用Git了。本节依然是Git时间,这次我们将会把酷欧天气的代码托管到GitHub上面。
GitHub是全球最大的代码托管网站,主要是借助Git来进行版本控制的。任何开源软件都可以免费地将代码提交到GitHub上,以零成本的代价进行代码托管。GitHub的官网地址是https://github.com/ 。官网的首页如图14.2所示。
图 14.2 GitHub首页
首先你需要有一个GitHub账号才能使用GitHub的代码托管功能,点击Sign up for GitHub按钮进行注册,然后填入用户名、邮箱和密码,如图14.3所示。
图 14.3 注册账号
点击Create an account按钮来创建账户,接下来会让你选择个人计划,收费计划有创建私人版本库的权限,而我们的酷欧天气是开源软件,所以这里选择免费计划就可以了,如图14.4所示。
图 14.4 选择免费计划
接着点击Continue按钮会进入一个问卷调查界面,如图14.5所示。
图 14.5 问卷调查界面
如果你对这个有兴趣就填写一下,没兴趣的话直接点击最下方的skip this step跳过就可以了。
这样我们就把账号注册好了,会自动跳转到GitHub的个人主页,如图14.6所示。
图 14.6 GitHub个人主页
接下来就可以点击Start a project按钮来创建一个版本库了。由于我们是刚刚注册的账号,在创建版本库之前还需要做一下邮箱验证,验证成功之后就能开始创建了。这里将版本库命名为coolweather,然后选择添加一个Android项目类型的.gitignore文件,并使用Apache License 2.0来作为酷欧天气的开源协议,如图14.7所示。
图 14.7 创建版本库
接着点击Create repository按钮,coolweather这个版本库就创建完成了,如图14.8所示。版本库主页地址是https://github.com/guolindev/coolweather 。
图 14.8 版本库主页
可以看到,GitHub已经自动帮我们创建了.gitignore、LICENSE和README.md这3个文件,其中编辑README.md文件中的内容可以修改酷欧天气版本库主页的描述。
创建好了版本库之后,我们就需要创建酷欧天气这个项目了。
在Android Studio中新建一个Android项目,项目名叫作CoolWeather,包名叫作com.coolweather.android,如图14.9所示。
图 14.9 创建CoolWeather项目
之后的步骤不用多说,一直点击Next就可以完成项目的创建,所有选项都使用默认的就好。
接下来的一步非常重要,我们需要将远程版本库克隆到本地。
首先必须知道远程版本库的Git地址,点击Clone or download按钮就能够看到了,如图14.10所示。
图 14.10 查看版本库的Git地址
点击右边的复制按钮可以将版本库的Git地址复制到剪贴板,酷欧天气版本库的Git地址是https://github.com/zjgwhcn/coolweather.git
非:https://github.com/guolindev/coolweather.git 。
然后打开Git Bash并切换到CoolWeather的工程目录下,如图14.11所示。
图 14.11 在Git Bash中进入CoolWeather工程目录
接着输入git clone https://github.com/zjgwhcn/coolweather.git 来把远程版本库克隆到本地,如图14.12所示。
图 14.12 将远程版本库克隆到本地
看到图中所给的文字提示就表示克隆成功了,并且.gitignore、LICENSE和README.md这3个文件也已经被复制到了本地,可以进入到coolweather目录,并使用ls -al 命令查看一下,如图14.13所示。
图 14.13 查看克隆到本地的文件
现在我们需要将这个目录中的所有文件全部复制粘贴到上一层目录中,这样就能将整个CoolWeather工程目录添加到版本控制中去了。注意.git是一个隐藏目录,在复制的时候千万不要漏掉。另外,上一层目录中也有一个.gitignore文件,我们直接将其覆盖即可。复制完之后可以把coolweather目录删除掉,最终CoolWeather工程的目录结构如图14.14所示。
图 14.14 CoolWeather工程的目录结构
接下来我们应该把CoolWeather项目中现有的文件提交到GitHub上面,这就很简单了,先将所有文件添加到版本控制中,如下所示:
git add .
然后在本地执行提交操作:
git commit -m "First commit."
最后将提交的内容同步到远程版本库,也就是GitHub上面:
git push origin master
注意,在最后一步的时候GitHub要求输入用户名和密码来进行身份校验,这里输入我们注册时填入的用户名和密码就可以了,如图14.15所示。
图 14.15 将提交的内容同步到远程版本库
这样就已经同步完成了,现在刷新一下酷欧天气版本库的主页,你会看到刚才提交的那些文件已经存在了,如图14.16所示。
图 14.16 在GitHub上查看提交的内容
14.3 创建数据库和表
从本节开始,我们就要真正地动手编码了,为了要让项目能够有更好的结构,这里需要在com.coolweather.android包下再新建几个包,如图14.17所示。
图 14.17 项目的新结构
其中db包用于存放数据库模型相关的代码,gson包用于存放GSON模型相关的代码,service包用于存放服务相关的代码,util包用于存放工具相关的代码。
根据14.1节进行的技术可行性分析,第一阶段我们要做的就是创建好数据库和表,这样从服务器获取到的数据才能够存储到本地。关于数据库和表的创建方式,我们早在第6章中就已经学过了。那么为了简化数据库的操作,这里我准备使用
LitePal 来管理酷欧天气的数据库。(需要再学习)
首先需要将项目所需的各种依赖库进行声明,编辑app/build.gradle文件,在dependencies闭包中添加如下内容:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:24.2.1'
testImplementation 'junit:junit:4.12'
implementation 'org.litepal.android:core:1.4.1'
implementation 'com.squareup.okhttp3:okhttp:3.4.1'
implementation 'com.google.code.gson:gson:2.7'
implementation 'com.github.bumptech.glide:glide:3.7.0'
}
这里声明的4个库我们之前都是使用过的,LitePal用于对数据库进行操作,OkHttp用于进行网络请求,GSON用于解析JSON数据,Glide用于加载和展示图片。酷欧天气将会对这几个库进行综合运用,这里直接一次性将它们都添加进来。
然后我们来设计一下数据库的表结构,表的设计当然是仁者见仁智者见智,并不是说哪种设计就是最规范最完美的。这里我准备建立3张表:province、city、county,分别用于存放省、市、县的数据信息。对应到实体类中的话,就应该建立Province 、City 、County 这3个类。
那么,在db包下新建一个Province 类,代码如下所示:
public class Province extends DataSupport {
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;
}
}
其中,id 是每个实体类中都应该有的字段,provinceName 记录省的名字,provinceCode 记录省的代号。另外,LitePal中的每一个实体类都是必须要继承自DataSupport 类的。
接着在db包下新建一个City 类,代码如下所示:
public class City extends DataSupport {
private int id;
private String cityName;
private int cityCode;
private int provinceId;
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;
}
}
其中,cityName 记录市的名字,cityCode 记录市的代号,provinceId 记录当前市所属省的id值。
然后在db包下新建一个County 类,代码如下所示:
public class County extends DataSupport {
private int id;
private String countyName;
private String weatherId;
private int cityId;
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;
}
}
其中,countyName 记录县的名字,weatherId 记录县所对应的天气id,cityId 记录当前县所属市的id值。
可以看到,实体类的内容都非常简单,就是声明了一些需要的字段,并生成相应的getter 和setter 方法就可以了。
接下来需要配置litepal.xml文件。右击app/src/main目录→New→Directory,创建一个assets目录,然后在assets目录下再新建一个litepal.xml文件,接着编辑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,并将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:supportsRtl="true"
android:theme="@style/AppTheme">
...
</application>
</manifest>
这样我们就将所有的配置都完成了,数据库和表会在首次执行任意数据库操作的时候自动创建。
好了,第一阶段的代码写到这里就差不多了,我们现在提交一下。首先将所有新增的文件添加到版本控制中:
git add .
接着执行提交操作:
git commit -m "加入创建数据库和表的各项配置。"
最后将提交同步到GitHub上面:
git push origin master
OK!第一阶段完工,下面让我们赶快进入到第二阶段的开发工作中吧。
14.4 遍历全国省市县数据
在第二阶段中,我们准备把遍历全国省市县的功能加入,这一阶段需要编写的代码量比较大,你一定要跟上脚步。
我们已经知道,全国所有省市县的数据都是从服务器端获取到的,因此这里和服务器的交互是必不可少的,所以我们可以在util包下先增加一个HttpUtil 类,代码如下所示:
public class HttpUtil {
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);
}
}
由于OkHttp的出色封装,这里和服务器进行交互的代码非常简单,仅仅3行就完成了。现在我们发起一条HTTP请求只需要调用sendOkHttpRequest() 方法,传入请求地址,并注册一个回调来处理服务器响应就可以了。
另外,由于服务器返回的省市县数据都是JSON格式的,所以我们最好再提供一个工具类来解析和处理这种数据。在util包下新建一个Utility 类,代码如下所示:
public class Utility {
/**
* 解析和处理服务器返回的省级数据
*/
public static boolean handleProvinceResponse(String response) {
if (!TextUtils.isEmpty(response)) {
try {
JSONArray allProvinces = new JSONArray(response);
for (int i = 0; i < allProvinces.length(); i++) {
JSONObject provinceObject = allProvinces.getJSONObject(i);
Province province = new Province();
province.setProvinceName(provinceObject.getString("name"));
province.setProvinceCode(provinceObject.getInt("id"));
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;
}
}
可以看到,我们提供了handleProvincesResponse() 、handleCitiesResponse() 、handleCountiesResponse() 这3个方法,分别用于解析和处理服务器返回的省级、市级和县级数据。处理的方式都是类似的,先使用JSONArray和JSONObject将数据解析出来,然后组装成实体类对象,再调用save() 方法将数据存储到数据库当中。由于这里的JSON数据结构比较简单,我们就不使用GSON来进行解析了。
需要准备的工具类就这么多,现在可以开始写界面了。由于遍历全国省市县的功能我们在后面还会复用,因此就不写在活动里面了,而是写在碎片里面,这样需要复用的时候直接在布局里面引用碎片就可以了。
在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>
布局文件中的内容并不复杂,我们先是定义了一个头布局来作为标题栏,将布局高度设置为actionBar的高度,背景色设置为colorPrimary。然后在头布局中放置了一个TextView用于显示标题内容,放置了一个Button用于执行返回操作,注意我已经提前准备好了一张ic_back.png图片用于作为按钮的背景图。这里之所以要自己定义标题栏,是因为碎片中最好不要直接使用ActionBar或Toolbar,不然在复用的时候可能会出现一些你不想看到的效果。
接下来在头布局的下面定义了一个ListView,省市县的数据就将显示在这里。之所以这次使用了ListView,是因为它会自动给每个子项之间添加一条分隔线,而如果使用RecyclerView想实现同样的功能则会比较麻烦,这里我们总是选择最优的实现方案。
接下来也是最关键的一步,我们需要编写用于遍历省市县数据的碎片了。新建ChooseAreaFragment 继承自Fragment,代码如下所示:
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);
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.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();
}
}
});
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 {
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(selected
Province.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();
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) {
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();
}
}
}
这个类里的代码虽然非常多,可是逻辑却不复杂,我们来慢慢理一下。在onCreateView() 方法中先是获取到了一些控件的实例,然后去初始化了ArrayAdapter,并将它设置为ListView的适配器。接着在onActivityCreated() 方法中给ListView和Button设置了点击事件,到这里我们的初始化工作就算是完成了。
在onActivityCreated() 方法的最后,调用了queryProvinces() 方法,也就是从这里开始加载省级数据的。queryProvinces() 方法中首先会将头布局的标题设置成中国,将返回按钮隐藏起来,因为省级列表已经不能再返回了。然后调用LitePal的查询接口来从数据库中读取省级数据,如果读取到了就直接将数据显示到界面上,如果没有读取到就按照14.1节讲述的接口组装出一个请求地址,然后调用queryFromServer() 方法来从服务器上查询数据。
queryFromServer() 方法中会调用HttpUtil的sendOkHttpRequest() 方法来向服务器发送请求,响应的数据会回调到onResponse() 方法中,然后我们在这里去调用Utility的handleProvincesResponse() 方法来解析和处理服务器返回的数据,并存储到数据库中。接下来的一步很关键,在解析和处理完数据之后,我们再次调用了queryProvinces() 方法来重新加载省级数据,由于queryProvinces() 方法牵扯到了UI操作,因此必须要在主线程中调用,这里借助了runOnUiThread() 方法来实现从子线程切换到主线程。现在数据库中已经存在了数据,因此调用queryProvinces() 就会直接将数据显示到界面上了。
当你点击了某个省的时候会进入到ListView的onItemClick() 方法中,这个时候会根据当前的级别来判断是去调用queryCities() 方法还是queryCounties() 方法,queryCities() 方法是去查询市级数据,而queryCounties() 方法是去查询县级数据,这两个方法内部的流程和queryProvinces() 方法基本相同,这里就不重复讲解了。
另外还有一点需要注意,在返回按钮的点击事件里,会对当前ListView的列表级别进行判断。如果当前是县级列表,那么就返回到市级列表,如果当前是市级列表,那么就返回到省级表列表。当返回到省级列表时,返回按钮会自动隐藏,从而也就不需要再做进一步的处理了。
这样我们就把遍历全国省市县的功能完成了,可是碎片是不能直接显示在界面上的,因此我们还需要把它添加到活动里才行。修改activity_main.xml中的代码,如下所示:
<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>
布局文件很简单,只是定义了一个FrameLayout,然后将ChooseAreaFragment添加进来,并让它充满整个布局。
另外,我们刚才在碎片的布局里面已经自定义了一个标题栏,因此就不再需要原生的ActionBar了,修改res/values/styles.xml中的代码,如下所示:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
...
</style>
</resources>
现在第二阶段的开发工作也完成得差不多了,我们可以运行一下来看看效果。不过在运行之前还有一件事没有做,那就是声明程序所需要的权限。修改AndroidManifest.xml中的代码,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.coolweather.android">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
由于我们是通过网络接口来获取全国省市县数据的,因此必须要添加访问网络的权限才行。
现在可以运行一下程序了,结果如图14.18所示。
由于OkHttp的出色封装,这里和服务器进行交互的代码非常简单,仅仅3行就完成了。现在我们发起一条HTTP请求只需要调用sendOkHttpRequest() 方法,传入请求地址,并注册一个回调来处理服务器响应就可以了。
另外,由于服务器返回的省市县数据都是JSON格式的,所以我们最好再提供一个工具类来解析和处理这种数据。在util包下新建一个Utility 类,代码如下所示:
public class Utility {
/**
* 解析和处理服务器返回的省级数据
*/
public static boolean handleProvinceResponse(String response) {
if (!TextUtils.isEmpty(response)) {
try {
JSONArray allProvinces = new JSONArray(response);
for (int i = 0; i < allProvinces.length(); i++) {
JSONObject provinceObject = allProvinces.getJSONObject(i);
Province province = new Province();
province.setProvinceName(provinceObject.getString("name"));
province.setProvinceCode(provinceObject.getInt("id"));
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;
}
}
可以看到,我们提供了handleProvincesResponse() 、handleCitiesResponse() 、handleCountiesResponse() 这3个方法,分别用于解析和处理服务器返回的省级、市级和县级数据。处理的方式都是类似的,先使用JSONArray和JSONObject将数据解析出来,然后组装成实体类对象,再调用save() 方法将数据存储到数据库当中。由于这里的JSON数据结构比较简单,我们就不使用GSON来进行解析了。
需要准备的工具类就这么多,现在可以开始写界面了。由于遍历全国省市县的功能我们在后面还会复用,因此就不写在活动里面了,而是写在碎片里面,这样需要复用的时候直接在布局里面引用碎片就可以了。
在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>
布局文件中的内容并不复杂,我们先是定义了一个头布局来作为标题栏,将布局高度设置为actionBar的高度,背景色设置为colorPrimary。然后在头布局中放置了一个TextView用于显示标题内容,放置了一个Button用于执行返回操作,注意我已经提前准备好了一张ic_back.png图片用于作为按钮的背景图。这里之所以要自己定义标题栏,是因为碎片中最好不要直接使用ActionBar或Toolbar,不然在复用的时候可能会出现一些你不想看到的效果。
接下来在头布局的下面定义了一个ListView,省市县的数据就将显示在这里。之所以这次使用了ListView,是因为它会自动给每个子项之间添加一条分隔线,而如果使用RecyclerView想实现同样的功能则会比较麻烦,这里我们总是选择最优的实现方案。
接下来也是最关键的一步,我们需要编写用于遍历省市县数据的碎片了。新建ChooseAreaFragment 继承自Fragment,代码如下所示:
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);
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.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();
}
}
});
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 {
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(selected
Province.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();
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) {
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();
}
}
}
这个类里的代码虽然非常多,可是逻辑却不复杂,我们来慢慢理一下。在onCreateView() 方法中先是获取到了一些控件的实例,然后去初始化了ArrayAdapter,并将它设置为ListView的适配器。接着在onActivityCreated() 方法中给ListView和Button设置了点击事件,到这里我们的初始化工作就算是完成了。
在onActivityCreated() 方法的最后,调用了queryProvinces() 方法,也就是从这里开始加载省级数据的。queryProvinces() 方法中首先会将头布局的标题设置成中国,将返回按钮隐藏起来,因为省级列表已经不能再返回了。然后调用LitePal的查询接口来从数据库中读取省级数据,如果读取到了就直接将数据显示到界面上,如果没有读取到就按照14.1节讲述的接口组装出一个请求地址,然后调用queryFromServer() 方法来从服务器上查询数据。
queryFromServer() 方法中会调用HttpUtil的sendOkHttpRequest() 方法来向服务器发送请求,响应的数据会回调到onResponse() 方法中,然后我们在这里去调用Utility的handleProvincesResponse() 方法来解析和处理服务器返回的数据,并存储到数据库中。接下来的一步很关键,在解析和处理完数据之后,我们再次调用了queryProvinces() 方法来重新加载省级数据,由于queryProvinces() 方法牵扯到了UI操作,因此必须要在主线程中调用,这里借助了runOnUiThread() 方法来实现从子线程切换到主线程。现在数据库中已经存在了数据,因此调用queryProvinces() 就会直接将数据显示到界面上了。
当你点击了某个省的时候会进入到ListView的onItemClick() 方法中,这个时候会根据当前的级别来判断是去调用queryCities() 方法还是queryCounties() 方法,queryCities() 方法是去查询市级数据,而queryCounties() 方法是去查询县级数据,这两个方法内部的流程和queryProvinces() 方法基本相同,这里就不重复讲解了。
另外还有一点需要注意,在返回按钮的点击事件里,会对当前ListView的列表级别进行判断。如果当前是县级列表,那么就返回到市级列表,如果当前是市级列表,那么就返回到省级表列表。当返回到省级列表时,返回按钮会自动隐藏,从而也就不需要再做进一步的处理了。
这样我们就把遍历全国省市县的功能完成了,可是碎片是不能直接显示在界面上的,因此我们还需要把它添加到活动里才行。修改activity_main.xml中的代码,如下所示:
<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>
布局文件很简单,只是定义了一个FrameLayout,然后将ChooseAreaFragment添加进来,并让它充满整个布局。
另外,我们刚才在碎片的布局里面已经自定义了一个标题栏,因此就不再需要原生的ActionBar了,修改res/values/styles.xml中的代码,如下所示:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
...
</style>
</resources>
现在第二阶段的开发工作也完成得差不多了,我们可以运行一下来看看效果。不过在运行之前还有一件事没有做,那就是声明程序所需要的权限。修改AndroidManifest.xml中的代码,如下所示:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.coolweather.android">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>
由于我们是通过网络接口来获取全国省市县数据的,因此必须要添加访问网络的权限才行。
现在可以运行一下程序了,结果如图14.18所示。
图 14.18 显示省级数据
可以看到,全国所有省级数据都显示出来了。我们还可以继续查看市级数据,比如点击江苏省,结果如图14.19所示。
图 14.19 显示市级数据
这个时候标题栏上会出现一个返回按钮,用于返回上一级列表。
然后再点击苏州市查看县级数据,结果如图14.20所示。
图 14.20 显示县级数据
好了,这样第二阶段的开发工作也都完成了,我们仍然要把代码提交一下。
git add .
git commit -m "完成遍历省市县三级列表的功能。"
git push origin master
到目前为止进度算是相当不错啊,那么我们就趁热打铁,来进行第三阶段的开发工作。
14、进入实战——开发酷欧天气相关推荐
- 用kotlin方式打开《第一行代码:Android》之开发酷欧天气(2)
参考:<第一行代码:Android>第2版--郭霖 注1:本文为原创,例子可参考郭前辈著作:<第一行代码:Android>第2版 注2:本文不赘述android开发的基本理论, ...
- 用kotlin方式打开《第一行代码:Android》之开发酷欧天气(1)
参考:<第一行代码:Android>第2版--郭霖 注1:本文为原创,例子可参考郭前辈著作:<第一行代码:Android>第2版,转载请注明出处! 注2:本文不赘述androi ...
- 安卓学习专栏——实战项目酷欧天气(4)给天气页面加上背景图片
步骤 系列文章 前言 实现效果 项目结构 1.获取必应每日一图 1.1修改修改activity_weather.xml 1.2修改WeatherActivity 2.背景图和状态栏效果修改 2.1修改 ...
- 安卓学习专栏——实战项目酷欧天气(2)遍历全国省市县数据
步骤 系列文章 前言 1.实现效果 2.项目结构 util包 util包下新建HttpUtil util包下新建Utility 3.新建choose_area.xml布局 4.新建ChooseArea ...
- 第一行代码 开发酷欧天气DataSupport,ProgressDialog,加载失败,PreferenceManager.getDefaultSharedPreferences()方法
第一行代码学到开发酷欧天气时,在继承DataSupport类时发现DataSupport过时,于是发现LitePalSupport可以替代DataSupport.后面会用到一个DataSupport. ...
- android开发酷欧天气,酷欧天气的开发
简介 参考<第一行代码>,开发出一款全国省市县的天气预报app. 创建数据库和表 使用LitePal对数据库进行操作,创建三个实体类分别是Province.City和County. 1. ...
- 《第一行代码》总结之实战酷欧天气、发布应用(九)
第十四章:进入实战,开发酷欧天气 实现一个功能较为完整的天气预报程序.中文:酷欧天气:英文:Cool weather 14.1功能需求和技术可行性分析. (1)应具备以下功能 ...
- 酷欧天气(CoolWeather)应用源码
<ignore_js_op> 181420yank2y45klayhaan.jpg (35 KB, 下载次数: 0) 下载附件 保存到相册 2016-3-29 15:09 上传 酷欧天气 ...
- Android实战:CoolWeather酷欧天气(加强版数据接口)代码详解(上)
-----------------------------------该文章代码已停更,可参考浩比天气(更新于2019/6/25)----------------------------------- ...
最新文章
- 企业库应用实践系列三:自定义构造函数
- Xamarin Android提示内存溢出错误
- Leecode06. Z 字形变换——Leecode大厂热题100道系列
- python模拟点击网页按钮_网页自动化开发(第一章)
- 自己动手写事件总线(EventBus)
- c语言中小数在内存中的存储,c语言中小数在内存中的存储
- 程序员为什么焦虑于编程语言和框架?
- android 工作总结,Android项目的个人总结
- 让html的text输入框只能输入数字和1个小数点
- go有没有php的array,实现类似php的array_column方法
- ADS史密斯圆阻抗匹配
- 校园网连不上,火绒检测dns错误但修复不了,360直接搞定,nice!
- PMBOK(第六版) PMP笔记——《七》第七章(项目成本管理)
- 网康NGFW下一代防火墙远程命令执行漏洞复现
- WordPress无插件实现主题彩色标签云的N种方法总结
- firefox os资源
- 项目启动tomcat失败的几种可能原因和解决方法
- 如影智能唐沐:别把智能家居做成极客玩具
- xshell连接centos vi编辑器不能使用小键盘
- dfa算法 java_Java实现DFA算法对敏感词、广告词过滤功能示例
热门文章
- prometheus 结合cAdvisor、AlertManager、node-exporter、 监控容器并实现邮箱告警
- Vue公众号微信支付
- 思维【敏捷开发ACP】总目录
- androidstudio简单页面设计
- mysql 去除全角空格_去除字符串内多余空格
- 自动投票系统【四】C# 清除Cookies
- web前端大作业--响应式风景旅游网页设计(国庆旅游主题-HTML+CSS+JavaScript)实现
- RDD实战电影点评系统案例
- abb机器人离线编程软件解密_一文看懂最新机器人离线编程软件【详细】
- 中断服务子程序c语言格式,中断服务子程序是如何被执行的 ?