Hard Prerequisites |
|
In this project (and the following projects), you work with a starter app called MarsRealEstate, which shows properties for sale on Mars. This app connects to a web service to retrieve and display the property data, including details such as the price and whether the property is available for sale or rent. The images representing each property are real-life photos from Mars captured from NASA’s Mars rovers.
The version of the app you build in this project 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 properties on Mars in a text view:
The architecture for the MarsRealEstate app has two main modules:
The app has a ViewModel for each fragment. For this project, you create a layer for the network service, and the ViewModel communicates directly with that network layer. This is similar to what you did in previous projects when the ViewModel communicated with the Room database.
The overview ViewModel is responsible for making the network call to get the Mars real estate information. The detail ViewModel holds details for the single piece of Mars real estate that’s displayed in the detail fragment. For each ViewModel, you use LiveData with lifecycle-aware data binding to update the app UI when the data changes.
You use the Navigation component to both navigate between the two fragments, and to pass the selected property as an argument.
In this task, you download and run the starter app for MarsRealEstate and familiarize yourself with the structure of the project.
Open app/java/overview/OverviewFragment.kt. The OverviewFragment lazily initializes the OverviewViewModel, which means the OverviewViewModel is created the first time it is used.
Examine the onCreateView() method. This method inflates the fragment_overview layout using data binding, sets the binding lifecycle owner to itself (this), and sets the viewModel variable in the binding object to it. Because we’ve set the lifecycle owner, any LiveData used in data binding will automatically be observed for any changes, and the UI will be updated accordingly.
Open app/java/overview/OverviewViewModel. Because the response is a LiveData and we’ve set the lifecycle for the binding variable, any changes to it will update the app UI.
Examine the init block. When the ViewModel is created, it calls the getMarsRealEstateProperties() method.
Examine the getMarsRealEstateProperties() method. In this starter app, this method contains a placeholder response. The goal for this project is to update the response LiveData within the ViewModel using real data you get from the internet.
Open app/res/layout/fragment_overview.xml. This is the layout for the overview fragment you work with in this project, and it includes the data binding for the view model. It imports the OverviewViewModel and then binds the response from the ViewModel to a TextView. In later projects, you replace the text view with a grid of images in a RecyclerView.
Compile and run the app. All you see in the current version of this app is the starter response—”Set the Mars API Response here!”
The Mars real estate data is stored on a web server, as a REST web service. Web services use the REST architecture are built using standard web components and protocols.
You make a request to a web service in a standardized way via URIs. The familiar web URL is actually a type of URI, and both are used interchangeably throughout this course. For example, in the app for this lesson, you retrieve all the data from the following server:
https://android-kotlin-fun-mars-server.appspot.com
If you type the following URL in your browser, you get a list of all available real estate properties on Mars!
https://android-kotlin-fun-mars-server.appspot.com/realestate
The response from a web service is commonly formatted in JSON, an interchange format for representing structured data. You learn more about JSON in the next task, but the short explanation is that a JSON object is a collection of key-value pairs, sometimes called a dictionary, a hash map, or an associative array. A collection of JSON objects is a JSON array, and it’s the array you get back as a response from a web service.
To get this data into the app, your app needs to establish a network connection and communicate with that server, and then receive and parse the response data into a format the app can use. In this projects, you use a REST client library called Retrofit to make this connection.
Open build.gradle (Module: app).
In the dependencies section, add these lines for the Retrofit libraries:
implementation “com.squareup.retrofit2:retrofit:$version_retrofit”
implementation “com.squareup.retrofit2:converter-scalars:$version_retrofit”
Notice that the version numbers are defined separately in the project Gradle file. The first dependency is for the Retrofit 2 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.
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 these built-in features, update the module’s build.gradle file, as shown below:
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
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 useful objects. Retrofit includes built-in support for popular web data formats such as XML and JSON. Retrofit ultimately creates most of the network layer for you, including critical details such as running the requests on background threads.
The MarsApiService class holds the network layer for the app; that is, this is the API that your ViewModel will use to communicate with the web service. This is the class where you will implement the Retrofit service API.
private const val BASE_URL =
"https://android-kotlin-fun-mars-server.appspot.com"
private val retrofit = Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.baseUrl(BASE_URL)
.build()
Retrofit needs at least two things available to it to build a web services API: the base URI for the web service, and a converter factory. The converter tells Retrofit what 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 a ScalarsConverter that supports strings and other primitive types, so you call addConverterFactory() on the builder with an instance of ScalarsConverterFactory. Finally, you call build() to create the Retrofit object.
interface MarsApiService {
@GET("realestate")
fun getProperties():
Call<String>
}
Right now the goal is to get the JSON response string from the web service, and you only need one method to do that: getProperties(). To tell Retrofit what this method should do, use a @GET annotation and specify the path, or endpoint, for that web service method. In this case the endpoint is called realestate. When the getProperties() method is invoked, Retrofit appends the endpoint realestate to the base URL (which you defined in the Retrofit builder), and creates a Call object. That Call object is used to start the request.
object MarsApi {
val retrofitService : MarsApiService by lazy {
retrofit.create(MarsApiService::class.java) }
}
private fun getMarsRealEstateProperties() {
_response.value = "Set the Mars API Response here!"
}
This is the method where you’ll call the Retrofit service and handle the returned JSON string. Right now there’s just a placeholder string for the response.
Delete the placeholder line that sets the response to “Set the Mars API Response here!”
Inside getMarsRealEstateProperties(), add the code shown below. Import retrofit2.Callback and com.example.android.marsrealestate.network.MarsApi when requested.
The MarsApi.retrofitService.getProperties() method returns a Call object. Then you can call enqueue() on that object to start the network request on a background thread.
MarsApi.retrofitService.getProperties().enqueue(
object: Callback<String> {
})
Android Studio adds the code with TODOs in each method:
override fun onFailure(call: Call<String>, t: Throwable) {
TODO("not implemented")
}
override fun onResponse(call: Call<String>,
response: Response<String>) {
TODO("not implemented")
}
In onFailure(), delete the TODO and set the _response to a failure message, as shown below. The _response is a LiveData string that determines what’s shown in the text view. Each state needs to update the _response LiveData.
override fun onFailure(call: Call<String>, t: Throwable) {
_response.value = "Failure: " + t.message
}
override fun onResponse(call: Call<String>,
response: Response<String>) {
_response.value = response.body()
}
Click the Logcat tab in Android Studio and note the error in the log, which starts with a line like this:
Process: com.example.android.marsrealestate, PID: 10646
java.lang.SecurityException: Permission denied (missing INTERNET permission?)
The error message tells you that your app might be missing 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 tell Android that the app needs access to the internet.
<uses-permission android:name="android.permission.INTERNET" />
Compile and run the app again. If everything is working correctly with your internet connection, you see JSON text containing Mars Property data.
Tap the Back button in your device or emulator to close the app.
Put your device or emulator into airplane mode, and then reopen the app from the Recents menu, or restart the app from Android Studio.
Turn airplane mode off again.
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 a 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 Mars Property Kotlin objects. You change the app so that instead of displaying the raw JSON, the app displays the number of Mars Properties returned.
pen build.gradle (Module: app).
In the dependencies section, add the code shown below to include the Moshi dependency. As with Retrofit, $version_moshi is defined separately in the project-level Gradle file. This dependency adds support for the Moshi JSON library with Kotlin support.
implementation “com.squareup.moshi:moshi-kotlin:$version_moshi”
Locate the lines for the Retrofit scalar converter in the dependencies block:
implementation “com.squareup.retrofit2:retrofit:$version_retrofit”
implementation “com.squareup.retrofit2:converter-scalars:$version_retrofit”
Change these lines to use converter-moshi:
implementation “com.squareup.retrofit2:converter-moshi:$version_retrofit”
Click Sync Now to rebuild the project with the new dependencies.
Note: The project may show compiler errors related to the removed Retrofit scalar dependency. You fix those in the next steps.
A sample entry of the JSON response you get from the web service looks something like this:
[{"price":450000,
"id":"424906",
"type":"rent",
"img_src":"http://mars.jpl.nasa.gov/msl-raw-images/msss/01000/mcam/1000ML0044631300305227E03_DXXX.jpg"},
...]
The JSON response shown above is an array, which is indicated by the square brackets. The array contains JSON objects, which are surrounded by curly braces. Each object contains a set of name-value pairs, separated by colons. Names are surrounded by quotes. Values can be numbers or strings, and strings are also surrounded by quotes. For example, the price for this property is $450,000 and the img_src is a URL, which is the location of the image file on the server.
In the example above, notice that each Mars property entry has these JSON key and value pairs:
price: the price of the Mars property, as a number.
id: the ID of the property, as a string.
type: either "rent" or "buy".
img_src: The image's URL as a string.
Moshi parses this JSON data and converts it into Kotlin objects. To do this, it needs to have a Kotlin data class to store the parsed results, so the next step is to create that class.
Open app/java/network/MarsProperty.kt.
Replace the existing MarsProperty class definition with the following code:
data class MarsProperty(
val id: String, val img_src: String,
val type: String,
val price: Double
)
Notice that each of the variables in the MarsProperty class corresponds to a key name in the JSON object. To match the types in the JSON, you use String objects for all the values except price, which is a Double. A Double can be used to represent any JSON number.
When Moshi parses the JSON, it matches the keys by name and fills the data objects with appropriate values.
@Json(name = "img_src") val imgSrcUrl: String,
Sometimes the key names in a JSON response can make confusing Kotlin properties, or may not match your coding style—for example, in the JSON file the img_src key uses an underscore, whereas Kotlin properties commonly 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 is mapped to the JSON attribute img_src using @Json(name = “img_src”).
With the MarsProperty data class in place, you can now update the network API and ViewModel to include the Moshi data.
Open network/MarsApiService.kt. You may see missing-class errors for ScalarsConverterFactory. This is because of the Retrofit dependency change you made in Step 1. You fix those errors soon.
At the top of the file, just before the Retrofit builder, add the following code to create the Moshi instance. Import com.squareup.moshi.Moshi and com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory when requested.
private val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
Similar to what you did with Retrofit, here you create a moshi object using the Moshi builder. For Moshi’s annotations to work properly with Kotlin, add the KotlinJsonAdapterFactory, and then call build().
private val retrofit = Retrofit.Builder()
.addConverterFactory(MoshiConverterFactory.create(moshi))
.baseUrl(BASE_URL)
.build()
Code to delete:
import retrofit2.converter.scalars.ScalarsConverterFactory
Update the MarsApiService interface to have Retrofit return a list of MarsProperty objects, instead of returning Call<String>.
interface MarsApiService {
@GET("realestate")
fun getProperties():
Call<List<MarsProperty>>
}
Open OverviewViewModel.kt. Scroll down to the call to getProperties().enqueue() in the getMarsRealEstateProperties() method.
Change the argument to enqueue() from Callback>. Import com.example.android.marsrealestate.network.MarsProperty when requested.
MarsApi.retrofitService.getProperties().enqueue(
object: Callback<List<MarsProperty>> {
override fun onFailure(call: Call<List<MarsProperty>>, t: Throwable) {
override fun onResponse(call: Call<List<MarsProperty>>,
response: Response<List<MarsProperty>>) {
_response.value =
"Success: ${response.body()?.size} Mars properties retrieved"
Note: If your internet connection is not working, make sure that you turned off airplane mode on your device or emulator.
Now the Retrofit API service is running, but it uses a callback with two callback methods that you had to implement. One method handles success and another handles failure, and the failure result reports exceptions. Your code would be more efficient and easier to read if you could use coroutines with exception handling, instead of using callbacks. In this task, you convert your network service and the ViewModel to use coroutines.
@GET("realestate")
suspend fun getProperties(): List<MarsProperty>
In the OverviewViewModel.kt file, delete all the code inside getMarsRealEstateProperties(). You’ll use coroutines here instead of the call to enqueue() and the onFailure() and onResponse() callbacks.
Inside getMarsRealEstateProperties(), launch the coroutine using viewModelScope.
viewModelScope.launch {
}
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.
Inside the launch block, add a try/catch block to handle exceptions:
try {
} catch (e: Exception) {
}
val listResult = MarsApi.retrofitService.getProperties()
Calling getProperties() from the MarsApi service creates and starts the network call on a background thread.
_response.value =
"Success: ${listResult.size} Mars properties retrieved"
Inside the catch {} block, handle the failure response:
_response.value = "Failure: ${e.message}"
private fun getMarsRealEstateProperties() {
viewModelScope.launch {
try {
val listResult = MarsApi.retrofitService.getProperties()
_response.value = "Success: ${listResult.size} Mars properties retrieved"
} catch (e: Exception) {
_response.value = "Failure: ${e.message}"
}
}
}