Android Tutorial using the Star Wars API with Architecture components, Retrofit 2, RxJava 2 and Dagger 2

Fahri Can
12 min readJan 5, 2020

--

Photo by James Pond on Unsplash

In this blog, I will show how to use Kotlin, Data Binding, ViewModel, LiveData, Retrofit 2, RxJava 2 and Dagger 2 to fetch data from the Star Wars API. It will be a simple app, which shows a list of vehicle names, cargo capacities, and vehicle classes. Therefore, RecyclerView and CardView will be used. This is a step by step tutorial.

Prerequisites

You should already be familiar with Kotlin, Retrofit 2 and Android development. A little bit of experience with RxJava 2 (threading of asynchronous web calls) and Dagger 2 (for dependency injection) would be nice. The Stars Wars API does not require any access token nor an API key. You can fetch data directly from the vehicles endpoint. I am using Android Studio 3.5.3. This article is not about MVVM.

Why this article?

Nowadays in most job descriptions, you might see Android Jetpack, with third-party libraries like Dagger 2 and RxJava 2 as a requirement. I just want to give a real-world example of how to use Android Architecture components with those third-party libraries.

What the app will look like

The app will contain a list of cards that shows some vehicle information. The user can scroll up and down and fetch for new information.

portrait mode finished app
landscape mode finished app

Start new project

Go to Android Studio and create a new Empty Project (Language Kotlin, API 21).

Check “Use androidx.* artifacts”.

Gradle dependencies

Go to your build.gradle(Module: app) file. At the beginning of the file, you have to enable the Kotlin plugin for the annotation processor. The reason is: Kotlin has a different way to process the annotation than Java. Just paste apply plugin: ‘kotlin-android-extensions’ this plugin below:

apply plugin: ‘kotlin-kapt’

Below buildTypes {} add dataBinding {} and set it to true. Otherwise, you can’t use the Android Architecture component Data Binding.

dataBinding {
enabled = true
}

Below dataBinding {} add compileOptions {} and kotlinOptions{}. Otherwise, you can’t run the app. This is because when using the latest RxJava 2 features, older Java versions (before Java 8) can’t handle them.

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions { jvmTarget = "1.8" }

You will see that buildTypes {}, dataBinding {}, compileOptions {} are all located inside android {}. Now create variables outside android {} to store dependency versions.

Now add the following dependencies inside dependencies {}. For the dagger-compiler and dagger-android-processor you will see that kapt is the annotation processor.

Star Wars API

Let’s take a look at what the vehicles endpoint looks like. Here, you see that a GET request to /api/vehicles/ gives an object which contains four properties. The information you are looking for is in the array results.

The array results contain objects. The goal is to display name, cargo_apacity, and vehicle_class.

Let’s start development.

I would recommend you create the following packages to have a better project structure. Move MainActivity in the view package. Then you should see seven packages.

Then open AndroidManifest.xml and add Internet permissions otherwise, you can’t set a GET request.

<uses-permission android:name="android.permission.INTERNET" />

Create the models

Now create the first model. Copy-paste the JSON object of the top result from the vehicles endpoint. Make a Kotlin data class for the JSON object. I am using an Android Studio plugin called: “JSON To Kotlin Class ​(JsonToKotlinClass)​”.

Create the data class in the model package and name it VehiclesResult.kt. You will notice that the list type of results is “Any”. Change it to Vehicle.

After VehiclesResult.kt create the Vehicle.kt with one object from results. The data class Vehicle.kt should also be in the model package.

The Vehicle.kt should look like this:

Create an interface for the API endpoint

Create a new interface in the package api_endpoint and name it StarWarsApi.kt. Here, there will be a GET request made to the endpoint api/vehicles. You want a Single of VehiclesResult as a return type. Single because when the network call is made you expect a value or an error. For this endpoint, you don’t expect it to return additional values over time.

A Single is something like an Observable, but instead of emitting a series of values — anywhere from none at all to an infinite number — it always either emits one value or an error notification.

Create the layout for one vehicle.

Go to the folder reslayout create a new layout file there and name it item_vehicle.xml. Here you will display name, cargo_apacity, and vehicle_class as a CardView. But before, make sure your root ViewGroup is <layout></layout> so you can use Data Binding. The tags <layout></layout> will create the class ItemVehicleBinding for use. Later, you can use ItemVehicleBinding class in the ViewHolder to tell the Adapter which properties to bind. Otherwise, you would have to create four variables (1 x CardView, 3 x TextView) in the ViewHolder.

Inside <layout></layout> create the tags <data></data> to enable using one of the classes in the layout. Therefore, the tag <variable /> has to be created with a name property type with the location of the class. You can define any name you want with this name. You access the properties of your class defined in type in your layout. In my example, the variable name is vehicle.

For instance, you want to access the properties of vehicle inside a TextView to display something:

android:text="@{vehicle.films}"

The important thing is your variable with property access is inside “@{}”. This is one-way data binding.

Here is the complete layout for item_vehicle.xml:

Time to create the ViewHolder and Adapter

Create a new Kotlin File in the package adapter. I named it VehicleViewHolderAdapter.kt.

Start with the ViewHolder

Create a class named VehicleViewHolder which has the field type ItemVehicleBinding in the constructor. Then let your ViewHolder extend from RecyclerView.ViewHolder. The parent class RecyclerView.ViewHolder expects an argument of the type View. Your field of type ItemVehicleBinding contains a property root of the type View. Every class which is created through Data Binding contains this root property of the type View. This class property saves lines of code, otherwise you would have to create 1 x CardView, 3 x TextView properties.

Move on to the Adapter

In the same file where the VehicleViewHolder was created, create the VehicleAdapter above or below it. Let the VehicleAdapter extend from the RecyclerView.Adapter<VehicleViewHolder>(). The Adapter will bind each vehicle on the CardView from item_vehicle.xml. So give it a class property which is from type ArrayList<Vehicle>. Then start to implement the methods onCreateViewHolder(), getItemCount(), onBindViewHolder().

onCreateViewHolder() -> Normally you would start with LayoutInflater.from() but now you have to use instead DataBindingUtil.inflate()

As you can see, DataBindingUtil.inflate() takes as first argument LayoutInflater.from()

getItemCount() -> Just returns the size of the vehicle list. So the Adapter knows how many cards to create.

override fun getItemCount(): Int = vehicleList.size

onBindViewHolder() -> Whenever a CardView (from item_vehicle.xml) for the vehicle gets created it will be fetched from the current position of the vehicle list.

Okay, you are almost finished with the VehicleAdapter. You need one more method, which avoids duplicate vehicles. It should also add all-new vehicles and notifies other components about the changed dataset.

It is time for the RecyclerView

Since most Android Developers immediately associate with ViewHolder and Adapter the RecyclerView, I suggest let us continue with that. All information will be displayed on the MainActivity so open the activity_main.xml. Additionally, the user should be able to manually trigger the data collection. He should be able to see a progress circle while the data is loading. If something went wrong he should see an error message. In conclusion, the following Views and ViewGroups are needed: SwipeRefreshLayout, ConstraintLayout, RecyclerView, TextView, and ProgressBar.

Complete the API GET request

Okay, you have all the things ready to display the data so far. But the API call to https://swapi.dev/api/vehicles/ is still missing. Head to the service package and create a class called NetworkService.kt. Here, you need a class property of the type StarWarsApi as you need the endpoint to load all the vehicles. As you already know, Dagger should handle the instantiation.

@Inject
lateinit var starWarsApi: StarWarsApi

Below the property at a companion object {} and create inside it a constant string inside it to store the base URL.

companion object {
val BASE_URL = "https://swapi.dev/"
}

Dependency injection

Dependency injection allows you to easily separate logic for Unit Testing. The dependency injection library Dagger 2 enables the instantiation of fields and injects them into the right class. All the instantiation (in @Modules) and injecting (in @Component) can be collected in specific classes.

@Module is needed

Now it is time to start with dependency injection. Go to the di package and create a new class called ApiModule.kt. Above the class ApiModule {} annotate it with @Module.

@Module
class ApiModule {

}

Methods in @Module provide something, so start with @Provides then write your method. It is standard that those method names start with provideAnyName(). The return type of the method is very important. Whenever another provideMethod() in ApiModule needs an argument to instantiate something. Dagger will look at the return type of the other methods in ApiModule. If a correct return type is there, Dagger will automatically link those methods.

Note that Moshi is used here for the ConverterFactory instead of Gson. Here, you find resources where Moshi might be better than Gson:

https://stackoverflow.com/questions/43577623/moshi-vs-gson-in-android

https://medium.com/@dannydamsky99/heres-why-you-probably-shouldn-t-be-using-the-gson-library-in-2018-4bed5698b78b

Next step @Component

In the same package, di create a new Interface called ApiComponent.kt. Annotate it with @Component then add parentheses and inside those write:

modules = [ApiModule::class]

The keyword modules will link the ApiModule with ApiComponent.

@Component(modules = [ApiModule::class])
interface ApiComponent {
}

Dagger needs to know now, which classes to inject with fields annotated with @Inject. Create this method inside the interface:

fun inject(networkService: NetworkService)

Now you can go back to the NetworkService class. Before you can use dependency injection in NetworkService you have to do two steps first.

Taken from a MacBook

Click on Build -> Clean Project

Taken from a MacBook

Click on Build -> Build Project

For classes that are not Activities or Fragments, just create an init {} block to enable injecting the class. Inside init {} goes following line:

init {
DaggerApiComponent.create().inject(this)
}

This class DaggerApiComponent was created by Dagger. Notice the word Dagger is always added before the Component interface name.

The last thing which is missing in NetworkService is a method to trigger the fetching of vehicles. The StarWarsApi is encapsulated in NetworkService. NetworkService has to offer a method to trigger the call to the endpoint. Create the following method inside NetworkService:

fun fetchVehicle(): Single<VehiclesResult> {
return starWarsApi.getVehicles()
}

This is the final result of the class NetworkService:

ViewModel the last class to create

Okay, what is still left is to create a ViewModel. Go to your viewmodel package and create VehicleViewModel.kt. Make sure it extends from ViewModel(). The ViewModel is needed to avoid errors when the device switches from portrait mode to landscape mode and vice versa.

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

What kind of class properties do you need now? All UI related variables should be stored in the ViewModel. So you need a NetworkService to make the API call, a CompositeDisposable to use RxJava 2 to make the API call on a separate thread, 3 x MutableLiveData and 3 x LiveData to observe different network call outcomes in the ViewModel.

Next thing is to head to the ApiModule class again and write provide-methods that instantiate the properties.

Next, go to the ApiComponent and add the inject method for VehicleViewModel. From here, enable property injection in VehicleViewModel.

fun inject(vehicleViewModel: VehicleViewModel)

Click on Build -> Clean Project then Click on Build -> Build Project. When you go back to the VehicleViewModel, you can now inject it. In the constructor of the VehicleViewModel should also happen the API call.

init {
DaggerApiComponent.create().inject(this)
fetchVehicles()
}

Remember, the API call will be stored in compositeDisposable. Don’t forget to dispose of all Observables in onCleared(). Doing this starts early to prevent any types of memory leaks.

override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
}

The next thing is to remember that the user should be able to manually refresh the fetched data. This leads to the following line:

fun refresh(){
fetchVehicles()
}

Click on “Create function fetchVehicles”. The implementation of RxJava 2 starts here. After that, the API call and background threading start.

subscribeOn(): The subscribeOn() operator specifies a different Scheduler on which the Observable should operate.

Schedulers.io() — This is used to perform non-CPU-intensive operations like making network calls, reading disc/files, database operations, etc., This maintains a pool of threads.

observeOn(): The observeOn() operator specifies a different Scheduler that the Observable will use to send notifications to its observers.

AndroidSchedulers.mainThread: A Scheduler which executes actions on the Android main thread.

map: Transform the items emitted by an Observable by applying a function to each item

subscribeWith(): Attaches a given Observer to this Observable and returns the given Observer.

Okay, implement now the Observer which stores the list of vehicles in the property vehicleList. If something went wrong, it will display an error message and log it to the console. In both cases, success or error you have to set the value of inProgress to true and let the operations happen. Then at the end of both operations set the value of inProgress to false.

Finally, time to implement the last class

Head to MainActivity and create VehicleAdapter and VehicleViewModel class properties. Annotate the VehicleAdapter with @Inject.

@Inject
lateinit var vehicleAdapter: VehicleAdapter
private val vehicleViewModel: VehicleViewModel by viewModels()

Go to ApiModule and write two methods provideVehicleList() and provideVehicleAdapter().
Dagger automatically links the outcome of provideVehicleList() to provideVehicleAdapter().

Now go to ApiComponent and add this line:

fun inject(mainActivity: MainActivity)

Then go inside onCreate() there you have to use Dagger for dependency injection (maybe you have to clean and rebuild the project).

DaggerApiComponent.create().inject(this)

You might remember that in activity_main.xml you already have a SwipeRefreshLayout and RecyclerView. Start to implement the logic for SwipeRefreshLayout. Thanks to Kotlin Android Extensions, you can use the ID (main_swipe_refresh_layout) from SwipeRefreshLayout to add the listener. You can see that it is enabled in build.gradle (Module: app) at line number 5:

apply plugin: 'kotlin-android-extensions'

Don’t forget to fetch for new vehicles when a swipe refresh is triggered.

main_swipe_refresh_layout.setOnRefreshListener {
main_swipe_refresh_layout.isRefreshing = false
vehicleViewModel.refresh()
}

Use the ID from RecyclerView to assign values for layoutManager and adapter.

main_recycler_view.apply {
layoutManager = LinearLayoutManager(context)
adapter = vehicleAdapter
}

Observing of LiveData from ViewModel MutableLiveData properties should happen in onCreate():

observeLiveData()

Here is a summary of onCreate():

Go ahead and create observeLiveData(). In observeLiveData() the three LiveData properties from ViewModel will update the UI. Never use MutableLiveData to update the UI! You should always use LiveData variables to update the UI! That’s why MutableLiveData properties should kept private inside the ViewModel. For more information, click here: Fun with LiveData (Android Dev Summit 2018)

private fun observeLiveData() {
observeInProgress()
observeIsError()
observeVehicleList()
}

Start to implement the three methods inside observeLiveData(). The first method observeVehicleList() is for making the RecyclerView visible and setting the list of vehicles into the adapter.

The second method observeInProgress() is for checking if the data is still loading. If yes, then display ProgressBar, hide the error text and RecyclerView. Otherwise, just hide the ProgressBar.

The third method observeIsError() just checks if an error occurred while loading the data. If yes, display the error text, otherwise, hide it.

Here a summary of the MainActivity:

Here is the completed project:

Acknowledgments

Special thanks to The Star Wars API for providing this free API. Here is the GitHub page: https://github.com/phalt/swapi. May the force be with you!

--

--

Fahri Can
Fahri Can

Written by Fahri Can

Android Dev | Blogger | AI & Crypto Enthusiast

No responses yet