1. Before you begin
Most of the Android apps in the market connect to the Internet to perform some network operations. Such as retrieving emails, messages or similar information from a backend server. Gmail, YouTube, and Google Photos are some example apps that connect to the internet to display the user data.
In this codelab, you use open source developed libraries to build the network layer and get data from a backend server. This greatly simplifies fetching the data, and also helps the app conform to Android best practices, such as performing the operations on a background thread. You will also update the app's user interface if the internet is slow or unavailable; this will keep the user informed about any network connectivity issues.
What you should already know
- How to create and use fragments.
- How to use Android architecture components
ViewModel
, andLiveData
. - How to add dependencies in a gradle file.
What you'll learn
- What a REST web service is.
- Using the Retrofit library to connect to a REST web service on the internet and get a response.
- Using the Moshi library to parse the JSON response into a data object.
What you'll do
- Modify a starter app to make a web service API request and handle the response.
- Implement a network layer for your app using the Retrofit library.
- Parse the JSON response from the web service into your app's
LiveData
objects with the Moshi library. - Use Retrofit's support for coroutines to simplify the code.
What you need
- A computer with Android Studio installed.
- Starter code for the MarsPhotos app.
2. App overview
In this pathway, you work with a starter app called MarsPhotos, which shows images of Mars surface. This app connects to a web service to retrieve and display the Mars photos. The images are real-life photos from Mars captured from NASA's Mars rovers. Following is the screenshot of the final app, which contains a grid of thumbnail property images, built with a RecyclerView
.
The version of the app you build in this codelab won't have a lot of visual flash: it focuses on the networking layer part of the app to connect to the internet and download the raw property data using a web service. To ensure that the data is correctly retrieved and parsed, you'll just print the number of photos received from the backend server in a text view:
3. Explore the MarsPhotos starter app
Download starter code
This codelab provides starter code for you to extend with features taught in this codelab. Starter code may contain code that is both familiar and unfamiliar to you from previous codelabs. You will learn more about unfamiliar code in later codelabs.
If you use the starter code from GitHub, note that the folder name is android-basics-kotlin-mars-photos-app
. Select this folder when you open the project in Android Studio.
- Navigate to the provided GitHub repository page for the project.
- Verify that the branch name matches the branch name specified in the codelab. For example, in the following screenshot the branch name is main.
- On the GitHub page for the project, click the Code button, which brings up a popup.
- In the popup, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- In the Welcome to Android Studio window, click Open.
Note: If Android Studio is already open, instead, select the File > Open menu option.
- In the file browser, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
to build and run the app. Make sure it builds as expected.
Run starter code
- Open the downloaded project in Android Studio. The folder name of the project is
android-basics-kotlin-mars-photos-app
. The starter code folder structure should look like below. - In the Android pane, expand app > java. Notice that the app has a package folder called
overview
. This is the UI layer of the app.
- Run the app. When you compile and run the app, you should see the following screen with a placeholder text in the center. By the end of this codelab you will be updating this placeholder text with the number of photos retrieved.
- Browse the files to understand the starter code. For layout files, you can use the Split option in the top right corner to see a preview of the layout and the XML at the same time.
Starter code walkthrough
In this task, you familiarize yourself with the structure of the project. Here's a walkthrough of important files and folders in the project.
OverviewFragment:
- This is the fragment displayed within the
MainActivity
. The placeholder text you saw in the previous step is displayed in this fragment. - In the next codelab, this fragment will display the data received from the Mars photos backend server.
- This class holds a reference to the
OverviewViewModel
object. - The
OverviewFragment
has anonCreateView()
function that inflates thefragment_overview
layout using Data Binding, sets the binding lifecycle owner to itself, and sets theviewModel
variable in the binding object to it. - Because the lifecycle owner is assigned, any
LiveData
used in Data Binding will automatically be observed for any changes, and the UI will be updated accordingly.
OverviewViewModel:
- This is the corresponding view model for the
OverviewFragment
. - This class contains a
MutableLiveData
property named_status
along with its backing property. Updating the value of this property, updates the placeholder text displayed on the screen. - The
getMarsPhotos()
method updates the placeholder response. Later in the codelab you will use this to display the data fetched from the server. The goal for this codelab is to update thestatus
LiveData
within theViewModel
using real data you get from the internet.
res/layout/fragment_overview.xml
:
- This layout is set up to use data binding and consists of a single
TextView
. - It declares an
OverviewViewModel
variable and then binds thestatus
from theViewModel
to theTextView
.
MainActivity.kt
: the only task for this activity is to load the activity's layout, activity_main
.
layout/activity_main.xml:
this is the main activity layout with a single FragmentContainerView
pointing to the fragment_overview
, the overview fragment will be instantiated when the app is launched.
4. App Overview
In this codelab, you create a layer for the network service that communicates with the backend server and fetch the required data. You will use a third party library to implement this, called Retrofit. You will learn more on this later. The ViewModel
communicates directly with that network layer, the rest of the app is transparent to this implementation.
The OverviewViewModel
is responsible for making the network call to get the Mars photos data. In the ViewModel
, you use LiveData
with lifecycle-aware data binding to update the app UI when the data changes.
5. Web services and Retrofit
Mars photos data is stored on a web server. To get this data into your app you need to establish a connection and communicate with the server on the internet.
Most web servers today run web services using a common stateless web architecture known as REST, which stands for REpresentational State Transfer. Web services that offer this architecture are known as RESTful services.
Requests are made to RESTful web services in a standardized way via URIs. An URI (Uniform Resource Identifier) identifies a resource in the server by name, without implying its location or how to access it. For example, in the app for this lesson, you retrieve the image urls using the following server URI (This server hosts both Mars real-estate and Mars photos):
android-kotlin-fun-mars-server.appspot.com
A Uniform Resource Locator (URL) is a URI that specifies the means of acting upon or obtaining the representation of a resource, i.e. specifying both its primary access mechanism and network location.
For example:
The following URL gets a list of all available real estate properties on Mars!
https://android-kotlin-fun-mars-server.appspot.com/realestate
The following URL gets a list of Mars photos:
https://android-kotlin-fun-mars-server.appspot.com/photos
These URLs refer to a resource identified such as /realestate or /photos, that is obtainable via the Hypertext Transfer Protocol (http:) from the network. You will be using the /photos endpoint in this codelab.
Web service request
Each web service request contains a URI, and is transferred to the server using the same HTTP protocol that's used by web browsers, like Chrome. HTTP requests contain an operation to tell the server what to do.
Common HTTP operations include:
- GET for retrieving server data
- POST or PUT for add/create/update the server with new data
- DELETE for deleting data from the server
Your app will make an HTTP GET request to the server for the Mars photos information, and then the server returns a response to our app including image urls.
The response from a web service is commonly formatted in one of the common web formats like XML or JSON – formats for representing structured data in key-value pairs. You learn more about JSON in the later task.
In this task, you establish a network connection to the server, communicate with the server, and receive a JSON response. You will be using a backend server that is already written for you. In this codelab you will use the Retrofit library, a third party library to communicate with the backend server.
External Libraries
External libraries or third party libraries are like extensions to the core Android APIs. They are mostly open source, community-developed, and maintained by the collective contributions from the huge android community around the world. This enables Android developers like you to build better apps.
Retrofit Library
The Retrofit library that you are going to use in this codelab to talk to the RESTful Mars web service is a good example of a well-supported and maintained library. You can tell this by looking at its GitHub page, checkout the open issues (some of them are feature requests) and closed issues. If the developers are resolving the issues and responding to the feature requests on a regular basis, then it implies that this library is well maintained and is a good candidate to use in the app. They have a Retrofit documentation page too.
Retrofit library will communicate with the backend. It creates URI's for the web service based on the parameters we pass to it. You will see more on this in later sections.
Add Retrofit dependencies
Android Gradle allows you to add external libraries to your project. In addition to the library dependency, you should also include the repository where the library is hosted. The Google libraries such as ViewModel and LiveData from the Jetpack library are hosted in the Google repository. The majority of community libraries like Retrofit are hosted on Google and MavenCentral repositories.
- Open the project's top-level level
build.gradle(Project: MarsPhotos)
file. Notice the repositories listed under therepositories
block. You should see two repositories,google()
,mavenCentral()
.
repositories {
google()
mavenCentral()
}
- Open module level gradle file,
build.gradle (Module: MarsPhots.app)
. - In the
dependencies
section, add these lines for the Retrofit libraries:
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with Scalar Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
The first dependency is for the Retrofit2 library itself, and the second dependency is for the Retrofit scalar converter. This converter enables Retrofit to return the JSON result as a String
. The two libraries work together.
- Click Sync Now to rebuild the project with the new dependencies.
Add support for Java 8 language features
Many third party libraries including Retrofit2 use Java 8 language features. The Android Gradle plugin provides built-in support for using certain Java 8 language features.
- To use the built-in features, you need the following code in your module's
build.gradle
file. This step is already done for you, make sure the following code is present in yourbuild.gradle(Module: MarsPhotos.app)
.
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
6. Connecting to the Internet
You will use the Retrofit library to talk to the Mars web service and display the raw JSON response as a String
. The placeholder TextView
will either display the returned JSON response string or a message indicating a connection error.
Retrofit creates a network API for the app based on the content from the web service. It fetches data from the web service and routes it through a separate converter library that knows how to decode the data and return it in the form of objects like String
. Retrofit includes built-in support for popular data formats such as XML and JSON. Retrofit ultimately creates the code to call and consume this service for you, including critical details such as running the requests on background threads.
In this task, you will add a network layer to your MarsPhotos project that your ViewModel
will use to communicate with the web service. You will implement the Retrofit service API, with the following steps.
- Create a network layer, the
MarsApiService
class. - Create a Retrofit object with the base URL and the converter factory.
- Create an interface that explains how Retrofit talks to our web server.
- Create a Retrofit service and expose the instance to the api service to the rest of the app.
Implement the above steps:
- Create a new package called network. In your Android project pane, right-click on the package,
com.example.android.marsphotos
. Select New > Package. In the popup, append network to the end of the suggested package name. - Create a new Kotlin file under the new package network. Name it
MarsApiService.
- Open
network/MarsApiService.kt
. Add the following constant for the base URL for the web service.
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
- Just below that constant, add a Retrofit builder to build and create a Retrofit object.
private val retrofit = Retrofit.Builder()
Import retrofit2.Retrofit
, when prompted.
- Retrofit needs the base URI for the web service, and a converter factory to build a web services API. The converter tells Retrofit what to do with the data it gets back from the web service. In this case, you want Retrofit to fetch a JSON response from the web service, and return it as a
String
. Retrofit has aScalarsConverter
that supports strings and other primitive types, so you calladdConverterFactory()
on the builder with an instance ofScalarsConverterFactory
.
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
Import retrofit2.converter.scalars.ScalarsConverterFactory
when prompted.
- Add the base URI for the web service using
baseUrl()
method. Finally, callbuild()
to create the Retrofit object.
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
- Below the call to the Retrofit builder, define an interface called
MarsApiService
, that defines how Retrofit talks to the web server using HTTP requests.
interface MarsApiService {
}
- Inside the
MarsApiService
interface, add a function calledgetPhotos()
to get the response string from the web service.
interface MarsApiService {
fun getPhotos()
}
- Use the
@GET
annotation to tell Retrofit that this is GET request, and specify an endpoint, for that web service method. In this case the endpoint is calledphotos
. As mentioned in the previous task, you will be using /photos endpoint in this codelab.
interface MarsApiService {
@GET("photos")
fun getPhotos()
}
Import retrofit2.http.GET
when requested.
- When the
getPhotos()
method is invoked, Retrofit appends the endpointphotos
to the base URL (which you defined in the Retrofit builder) used to start the request. Add a return type of the function toString
.
interface MarsApiService {
@GET("photos")
fun getPhotos(): String
}
Object declarations
In kotlin, object declarations are used to declare singleton objects. Singleton pattern ensures that one, and only one, instance of an object is created, has one global point of access to that object. Object declaration's initialization is thread-safe and done at first access.
Kotlin makes it easy to declare singletons. Following is an example of an object declaration and its access. Object declaration always has a name following the object
keyword.
Example:
// Object declaration
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ...
}
val allDataProviders: Collection<DataProvider>
get() = // ...
}
// To refer to the object, use its name directly.
DataProviderManager.registerDataProvider(...)
The call to create() function on a Retrofit object is expensive and the app needs only one instance of Retrofit API service. So, you expose the service to the rest of the app using object declaration.
- Outside the
MarsApiService
interface declaration, define a public object calledMarsApi
to initialize the Retrofit service. This is the public singleton object that can be accessed from the rest of the app.
object MarsApi {
}
- Inside the
MarsApi
object declaration, add a lazily initialized retrofit object property namedretrofitService
of the typeMarsApiService
. You make this lazy initialization, to make sure it is initialized at its first usage. You will fix the error in the next steps.
object MarsApi {
val retrofitService : MarsApiService by lazy {
}
}
- Initialize the
retrofitService
variable using theretrofit.create()
method with theMarsApiService
interface.
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java) }
}
The Retrofit setup is done! Each time your app calls MarsApi.retrofitService
, the caller will access the same singleton Retrofit object that implements MarsApiService
which is created on the first access. In the next task, you will use the Retrofit object you have implemented.
Call the web service in OverviewViewModel
In this step, you will implement the getMarsPhotos()
method that calls the retrofit service and then handles the returned JSON string.
ViewModelScope
A ViewModelScope
is the built-in coroutine scope defined for each ViewModel
in your app. Any coroutine launched in this scope is automatically canceled if the ViewModel
is cleared.
You will use ViewModelScope
to launch the coroutine and make the Retrofit network call in the background.
- In
MarsApiService
, makegetPhotos()
a suspend function. So that you can call this method from within a coroutine.
@GET("photos")
suspend fun getPhotos(): String
- Open
overview/OverviewViewModel
. Scroll down to thegetMarsPhotos()
method. Delete the line that sets the status response to"Set the Mars API Response here!".
The methodgetMarsPhotos()
should be empty now.
private fun getMarsPhotos() {
}
- Inside
getMarsPhotos()
, launch the coroutine usingviewModelScope.launch
.
private fun getMarsPhotos() {
viewModelScope.launch {
}
}
Import androidx.lifecycle.
viewModelScope
and kotlinx.coroutines.launch
when prompted.
- Inside
viewModelScope
, use the singleton objectMarsApi
, to call thegetPhotos()
method from theretrofitService
interface. Save the returned response in aval
calledlistResult
.
viewModelScope.launch {
val listResult = MarsApi.retrofitService.getPhotos()
}
Import com.example.android.marsphotos.network.MarsApi
when prompted.
- Assign the result we just received from the backend server to the
_status.value.
val listResult = MarsApi.retrofitService.getPhotos()
_status.value = listResult
- Run the app, notice that the app closes immediately, it may or may not display an error popup.
- Click the Logcat tab in Android Studio and note the error in the log, which starts with a line like this, "
------- beginning of crash
"
--------- beginning of crash 22803-22865/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher Process: com.example.android.marsphotos, PID: 22803 java.lang.SecurityException: Permission denied (missing INTERNET permission?) ...
This error message indicates the app might be missing the INTERNET
permissions. You will resolve this by adding internet permissions to the app in the next task.
7. Add Internet permission and Exception Handling
Android Permissions
The purpose of permissions on Android is to protect the privacy of an Android user. Android apps must declare or request permissions to access sensitive user data such as contacts, call logs, and certain system features such as camera or internet.
In order for your app to access the Internet, it needs the INTERNET
permission. Connecting to the internet introduces security concerns, which is why apps do not have internet connectivity by default. You need to explicitly declare that the app needs access to the internet. This is considered a normal permission. To learn more about Android permissions and its types please refer to the documentation.
In this step, your app declares the permission(s) it requires by including <uses-permission>
tags in the AndroidManifest
file.
- Open
manifests/AndroidManifest.xml
. Add this line just before the<application>
tag:
<uses-permission android:name="android.permission.INTERNET" />
- Compile and run the app again. If you have a working internet connection, you should see the JSON text containing data related to Mars photos. You will learn more on JSON format later in the codelab.
- Tap the Back button in your device or emulator to close the app.
- Put your device or emulator into airplane mode, to simulate a network connection error. Reopen the app from the recents menu, or restart the app from Android Studio.
- Click the Logcat tab in Android Studio and note the fatal exception in the log, which looks like this:
3302-3302/com.example.android.marsphotos E/AndroidRuntime: FATAL EXCEPTION: main Process: com.example.android.marsphotos, PID: 3302 java.net.SocketTimeoutException: timeout ...
This error message indicates the application tried to connect and timed out. Exceptions like this are very common in real-time. In the next step you will learn how to handle such exceptions.
Exception Handling
Exceptions are errors that can occur during the runtime(not compile time) and terminate the app abruptly without notifying the user. This can result in a poor user experience. Exception handling is a mechanism by which you prevent the app from terminating abruptly, and handle it in a user friendly way.
The reason for exceptions could be as simple as division by zero or an error in the network. These exceptions are similar to the NumberFormatException
that you learned in a previous codelab.
Examples of potential issues while connecting to a server:
- The URL or URI used in the API is incorrect.
- The server is unavailable and the app could not connect to it.
- Network latency issue.
- Poor or no internet connection on the device.
These exceptions can't be caught during the compile time. You can use a try-catch
block to handle the exception in runtime. For further learning please refer to the documentation.
Example syntax for try-catch block
try {
// some code that can cause an exception.
}
catch (e: SomeException) {
// handle the exception to avoid abrupt termination.
}
Inside the try
block you perform the code where you anticipate an exception, in your app this would be a network call. In the catch
block, you will implement the code that prevents abrupt termination of the app. If there is an exception, then the catch
block will be executed to recover from the error instead of terminating the app abruptly.
- Open
overview/OverviewViewModel.kt
. Scroll down to thegetMarsPhotos()
method. Inside the launch block, add atry
block aroundMarsApi
call to handle exceptions. Addcatch
block after thetry
block:
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
_status.value = listResult
} catch (e: Exception) {
}
}
- Inside the
catch {}
block, handle the failure response. Display the error message to the user by setting thee.message
to the_status.
value
.
catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
- Run the app again, with the airplane mode turned on. The app does not close abruptly this time, but displays an error message instead.
- Turn off airplane mode on your phone or emulator. Run and test your app, make sure everything is working fine and you are able to see the JSON string.
8. Parse the JSON response with Moshi
JSON
The requested data is typically formatted in one of the common data formats like XML or JSON. Each call returns structured data and your app needs to know what that structure is in order to read the data from the response.
For example, in this app, you will be retrieving the data from this server: https:// android-kotlin-fun-mars-server.appspot.com/photos. If you enter this URL in the browser, you will see a list of IDs and image URLs of the surface of Mars in a JSON format!
Structure of sample JSON response:
- JSON response is an array, indicated by the square brackets. The array contains JSON objects.
- The JSON objects are surrounded by curly brackets.
- Each JSON object contains a set of name-value pairs separated by a comma.
- The name and value in a pair are separated by a colon.
- Names are surrounded by quotes.
- Values can be numbers, strings, a boolean, an array, an object (JSON object), or null.
For example the img_src
is a url, which is a string. If you paste the url into a web browser, you will see a Mars surface image.
Now you're getting a JSON response from the Mars web service, which is a great start. But what you really need are Kotlin objects, not a big JSON string. There's an external library called Moshi, which is an Android JSON parser that converts a JSON string into Kotlin objects. Retrofit has a converter that works with Moshi, so it's a great library for your purposes here.
In this task, you use the Moshi library with Retrofit to parse the JSON response from the web service into useful Kotlin objects which represent Mars photos. You will change the app so that instead of displaying the raw JSON, the app displays the number of Mars photos returned.
Add Moshi library dependencies
- Open build.gradle (Module: app).
- In the dependencies section, add the code shown below to include the Moshi dependency. This dependency adds support for the Moshi JSON library with Kotlin support.
// Moshi
implementation 'com.squareup.moshi:moshi-kotlin:1.13.0'
- Locate the lines for the Retrofit scalar converter in the
dependencies
block and change these dependencies to useconverter-moshi
:
Replace this
// Retrofit
implementation "com.squareup.retrofit2:retrofit:2.9.0"
// Retrofit with scalar Converter
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
with this
// Retrofit with Moshi Converter
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
- Click Sync Now to rebuild the project with the new dependencies.
Implement the Mars Photos data class
A sample entry of the JSON response you get from the web service looks something like this, similar to what you have seen earlier:
[{
"id":"424906",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"
},
...]
In the example above, notice that each Mars photo entry has these JSON key and value pairs:
id
: the ID of the property, as a string. Since it is wrapped in" "
it is of the typeString
notInteger
.img_src
: The image's URL as a string.
Moshi parses this JSON data and converts it into Kotlin objects. To do this, Moshi needs to have a Kotlin data class to store the parsed results, so in this step you will create the data class, MarsPhoto
.
- Right-click on the network package and select New > Kotlin File/Class.
- In the popup, select Class and enter
MarsPhoto
as the name of the class. This creates a new file calledMarsPhoto.kt
in thenetwork
package. - Make
MarsPhoto
a data class by adding thedata
keyword before the class definition. Change the {} braces to () parentheses. This leaves you with an error, because data classes must have at least one property defined.
data class MarsPhoto(
)
- Add the following properties to the
MarsPhoto
class definition.
data class MarsPhoto(
val id: String, val img_src: String
)
Notice that each of the variables in the MarsPhoto
class corresponds to a key name in the JSON object. To match the types in our specific JSON response, you use String
objects for all the values.
When Moshi parses the JSON, it matches the keys by name and fills the data objects with appropriate values.
@Json Annotation
Sometimes the key names in a JSON response can make confusing Kotlin properties, or may not match recommended coding style—for example, in the JSON file the img_src
key uses an underscore, whereas Kotlin convention for properties use upper and lowercase letters ("camel case").
To use variable names in your data class that differ from the key names in the JSON response, use the @Json
annotation. In this example, the name of the variable in the data class is imgSrcUrl
. The variable can be mapped to the JSON attribute img_src
using @Json(name = "img_src")
.
- Replace the line for the
img_src
key with the line shown below. Importcom.squareup.moshi.Json
when requested.
@Json(name = "img_src") val imgSrcUrl: String
Update MarsApiService and OverviewViewModel
In this task you will create a Moshi object using the Moshi Builder, similar to the Retrofit builder.
You will replace ScalarsConverterFactory
with the KotlinJsonAdapterFactory
to let Retrofit know it can use Moshi to convert the JSON response into Kotlin objects. You will then update the network API and ViewModel
to use the Moshi object.
- Open
network/MarsApiService.kt
. Notice the unresolved reference errors forScalarsConverterFactory
. This is because of the Retrofit dependency change you made in a previous step. Delete the import forScalarConverterFactory
. You will fix the other error soon.
Remove:
import retrofit2.converter.scalars.ScalarsConverterFactory
- At the top of the file, just before the Retrofit builder, add the following code to create the Moshi object, similar to the Retrofit object.
private val moshi = Moshi.Builder()
Import com.squareup.moshi.Moshi
and com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
when requested.
- For Moshi's annotations to work properly with Kotlin, in the Moshi builder, add the
KotlinJsonAdapterFactory
, and then callbuild()
.
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
- In the
retrofit
object declaration change the Retrofit builder to use theMoshiConverterFactory
instead of theScalarConverterFactory
, and pass in themoshi
instance you just created.
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
Import retrofit2.converter.moshi.MoshiConverterFactory
when requested.
- Now that you have the
MoshiConverterFactory
in place, you can ask Retrofit to return a list ofMarsPhoto
objects from the JSON array instead of returning a JSON string. Update theMarsApiService
interface to have Retrofit return a list ofMarsPhoto
objects, instead of returningString
.
interface MarsApiService {
@GET("photos")
suspend fun getPhotos(): List<MarsPhoto>
}
- Do similar changes to the
viewModel
, openOverviewViewModel.kt
. Scroll down togetMarsPhotos()
method. - In the method
getMarsPhotos()
,listResult
is aList<MarsPhoto>
not aString
anymore. The size of that list is the number of photos that were received and parsed. To print the number of photos retrieved update_status.
value
as follows.
_status.value = "Success: ${listResult.size} Mars photos retrieved"
Import com.example.android.marsphotos.network.MarsPhoto
when prompted.
- Make sure airplane mode is turned off in your device or emulator. Compile and run the app. This time the message should show the number of properties returned from the web service not a big JSON string:
9. Solution code
build.gradle(Module: MarsPhotos.app)
These are the new dependencies to be included.
dependencies { ... // Moshi implementation 'com.squareup.moshi:moshi-kotlin:1.13.0' // Retrofit with Moshi Converter implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' ... }
Manifests/AndroidManifest.xml
Add the internet permission, <uses-permission..>
code from the below code snippet.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.marsphotos">
<!-- In order for our app to access the Internet, we need to define this permission. -->
<uses-permission android:name="android.permission.INTERNET" />
<application
...
</application>
</manifest>
network/MarsPhoto.kt
package com.example.android.marsphotos.network
import com.squareup.moshi.Json
/**
* This data class defines a Mars photo which includes an ID, and the image URL.
* The property names of this data class are used by Moshi to match the names of values in JSON.
*/
data class MarsPhoto(
val id: String,
@Json(name = "img_src") val imgSrcUrl: String
)
network/MarsApiService.kt
package com.example.android.marsphotos.network
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.http.GET
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
/**
* Build the Moshi object with Kotlin adapter factory that Retrofit will be using.
*/
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
/**
* The Retrofit object with the Moshi converter.
*/
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
/**
* A public interface that exposes the [getPhotos] method
*/
interface MarsApiService {
/**
* Returns a [List] of [MarsPhoto] and this method can be called from a Coroutine.
* The @GET annotation indicates that the "photos" endpoint will be requested with the GET
* HTTP method
*/
@GET("photos")
suspend fun getPhotos() : List<MarsPhoto>
}
/**
* A public Api object that exposes the lazy-initialized Retrofit service
*/
object MarsApi {
val retrofitService: MarsApiService by lazy { retrofit.create(MarsApiService::class.java) }
}
Overview/OverviewViewModel.kt
package com.example.android.marsphotos.overview
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.android.marsphotos.network.MarsApi
import kotlinx.coroutines.launch
/**
* The [ViewModel] that is attached to the [OverviewFragment].
*/
class OverviewViewModel : ViewModel() {
// The internal MutableLiveData that stores the status of the most recent request
private val _status = MutableLiveData<String>()
// The external immutable LiveData for the request status
val status: LiveData<String> = _status
/**
* Call getMarsPhotos() on init so we can display status immediately.
*/
init {
getMarsPhotos()
}
/**
* Gets Mars photos information from the Mars API Retrofit service and updates the
* [MarsPhoto] [List] [LiveData].
*/
private fun getMarsPhotos() {
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getPhotos()
_status.value = "Success: ${listResult.size} Mars photos retrieved"
} catch (e: Exception) {
_status.value = "Failure: ${e.message}"
}
}
}
}
10. Summary
REST web services
- A web service is software-based functionality offered over the internet that enables your app to make requests and get data back.
- Common web services use a REST architecture. Web services that offer REST architecture are known as RESTful services. RESTful web services are built using standard web components and protocols.
- You make a request to a REST web service in a standardized way, via URIs.
- To use a web service, an app must establish a network connection and communicate with the service. Then the app must receive and parse response data into a format the app can use.
- The Retrofit library is a client library that enables your app to make requests to a REST web service.
- Use converters to tell Retrofit what to do with data it sends to the web service and gets back from the web service. For example, the
ScalarsConverter
converter treats the web service data as aString
or other primitive. - To enable your app to make connections to the internet, add the
"android.permission.INTERNET"
permission in the Android manifest.
JSON parsing
- The response from a web service is often formatted in JSON, a common format for representing structured data.
- A JSON object is a collection of key-value pairs.
- A collection of JSON objects is a JSON array. You get a JSON array as a response from a web service.
- The keys in a key-value pair are surrounded by quotes. The values can be numbers or strings.
- The Moshi library is an Android JSON parser that converts a JSON string into Kotlin objects. Retrofit has a converter that works with Moshi.
- Moshi matches the keys in a JSON response with properties in a data object that have the same name.
- To use a different property name for a key, annotate that property with the
@Json
annotation and the JSON key name.
11. Learn more
Android developer documentation:
Kotlin documentation:
- Exceptions: try, catch, finally, throw, Nothing
- Coroutines codelab
- Coroutines, official documentation
- Coroutine context and dispatchers
Other: