使用Volley创建一个火星天气应用
英文原文: Creating a Weather Application for Mars Using Volley
这篇文章是系列文章An Introduction to Volley的一部分,上一篇文章是:An Introduction to Volley。
介绍
在这篇文章中,我会演示一个从前一篇文章 中学到的有关Volley的使用案例。我们将创建一个火星天气的应用,使用{MAAS} API公开的探索者号搜集的数据。
首先,我们在Android Studio中配置好项目,并设计好用户界面。然后使用Volley完成应用的核心部分。因为每个漂亮的应用往往都有一些特色图片,所以我将演示如何使用 Flickr的api获取一张随机图片。我们将使用Volley来下载图片,主要是考虑到其强大的缓存系统。最后我们将添加一些漂亮的细节让app看起来 更美观。
- 项目设置
首先,在Android Studio中创建一个新的项目。因为Volley是向后兼容的,所以你可以选择任意级别的api。我选择的是API 21,不过只要api在8以上应该都是很ok的。
第一步: 用户界面
我们的应用只有一个简单的Activity。直接使用Android Studio建议的MainActivity.java。打开布局编辑器,双击activity_main.xml。因为屏幕70%的范围都被图片占用,剩下的部分是天气信息,所以我们要使用xml的layout_weight属性。你当然也可以使用绝对值,但是不同的设备值是不同的。如果使用一个绝对的值,在很小的设备上图像可能是90-10的比率,在较大的设备上可能是70-30甚至60-40的比率。而layout_weight属性可以解决这个问题。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.68"
android:background="#FF5722">
<!-- image -->
</RelativeLayout>
<RelativeLayout
android:layout_weight="0.33"
android:layout_height="0dp"
android:layout_width="match_parent"
android:paddingTop="@dimen/activity_horizontal_margin"
android:background="#212121">
<!-- TextViews -->
</RelativeLayout>
</LinearLayout>
在里面的第一个child中,添加ImageView:
<ImageView
android:id="@+id/main_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
在第二个RelativeLayout中,我们添加了几个TextView元素。其中两个分别显示平均温度与天气状态,第三个是显示错误信息的标签。
<TextView
android:id="@+id/error"
android:layout_centerInParent="true"
android:visibility="gone"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:textSize="20sp"
android:textColor="#FF5722"
android:layout_margin="@dimen/activity_horizontal_margin"
android:gravity="center"
android:text="I'm sorry.\\nI wasn't able to retrieve real time data."/>
<TextView
android:id="@+id/degrees"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="90sp"
android:textColor="#FF5722"
android:text="-36°"/>
<TextView
android:id="@+id/weather"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/degrees"
android:textSize="30sp"
android:gravity="center"
android:textColor="#FF5722"
android:text="Sunny"/>
现在布局已经完成了。如果你愿意,可以自己添加更多的详细信息,但是复杂和详细的用户界面并不在本教程的范围之内。
第二步: 主题和权限
在开始深入应用的核心功能之前,还要先处理两个事情。
1.将继承的主题改成android:Theme.Material.Light.NoActionBar,这样我们就不必在运行时隐藏actionbar。
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar"/>
2.在项目的manifest中添加网络权限。
<uses-permission android:name="android.permission.INTERNET" />
- 应用核心
第一步: 导入Volley
正如我在上篇文章中讲述的,使用Volley最简单且可靠的方式是将library作为一个module导入。下载library的 源代码 ,通过File > New > Module将其导入,然后在 build.gradle文件中告诉compiler把它包含在项目中。
compile project(":volley")
第二步: 实现帮助类
上篇文章 我们已经指出了,如果你想发起多个请求,最好使用共享的请求队列,要避免每次布置一个请求都使用Volley.newRequestQueue来创建一个新的队列,因为我们不想看到内存泄漏或者其他的问题发生。
为了达到这个目的,你首先需要创建一个使用了单例模式的类。这个类被作为静态的,全局的对象引用,然后由它处理RequestQueue对象。这样,整个应用中就只有一个RequestQueue。接下来,继承Application类,你需要告诉系统在应用启动的时候创建这个对象,这个过程甚至发生在第一个Activity创建之前。
因为我们是在安卓系统中,所以我们要简单的修改一下常规单例模式的结构。这个类需要在Application.onCreate方法中创建一个自己的实例 - 而不是在常规的getInstance方法中判断它为null的时候产生一个实例。
为此,我们创建一个继承自Application类的MarsWeather.java类,重写onCreate方法,初始化这个静态实例的RequestQueue对象,在这个单例类中,我们使用一个公共的synchronized方法getInstance来构造类的对象。在getInstance方法内部,返回一个mInstance变量。onCreate是在应用启动的时候触发,因此mInstance变量在getInstance方法被调用的第一时间就已经设置好了。
public class MarsWeather extends Application {
private RequestQueue mRequestQueue;
private static MarsWeather mInstance;
@Override
public void onCreate() {
super.onCreate();
mInstance = this;
mRequestQueue = Volley.newRequestQueue(getApplicationContext());
}
public static synchronized MarsWeather getInstance() {
return mInstance;
}
}
接下来,在AndroidManifest.xml文件中告诉系统你想让MarsWeather在应用启动的时候被加载,在标签下,如下添加属性名称:
android:name=".MarsWeather"
好了,一个Application类的实例就创建好了,它甚至要比MainActivity都要先创建。除了那些标准的操作,比如调用super.onCreate,onCreate还产生了一个RequestQueue的实例。除此之外,我们还需要实现另外三个方法才能完成这个帮助类。
第一个方法是getRequestQueue,替代了Volley.newRequestQueue,直接返回在onCreate中实例化了的mRequestQueue。
我们还需要一个添加请求到队列的方法add,以及负责取消请求的方法cancel。这三个方法实现如下:
public RequestQueue getRequestQueue() {
return mRequestQueue;
}
public <T> void add(Request<T> req) {
req.setTag(TAG);
getRequestQueue().add(req);
}
public void cancel() {
mRequestQueue.cancelAll(TAG);
}
TAG是一个用于识别请求的标记。在这个例子中,可以是任意值:
public static final String TAG = MarsWeather.class.getName();
第三步: 实现自定义的Request
正如你已经知道的,Volley提供了三种标准的请求类型:StringRequest, ImageRequest, 和 JsonRequest。我们的应用将使用后面的那个去获取天气数据和随机图片列表。
默认情况下,Volley将请求的优先级设置为普通。一般这也没什么问题,然而在我们的应用中,两种请求的差别很大,有必要让它们在队列中有不同的优先级。获取天气数据的优先级要比获取随机图片的高。
因此我们需要自定义JsonRequest类。创建一个继承自JsonObjectRequest的CustomJsonRequest.java,然后如下重写getPriority方法
public class CustomJsonRequest extends JsonObjectRequest {
public CustomJsonRequest(int method, String url, JSONObject jsonRequest,
Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) {
super(method, url, jsonRequest, listener, errorListener);
}
private Priority mPriority;
public void setPriority(Priority priority) {
mPriority = priority;
}
@Override
public Priority getPriority() {
return mPriority == null ? Priority.NORMAL : mPriority;
}
}
第四步: 获取数据
接下来总轮到算到了教程最有趣的部分-获取天气数据,请求的后端地址是:
http://marsweather.ingenology.com/v1/latest/
这个api是可以直接浏览的,那我们打开这个链接查看返回的JSON结果。这个JSON包含了一个简单的数据对象,由一些列的字符串组成,从温度到风向以及日落时间。
我们首先在MainActivity class中声明以下的变量:
TextView mTxtDegrees, mTxtWeather, mTxtError;
MarsWeather helper = MarsWeather.getInstance();
final static string RECENT_API_ENDPOINT = "http://marsweather.ingenology.com/v1/latest/";
你可以在onCreate之外调用MarsWeather.getInstance。既然这个类早就初始化了,你也就不必等到onStart方法执行的时候才调用它。当然界面中的view还是需要在onCreate方法中引用。
mTxtDegrees = (TextView) findViewById(R.id.degrees);
mTxtWeather = (TextView) findViewById(R.id.weather);
mTxtError = (TextView) findViewById(R.id.error);
完成这些工作之后,我们开始实现loadWeatherData方法。我们创建了一个自定义的Volley请求并设置优先级为高。然后我们调用了帮助类的add方法将请求添加进队列中。最重要的其实是result listener部分,因为它将关系到用户界面。
private void loadWeatherData() {
CustomJsonRequest request = new CustomJsonRequest
(Request.Method.GET, RECENT_API_ENDPOINT, null, new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
String minTemp, maxTemp, atmo;
int avgTemp;
response = response.getJSONObject("report");
minTemp = response.getString("min_temp"); minTemp = minTemp.substring(0, minTemp.indexOf("."));
maxTemp = response.getString("max_temp"); maxTemp = maxTemp.substring(0, maxTemp.indexOf("."));
avgTemp = (Integer.parseInt(minTemp)+Integer.parseInt(maxTemp))/2;
atmo = response.getString("atmo_opacity");
mTxtDegrees.setText(avgTemp+"°");
mTxtWeather.setText(atmo);
} catch (Exception e) {
txtError(e);
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
txtError(error);
}
});
request.setPriority(Request.Priority.HIGH);
helper.add(request);
}
你可以看到,这个方法获取了最低和最高温度,计算了平均温度,然后用它们更新了用户界面。我还实现了一个处理错误的简单方法。
private void txtError(Exception e) {
mTxtError.setVisibility(View.VISIBLE);
e.printStackTrace();
}
我们现在只需要在onCreat中调用已经完成的loadWeatherData方法,app现在已经可以显示来自火星上的天气数据了。
- 获取图片数据
既然已经完成了app的核心部分,那么我可以考虑如何让app看起来更吸引人一些。可以随机的获取一张火星图片,然后显示给用户。
第一步: 获取一张随机的图片
你需要一个Flickr API key来获取一组随机图片。图片的后端地址是:
https://api.flickr.com/services/rest/?format=json&nojsoncallback=1&
sort=random&method=flickr.photos.search&tags=mars,planet,rover&tag_mode=all&
api_key=\[YOUR_KEY\]
可以看到,这个请求非常简单。你可以告诉Flickr给你一个JSON格式的结果(format=json),但是我们并不会指定一个JSON callback(nojsoncallback=1)。你是要搜索一张图片(method=flickr.photos.search),并且感兴趣的是和火星相关的标签(tags=mars,planet,rover)。可以查阅documentation 以了解关于请求URL格式的更多信息。
声明如下变量:
final static String
FLICKR_API_KEY = "\[INSERT HERE YOUR API KEY\]",
IMAGES_API_ENDPOINT = "https://api.flickr.com/services/rest/?format=json&nojsoncallback=1&sort=random&method=flickr.photos.search&" +
"tags=mars,planet,rover&tag_mode=all&api_key=";
接下来实现searchRandomImage方法:
private void searchRandomImage() throws Exception {
if (FLICKR_API_KEY.equals(""))
throw new Exception("You didn't provide a working Flickr API!");
CustomJsonRequest request = new CustomJsonRequest
(Request.Method.GET, IMAGES_API_ENDPOINT+ FLICKR_API_KEY, null, new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
JSONArray images = response.getJSONObject("photos").getJSONArray("photo");
int index = new Random().nextInt(images.length());
JSONObject imageItem = images.getJSONObject(index);
String imageUrl = "http://farm" + imageItem.getString("farm") +
".static.flickr.com/" + imageItem.getString("server") + "/" +
imageItem.getString("id") + "_" + imageItem.getString("secret") + "_" + "c.jpg";
// TODO: do something with *imageUrl*
} catch (Exception e) { imageError(e); }
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
imageError(error);
}
});
request.setPriority(Request.Priority.LOW);
helper.add(request);
}
可以看到Flickr发送回了一个包含图片的JSONArray。在这个方法中我产生了一个在0和数组大小之间的随机数字,以随机的从图片数组中获取一张图片。从数组的索引中获取相应的item然后根据这些指导构造一个图片的URL。
跟前面一样,我们需要一个处理错误的方法:
int mainColor = Color.parseColor("#FF5722");
private void imageError(Exception e) {
mImageView.setBackgroundColor(mainColor);
e.printStackTrace();
}
最后,在onCreate中调用searchRandomImage,别忘记捕获可能出现的异常。
第二步: 显示图片
我们已经有了待加载的URL,可以开始显示图片了。从前面的文章中,你已经学会了该如何做。
private void loadImg(String imageUrl) {
// Retrieves an image specified by the URL, and displays it in the UI
ImageRequest request = new ImageRequest(imageUrl,
new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap bitmap) {
mImageView.setImageBitmap(bitmap);
}
}, 0, 0, ImageView.ScaleType.CENTER_CROP, Bitmap.Config.ARGB_8888,
new Response.ErrorListener() {
public void onErrorResponse(VolleyError error) {
imageError(error);
}
});
// we don't need to set the priority here;
// ImageRequest already comes in with
// priority set to LOW, that is exactly what we need.
helper.add(request);
}
In the onResponse method we wrote in the previous step, we are finally able to handle the result.
loadImg(imageUrl);
第三步: 每天显示一张新的图片
你可能已经注意到我们在应用每次启动的时候获取一张随机图片,绕过了Volley的缓存机制。需要想办法在一天中只显示同一张图片。最简单的办法就是使用安卓的SharedPreferences,先从声明所需的变量开始:
SharedPreferences mSharedPref;
int today = Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
final static String SHARED_PREFS_IMG_KEY = "img",
SHARED_PREFS_DAY_KEY = "day";
接下来,在onCreate方法中,在调用searchRandomImage之前初始化mSharedPref。
mSharedPref = getPreferences(Context.MODE_PRIVATE);
思路是在每次获取一张新的随机图片的时候都保存当天的日期(其实保存的是某月的第几天)。当然,我们还同时保存了该天图片的URL。当应用启动的时候,我们检查在SharedPreferences中是否有当天的入口,如果已经有匹配,我们使用保存了的URL,反之获取一张随机的图片,并在SharedPreferences中保存下URL。
相关的代码是在searchRandomImage方法中,在定义完imageUrl之后,添加如下的代码:
// right after *String imageUrl = .... *
// store the pict of the day
SharedPreferences.Editor editor = mSharedPref.edit();
editor.putInt(SHARED_PREFS_DAY_KEY, today);
editor.putString(SHARED_PREFS_IMG_KEY, imageUrl);
editor.commit();
// and then there's *loadImage(imageUrl);*
然后在onCreate方法中,mSharedPref定义之后的代码就变成了:
if (mSharedPref.getInt(SHARED_PREFS_DAY_KEY, 0) != today) {
// search and load a random mars pict
try {
searchRandomImage();
} catch (Exception e) {
// please remember to set your own Flickr API!
// otherwise I won't be able to show
// a random Mars picture
imageError(e);
}
} else {
// we already have a pict of the day: let's load it
loadImg(mSharedPref.getString(SHARED_PREFS_IMG_KEY, ""));
}
loadWeatherData();
好了,应用已经完成,可以在GitHub上免费下载本教程的完整源码。如果在这篇文章中遇到什么问题可以参考完整的源码。
**Bonus Tip: 改善用户界面
**
第一步: 字体
用户界面中的字体决定了一个应用的直观感受。我们先从改变默认的Roboto字体开始,用更好看的字体来替代,比如 Lato light。
在assets文件夹下创建一个名叫fonts的文件夹。如果你没找到assets文件夹,你需要在java文件夹的同一级目录下创建一个。文件夹的结构差不多是这样:app\src\main\assets\fonts。
将Lato-light.ttf拷贝到fonts文件夹中。在onCreate方法中你需要重写那些想使用新字体的View的默认typeface。
mTxtDegrees.setTypeface(Typeface.createFromAsset(getAssets(), "fonts/Lato-light.ttf"));
mTxtWeather.setTypeface(Typeface.createFromAsset(getAssets(), "fonts/Lato-light.ttf"));
第二步: 透明的Status Bar
根据安卓Material Design的规范,我们可以是状态栏透明,这样背景就可以透过状态栏部分可见。
你可以通过应用主题的一点小小改动达到此目的。 如下编辑v21\style.xml:
<resources>
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:windowTranslucentStatus">true</item>
</style>
</resources>
确保AndroidManifest.xml已经设置成使用这个主题:
<application
android:name=".MarsWeather"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
总结
我们完成了一个漫长的旅程。在第一篇文章中,我们讨论了Volley以及应用。这篇教程中,我们通过创建一个火星天气应用来实践了所学的基本知识。你应该对Volley库及其应用范围都有了良好的认识。
源码:GitHub 。
我整合了volley的Eclipse版本的源码,可直接导入 : http://pan.baidu.com/s/1kT6AbRL ,另外demo中的flickr
api没有key,所以无法请求到图片数据,需要你自己申请一个。