开源项目|易天气

一款查询中国天气的软件
数据来源:http://apis.baidu.com/apistore/weatherservice

实战中学习RetrofitRxJavaRxAndroid的实际应用,若未接触过可先移步RetrofitRxJava&RxAndroid进行学习。

先看效果图:

主界面

开启我们的项目,首先启动Android Studio建立一个工程,在build.gradle添加如下信息:

1
2
3
4
5
6
7
8
9
compile 'io.reactivex:rxjava:1.1.0'
compile 'io.reactivex:rxandroid:1.1.0'
compile 'com.google.code.gson:gson:2.6.2'
compile 'com.squareup.picasso:picasso:2.5.2'
compile 'com.squareup.retrofit2:retrofit:2.0.0-beta4'
compile 'com.squareup.retrofit2:converter-gson:2.0.0-beta4'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0-beta4'
compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.yalantis:phoenix:1.2.3'

ps:Picasso原本是用不上的,不过还是手贱添加了依赖,后面比较牵强地用上了,实际在此项目中并无明显作用

然后我们来定义一个接口NetService

1
2
3
4
5
6
7
8
9
10
11
public interface NetService {
@Headers("apikey:a77bae703557e5884b6873af93b603d1")
@GET("/apistore/weatherservice/recentweathers")
Observable<Result<Data>> getWeather(@Query("cityname") String cityName, @Query("cityid") String cityId);
@Headers("apikey:a77bae703557e5884b6873af93b603d1")
@GET("/apistore/weatherservice/citylist")
Observable<Result<List<City>>> getCity(@Query("cityname") String cityName);
}

ps:代码中的Observable类是属于RxJava中的,返回的结果集是实体类,具体声明依实际项目需要,此处解析的实体类可参考文末源码中的定义。

接着配置一下Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class BaseApplication extends Application {
private static Context context;
private static OkHttpClient client;
private static Retrofit retrofit;
private static NetService service;
private final static long READ_TIME_OUT = 8000;
private final static String BASEURL = "http://apis.baidu.com";
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
initHttpClient();
initRetrofit();
initNetService();
}
private void initHttpClient() {
client = new OkHttpClient.Builder()
.readTimeout(READ_TIME_OUT, TimeUnit.MILLISECONDS)
.build();
}
private void initRetrofit() {
if (retrofit == null) {
retrofit = new Retrofit.Builder()
.baseUrl(BASEURL)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
}
}
private void initNetService() {
service = retrofit.create(NetService.class);
}
public static Context getContext() {
return context;
}
public static OkHttpClient getHttpClient() {
return client;
}
public static Retrofit getRetrofit() {
return retrofit;
}
public static NetService getService() {
return service;
}
}

ps:记得在AndroidManifest.xml文件中配置Application标签,增加android:name属性

开始首页的界面布局就一个ViewPager,每添加一个城市,就往ViewPager里添加一个Fragment。此处使用的PagerAdapterFragmentStatePagerAdapter,为什么不用FragmentPagerAdapter呢?因为要动态管理城市的天气信息,若使用FragmentPagerAdapter,在删除城市时,页面依然保留在内存中,并没有完全从ViewPager中移除,会造成不好的用户体验,但使用FragmentStatePagerAdapter会造成性能上的损失,在更新城市列表时会有一定的延时,目前还没有好的解决方式,有兴趣的读者可去搜索一下两者的区别。具体代码如下:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/id_weather_page"
android:layout_width="match_parent"
android:layout_height="match_parent">
</android.support.v4.view.ViewPager>

ViewPager的Adapter如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SimplePagerAdapter extends FragmentStatePagerAdapter {
private List<Fragment> fragments;
public SimplePagerAdapter(FragmentManager fm, List<Fragment> fragments) {
super(fm);
this.fragments = fragments;
}
@Override
public Fragment getItem(int position) {
return fragments.get(position);
}
@Override
public int getCount() {
return fragments.size();
}
@Override
public int getItemPosition(Object object) {
return PagerAdapter.POSITION_NONE;
}
}

MainActivity代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class MainActivity extends BaseActivity {
private ViewPager mWeatherPager;
private SimplePagerAdapter mAdapter;
private List<Fragment> mFragments;
private ArrayList<City> cityList;
public static MainActivity instance;
@Override
protected void initView(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
instance = this;
mWeatherPager = (ViewPager) findViewById(R.id.id_weather_page);
}
@Override
protected void fetchData() {
try {
cityList = (ArrayList<City>) SerializeUtil.getObject(this, "cities");
} catch (IOException e) {
} catch (ClassNotFoundException e) {
} finally {
if (cityList == null || cityList.size() <= 0) {
cityList = new ArrayList<>();
City city = new City();
city.setProvince_cn(getString(R.string.default_province_cn));
city.setDistrict_cn(getString(R.string.default_district_cn));
city.setName_cn(getString(R.string.default_name_cn));
city.setName_en(getString(R.string.default_name_en));
city.setArea_id(getString(R.string.default_area_id));
cityList.add(city);
}
}
mFragments = new ArrayList<>();
for (City city : cityList) {
mFragments.add(WeatherFragment.newInstance(city.getName_cn(), city.getArea_id()));
}
mAdapter = new SimplePagerAdapter(getSupportFragmentManager(), mFragments);
mWeatherPager.setAdapter(mAdapter);
mWeatherPager.setOffscreenPageLimit(mFragments.size() - 1);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
ArrayList<City> cities = (ArrayList<City>) data.getExtras().getSerializable("cities");
if (cities.size() == 0){
City city = new City();
city.setProvince_cn(getString(R.string.default_province_cn));
city.setDistrict_cn(getString(R.string.default_district_cn));
city.setName_cn(getString(R.string.default_name_cn));
city.setName_en(getString(R.string.default_name_en));
city.setArea_id(getString(R.string.default_area_id));
cities.add(city);
}
cityList.clear();
cityList.addAll(cities);
mFragments.clear();
for (City city : cityList) {
mFragments.add(WeatherFragment.newInstance(city.getName_cn(), city.getArea_id()));
}
mAdapter.notifyDataSetChanged();
mWeatherPager.setOffscreenPageLimit(mFragments.size() - 1);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
try {
SerializeUtil.saveObject(this, cityList, "cities");
} catch (IOException e) {
}
}
public ArrayList<City> getCityList() {
return cityList;
}
}

此处用到序列化操作,把每次获取的数据在退出应用时进行保存,由于城市的ID是唯一的,故保存的文件名为城市名,序列化的具体代码可参考源码。此处启动Activity的方式采用startActivityForResult的方式,具体启动在Fragment里实现,应注意使用的是getActivity().startActivityForResult(requestCode,intent),这样返回的结果将会在Fragment依附的ActivityMainActivity里进行处理。


说了怎么多,重点来了,就是我们的WeatherFragment,对于Fragment,编译器是不推荐我们直接new一个Fragment,而是让我们自己实现newInstance的方法,并在方法里实现setArguments()方法,具体如下:

1
2
3
4
5
6
7
8
public static WeatherFragment newInstance(String cityName, String cityId) {
WeatherFragment fragment = new WeatherFragment();
Bundle args = new Bundle();
args.putString("city_name", cityName);
args.putString("city_id", cityId);
fragment.setArguments(args);
return fragment;
}

然后在onCreate()里获取参数

1
2
3
4
5
6
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
cityName = getArguments().getString("city_name");
cityId = getArguments().getString("city_id");
}

省略View的初始化过程,具体查看源码,看重点的方法fetchData()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public void fetchData() {
try {
Data data = (Data) SerializeUtil.getObject(getActivity(), cityId);
if (data != null) {
setData(data);
}
} catch (ClassNotFoundException e) {
} catch (IOException e) {
}
if (!ConnUtil.isNetConnected(getActivity())) {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
refreshView.setRefreshing(false);
Snackbar.make(indexLayout, getString(R.string.net_error), Snackbar.LENGTH_LONG)
.setAction(getString(R.string.setting), new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(Settings.ACTION_WIRELESS_SETTINGS));
}
}).show();
}
}, 1000);
return;
}
BaseApplication.getService().getWeather(cityName, cityId)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map(new Func1<Result<Data>, Data>() {
@Override
public Data call(Result<Data> dataResult) {
if (dataResult == null || dataResult.getErrNum() != 0) {
return null;
}
return dataResult.getRetData();
}
})
.subscribe(new Action1<Data>() {
@Override
public void call(Data data) {
refreshView.setRefreshing(false);
if (data == null) {
SnackbarUtil.show(indexLayout, getString(R.string.data_error));
return;
}
setData(data);
SnackbarUtil.show(indexLayout, getString(R.string.data_success));
try {
SerializeUtil.saveObject(getActivity(), data, cityId);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}

逻辑比较简单,先是从文件中获取数据展示到界面上,setData()方法是将数据与View绑定,接着判断是否连接网络,若无网络弹出提示并结束,否则从网络获取数据。subscribeOn()observeOn()分别用于切换线程。

源码地址