1.建立三张表,分别存放Province ,City ,County 的数据
建表语句:
Province:
create table Province(
id integer primary key autoincrement,
province_name text,
province_code text)
City:
create table City(
id integer primary key autoincrement,
city_name text,
city_code text,
province_id integer)
Country;
create table Country(
id integer primary key autoincrement,
country_name text,
country_code text,
city_id integer)
2.将建表语句写入到代码中
新建一个CoolWeatherOpenHelper类继承于SQLiteOpenHelper
将三条建表语句定义成常量,然后在onCrea方法中去执行创建
3.创建3张表的实体类
方便后续开发的工作
以Province为例:
public class Province{
private int id;
private String province_name;
private String province_code;
//添加getset方法
}
4.创建一个CoolWeatherDB类
把一些常用的数据库操作封装起来,以方便后面的使用。
CoolWeatherDB是一个单例类,我们将它的构造方法私有化,并提供了一个getInstance()方法来获取CoolWeatherDB的实例,这样就可以保证全局范围内只会有一个CoolWeatherDB的实例。接下来我们在CoolWeatherDB中提供了六组方法,saveProvince()、loadProvinces()、saveCity()、loadCities()、saveCounty()、loadCounties(),分别用于存储省份数据、读取所有省份数据、存储城市数据、读取某省内所有城市数据、存储县数据、读取某市内所有县的数据。有了这几个方法,我们后面很多的数据库操作都将会变得非常简单。
1.增加一个HttpUtil类
因为所有的数据都是从服务器上获取的,所以和服务器的交互必不可少
2.创建一个Utility类
解析和处理从服务器返回的省市县数据
提供了handleProvincesResponse()、handleCitiesResponse()、handleCountiesResponse()这三个方法,分别用于解析和处理服务器返回的省级、市级和县级数据。解析的规则就是先按逗号分隔,再按单竖线分隔,接着将解析出来的数据设置到实体类中,最后调用CoolWeatherDB中的三个save()方法将数据存储到相应的表中。
3.创建显示省市县的布局文件
4.创建遍历省市县的活动
这个类里的代码虽然非常多,可是逻辑却不复杂,我们来慢慢理一下。在onCreate()方法中先是获取到了一些控件的实例,然后去初始化了ArrayAdapter,将它设置为ListView的适配器。之后又去获取到了CoolWeatherDB的实例,并给ListView设置了点击事件,到这里我们的初始化工作就算是完成了。
在onCreate()方法的最后,调用了queryProvinces()方法,也就是从这里开始加载省级数据的。queryProvinces()方法的内部会首先调用CoolWeatherDB的loadProvinces()方法来从数据库中读取省级数据,如果读取到了就直接将数据显示到界面上,如果没有读取到就调用queryFromServer()方法来从服务器上查询数据。
queryFromServer()方法会先根据传入的参数来拼装查询地址,这个地址就是我们在14.1节分析过的。确定了查询地址之后,接下来就调用HttpUtil的sendHttpRequest()方法来向服务器发送请求,响应的数据会回调到onFinish()方法中,然后我们在这里去调用Utility的handleProvincesResponse()方法来解析和处理服务器返回的数据,并存储到数据库中。接下来的一步很关键,在解析和处理完数据之后,我们再次调用了queryProvinces()方法来重新加载省级数据,由于queryProvinces()方法牵扯到了UI操作,因此必须要在主线程中调用,这里借助了runOnUiThread()方法来实现从子线程切换到主线程,它的实现原理其实也是基于异步消息处理机制的。现在数据库中已经存在了数据,因此调用queryProvinces()就会直接将数据显示到界面上了。
当你点击了某个省的时候会进入到ListView的onItemClick()方法中,这个时候会根据当前的级别来判断是去调用queryCities()方法还是queryCounties()方法,queryCities()方法是去查询市级数据,而queryCounties()方法是去查询县级数据,这两个方法内部的流程和queryProvinces()方法基本相同,这里就不重复讲解了。
另外还有一点需要注意,我们重写了onBackPressed()方法来覆盖默认Back键的行为,这里会根据当前的级别来判断是返回市级列表、省级列表、还是直接退出。
5.配置AndroidManifest.xml文件
1.创建显示天气的布局文件
2.在Utility类中添加几个方法用来解析和处理服务器返回的JSON数据
/**
* 解析服务器返回的JSON数据,并将解析出的数据存储到本地。
*/
public static void handleWeatherResponse(Context context, String response) {
try {
JSONObject jsonObject = new JSONObject(response);
JSONObject weatherInfo = jsonObject.getJSONObject("weatherinfo");
String cityName = weatherInfo.getString("city");
String weatherCode = weatherInfo.getString("cityid");
String temp1 = weatherInfo.getString("temp1");
String temp2 = weatherInfo.getString("temp2");
String weatherDesp = weatherInfo.getString("weather");
String publishTime = weatherInfo.getString("ptime");
saveWeatherInfo(context, cityName, weatherCode, temp1, temp2,
weatherDesp, publishTime);
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
* 将服务器返回的所有天气信息存储到SharedPreferences文件中。
*/
public static void saveWeatherInfo(Context context, String cityName, String weatherCode, String temp1, String temp2, String weatherDesp, String publishTime) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年M月d日", Locale.CHINA);
SharedPreferences.Editor editor = PreferenceManager
.getDefaultSharedPreferences(context).edit();
editor.putBoolean("city_selected", true);
editor.putString("city_name", cityName);
editor.putString("weather_code", weatherCode);
editor.putString("temp1", temp1);
editor.putString("temp2", temp2);
editor.putString("weather_desp", weatherDesp);
editor.putString("publish_time", publishTime);
editor.putString("current_date", sdf.format(new Date()));
editor.commit();
}
3.创建显示天气的活动
同样,这个活动中的代码也非常长,我们还是一步步梳理下。在onCreate()方法中仍然先是去获取一些控件的实例,然后会尝试从Intent中取出县级代号,如果可以取到就会调用queryWeatherCode()方法,如果不能取到则会调用showWeather()方法,我们先来看下可以取到的情况。
queryWeatherCode()方法中并没有几行代码,仅仅是拼装了一个地址,然后调用queryFromServer()方法来查询县级代号所对应的天气代号。服务器返回的数据仍然会回调到onFinish()方法中,这里对返回的数据进行解析,然后将解析出来的天气代号传入到queryWeatherInfo()方法中。
queryWeatherInfo()方法也非常简单,同样是拼装了一个地址,然后调用queryFromServer()方法来查询天气代号所对应的天气信息。由于天气信息是以JSON格式返回的,因此我们在handleWeatherResponse()方法中使用JSONObject将数据全部解析出来,然后调用saveWeatherInfo()方法将所有的天气信息都存储到SharedPreferences文件中。注意除了天气信息之外,我们还存储了一个city_selected标志位,以此来辨别当前是否已经选中了一个城市。最后会去调用showWeather()方法来将所有的天气信息显示到界面上,showWeather()方法中的逻辑很简单,就是从SharedPreferences文件中将数据读取出来,然后一一设置到界面上即可。
刚才分析的是在onCreate()方法中可以取到县级代号的情况,那么不能取到的时候呢?原来就是直接调用showWeather()方法来显示本地存储的天气信息就可以了。
接下来我们要做的,就是如何从ChooseAreaActivity跳转到WeatherActivity了,修改ChooseAreaActivity中的代码,如下所示:
public class ChooseAreaActivity extends Activity {
……
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences prefs = PreferenceManager. getDefaultSharedPreferences(this);
if (prefs.getBoolean("city_selected", false)) {
Intent intent = new Intent(this, WeatherActivity.class);
startActivity(intent);
finish();
return;
}
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.choose_area);
listView = (ListView) findViewById(R.id.list_view);
titleText = (TextView) findViewById(R.id.title_text);
adapter = new ArrayAdapter(this, android.R.layout.simple_ list_item_1, dataList);
listView.setAdapter(adapter);
coolWeatherDB = CoolWeatherDB.getInstance(this);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView> arg0, View view, int index,
long arg3) {
if (currentLevel == LEVEL_PROVINCE) {
selectedProvince = provinceList.get(index);
queryCities();
} else if (currentLevel == LEVEL_CITY) {
selectedCity = cityList.get(index);
queryCounties();
} else if (currentLevel == LEVEL_COUNTY) {
String countyCode = countyList.get(index).getCountyCode();
Intent intent = new Intent(ChooseAreaActivity.this, WeatherActivity.class);
intent.putExtra("county_code", countyCode);
startActivity(intent);
finish();
}
}
});
queryProvinces(); // 加载省级数据
}
……
}
可以看到,这里我们主要修改了两处。第一,在onCreate()方法的一开始先从SharedPreferences文件中读取city_selected标志位,如果为true就说明当前已经选择过城市了,直接跳转到WeatherActivity即可。第二,在onItemClick()方法中加入一个if判断,如果当前级别是LEVEL_COUNTY,就启动WeatherActivity,并把当前选中县的县级代号传递过去。
1.在布局文件中加入更新天气和切换城市的按钮
2修改WeatherActivity中的代码
public class WeatherActivity extends Activity implements OnClickListener{
……
/**
* 切换城市按钮
*/
private Button switchCity;
/**
* 更新天气按钮
*/
private Button refreshWeather;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.weather_layout);
……
switchCity = (Button) findViewById(R.id.switch_city);
refreshWeather = (Button) findViewById(R.id.refresh_weather);
switchCity.setOnClickListener(this);
refreshWeather.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.switch_city:
Intent intent = new Intent(this, ChooseAreaActivity.class);
intent.putExtra("from_weather_activity", true);
startActivity(intent);
finish();
break;
case R.id.refresh_weather:
publishText.setText("同步中...");
SharedPreferences prefs = PreferenceManager. getDefaultSharedPreferences(this);
String weatherCode = prefs.getString("weather_code", "");
if (!TextUtils.isEmpty(weatherCode)) {
queryWeatherInfo(weatherCode);
}
break;
default:
break;
}
}
……
}
我们在onCreate()方法中获取到了两个按钮的实例,然后分别调用了setOnClickListener()方法来注册点击事件。当点击的是更新天气按钮时,会首先从SharedPreferences文件中读取天气代号,然后调用queryWeatherInfo()方法去更新天气就可以了。当点击的是切换城市按钮时,会跳转到ChooseAreaActivity,但是注意目前我们已经选中过了一个城市,如果直接跳转到ChooseAreaActivity,会立刻又跳转回来,因此这里在Intent中加入了一个from_weather_activity标志位。
接着在ChooseAreaActivity对这个标志位进行处理,如下所示:
public class ChooseAreaActivity extends Activity {
……
/**
* 是否从WeatherActivity中跳转过来。
*/
private boolean isFromWeatherActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
isFromWeatherActivity = getIntent().getBooleanExtra("from_weather_ activity", false);
SharedPreferences prefs = PreferenceManager. getDefaultSharedPreferences(this);
// 已经选择了城市且不是从WeatherActivity跳转过来,才会直接跳转到WeatherActivity
if (prefs.getBoolean("city_selected", false) && !isFromWeatherActivity) {
Intent intent = new Intent(this, WeatherActivity.class);
startActivity(intent);
finish();
return;
}
……
}
……
@Override
public void onBackPressed() {
if (currentLevel == LEVEL_COUNTY) {
queryCities();
} else if (currentLevel == LEVEL_CITY) {
queryProvinces();
} else {
if (isFromWeatherActivity) {
Intent intent = new Intent(this, WeatherActivity.class);
startActivity(intent);
}
finish();
}
}
}
可以看到,这里我们加入了一个isFromWeatherActivity变量,以此来标记是不是从WeatherActivity跳转过来的,只有已经选择了城市且不是从WeatherActivity跳转过来的时候才会直接跳转到WeatherActivity。另外,我们在onBackPressed()方法中也进行了处理,当按下Back键时,如果是从WeatherActivity跳转过来的,则应该重新回到WeatherActivity。
1.首先在service包下新建一个AutoUpdateService继承自Service,
代码如下所示:
public class AutoUpdateService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
updateWeather();
}
}).start();
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int anHour = 8 * 60 * 60 * 1000; // 这是8小时的毫秒数
long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
Intent i = new Intent(this, AutoUpdateReceiver.class);
PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, 0);
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi);
return super.onStartCommand(intent, flags, startId);
}
/**
* 更新天气信息。
*/
private void updateWeather() {
SharedPreferences prefs = PreferenceManager. getDefaultSharedPreferences(this);
String weatherCode = prefs.getString("weather_code", "");
String address = "http://www.weather.com.cn/data/cityinfo/" + weatherCode + ".html";
HttpUtil.sendHttpRequest(address, new HttpCallbackListener() {
@Override
public void onFinish(String response) {
Utility.handleWeatherResponse(AutoUpdateService.this, response);
}
@Override
public void onError(Exception e) {
e.printStackTrace();
}
});
}
}
可以看到,在onStartCommand()方法中先是开启了一个子线程,然后在子线程中调用updateWeather()方法来更新天气,我们仍然会将服务器返回的天气数据交给Utility的handleWeatherResponse()方法去处理,这样就可以把最新的天气信息存储到SharedPreferences文件中。
2.新建AutoUpdateReceiver继承自BroadcastReceiver
代码如下所示:
public class AutoUpdateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent i = new Intent(context, AutoUpdateService.class);
context.startService(i);
}
}
3.激活服务
这里只是在onReceive()方法中再次去启动AutoUpdateService,就可以实现后台定时更新的功能了。不过,我们还需要在代码某处去激活AutoUpdateService这个服务才行。修改WeatherActivity中的代码,如下所示:
public class WeatherActivity extends Activity implements OnClickListener{
……
private void showWeather() {
SharedPreferences prefs = PreferenceManager. getDefaultSharedPreferences(this);
cityNameText.setText( prefs.getString("city_name", ""));
temp1Text.setText(prefs.getString("temp1", ""));
temp2Text.setText(prefs.getString("temp2", ""));
weatherDespText.setText(prefs.getString("weather_desp", ""));
publishText.setText("今天" + prefs.getString("publish_time", "") + "发布");
currentDateText.setText(prefs.getString("current_date", ""));
weatherInfoLayout.setVisibility(View.VISIBLE);
cityNameText.setVisibility(View.VISIBLE);
Intent intent = new Intent(this, AutoUpdateService.class);
startService(intent);
}
}
可以看到,这里在showWeather()方法的最后加入启动AutoUpdateService这个服务的代码,这样只要一旦选中了某个城市并成功更新天气之后,AutoUpdateService就会一直在后台运行,并保证每8小时更新一次天气。
4.在AndroidManifest.xml中注册新增的服务和广播接收器