Practical Android MVVM: Yes-No Gif Demo App
In this article, we’re going through a simple example of Android MVVM and LiveData with Retrofit2 and RxJava2.
Some concepts, components, and libraries that appear in this reading:
- Android Jetpack: ViewModel and LiveData
- MVVM
- Repository
- RxJava2 (RxAndroid)
- Retrofit2
- API REST / JSON
- Gson
- Glide
Example API
For this example, I’ve decided to use a cool and crazy API:
This API provides us with random GIFs labeled by Yes or No. You are supposed to think of a yes-no question and then pressed the button to request and load a yes-no GIF to the previous API.
At the end of this document, you can find a link to the repository in Github.
Project Structure
We can see in this capture the project structure in packages. This is a very small project, so we’ve distributed classes and functionality across packages and not modules.
We can see three main packages: model, view, and viewmodel (MVVM architecture pattern):
model
- data: data sources (only API in this example)
- repository: repository and data mappers
- domain or business logic
viewmodel
- ViewModel classes
view
- ui: Activities and Fragments
Architecture (MVVM Pattern)
I would like to highlight that this is a non-complex project so MVVM by itself is good enough to design a smart architecture. But if we were working on a very complex project, we would also need to use CLEAN Architecture to complement the MVVM pattern.
In a CLEAN Architecture among other things we would introduce:
- Use Cases to manage business rules and uncouple ViewModel from Repository (avoiding that a change in Repository interface might propagate to ViewModel). Here it would introduce unnecessary complexity. In a non-complex project, this logic can be managed perfectly by ViewModels.
So MVVM applied to this example would be:
Later we’ll explain more in detail ViewModel and LiveData. Also RxJava.
Here, talking about the architecture’s big picture, we can say that we’re using MVVM architecture pattern and Repository design pattern:
MVVM Architecture Pattern:
- ViewModel permits us to uncouple Fragment (UI) from Repository and it is done in a lifecycle-aware way (due to LiveData) and resisting configuration changes of the App
Repository Design Pattern:
- It is a clean interface to obtain data and to uncouple data sources (API, Database, etc) from the rest of the App.
- It is recommended to use model classes for each data source. In this example, the only data source is Api or Network and it uses its own answer version called
AnswerApiDTO
which will be mapped toAnswer
domain class in Repository.
API call and JSON Response
For this example, we are using the Yes-No API which returns a random answer : “Yes” or “No” or “Maybe”. And also it includes a link to a comical GIF in which we can see famous people giving their approval or disapproval… or none of them.
The endpoint that we are calling is:
The JSON Response we obtain is something like that:
{
"answer": "yes",
"forced": false,
"image": "https://yesno.wtf/assets/yes/......gif"
}
JSON is a standard format normally used to represent and interchange data between a client and a server. It is a human-readable format based on a set of attribute-value pairs and array data types.
In this JSON response, we have got three attributes. In the next sections, we will see how to model this response with a data class and how to use GSON as an adapter for Retrofit to convert from a JSON response to a data object.
Fetching data with Retrofit2
Retrofit is a type-safe HTTP client for Android and Java. With Retrofit, we can send data and fetch data easily to and from an HTTP server.
Thanks to Retrofit we can see HTTP API as a Kotlin interface.
Step 1: Create the interface service class
This interface contains methods that correspond to HTTP methods like: GET, POST, PUT, PATCH, DELETE, and others.
- In our example, we only need to do a GET request so we use annotation
@GET
followed by the relative URL of the endpoint (this relative URL “/API” is concatenated to the base URL specified later in the RetrofitClient builder).
Also, we could pass parameters using annotations like @Path
or @Query
depending on the parameter type.
- Here, we don’t need to pass parameters.
We call our interface AnswerApiService
because this API endpoint returns a random answer (yes, no or maybe) and a GIF URL. All this is encapsulated by data class AnswerApiDTO
(as we can see later). We also talk later about the Rx Observable object returned by this method. For now, this is the Retrofit service which contains the desired API endpoint.
Step 2: Retrofit client configuration
Now we are going to configure Retrofit. This way Retrofit can convert our AnswerService interface into a callable object. We set it with:
- base URL: this base URL is the common base URL for all the endpoints relative URLs written in the service interfaces
OkHttpClient
object: we create only one instance of OkHttpClient and reuse it for all the HTTP calls (this is the way OkHttpClient recommends)GsonConverterFactor
object asConvertedFactory
to create an object of our AnswerService interface which uses Gson to deserialize JSON to Kotlin when receiving data from server (and to serialize Kotlin to JSON when sending data to the server)RxJava2CallAdapterFactory
object as aCallAdapterFactory
to create an Rx Observable to wrap response data
Gson converter (JSON <-> Kotlin data class) and Answer model
Gson is a Java library that can be used to convert Java Objects into their JSON representation. It can also be used to convert a JSON string to an equivalent Java object. Gson can work with arbitrary Java objects including pre-existing objects that you do not have source-code of.
We model JSON data response with the following Kotlin data class using annotation @SerializedName
from Gson library:
Do you remember JSON response?
{
"answer": "yes",
"forced": false,
"image": "https://yesno.wtf/assets/yes/.......gif"
}
In the object RetrofitClient
we configured Retrofit passing a GsonConverterFactory
.
Repository returns Observable of a domain class
Repository returns Observable of a domain class (not a data source class).
As we can see above, repository implementation receives an Observable<AnswerApiDTO>
from AnswerApiService
and maps it to Observable<Answer>
before returning it.
The class to maps data is DataMappers
:
Normally, Repository implementation fetches data from different data sources in different ways (order-based strategies) but in this example, we only have Api as the unique data source. So Repository only calls AnswerApiService
.
Subscribing to the Rx Observable returned by our Repository
As we already know, the getAnswer()
method of AnswerRepository
returns an Observable<Answer>
object.
As we can see below in the code, we subscribe to this Observable<Answer>
following the Observable Contract:
- We provide implementations for callback functions:
onNext()
,onError()
andonCompleted().
There are 3 important aspects of this subscription:
- We tell Retrofit, who creates and returns the
Observable<Answer>
, that we want to fetch data operation from an IO thread bysubscribeOn(Schedulers.io())
- With the fetched data we are going to update the viewmodel’s livedata called
answer
. There are two methods to update livedata’s value:setValue()
andpostValue().
First one,setvalue(),
updates the value immediately and it must be called from UI thread. Second one,postValue()
, posts a task to the main thread to update the value but it isn’t done by itself right now. In this example, we will usesetValue()
method, so we will tell Retrofit to observe withobserveOn(AndroidSchedulers.mainThread())
subscribe()
method receives three functions as parameters. These callbacks are:onNext()
,onError()
andonCompleted()
and they are executed from the main thread as we’ve indicated in the previous point.
About this last, we are programming in Kotlin and the subscribe method invoked is:
subscribe (
onNext: (Answer) -> Unit,
onError: (Throwable) -> Unit,
onCompleted: () -> Uniy
)
and to pass to subscribe()
the three anonymous functions implementations we use kotlin lambdas resulting this:
subscribe (
{ value -> answer.setvalue(Result.Success(value))},
{ error -> answer.setValue(Rsult.Failure(error))},
{ println(“Getting answer completed”)}
)
ViewModel and LiveData
ViewModel and LiveData are both Android Jetpack Architecture Components.
ViewModels
- manage and store UI-related data
- usually, keep this data inside
LiveData
which contains data observed by UI controllers likeActivities
andFragments
- obtain this data, for example, requesting it to the repository
- maintain this data during configuration changes
- also may receive requests from view in response to user interactions
- must never reference a view or lifecycle or a context (if they need to access system services then they should extend
AndroidViewModel
) - its scope match lifecycle passed when it is created through a ViewModelProvider. So if we pass an activity as lifecycle owner then ViewModel will live until the activity is finished and if we pass a fragment as lifecycle owner then until the fragment is detached and gone away permanently
- for example during a screen orientation change the activity is re-created so it receives the same
ViewModel
instance that was created by the first activity. This is also true for a fragment because although during a configuration change the fragment is destroyed, after that it is re-created (I mean a new instance of the same fragment) (we don’t navigate to another fragment)
LiveData
- holds data which are observed by activities and fragments
- is lifecycle-aware, when wrapped data changes then it only notifies its observers if they are in an active state considering their lifecycle
Our AnswerViewModel
contains theLiveData answer
:
val answer by lazy {
MutableLiveData<Result<Answer>>()
}
Remember Answer
domain class is:
Result
wraps Answer
to provide status information about the networking request:
And remember that inside subscribe Rx block we update LiveData
answer
:
subscribe (
{ value -> answer.setvalue(Result.Success(value))},
{ error -> answer.setValue(Result.Failure(error))},
{ println(“Getting answer completed”)}
)
You can get the code of the rest of the code in the project on Github (there is a link at the end of this document).
Fragment observes LiveData
AnswerFragment
observes Viewmodel’s LiveData answer:
Once we have the image URL (answer.image
in the previous snippet), then we call loadGirFromURL()
method to use Glide for loading and rendering the GIF.
Glide
An image loading and caching library for Android focused on smooth scrolling.
Glide is a fast and efficient open source media management and image loading framework for Android that wraps media decoding, memory and disk caching, and resource pooling into a simple and easy to use interface.
We invoke Glide passing it:
- an image URL, so Glide will download the GIF from the web
- an imageview where painting this GIF
- a context because Glide is lifecycle-aware and it pauses, resumes or cancels current resource request based on lifecycle’s state of the fragment or activity passed as a parameter
As I said before in the Architecture diagram, Glide covers UI-related and fetching-data-related functionality, so it breaks the architecture aspects separation principle in some way. It has responsibilities from UI and Data.
Finally, you can take a look at AnswerFragment
class on the repository.
I hope you’ve found this article enjoyable and useful!Any doubt, question or observation? Please, leave a comment!
Get the project’s source code on my Github!