Displaying Common States using an Activity Scoped ViewModel

Matthew Meehan
5 min readSep 2, 2021
Image by andrekheren

Following the introduction of Navigation Component, the Single-Activity Architecture has gained a lot of popularity. With a singular parent activity presiding over many fragments, it would make sense that some of the common UI states could be handled by the activity rather than individual fragments. For example, instead of putting a progress indicator in each individual fragment, the activity could contain a single instance of the indicator to be shown while the fragment is loading data. In order to achieve this form of fragment to activity communication, it would be good practice to utilize an Activity-Scoped ViewModel.

The first step to setting this project up would be to create the activity layout with all of the required UI components.

Here in the main activity layout we have:

  • Our Fragment Container View, which is where the fragments from our navigation component will be displayed.
  • A progress indicator
  • Two text views for displaying an error header and message

By default the progress indicator and textViews have their visibility set to gone, with the container being the only visible part of the layout.

Now that our layout is set up, the main activity will need some logic for each state.

The three functions above modify the visibility of the layout components defined previously:

  • The startLoading function hides the fragment container (making the current fragment in the navigation graph gone) and makes the loading indicator visible.
  • The LoadingComplete function does the opposite; making the fragment visible again while the loading indicator becomes hidden.
  • Finally, the showError function make both of those components invisible while revealing the two error text views. This function also sets the text of each of these text views: one for the header and one for the error message.

Next, each of the states for the Main Activity will be defined inside a sealed class. This way, we have organized the possible viewModel states into a common type.

Once this is complete, the viewModel can finally be created.

This implementation of the activity viewModel is extremely simple. In this example project, it’s used simply as a shared state container, which can be accessed by any child fragment to modify the state of the Main Activity. Now, in order for the UI to respond to state changes, this state will need to be observed in the activity.

A few things are happening in the code above:

  • The viewModel is initialized using the viewModels() delegated property
  • A new function called setObservers() is called from within onCreate
  • A coroutine is launched within the activity’s lifecycleScope when the lifecycle is started for observing (collecting is the Flow term) the state of the viewModel
  • The when(state) block maps the states from the MainViewModelState sealed class to the proper function to modify the UI

With this implemented, the activity is now monitoring the current state of the viewModel, and will change the UI accordingly upon any state changes.

Now it’s time to explore how a child Fragment will share this viewModel and modify the main activity’s UI. For this example, the fragment will have only two components: a textView to display a cat fact to the user and a button to get a new cat fact.

With the Fragment defined, it’s already possible to access the shared viewModel. Adding this line to the Fragment will allow access to the parent activity’s viewModel.

Notice that the new delegated property is activityViewModels(). This ensures that the MainAcitivtyViewModel is retrieved from the Activity Scope, where it is shared between the parent MainActivity and the child CatFactFragment. Using this tool, the exposed state of the parent activity can be shared and thereby modified in any child fragment like this:

This code would change the state of the MainActivityViewModel, causing the startLoading function to be called, which in turn would hide the current CatFactFragment to show the loading indicator instead. Success!

Although this shortcut demonstrates an achievement of our goal, we can do better by fleshing out the example app to be more realistic. Similar to the MainActivity above, a sealed class will be defined to encapsulate the various states for this particular fragment.

For the sake of this bare-bones example, these states are almost identical to the ones defined in the activity; however, in a real scenario, there will likely be more specialized states that are unique to each fragment. These states would not be reflected in the parent activity states.

Once the states are defined, the Fragment viewModel implementation can be started. The first piece to add, similar to the MainActivityViewModel, is the current state.

In this code, the publicly exposed state field is defined as non-mutable. This is done to protect the state so that only the viewModel can privately modify it, while the view can still observe it. This is unlike the shared activity state, which should be modifiable by its children.

For this app, the behavior of the cat facts api was mimicked via local data defined in the viewModel. This list contains 10 facts returned from the Api:

This was written to avoid implementing any networking logic that would complicate the example app.

Now, to simulate a real network call, a slight delay was added between the emission of the loading state and the state that would follow (either error or loaded).

The idea behind this code is to make a fake network call in order for this example to be more realistic:

  • A coroutine is launched and the state is set to loading
  • A delay of 3 seconds simulates a request being made to the network
  • The retrieved data is initialized as a random cat fact from the list (can occasionally be null to set off the error state)
  • The proper state is emitted depending on the data

And finally, to put all of the pieces together, we can add references to both of these viewModels in the Fragment.

When declaring these viewModels, make extra sure that the proper delegated property is used. The MainActivityViewModel should use the activityViewModels scope (as explained previously) and the CatFactViewModel should use the regular viewModels scope.

Similar to the MainActivity, observing logic can now be added to the fragment to control both the fragment’s ui and the main activity’s ui state.

As shown in the above logic, the MainActivity’s ViewModel state can be modified by the child fragment. Combined with the state collection logic in the activity, this means that any child fragments can now safely affect the parent’s UI. The resulting application behaves like this:

And the error state described earlier (when the “fetched” cat fact is null) would look like this:

Using an Activity scoped viewModel is a great tool when using the Single-Activity Architecture. Shared UI elements can now be housed and modified in the MainActivity instead of in each fragment. Additionally, this approach avoids getting the activity from the fragment and casting it to a specific activity type. Hopefully this example was helpful!

The source code for this project can be found on Github.

--

--