Introduction
In this tutorial, I will show you a possible use case of what we learnt in the previous article about Volley. We will create a weather application for Mars, using the information collected by the Curiosity rover, which is made available to everyone by NASA through the {MAAS} API.
First, we will set up the project in Android Studio and design the user interface. We will then structure the core of the application using Volley. Since every beautiful application features some images, I will show you how to fetch a random one using Flickr's API. We will download the picture with Volley, mostly because of its great caching system. Finally, we will add some fancy details to give the application a gorgeous look and feel.
1. Project Setup
First, create a new project in Android Studio. Since Volley is backwards compatible, you can choose whatever API level you prefer. I opted for API 21, but you should be fine as long as the API level is 8 (Froyo) or higher.
Step 1: User Interface
Our application has a single, simple activity. You can call it MainActivity.java, as suggested by Android Studio. Open the layout editor and double-click activity_main.xml.
Since we would like to have about 70% of the screen dedicated to the image and
the rest to the weather information, we need to use the XML attribute layout_weight
. Of course, we can use absolute
values too, but it wouldn't be the same. Unfortunately,
the Android world features displays that are anything but homogenous, and
specifying an absolute value for the height of the image could result in a 90-10 ratio on very small
devices and a 70-30, or even a 60-40 relation, on larger devices. The layout_weight
attribute is what you need to solve this problem.
<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>
Inside the
first child, add the ImageView
:
<ImageView android:id="@+id/main_bg" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop"/>
In the
second RelativeLayout
, we add a list of TextView
items. Two of them are views in which
the average temperature and the atmosphere opacity are shown. The third is an error label.
<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"/>
The layout should now be complete. You can add more details if you want, but a complex and detailed user interface is not within the scope of this tutorial.
Step 2: Theme and Permissions
There are two more things we need to take care of before starting to dig into the core of the application. Change the inherited theme of the application to android:Theme.Material.Light.NoActionBar
. This means that we don't need to hide the action bar at run time.
<style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar"/>
Finally, add the internet permission to the project's manifest.
<uses-permission android:name="android.permission.INTERNET" />
2. Application Core
Step 1: Import Volley
As we discussed in the previous article, the simplest and most reliable way to use Volley is by importing the library as a new module. Download the source code of the library, import it via File > New > Module, and tell the compiler in the project's build.gradle file to include it in the project.
compile project(":volley")
Step 2: Implement Helper Class
As I
already pointed out in the previous article, if you need to fire multiple requests, it is better to use a shared request queue. You should avoid creating a request queue each time you schedule a request by invoking Volley.newRequestQueue
, because you don't want to end up with memory leaks and other unwanted problems.
To do that, you first have to create a class using the singleton pattern. The class is referenced using a static, globally visible variable, which then handles the object RequestQueue
. This way, you end up with a single RequestQueue
for the application. Then, extending the Application
class, you have to tell to the operating system to generate this object at application startup, even before the first activity is created.
Since we're in the Android environment, we slightly modify the common singleton structure. The class needs to create a new instance of itself in the Application.onCreate
method—not in a generic getInstance
method when it is null
.
To achieve
this, create a new class and name it MarsWeather.java. Next, extend the Android Application
class, override theonCreate
method, and initialize the RequestQueue
object of the
static instance.
In the singleton class, we construct the object of the class using a public
and synchronized
function getInstance
. Inside this method, we return the mInstance
variable. The onCreate
method is invoked when the application is started so the mInstance
variable will already be set the first time the getInstance
method is invoked.
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; } }
Next,
tell in the AndroidManifest.xml file that you want MarsWeather
to be loaded at application startup. In the <application>
tag, add the attribute name
as follows:
android:name=".MarsWeather"
That's it. An instance of the Application
class is created, even before MainActivity
is created. Along with all the other standard operations, onCreate
generates an instance of the RequestQueue
.
We need to implement three other methods to finish up the helper class. The first method replaces Volley.newRequestQueue
, which I'll name getRequestQueue
. We also need a method to add a request to the queue, add
, and a method that's responsible
for canceling requests, cancel
. The following code block shows what the implementation looks like.
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
is a generic token you use to identify the request. In this specific case, it can be whatever you want:
public static final String TAG = MarsWeather.class.getName();
Step 3: Implement the Custom Request
As you
already know, Volley provides three standard request types: StringRequest
, ImageRequest
, and JsonRequest
. Our application is going to use the latter to fetch weather data and retrieve the list of random images.
By default, Volley sets
the priority of the request to NORMAL
. Usually that would be
fine, but in our application we have two requests that are quite different and we therefore need to have a different priority in the
queue. Fetching the weather data needs to have a higher priority than fetching the URL of the random image.
For that reason, we need to customize theJsonRequest
class. Create a new
class named CustomJsonRequest.java, and make sure it extends JsonObjectRequest
. Next,
override the getPriority
method as shown below.
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; } }
Step 4: Fetching Data
We’re finally arrived to the most interesting part of this tutorial in which we write the implementation to fetch the weather data. The endpoint of the request is:
http://marsweather.ingenology.com/v1/latest/
The APIs are browsable so open the link to inspect the resulting JSON. The JSON contains a simple object,
result
, that includes a series of strings, ranging from temperatures to
wind direction and sunset time.
Start by declaring the following variables in the MainActivity
class:
TextView mTxtDegrees, mTxtWeather, mTxtError; MarsWeather helper = MarsWeather.getInstance(); final static string RECENT_API_ENDPOINT = "http://marsweather.ingenology.com/v1/latest/";
You can call MarsWeather.getInstance
outside of onCreate
. Since the class will already be initialized, you don’t need to wait for the onStart
method to call it. Of course, you have to set the references of the user interface views in the onCreate
method.
mTxtDegrees = (TextView) findViewById(R.id.degrees); mTxtWeather = (TextView) findViewById(R.id.weather); mTxtError = (TextView) findViewById(R.id.error);
After
doing that, it's time to implement the loadWeatherData
method. We create a custom Volley request and set the priority to HIGH
. We then invoke the helper's add
method to add it to the request queue. The important thing to note is the result listener, since it's going to affect the user interface.
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); }
As you can see, the method takes the minimum and maximum temperatures, computes the average temperature, and update the user interface. I also implemented a simple method to handles errors.
private void txtError(Exception e) { mTxtError.setVisibility(View.VISIBLE); e.printStackTrace(); }
We now only need to call
loadWeatherData
in onCreate
and you're done. The app is now
ready to show the weather of Mars.
3. Fetching Image Data
Now that you have the core of the app ready and working, we can focus on making the app visually more appealing. We are going to do this by fetching a random Mars image and displaying it to the user.
Step 1: Fetch a Random Picture
You will need a Flickr API key to fetch a random list of contextualized images. The image endpoint is the following:
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]
As you can
see, the request is fairly simple. You are telling Flickr to give you
results formatted as JSON (format=json
), but we don't specify a JSON callback (nojsoncallback=1
). You are searching an image (method=flickr.photos.search
) and the tags you are interested in are related to Mars (tags=mars,planet,rover
). Take a look at the documentation for more information about the format of the request URL.
Start by declaring the following variables:
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=";
Next, implement the searchRandomImage
method:
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); }
As you can see, Flickr sends back a JSONArray
containing the images. The method I wrote to fetch a random image generates a random number between zero and to the size of the array. It takes the item corresponding to that index from the array of results and constructs the URL for the image following these guidelines.
Like before, we need a method for error handling:
int mainColor = Color.parseColor("#FF5722"); private void imageError(Exception e) { mImageView.setBackgroundColor(mainColor); e.printStackTrace(); }
Finally, call searchRandomImage
in the onCreate
method and don't forget to catch any exceptions.
Step 2: Show the Picture
Now that we
have an URL to load, we can show the picture. You already learned how to do this in the previous article.
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);
Step 3: Showing a New Image Every Day
Maybe you already noticed that we are bypassing Volley's caching system by fetching a random image every time the application is launched. We need to find a way to show the same image on a particular day.
The simplest way to achieve this is by using Android’s
SharedPreferences
. Start by declaring the variables we'll need for this.
SharedPreferences mSharedPref; int today = Calendar.getInstance().get(Calendar.DAY_OF_MONTH); final static String SHARED_PREFS_IMG_KEY = "img", SHARED_PREFS_DAY_KEY = "day";
Next,
in the onCreate
method, before the call to searchRandomImage
, initialize mSharedPref
.
mSharedPref = getPreferences(Context.MODE_PRIVATE);
The idea is to store
the current day every time we fetch a new random picture. Of course, we store the URL of the image alongside the day. When the application launches, we check whether we already have an entry in the SharedPreferences
for the current day. If we have a match, we use the stored URL. Otherwise we fetch a random image and store its URL in the SharedPreferences
.
In searchRandomImage
, after the
definition of imageUrl
, add the following lines of code:
// 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);*
TheonCreate
method, after the definition on mSharedPref
, now becomes:
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();
That's it. Your application is ready. Feel free to download the source files of this tutorial on GitHub to see the completed project. Take a look at the project it if you're running into issues.
Bonus Tip: Improving the User Interface
Step 1: Font
The font used in a user interface often determines the look and feel of an application. Let's start by changing the default Roboto font with a more appealing font, such as Lato light.
Create a new folder named fonts in the assets folder. If you can’t find the assets folder, you have to create it at the same level as the java folder. The folder structure should look something like app\src\main\assets\fonts.
Copy the file Lato-light.ttf in the fonts folder. In the onCreate
method, you need to override the default typeface of the views in which you'd like to use the new font.
mTxtDegrees.setTypeface(Typeface.createFromAsset(getAssets(), "fonts/Lato-light.ttf")); mTxtWeather.setTypeface(Typeface.createFromAsset(getAssets(), "fonts/Lato-light.ttf"));
Step 2: Transparent Status Bar
Following the guidelines for Android Material Design, we can make the status bar
transparent. This way, the background will be partially visible through the status bar.
You can
achieve this by making a small change in the application's theme. Edit the project's v21\style.xml file like this:
<resources><style name="AppTheme" parent="android:Theme.Material.Light.NoActionBar"><item name="android:windowTranslucentStatus">true</item></style></resources>
Make sure that the AndroidManifest.xml is already set to use the theme:
<application android:name=".MarsWeather" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" >
Conclusion
We made a long journey. In the first article, we started talking about Volley and its applications. In this tutorial, we looked at a practical way to implement the concepts we learned by building a weather application for Mars. You should now have a good understanding of the Volley library, how it works, and what you can use it for.