Chapters

Hide chapters

Real-World Android by Tutorials

Second Edition · Android 12 · Kotlin 1.6+ · Android Studio Chipmunk

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

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, Fragment and ViewBinding.
  • 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 ViewModel Android component doesn’t necessarily mean that you’re following the MVVM pattern. You can use a ViewModel in 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:

Figure 6.1 — High-Level View of the Architecture
Figure 6.1 — High-Level View of the Architecture

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.

Figure 6.2 — Petsave Starter App
Figure 6.2 — Petsave Starter App

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:

  1. A RecyclerView for the list.
  2. An Adapter for the RecyclerView.
  3. View state and events for state management.
  4. Use cases.
  5. A ViewModel to handle events and update the view state.
  6. 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:

  1. onViewCreated() executes immediately after onCreateView(). The framework makes sure that you’ve correctly initialized all Views at this point, so you should do all your View setup here. For instance, if you use LiveDatas, observing them here ensures they’re unsubscribed in onDestroyView(). By creating the view in onCreateView(), then initializing it in onViewCreated(), you maintain a good separation of concerns (SoC). Plus, you don’t need to worry about null pointer exceptions (NPEs) on View access.

  2. Create a method to glue together all the UI setup code. Inside it, you delegate each component’s setup to other methods.

  3. Create an adapter value and initialize it.

  4. Run some standard RecyclerView code. You set the Adapter, a GridLayoutManager with two items per row, and tell the RecyclerView that all elements have the same size. This way, the RecyclerView can 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:

  1. Null out the Adapter property in onDestroyView.
  2. Null out the Adapter reference in the RecyclerView itself, 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:

  1. Creation of the View to recycle for other items of the same type.
  2. Binding the data of each item you want to display to the View created 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
  }
}
  1. This method has the job of checking if oldItem and newItem are 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 return false instead of true — which will cause the item to flicker in the RecyclerView!
  2. This method is called only if areItemsTheSame returns true. Here’s where you should compare the contents. Since UIAnimal is 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.

Figure 6.3 — Petsave Starter App
Figure 6.3 — Petsave Starter App

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:

  1. A Boolean representing the loading state.
  2. A list of items to display.
  3. A Boolean representing the no more animals nearby state.
  4. 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:

  1. The UI sends events to the ViewModel.
  2. ViewModel reacts to those events by triggering the use cases.
  3. The use cases return state information.
  4. ViewModel updates 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:

  1. A mapper that translates the domain model to the UI model.
  2. A CompositeDisposable for RxJava. You don’t need to inject schedulers because RxJava provides a way to override them all while testing.
  3. 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. ViewModel will 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:

  1. You create a private MutableStateFlow with the initial state of AnimalsNearYouViewState. You’ll use this property to update the state, which will be exposed to AnimalsNearYouFragment through an immutable StateFlow. Unlike LiveData, StateFlow doesn’t benefit from lifecycle-aware behavior, so you’ll have to take some extra steps when subscribing to the stream.
  2. 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.
  3. You set _state to the immutable StateFlow that actually exposes the state.
  4. You create the only public method in the ViewModel. AnimalsNearYouFragment calls 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:

  1. The if condition checks if the state already has animals. Fragment will send the RequestInitialAnimalsList event 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 call loadNextAnimalPage().
  2. You create a CoroutineExceptionHandler through a custom createExceptionHandler extension function on viewModelScope. It takes in a lambda, which in turn takes a Throwable. You call onFailure() in the lambda, then pass it that same Throwable.
  3. You launch a coroutine on viewModelScope, passing in the CoroutineExceptionHandler to the launch extension 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:

  1. For now, you’re only handling NetworkException and NetworkUnavailableException. The former is a new exception that avoids having Retrofit code in the presentation layer. Check requestMoreAnimals in PetFinderAnimalRepository and you’ll see that it throws a NetworkException — a domain exception — when Retrofit’s HttpException occurs.
  2. You update the state by calling update on the MutableStateFlow. 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. The update method has a lambda as a parameter, which in turn reveives an object of type AnimalsNearYouViewState as 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 this copy method, which really comes in handy here.
  3. Again, you use Event to wrap Throwable so 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:

  1. 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.
  2. You inject an AnimalRepository, allowing the use case to access the data sources.
  3. 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 ViewModel simple 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:

  1. It’s a suspend function. Neat!
  2. 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’s suspend functions 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, withContext returns the scope’s result, so by calling return here you have to make the scope return something of type Pagination.
  3. You’re calling requestMoreAnimals on the repository and destructuring its result.
  4. If there are no animals, you throw the NoMoreAnimalsException exception, which you’ll handle in onFailure.
  5. You call storeAnimals to store the animals you got from the API in the database.
  6. 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:

  1. Call the use case, passing in the current page after incrementing the value.
  2. Pass the pagination result to onPaginationInfoObtained.

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:

  1. You go through the animal list and map each element to its UI counterpart.
  2. Room handles the Flowable in 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 call subscribeOn(Schedulers.computation()) to move that work off the main thread.
  3. Calling observeOn(AndroidSchedulers.mainThread()) ensures you access the items on the UI thread. You need to do that to update the UI.
  4. You pass each new list to onNewAnimalList, which you’ll create in a minute. If an error occurs, you pass Throwable to the already familiar onFailure.
  5. 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:

  1. 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 an updatedAt field for each item, and use it to order the list.
  2. 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:

  1. You only care about observing the state while the Fragment’s View is alive. As such, you call launch on viewLifecycleOwner so the coroutine gets scoped to the View.
  2. 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 the View’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 the ON_START event occurs. On a different note, you’re telling the coroutine to run when the lifecycle reaches the STARTED state because at that point, while the View is already laid out and ready, it’s still not visible to the user — which makes this a great moment for an update.
  3. You call collect on the StateFlow to 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:

  1. You update every property of the state. If you don’t need to update something, it has no place in the view state.
  2. 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.
  3. 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 Snackbar to display a simple message on the screen. You can also see how Event lets you handle each error just once, through its getContentIfNotHandled().
  4. 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. :]

Figure 6.4 — These Pictures Make Everything Worthwhile!
Figure 6.4 — These Pictures Make Everything Worthwhile!

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 Fragment to access viewState only 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 noMoreAnimalsNearby alludes to the domain of the app, isLastPage is 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 CoroutineExceptionHandler or, for more control, with try-catch blocks.
  • Encapsulate your logic in use cases.
  • Inject your dependencies with Hilt, using the Android-specific features it provides.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.