For this project, we'll create a real-world android app: a Weather App. During the development of this project, you will learn some fundamentals of Android, how it works with a third party REST API, and more.
Project Goals:
- Make an Android app with a simple and clean use of a third-party REST API to display weather information on Android UI.
- Work with JSON and JSON Object in Android.
- Use HttpURLConnection class to open a connection to the third-party API.
- Learn how fragments are used in Android.
Prerequisites:
- Android studio >3.0: You can download it here.
- OpenWeatherMap API Key: You can obtain one by signing up at the <a href = https://home.openweathermap.org/users/sign_in target="_blank"> OpenWeatherMap website. This API key is unique to a user and is used for authentication by OpenWeatherMap API.
After doing all these things, you are ready to jump into the tutorial, Let’s get started!
If you get stuck at any stage, you can download all the source files for this project here.
Step 1. Create a New Android Project
We’re going to call this application NewWeatherApp, but feel free to give it any other name of your choice. In company details, you can fill in anything. Enter a unique package name and set the target SDK to Android 5.0.
For now, this Project will have only a single Activity and will be based on a blank Activity template:
Step 2. Edit your Manifest.xml file.
Every Android application must have an AndroidManifest.xml file (with precisely that name) in its root directory. The manifest presents essential information about the application to the Android runtime. This is the information the system must have before it can run any of the application's code. Among other things, the manifest does the following:
- It names the Java package for the application. The package name serves as a unique identifier for the application.
- It describes the components of the application — the activities, services, broadcast receivers, and content providers that the application is composed of. These declarations let the Android system know what the components are and under what conditions they can be launched.
- It declares which permissions the application must have in order to access protected parts of the API and interact with other applications.
- It also declares the permissions that others are required to have in order to interact with the application's components.
- It lists the libraries that the application must be linked against.
The only permission you need is Internet permission. You need this in order to make API calls over the internet connection (line 4):
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.commonlounge.newweatherapp"><uses-permission android:name="android.permission.INTERNET"/><applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"...
Android has two orientations: Portrait Mode and Landscape Mode. When user switches from one mode to another, the current activity reloads on its own, resulting in removing all the cached data and user filled information in previous mode. Working on both orientations results in handling both the layouts separately which increases workload for the developer. To keep this tutorial simple, we're only going to support Portrait Mode for our app.
After making the changes for Internet permission and support for portrait mode only, your manifest code should look something like this:
<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.redorigami.simpleweather"android:versionCode="1"android:versionName="1.0" ><uses-sdkandroid:minSdkVersion="13"android:targetSdkVersion="19" /><uses-permission android:name="android.permission.INTERNET"/><applicationandroid:allowBackup="true"android:icon="@drawable/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme"><activityandroid:name="common.newweatherapp.WeatherActivity"android:label="@string/app_name"android:screenOrientation="portrait" ><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application></manifest>
Step 3. Edit the Activity’s layout
There isn't much to change in activity_weather.xml. It should already have a FrameLayout. Add a property to change the color of the background to any colour of your choice.
activity_weather.xml should look something like this:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:id="@+id/container"android:layout_width="match_parent"android:layout_height="match_parent"tools:ignore="MergeRootFrame"android:background="@color/cardview_dark_background" />
Step 4. Edit the fragment_weather.xml
Edit fragment_weather.xml by adding five TextView tags to show the following information:
- city and country
- current temperature
- an icon showing the current weather condition
- a timestamp telling the user when the weather information was last updated
- more detailed information about the current weather, such as description and humidity
- a RelativeLayout to arrange the text views. You can adjust the textSize to suit various devices
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:paddingBottom="@dimen/activity_vertical_margin"android:paddingLeft="@dimen/activity_horizontal_margin"android:paddingRight="@dimen/activity_horizontal_margin"android:paddingTop="@dimen/activity_vertical_margin"tools:context="common.newweatherapp.WeatherActivity" ><TextViewandroid:id="@+id/city_field"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentTop="true"android:layout_centerHorizontal="true"android:textAppearance="?android:attr/textAppearanceLarge" /><TextViewandroid:id="@+id/updated_field"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@+id/city_field"android:layout_centerHorizontal="true"android:textAppearance="?android:attr/textAppearanceMedium"android:textSize="13sp" /><TextViewandroid:id="@+id/weather_icon"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_centerVertical="true"android:layout_centerHorizontal="true"android:textAppearance="?android:attr/textAppearanceLarge"android:textSize="70sp"/><TextViewandroid:id="@+id/current_temperature_field"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_alignParentBottom="true"android:layout_centerHorizontal="true"android:textAppearance="?android:attr/textAppearanceLarge"android:textSize="40sp" /><TextViewandroid:id="@+id/details_field"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_below="@+id/weather_icon"android:layout_centerHorizontal="true"android:textAppearance="?android:attr/textAppearanceMedium"/></RelativeLayout>
Step 5. Add a menu item to change the city.
Edit menu/weather.xml and add an item for this option.
<menu xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"tools:context="com.redorigami.simpleweather.WeatherActivity" ><itemandroid:id="@+id/change_city"android:orderInCategory="1"android:title="@string/change_city"app:showAsAction="ifRoom"/></menu>
Step 6: Fetch data from OpenWeatherAPI
By using this API, we can fetch current weather reports in JSON format. In the query string, we pass the city's name and the results should be returned in the metric system.
You can also specify the city code to get the exact location’s weather too.
For example, to get the current weather information for Allahabad, India, using the metric system, we can send a request to,
http://api.openweathermap.org/data/2.5/weather?q=Allahabad&units=metric
The response we get back from API looks like this:
Create a new Java class and name it RemoteFetch.java. This class will be in the “java” directory in the root of your project. This class is responsible for fetching the weather data from the OpenWeatherMap API.
We’ll start this class with defining a final string having URL where our android application will hit in order to get the desired data.
We use the HttpURLConnection class to make the remote request. The OpenWeatherMap API expects the API key in an HTTP header named “x-api-key”. We will specify this in our request using the setRequestProperty method.
We use a BufferedReader to read the API's response into a StringBuffer. When we have the complete response, we convert it to a JSONObject object.
As you can see in the above response from the OpenWeatherAPI, the JSON data contains a field named cod. Its value is 200 if the request was successful. We use this value to check whether the JSON response has the current weather information or not.
The RemoteFetch.java class should look like this:
package common.newweatherapp;import java.io.BufferedReader;import java.io.InputStreamReader;import java.net.HttpURLConnection;import java.net.URL;import org.json.JSONObject;import android.content.Context;import com.redorigami.simpleweather.R;public class RemoteFetch {private static final String OPEN_WEATHER_MAP_API ="http://api.openweathermap.org/data/2.5/weather?q=%s&units=metric";public static JSONObject getJSON(Context context, String city){try {URL url = new URL(String.format(OPEN_WEATHER_MAP_API, city));HttpURLConnection connection =(HttpURLConnection)url.openConnection();connection.addRequestProperty("x-api-key",context.getString(R.string.open_weather_maps_app_id));BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));StringBuffer json = new StringBuffer(1024);String tmp="";while((tmp=reader.readLine())!=null)json.append(tmp).append("\n");reader.close();JSONObject data = new JSONObject(json.toString());if(data.getInt("cod") != 200){return null;}return data;}catch(Exception e){return null;}}}
Step 7: Store the City as a Preference
The user shouldn't have to specify the name of the city every time they want to use the app. The app should remember the last city the user was interested in. We do this by making use of SharedPreferences. However, instead of directly accessing these preferences from our Activity class, it is better to create a separate class for this purpose.
Create a new Java class and name it CityPreference.java. To store and retrieve the name of the city, create two methods setCity and getCity.
Android provides many ways of storing data of an application. One of these ways is called Shared Preferences. Shared Preferences allow you to save and retrieve data in the form of a {key,value} pair.
In order to use shared preferences, you have to call a method getSharedPreferences() that returns a SharedPreference instance pointing to the file that contains the values of preferences.
In order to keep the sharedPreferences file private to our application so that no other application can access this file, we keep the file mode to private.
The SharedPreferences object is initialized in the constructor.
the getString method in getCity method retrieves the value stored in sharedPreferences with key as “city”
the setString method in setCity method puts the value in sharedPreferences with key as “city”
The CityPreference.java class should look like this:
package common.newweatherapp;import android.app.Activity;import android.content.SharedPreferences;public class CityPreference {SharedPreferences prefs;public CityPreference(Activity activity){prefs = activity.getPreferences(Activity.MODE_PRIVATE);}public String getCity(){return prefs.getString("city", "Jerusalem, IL");}void setCity(String city){prefs.edit().putString("city", city).commit();}}
Step 8: Create Fragment class
Create a new Java class and name it WeatherFragment.java. A Fragment is a piece of an activity which enables more modular design of an activity, a fragment is kind of a sub-activity. It has its own layout and its own behavior with its own life-cycle callbacks. This fragment uses fragment_weather.xml as its layout that we defined in Step 4. Declare the five TextView objects and initialize them in the onCreateView method.
We will be making use of a Thread to asynchronously fetch data from the OpenWeatherMap API. We cannot update the user interface from such a background thread. We therefore need a Handler object, which we initialize in the constructor of the WeatherFragment class.
We’re using the inflator class to access the View of the fragment layout.
The inflator class has a function inflator.inflate() that is declared as:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
If attachToRoot parameter is set to true, then the layout file specified in the first parameter is inflated and attached to the ViewGroup specified in the second parameter.
Then the method returns this combined View, with the ViewGroup as the root. When attachToRoot is false, the layout file from the first parameter is inflated and returned as a View. The root of the returned View would simply be the root specified in the layout file. In either case, the ViewGroup’s LayoutParams are needed to correctly size and position the View created from the layout file.
Passing in true for attachToRoot results in a layout file’s inflated View being added to the ViewGroup right on the spot. Passing in false for attachToRoot means that the View created from the layout file will get added to the ViewGroup in some other way.
package common.newweatherapp;import java.text.DateFormat;import java.util.Date;import java.util.Locale;import org.json.JSONObject;import android.graphics.Typeface;import android.os.Bundle;import android.os.Handler;import android.support.v4.app.Fragment;import android.util.Log;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.TextView;import android.widget.Toast;import com.redorigami.simpleweather.R;public class WeatherFragment extends Fragment {Typeface weatherFont;private TextView cityField;private TextView weatherIcon;private TextView updatedField;private TextView currentTemperatureField;private TextView detailsField;Handler handler;public WeatherFragment(){handler = new Handler();}@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {View rootView = inflater.inflate(R.layout.fragment_weather, container, false);updatedField = (TextView)rootView.findViewById(R.id.updated_field);cityField = (TextView)rootView.findViewById(R.id.city_field);currentTemperatureField = (TextView)rootView.findViewById(R.id.current_temperature_field);weatherIcon = (TextView)rootView.findViewById(R.id.weather_icon);detailsField = (TextView)rootView.findViewById(R.id.details_field);return rootView;}}
Next, we will be creating a function updateWeatherData We start a new thread and call getJSON on the RemoteFetch class. If the value returned by getJSON is null, we display an error message to the user. If it isn't, we invoke the renderWeather method.
Only the main Thread can update the user interface of an Android app. Calling Toast (Toast class is used to show notification for a particular interval of time. After sometime it disappears. It doesn't block the user interaction) or renderWeather directly from the background thread would lead to a runtime error. That is why we call these methods using the handler's post method.
The renderWeather method uses the JSON data to update the TextView objects. The weather node of the JSON response is an array of data. In this tutorial, we will only be using the first element of the array of weather data.
private void updateWeatherData(final String city){new Thread(){public void run(){final JSONObject json = RemoteFetch.getJSON(getActivity(), city);if(json == null){handler.post(new Runnable(){public void run(){Toast.makeText(getActivity(),getActivity().getString(R.string.place_not_found),Toast.LENGTH_LONG).show();}});} else {handler.post(new Runnable(){public void run(){renderWeather(json);}});}}}.start();}private void renderWeather(JSONObject json){try {cityField.setText(json.getString("name").toUpperCase(Locale.US) +", " +json.getJSONObject("sys").getString("country"));JSONObject details = json.getJSONArray("weather").getJSONObject(0);JSONObject main = json.getJSONObject("main");detailsField.setText(details.getString("description").toUpperCase(Locale.US) +"\n" + "Humidity: " + main.getString("humidity") + "%" +"\n" + "Pressure: " + main.getString("pressure") + " hPa");currentTemperatureField.setText(String.format("%.2f", main.getDouble("temp"))+ " ℃");DateFormat df = DateFormat.getDateTimeInstance();String updatedOn = df.format(new Date(json.getLong("dt")*1000));updatedField.setText("Last update: " + updatedOn);/*setWeatherIcon(details.getInt("id"),json.getJSONObject("sys").getLong("sunrise") * 1000,json.getJSONObject("sys").getLong("sunset") * 1000);*/}catch(Exception e){Log.e("SimpleWeather", "Field not present in JSON Received");}}
Lastly, add a changeCity method to the fragment to let the user update the current city. The changeCity method will be called from the main Activity class.
public void changeCity(String city){updateWeatherData(city);}
Step 9: Edit the WeatherActivity
In the main activity, we must add the fragment to this activity to make a virtual connection between the activity and the fragment. We will use getSupportFragmentmanager class to commit the fragment to this activity.
if (savedInstanceState == null) {getSupportFragmentManager().beginTransaction().add(R.id.container, new WeatherFragment()).commit();}
Now, override the onCreateOptionsMenu and onOptionItemSelected methods
In onCreateOptionsMenu, Inflate the menu; this adds items to the action bar if it is present. In onOptionItemSelected, handle the only menu option we have. All you must do here is invoke the showDialog method.
In the showDialog method, we use AlertDialog.Builder to create a Dialog object that prompts the user to enter the name of a city. This information is passed on to the changeCity method, which stores the name of the city using the CityPreference class and calls the Fragment's changeCity method.
@Overridepublic boolean onCreateOptionsMenu(Menu menu) {// Inflate the menu; this adds items to the action bar if it is present.getMenuInflater().inflate(R.menu.weather, menu);return true;}@Overridepublic boolean onOptionsItemSelected(MenuItem item) {if(item.getItemId() == R.id.change_city){showInputDialog();}return false;}private void showInputDialog(){AlertDialog.Builder builder = new AlertDialog.Builder(this);builder.setTitle("Change city");final EditText input = new EditText(this);input.setInputType(InputType.TYPE_CLASS_TEXT);builder.setView(input);builder.setPositiveButton("Go", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {changeCity(input.getText().toString());}});builder.show();}public void changeCity(String city){WeatherFragment wf = (WeatherFragment)getSupportFragmentManager().findFragmentById(R.id.container);wf.changeCity(city);new CityPreference(this).setCity(city);}