6.
Building Features — Animals Near You
Written by Ricardo Costeira
Until now, you focused your efforts on building a solid foundation for PetSave. You created the main entities and value objects in your domain layer, and you have a data layer ready to handle and shape data. Now that you’ve laid that foundation, you’ll start building features that people can use. You’ll look at the presentation layer and set up the app’s user interface. You’ll also visit the domain layer again to create use cases.
In this chapter, you’ll learn:
- What a presentation layer is.
- How to create a deterministic data flow.
- How to leverage UI Android framework components like
ViewModel,FragmentandViewBinding. - What defines the state of your app and how to manage state.
- How to build use cases.
By the end of the chapter, you’ll have your first feature!
What Is a Presentation Layer?
To create this feature, you’ll start by adding a presentation layer. But what, exactly, is that and why do you need it?
The presentation layer encapsulates all the code related to the UI, holding all the UI-related components. In other words, this layer deals with framework code.
App UI and UX are typically more prone to change than business logic. That means you’ll find yourself changing UI code more often than any other code.
At the same time, UI toolkits are well known for being hard to test. In fact, the whole Android framework makes it hard to write tests. That’s why you should avoid using it in your business logic code as much as possible.
You can test Android UI with instrumented tests and Espresso. These tests need to run on a device, which makes them slow compared to unit tests. Plus, they’re also flakier, because the code changes more often. In some cases, the framework actually prevents you from being able to test at all!
For those reasons, it’s a good idea to make the UI as dumb as possible. You should strive to keep any logic unrelated to the UI decoupled from it. It’s a good thing you have a domain layer. :]
By keeping all the UI code in the same place, you protect both your business logic and yourself. It lets you test your logic, regardless of the UI. You’ll have less of a headache when you try to introduce that new shiny Android UI library in your codebase, because you can do so without messing with the logic — and knowing Android, that’s bound to happen!
Working with the presentation layer offers some challenges, however. You’ll learn about some of them next.
Lifecycles of UI Components
The presentation layer is both the easiest layer to understand and the hardest to work with.
Android UI components have their own individual lifecycles. Picture an Activity hosting a Fragment. The system can destroy and recreate that Fragment multiple times throughout the Activity’s lifetime. At the same time, that Fragment’s View can be destroyed and recreated multiple times while the Fragment lives on.
Juggling different lifecycles can be daunting. For instance, imagine that you have a Fragment that calls postDelayed on a local Handler, and you forget to remove the callbacks from the Handler in the Fragment’s onDestroy(). This might cause a memory leak, as the garbage collector can’t clean up the Fragment because something still references it.
In this case, the problem is simple to solve. Other cases, however, can become so complex that it’s difficult to even understand what’s going on.
State Management
There’s also the problem of state management, which is the information your app is holding at any given time. Your UI has a given state, which mutates according to user input and other events.
Different parts of that state may or may not affect one other. Depending on your implementation and needs, changes can even happen concurrently, meaning that even simple states can be hard to implement correctly. With time, bad state management leads to maintenance nightmares. Bugs start creeping in from one part of the code when you change something in another part.
Your app’s state not only includes the data regarding the business logic but also the framework component state. This includes things like the color of a Button or the visibility of a TextView. These types of intrinsic properties also represent the state.
Making Your Life Easier With Architecture
App development can be challenging. You have the typical software problems to deal with, like state management and increasing complexity. On top of that, you have to worry about the framework. As a developer, you must do everything you can to make development easier and, more importantly, fun! Choosing the right architecture is a great start.
Android has seen a few different architectural patterns throughout the years. The most common patterns are MVP (Model-View-Presenter) and MVVM (Model-View-ViewModel). The reason for their popularity is simple: They do an excellent job of decoupling the UI from the business logic. Keep the UI dumb, remember?
These patterns help keep your concerns separated and well defined. Still, things can get messy when you start considering state. For instance, presenters in MVP are usually stateless. Even if you make them stateful, the way the architecture works can make it hard to sync it with the View’s state.
MVVM Architecture
MVVM makes this a lot easier, as state management is built into the architecture. The View communicates with the ViewModel by subscribing to changes on its state. This lets you use the ViewModel to represent the View’s state.
Even so, it can get tricky if the View subscribes to a lot of different state properties — especially if those properties depend on each other. It’s not hard to imagine an Android ViewModel with a few different StateFlow instances emitting tightly coupled properties. For instance:
class MyViewModel() {
val isLoading: StateFlow<Boolean>
val items: StateFlow<List<Item>>
}
Handling the properties incorrectly can lead to impossible states, like showing a loading ProgressBar when you have already have the item list. Plus, as the number of properties increases, so does the complexity.
Note: Keep in mind that using the
ViewModelAndroid component doesn’t necessarily mean that you’re following the MVVM pattern. You can use aViewModelin many other ways. It’s not the best component name. :]
MVI Architecture
So, what should you use? You might have heard about the new kid on the block: MVI (Model-View-Intent). This pattern enforces a few interesting rules:
- Immutable state: You create updated copies of the state, rather than mutating it. This avoids bugs stemming from mutability.
- One single view state per screen: A view state can be a data class with all the state’s properties, or even a set of sealed classes representing the different possible states. Using sealed classes solves the problem of impossible states.
- Unidirectional data flow: Makes your state deterministic — which makes testing actually enjoyable!
You won’t exactly follow an MVI pattern in this chapter, as you don’t need to create reducers and/or intents. Instead, you’ll do something simpler, somewhere between MVVM and MVI. The Android community likes to call it a unidirectional data flow architecture. In fact, even Google’s architecture guide reccomends it now. Here’s a high-level view, where the black arrows represent data flow and the open arrow represents inheritance:
Now, it’s time to start coding your new feature!
Building Animals Near You
Make sure you’re running the starter project, then build and run the app. The bottom navigation bar is there to let you navigate between the screens of the two features.
The bottom navigation bar uses the Navigation component, from Android Jetpack Components. Clicking on the bar’s icons lets you navigate between screens, although they don’t show anything but an infinite spinner at this point.
Note: If you’re interested in using the Navigation component with the bottom navigation bar, you can check out Navigation Component for Android Part 3: Transition and Navigation here: https://www.raywenderlich.com/8279305-navigation-component-for-android-part-3-transition-and-navigation.
Open AnimalsNearYouFragment.kt in the animalsnearyou.presentation package. The app uses view binding to access view elements. If you don’t need two-way data binding or layout variables, view binding is the best choice. It provides the null and type safety that findViewById doesn’t. It’s also easier to use than data binding and compiles faster.
As with data binding, there’s one thing to remember when using view binding in a Fragment: Fragments can outlive their Views. So you need to clear up the binding in the Fragment’s onDestroyView:
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
This is why there are two different binding variables. The nullable one sets and destroys the binding, and the non-nullable binding accesses the view elements without the safe call operator, ?.. Accessing the latter from outside the View’s lifecycle will crash the app. If that happens, you’re doing something wrong. :]
To set up the UI, you need:
- A
RecyclerViewfor the list. - An
Adapterfor theRecyclerView. - View state and events for state management.
- Use cases.
- A
ViewModelto handle events and update the view state. - To observe the view state.
You’ll start with the UI components. The XML layouts are ready, so you’ll just work on the code.
Creating the UI Components
For your first step, you’ll create the UI for your feature in the existing AnimalsNearYouFragment.
In AnimalsNearYouFragment.kt, add the following code:
// 1
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
setupUI()
}
// 2
private fun setupUI() {
val adapter = createAdapter() // 3
setupRecyclerView(adapter)
}
private fun createAdapter(): AnimalsAdapter {
return AnimalsAdapter()
}
// 4
private fun setupRecyclerView(animalsNearYouAdapter: AnimalsAdapter) {
binding.animalsRecyclerView.apply {
adapter = animalsNearYouAdapter
layoutManager = GridLayoutManager(requireContext(), ITEMS_PER_ROW)
setHasFixedSize(true)
}
}
Here’s what’s happening above:
-
onViewCreated()executes immediately afteronCreateView(). The framework makes sure that you’ve correctly initialized allViewsat this point, so you should do all yourViewsetup here. For instance, if you useLiveDatas, observing them here ensures they’re unsubscribed inonDestroyView(). By creating the view inonCreateView(), then initializing it inonViewCreated(), you maintain a good separation of concerns (SoC). Plus, you don’t need to worry about null pointer exceptions (NPEs) onViewaccess. -
Create a method to glue together all the UI setup code. Inside it, you delegate each component’s setup to other methods.
-
Create an
adaptervalue and initialize it. -
Run some standard
RecyclerViewcode. You set theAdapter, aGridLayoutManagerwith two items per row, and tell theRecyclerViewthat all elements have the same size. This way, theRecyclerViewcan skip some measuring steps and perform some optimizations.
There’s a good reason why the adapter value only exists in setupUI()’s scope. Having an Adapter as a property of a Fragment is a known way of leaking the RecyclerView.
That’s because, when the View is destroyed, the RecyclerView is destroyed along with it. But if the Fragment references the Adapter, the garbage collector won’t be able to collect the RecyclerView instance because Adapter s and RecyclerViews have a circular dependency. In other words, they reference each other.
Making an Adapter a Property of a Fragment
If you need the Adapter as a property of a Fragment, don’t forget to either:
- Null out the
Adapterproperty inonDestroyView. - Null out the
Adapterreference in theRecyclerViewitself, before doing the same for the binding.
So, if you needed the Adapter as a property of the Fragment in this code, you’d have something like:
override fun onDestroyView() {
super.onDestroyView()
// either
adapter = null
// or
binding.recyclerView.adapter = null
_binding = null
}
By the way, AnimalsAdapter already exists. It’s in the common.presentation package because the search screen also uses it.
Open AnimalsAdapter.kt and have a look at the code inside. You’ll notice that this file contains the definition of a simple adapter, with a single view type. Since the code is so simple, ViewHolder is also here as an inner class. If you had more view types and more complex ViewHolders, it would be best to decouple them from the adapter for proper SoC.
Defining the UI Model
As you know, Adapter is the abstraction Android uses for the object responsible for providing the View for each item in a RecyclerView. Each View is then encapsulated in a ViewHolder that’s responsible for:
- Creation of the
Viewto recycle for other items of the same type. - Binding the data of each item you want to display to the
Viewcreated for that specific time.
To see how this works, examine AnimalsViewHolder’s bind:
fun bind(item: UIAnimal) {
binding.name.text = item.name
binding.photo.setImage(item.photo)
}
setImage is an ImageView extension function that internally calls Glide to load the picture.
This binding requires a name and a photo, which come from a UIAnimal. Like the domain and data layers, the presentation layer also has its own model. UIAnimal is a simple data class:
data class UIAnimal(
val id: Long,
val name: String,
val photo: String
)
Remember that you want to keep the UI dumb. Ideally, a UI model will consist of simple primitive types like this one. The model should have only the minimum necessary information to do its job — in other words, only enough to satisfy the UI’s needs.
Using DiffUtil for AnimalsAdapter
Items in the model can change over time, and so can the RecyclerView that displays them. To minimize the work and make the transition smooth, Android provides a special Adapter: the ListAdapter.
AnimalsAdapter extends ListAdapter, which requires a DiffUtil.ItemCallback. There’s an ITEM_COMPARATOR property at the bottom of the file with an anonymous class extending DiffUtil.ItemCallback. It already overrides the areItemsTheSame and areContentsTheSame abstract methods.
For your next step, complete them by replacing their contents with:
private val ITEM_COMPARATOR = object : DiffUtil.ItemCallback<UIAnimal>() {
override fun areItemsTheSame(oldItem: UIAnimal, newItem: UIAnimal): Boolean {
return oldItem.id == newItem.id // 1
}
override fun areContentsTheSame(oldItem: UIAnimal, newItem: UIAnimal): Boolean {
return oldItem == newItem // 2
}
}
- This method has the job of checking if
oldItemandnewItemare the same. That means you have to compare their identities and nothing else. If the contents of an item change and you compare them here, the method will returnfalseinstead oftrue— which will cause the item to flicker in theRecyclerView! - This method is called only if
areItemsTheSamereturnstrue. Here’s where you should compare the contents. SinceUIAnimalis a data class, using==will compare all of its properties.
Build and run. You’ll see the same screen as before because there’s no view state yet.
Creating the View State
Now, you need to create a class that stores the current state of your View.
To do this, open AnimalsNearYouViewState.kt in the animalsnearyou.presentation package and add the following code:
data class AnimalsNearYouViewState(
val loading: Boolean = true, // 1
val animals: List<UIAnimal> = emptyList(), // 2
val noMoreAnimalsNearby: Boolean = false, // 3
val failure: Event<Throwable>? = null // 4
)
This state is as simple as it gets. It contains:
- A Boolean representing the loading state.
- A list of items to display.
- A Boolean representing the no more animals nearby state.
- A value for possible errors. It defaults to
null, representing the absence of errors.
The default values represent the initial state. When you launch the app for the first time, you won’t have any items and will need to show a loading screen while you get them.
You want your UI to always have the latest view state. To do so, you use either something like LiveData or a reactive stream like StateFlow to emit the state for an observer in the UI.
You want this object to survive configuration changes, so you put it in the ViewModel. This way, if the configuration changes, you can display the state immediately. Even if you need to update it, at least you’re showing something already.
However, there’s something to note here: If you’re modeling errors as part of the state, you’ll display those errors as well! Imagine showing a Snackbar at the bottom saying Something went wrong every time you flip the phone. I’ve uninstalled apps for less!
Using Event prevents your app from handling the error more than once. You might’ve seen examples using SingleLiveEvent to work around this issue, but it doesn’t solve the problem.
Errors are results, or effects, that are consequences of specific actions. Therefore, some developers — like me! — prefer to treat them differently from the rest of the state. They can happen for a variety of reasons; they might not even relate to the state at all.
Just as you have a stream for your state, you can have a separate stream for your effects. That stream should hold things like errors, navigation, dialogs… anything you want to consume once.
A nice way to model this is with a hot reactive stream, like a PublishSubject or a SharedFlow. You emit on it, properly react to it and don’t look back.
Now that you understand the theory, you’re ready to work on your view state. You need to wire everything up so that the view can observe it.
Creating the Data Flow
You need to make some changes so that your view state works properly. Here’s how it should work when you’re done:
- The UI sends events to the
ViewModel. -
ViewModelreacts to those events by triggering the use cases. - The use cases return state information.
-
ViewModelupdates the view state, which the UI observes.
You’ll work through these, step-by-step.
Handling Events
Events are actions that the UI triggers. What does the UI need when you open the app? A list of animals! That’s what you’ll work on next.
In animalsnearyou.presentation, create a new AnimalsNearYouEvent.kt and write a sealed class to represent both the UI events and the event that requests the animals list:
sealed class AnimalsNearYouEvent {
object RequestInitialAnimalsList: AnimalsNearYouEvent()
}
Now, create AnimalsNearYouFragmentViewModel.kt in the same package. Start by defining the class:
class AnimalsNearYouFragmentViewModel constructor(
private val uiAnimalMapper: UiAnimalMapper, // 1
private val compositeDisposable: CompositeDisposable // 2
): ViewModel() {
override fun onCleared() {
super.onCleared()
compositeDisposable.clear() // 3
}
}
In this code, you have:
- A mapper that translates the domain model to the UI model.
- A
CompositeDisposablefor RxJava. You don’t need to inject schedulers because RxJava provides a way to override them all while testing. - Something to clear the disposable, which you never want to forget. You don’t need to worry about coroutines here; instead, you’ll use
viewModelScope.ViewModelwill clear them internally.
Exposing the State
Every time you get a new state, you need to update the UI. StateFlow is a good choice to do this.
In the same AnimalsNearYouFragmentViewModel.kt, add the following code:
private val _state = MutableStateFlow(AnimalsNearYouViewState()) // 1
private var currentPage = 0 // 2
val state: StateFlow<AnimalsNearYouViewState> = _state.asStateFlow() // 3
// 4
fun onEvent(event: AnimalsNearYouEvent) {
when(event) {
is AnimalsNearYouEvent.RequestInitialAnimalsList -> loadAnimals()
}
}
Here’s what’s going on:
- You create a private
MutableStateFlowwith the initial state ofAnimalsNearYouViewState. You’ll use this property to update the state, which will be exposed toAnimalsNearYouFragmentthrough an immutableStateFlow. UnlikeLiveData,StateFlowdoesn’t benefit from lifecycle-aware behavior, so you’ll have to take some extra steps when subscribing to the stream. - You need to track the page you’re on to request the right data. Knowing the exact page isn’t relevant for the UI state — unless it’s the last one, but that’s why you have
noMoreAnimalsNearby. This lets you keep this property out of the exposed state. - You set
_stateto the immutableStateFlowthat actually exposes the state. - You create the only public method in the
ViewModel.AnimalsNearYouFragmentcalls this method whenever it has an event to trigger.
Triggering the Initial API Request
Next, you’ll use loadAnimals to trigger the initial API request for animals. To do this, add this code below onEvent():
private fun loadAnimals() {
if (state.value.animals.isEmpty()) { // 1
loadNextAnimalPage()
}
}
private fun loadNextAnimalPage() {
val errorMessage = "Failed to fetch nearby animals"
val exceptionHandler = viewModelScope.createExceptionHandler(errorMessage) { onFailure(it) } // 2
viewModelScope.launch(exceptionHandler) { // 3
// request more animals!
}
}
Here’s what you’re doing:
- The
ifcondition checks if the state already has animals.Fragmentwill send theRequestInitialAnimalsListevent every time it’s created. Without this condition, you’d make a request every time the configuration changes. This way, you avoid making unnecessary API requests. If there are no animals, though, you callloadNextAnimalPage(). - You create a
CoroutineExceptionHandlerthrough a customcreateExceptionHandlerextension function onviewModelScope. It takes in a lambda, which in turn takes aThrowable. You callonFailure()in the lambda, then pass it that sameThrowable. - You launch a coroutine on
viewModelScope, passing in theCoroutineExceptionHandlerto thelaunchextension function.
CoroutineExceptionHandler is a global solution for exception handling that will catch exceptions even from child coroutines. It only works if you set it on the parent coroutine. It’ll ignore exceptions if you set it on a child coroutine.
You only call CoroutineExceptionHandler when the parent coroutine has already finished. As such, there’s no coroutine to recover from the exception it catches. If you need the coroutine to recover or you need more control over exceptions, go with try-catch, which also works with child coroutines.
You can call CoroutineExceptionHandler from any thread. If you need to access the UI thread in the lambda that you pass to a CoroutineExceptionHandler, you have to force it. That’s why createExceptionHandler is an extension function on viewModelScope. This scope runs on the UI thread, so calling launch inside the function will run on the UI thread as well.
Handling Errors
Getting back to the code, create onFailure below the method above:
private fun onFailure(failure: Throwable) {
when (failure) {
is NetworkException, // 1
is NetworkUnavailableException -> {
_state.update { oldState -> // 2
oldState.copy(
loading = false,
failure = Event(failure) // 3
)
}
}
}
}
Here’s what’s happening:
- For now, you’re only handling
NetworkExceptionandNetworkUnavailableException. The former is a new exception that avoids having Retrofit code in the presentation layer. CheckrequestMoreAnimalsinPetFinderAnimalRepositoryand you’ll see that it throws aNetworkException— a domain exception — when Retrofit’sHttpExceptionoccurs. - You update the state by calling
updateon theMutableStateFlow. This method is thread-safe, meaning that it’s OK if multiple events come in at the same time and try to change the state. Theupdatemethod has a lambda as a parameter, which in turn reveives an object of typeAnimalsNearYouViewStateas a parameter, only to return another instance of the same type. You can see where this is going, right? The lambda receives the old state as a parameter, and whatever it returns will become the new state. Notice that you’re not mutating the old state, but rather replacing it with an updated copy of itself. Data classes implement thiscopymethod, which really comes in handy here. - Again, you use
Eventto wrapThrowableso the UI reacts to it only once.
You’ll add more code here later. But first, you need to implement the logic to fetch the animals.
Your First Use Case
Use cases keep your app’s logic well-separated and testable. Each use case will be a class. The use case you’re about to create belongs in the app’s domain, but only animals near you uses it. For that reason, create RequestNextPageOfAnimals.kt in animalsnearyou.domain.usecases and add the following code:
// 1
class RequestNextPageOfAnimals @Inject constructor(
private val animalRepository: AnimalRepository, // 2
private val dispatchersProvider: DispatchersProvider // 3
) {
}
Here’s what’s happening:
- Use case names should be specific, but at the domain level. You can’t tell where the data comes from by the name, for instance.
- You inject an
AnimalRepository, allowing the use case to access the data sources. - You inject a coroutine dispatchers provider. Rule of thumb: Always inject coroutine dispatchers. They help with testing! Injecting it in the use case also helps keep the
ViewModelsimple since it doesn’t have to worry about which dispatcher it should run the use case on.
A use case has a purpose, so it makes sense for the class to have only one method. However, using it as requestNextPageOfAnimals.run() when your use case already has a good name is just adding noise. It would be a lot cooler to do requestNextPageOfAnimals().
You can do that by overloading the invoke operator of the class. Add the following operator to the class:
suspend operator fun invoke( // 1
pageToLoad: Int,
pageSize: Int = Pagination.DEFAULT_PAGE_SIZE
): Pagination {
// 2
return withContext(dispatchersProvider.io()) {
// 3
val (animals, pagination) =
animalRepository.requestMoreAnimals(pageToLoad, pageSize)
// 4
if (animals.isEmpty()) {
throw NoMoreAnimalsException("No animals nearby :(")
}
animalRepository.storeAnimals(animals) // 5
return@withContext pagination // 6
}
}
This code implements pagination. Note that:
- It’s a
suspendfunction. Neat! - It uses calls
withContext, which shifts code execution to a background thread — in this case, a thread on the IO dispatcher pool. Note that you don’t need this for Room or Retrofit. Room calls an IO dispatcher internally, and Retrofit’ssuspendfunctions already delegate to a background executor. Still, the code performs some operations before reaching Room and Retrofit, and coroutine context switching is cheap, so you might as well use an IO dispatcher, anyway. Apart from this,withContextreturns the scope’s result, so by callingreturnhere you have to make the scope return something of typePagination. - You’re calling
requestMoreAnimalson the repository and destructuring its result. - If there are no animals, you throw the
NoMoreAnimalsExceptionexception, which you’ll handle inonFailure. - You call
storeAnimalsto store the animals you got from the API in the database. - You return the pagination information that handles paging on the view.
That’s it. Plain and simple!
Now that you’ve created the use case, it’s time to use it.
Connecting the Layers
Go back to AnimalsNearYouFragmentViewModel. You have to inject the use case before you can use it.
Start by updating the constructor by adding this line above uiAnimalMapper:
private val requestNextPageOfAnimals: RequestNextPageOfAnimals,
Now, you can update loadNextAnimalPage(). In launch’s scope, add:
Logger.d("Requesting more animals.")
val pagination = requestNextPageOfAnimals(++currentPage) // 1
onPaginationInfoObtained(pagination) // 2
In this code, you:
- Call the use case, passing in the current page after incrementing the value.
- Pass the
paginationresult toonPaginationInfoObtained.
That last method doesn’t exist yet, so create it below loadNextAnimalPage:
private fun onPaginationInfoObtained(pagination: Pagination) {
currentPage = pagination.currentPage
}
Although the page should be the same one you asked for, you still update it for good hygiene. Also, don’t forget to update onFailure by adding this to the when:
is NoMoreAnimalsException -> {
_state.update { oldState ->
oldState.copy(
noMoreAnimalsNearby = true,
failure = Event(failure)
)
}
}
This updates the state to the no more animals nearby state.
Triggering the Event
You now need to trigger the event in AnimalsNearYouFragment. In onViewCreated, below setupUI, add:
requestInitialAnimalsList()
Create the method below setupRecyclerView, to keep the code organized:
private fun requestInitialAnimalsList() {
viewModel.onEvent(AnimalsNearYouEvent.RequestInitialAnimalsList)
}
You don’t have a viewModel property yet, so add it at the top of Fragment, above binding:
private val viewModel: AnimalsNearYouFragmentViewModel by viewModels()
The viewModels() delegate will create the ViewModel for you.
Build and run. It crashes! Check Logcat and you’ll find an error stating that AnimalsNearYouFragmentViewModel doesn’t have a zero-argument constructor.
If you were to do things manually, you’d have to create a ViewModelFactory which, in turn, would create your ViewModel. You’d then pass it as a lambda to the viewModels property delegate.
But you don’t have to do this manually — instead, you’ll use Hilt, which you’ll implement next.
Hilt on Android Components
Although you’ve already done some work with Hilt, you can’t inject dependencies yet.
With vanilla Dagger, you’d include the main modules in a main Component, then use that Component to create the dependency graph.
With Hilt, it’s simpler. A lot simpler. At the root of the project, locate and open PetSaveApplication.kt. Annotate the class:
@HiltAndroidApp
class PetSaveApplication: Application()
Done. :]
No main Dagger Component, no AndroidInjector, AndroidInjectionModule, @ContributesAndroidInjector, nothing. Just a single annotation!
Build and run — and you’ll still get the same runtime exception regarding AnimalsNearYouFragmentViewModel. To fix it, go to the class and annotate the constructor:
@HiltViewModel
class AnimalsNearYouFragmentViewModel @Inject constructor
This @HiltViewModel is a Hilt annotation specific to ViewModel injection. Using it together with the already known @Inject ensures your ViewModel instances get injected — you don’t need anything else. Yes, no more multibinding for ViewModel instances!
Binding the Repository
Build the app again. You’ll get a compile-time Hilt error stating that it doesn’t know how to inject AnimalRepository — which makes sense, since you didn’t @Bind the interface yet.
Open ActivityRetainedModule.kt in common.di and replace the comment with:
@Binds
@ActivityRetainedScoped
abstract fun bindAnimalRepository(repository: PetFinderAnimalRepository): AnimalRepository
The app follows a single Activity, multiple Fragments architecture. You want to retain the repository when you swap Fragments. You also want it to survive configuration changes. To enable this, you add the @ActivityRetainedScoped annotation to the binding method. It makes PetFinderAnimalRepository live as long as the Activity and also survive configuration changes. You could also add this annotation to the class itself — the effect would be the same.
I bet you’re starting to get the same feeling I had when I first used Hilt: “This seems too easy. When will it blow up in my face?”
Well… It kinda will as soon as you build and run the app. You’ll get the ViewModel runtime exception again!
While you marked the ViewModel for injection, Hilt can’t reach it yet. As far as Hilt knows, you’re not injecting it into any other component. That’s because it doesn’t know that AnimalsNearYouFragment is a target for injection.
As you might already expect, you can solve the problem with a simple annotation:
@AndroidEntryPoint
class AnimalsNearYouFragment : Fragment()
This annotation marks Android components for injection. Comparing this to what you had to do with vanilla Dagger, it’s pretty cool that you only need a simple annotation now.
Build and run. Yes, it crashes again. But this time, the error is different: Hilt Fragments must be attached to an @AndroidEntryPoint Activity.
Easy. Open MainActivity.kt in the common package and annotate it:
@AndroidEntryPoint
class MainActivity : AppCompatActivity()
Build and run. No crashes! Check Logcat and you’ll see that network requests are happening.
Your next step is to connect to your single source of truth — the database — and display its contents.
Displaying Cute Animals
Before you can make the view observe the data updates, you have to get the stream of data itself. For that purpose, create GetAnimals.kt in animalsnearyou.domain.usecases. In it, create the following use case:
class GetAnimals @Inject constructor(
private val animalRepository: AnimalRepository
) {
operator fun invoke() = animalRepository.getAnimals()
.filter { it.isNotEmpty() }
}
You might wonder whether it’s worthwhile to have a use case this small. Why not just call the repository in ViewModel? Well, while it seems like unneeded complexity, you can look at it as a case of “avoiding broken windows” — that is, inviting more bad behavior.
Say you add the repository to ViewModel. It’s just a matter of time until other developers use it for other things, instead of creating use cases for whatever they need. With this little bit of overhead, you gain a lot in terms of consistency and code management. In the end, as always, it’s a matter of coming to an agreement with your team about how to handle these cases.
Injecting the Use Case
Head back to AnimalsNearYouFragmentViewModel and inject the use case in the constructor, just above the other one:
private val getAnimals: GetAnimals,
You’ll use it in a new method, subscribeToAnimalUpdates. Create it just below onEvent():
private fun subscribeToAnimalUpdates() {
getAnimals()
.map { animals -> animals.map { uiAnimalMapper.mapToView(it) } } // 1
.subscribeOn(Schedulers.io()) // 2
.observeOn(AndroidSchedulers.mainThread()) // 3
.subscribe(
{ onNewAnimalList(it) }, // 4
{ onFailure(it) }
)
.addTo(compositeDisposable) // 5
}
Here’s what you’ve done above:
- You go through the animal list and map each element to its UI counterpart.
- Room handles the
Flowablein a background thread for you, but you still have some code (like mapping above and in the repository) between this code and Room. As such, you callsubscribeOn(Schedulers.computation())to move that work off the main thread. - Calling
observeOn(AndroidSchedulers.mainThread())ensures you access the items on the UI thread. You need to do that to update the UI. - You pass each new list to
onNewAnimalList, which you’ll create in a minute. If an error occurs, you passThrowableto the already familiaronFailure. - Never, ever forget to add the subscription to
CompositeDisposable. Otherwise, you might leak it.
onNewAnimalList will finally update the view state with the list of animals. Create it below subscribeToAnimalUpdates:
private fun onNewAnimalList(animals: List<UIAnimal>) {
Logger.d("Got more animals!")
// 1
val updatedAnimalSet = (state.value.animals + animals).toSet()
// 2
_state.update { oldState ->
oldState.copy(
loading = false,
animals = updatedAnimalSet.toList()
)
}
}
Step by step:
- The API returns unordered pages. The item with ID 79 can appear on page 12, while the item with ID 1000 can show up on the first page. Room returns the elements ordered by their IDs. This means that on each update, you can have new elements appearing amid old ones. This will cause some weird UI animations, with items appearing out of nowhere. To work around it, you concatenate the new list to the end of the current one, and convert the whole thing to a
Set. By definition, sets can’t have repeated elements. This way, you’ll get a nice animation where new items appear below the old ones. Another possible fix is to locally add something like anupdatedAtfield for each item, and use it to order the list. - Update the state with the new item list.
To invoke subscribeToAnimalUpdates(), create an init block in AnimalsNearYouFragmentViewModel.kt, like this:
@HiltViewModel
class AnimalsNearYouFragmentViewModel @Inject constructor(
private val getAnimals: GetAnimals,
private val requestNextPageOfAnimals: RequestNextPageOfAnimals,
private val uiAnimalMapper: UiAnimalMapper,
private val dispatchersProvider: DispatchersProvider,
private val compositeDisposable: CompositeDisposable
): ViewModel() {
init { // HERE
subscribeToAnimalUpdates()
}
// ...
}
Build and run to make sure everything’s OK.
Observing the State
The last step is to observe the state in the Fragment. In AnimalsNearYouFragment, add this method below setupRecyclerView():
private fun subscribeToViewStateUpdates(adapter: AnimalsAdapter) {
viewLifecycleOwner.lifecycleScope.launch { // 1
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { // 2
viewModel.state.collect { // 3
updateScreenState(it, adapter)
}
}
}
}
Going step by step:
- You only care about observing the state while the
Fragment’sViewis alive. As such, you calllaunchonviewLifecycleOwnerso the coroutine gets scoped to theView. - Since Kotlin Flows don’t come with native support to Android component lifecycles, you have to be explicit about how you want to handle it. By using
repeatOnLifecycle, you can force the coroutine to run when theView’s lifecycle is at least at the state you pass in as parameter (in this case,STARTED), and to get cancelled when the opposite lifecycle event happens (in this case,ON_STOP). Not only that, but it’ll restart again whenever theON_STARTevent occurs. On a different note, you’re telling the coroutine to run when the lifecycle reaches theSTARTEDstate because at that point, while theViewis already laid out and ready, it’s still not visible to the user — which makes this a great moment for an update. - You call
collecton theStateFlowto receive any state events that it might emit.
Rendering the State
updateScreenState() is responsible for rendering the view state. Add it below, along with the related methods:
private fun updateScreenState(
state: AnimalsNearYouViewState,
adapter: AnimalsAdapter
) {
// 1
binding.progressBar.isVisible = state.loading
adapter.submitList(state.animals)
handleNoMoreAnimalsNearby(state.noMoreAnimalsNearby)
handleFailures(state.failure)
}
// 2
private fun handleNoMoreAnimalsNearby(noMoreAnimalsNearby: Boolean) {
}
// 3
private fun handleFailures(failure: Event<Throwable>?) {
val unhandledFailure = failure?.getContentIfNotHandled() ?: return
val fallbackMessage = getString(R.string.an_error_occurred)
val snackbarMessage = if (unhandledFailure.message.isNullOrEmpty()) {
fallbackMessage
}
else {
unhandledFailure.message!! // 4
}
if (snackbarMessage.isNotEmpty()) {
Snackbar.make(requireView(), snackbarMessage, Snackbar.LENGTH_SHORT).show()
}
}
In the code above:
- You update every property of the state. If you don’t need to update something, it has no place in the view state.
- This is a placeholder method. It’ll prompt the user to try a different distance or postal code if there aren’t any more animals nearby. For the purposes of this chapter, this method isn’t worth implementing. Plus, you haven’t created the code for distance and postal code selection yet.
- Handling failures can be complex, involving things like retrying requests or screen navigation. In this case, you’re handling every failure the same way: by using a
Snackbarto display a simple message on the screen. You can also see howEventlets you handle each error just once, through itsgetContentIfNotHandled(). - Yes, those double bangs are on purpose. Don’t be afraid of using them when you want to make sure that nullable values exist. The sooner your app crashes, the sooner you can fix the problem. Of course, don’t use them without weighing the consequences. If, for some reason, you can’t use tests or don’t have a QA team testing the app, be more careful. My late uncle always said: “With great power, comes great responsibility.” Just kidding, I never really liked web development. :]
Calling the Subscriber
Last, but not least, you need to call subscribeToViewStateUpdates(). Add it at the end of setupUI(), below the setupRecyclerView() call, and pass in the adapter:
observeViewStateUpdates(adapter)
Build and run. You did it! Look at all those cute animals! Hopefully, you got lucky with the request and got real images instead of the placeholder Glide’s using. :]
Great job! You can finally visualize the work you did over the last few chapters. You built the basis for a scalable and maintainable app — you should be proud of yourself.
There’s a lot still missing here — and it will continue to be missing, because there isn’t enough time to fix everything. For instance, if an animal disappears from the API, you don’t have a way of syncing the cache. There also is no refresh mechanism.
You will add one last thing for this feature, though, because it exposes some interesting topics. In the app, scroll to the bottom of the list and you’ll notice it doesn’t add any more items. You’ll fix that next.
Allowing an Infinite Scroll
Paging is a hard problem to solve, but you won’t use the Paging library here. It adds a lot of complexity, and it’s not that compatible with this architecture because you’d need it in every layer. What matters here is the state management aspect of paging, not what you use to implement it.
Instead, you’ll use a simple infinite scrolling class, which you’ll attach to the RecyclerView. You’ve probably seen InfiniteScrollListener.kt in animalsnearyou.presentation already. Take a peek if you want; it just checks if RecyclerView scrolled close to the last item. Until now, you requested only the first page of animals. With infinite scrolling, you’ll start requesting more pages.
Start by adding a new event in AnimalsNearYouEvent, right below RequestInitialAnimalsList:
object RequestMoreAnimals: AnimalsNearYouEvent()
Now, switch to AnimalsNearYouFragment. Add this method just below setupRecyclerView():
private fun createInfiniteScrollListener(
layoutManager: GridLayoutManager
): RecyclerView.OnScrollListener {
return object : InfiniteScrollListener(
layoutManager,
AnimalsNearYouFragmentViewModel.UI_PAGE_SIZE
) {
override fun loadMoreItems() { requestMoreAnimals() }
override fun isLoading(): Boolean = viewModel.isLoadingMoreAnimals
override fun isLastPage(): Boolean = viewModel.isLastPage
}
}
isLoading() and isLastPage() both use properties that come from ViewModel. These properties don’t exist yet. The loadMoreItems override calls requestMoreAnimals(). This method also doesn’t exist yet, but it should be obvious what it does. Add it below:
private fun requestMoreAnimals() {
viewModel.onEvent(AnimalsNearYouEvent.RequestMoreAnimals)
}
Then, call createInfiniteScrollListener() in setupRecyclerView(), below setHasFixedSize(true), like so:
addOnScrollListener(createInfiniteScrollListener(layoutManager as GridLayoutManager))
Modifying ViewModel
Now, continue to ViewModel. First, you’ll create all the properties to get rid of the errors. At the beginning of the class, add:
companion object {
const val UI_PAGE_SIZE = Pagination.DEFAULT_PAGE_SIZE
}
This gets the page size limit defined in the domain. Then, just below state, add:
val isLastPage: Boolean
get() = state.value.noMoreAnimalsNearby
var isLoadingMoreAnimals: Boolean = false
private set
Finally, react to the event in onEvent() by adding this line to when:
is AnimalsNearYouEvent.RequestMoreAnimals -> loadNextAnimalPage()
You can build, and even run, the app now. In fact, the scrolling already works, although a few details are still missing.
Before dealing with those missing details, however, it’s important to talk about why although isLastPage derives its value from the domain, the same is not true for isLoadingMoreAnimals. In fact, this one has nothing to do with the view state.
That’s because isLoadingMoreAnimals is an implementation detail, since the view doesn’t need to know that the loading is ongoing — at least for now. Things would be different if the UI had something like a Loading more view type.
Another relevant point here is the use of isLastPage, instead of using viewModel.state.value.noMoreAnimalsNearby directly. There are two reasons for this:
- You want the
Fragmentto accessviewStateonly when collecting it. Try to avoid accessing it in other places or the code might get harder to maintain. - Notice the different meaning that each name conveys. While
noMoreAnimalsNearbyalludes to the domain of the app,isLastPageis actually an implementation detail of the infinite scroll. It just happens to have the view state as its source.
In the end, it’s a trade-off: What you lose by exposing properties other than the view state, you win in code simplicity, intent expression and SoC.
Using the Properties
You don’t need to do anything else for isLastPage — its custom getter will make sure you always get the most up-to-date value from the view state. However, for isLoadingMoreAnimals, you want it to be true when you’re waiting for the API request to finish and false when you have its result. You’ll make this happen in loadNextAnimalPage().
Right on top of the method, above errorMessage, add:
isLoadingMoreAnimals = true
And at the end, right after the onPaginationInfoObtained() call and still inside the coroutine’s scope, add:
isLoadingMoreAnimals = false
This avoids triggering more requests while another request is running. The infinite scrolling methods run on the UI thread, so there’s no risk of concurrency here.
Build and run. Look at the logs and you’ll see that the infinite scroll works one request at a time. If you’re patient enough, you’ll see that it stops loading more items when it reaches the end.
And that’s it — you’re done with the Animals near you feature. Great work! By implementing this feature, you learned the basics of state management.
You won’t add any tests in this chapter. In fact, if you try to run the tests in the androidTest package now, Hilt will complain about dependencies. That’s expected due to the current configuration, as you’re not providing every dependency needed. This will be fixed in the next chapter, where you’ll also take the development up a notch by implementing a constantly changing state.
Key Points
- Keep the UI as dumb as possible.
- View states represent the state that the user sees.
- UI models should contain the minimum information necessary for display, in the simplest format possible.
- You can handle exception handling in coroutines with a
CoroutineExceptionHandleror, for more control, withtry-catchblocks. - Encapsulate your logic in use cases.
- Inject your dependencies with Hilt, using the Android-specific features it provides.