UI Layer : Android app architecture notes

Jordan Mungujakisa
4 min readDec 3, 2023

--

Photo by Guido Coppa on Unsplash

To manage dependencies between different layers, make use of dependency injection with Hilt.

Points

  • Separation of concerns.
  • Drive UI from the data models (preferably persistent data models).
  • Single source of truth (Can be a database or view model or even the UI).
  • Unidirectional data flow.

Benefits of clean architecture

  • Maintainability
  • Scalability
  • Easy onboarding
  • Testability

UI layer

This consists of the UI elements and the state holders;

  • UI elements present data to the user and update on user interactions. This can be a composable in the case of Jetpack compose and Fragments/Activities in the case of the XML based views.
  • State holders, hold data, present it to the user and handle logic. For example, ViewModel, etc.
Components of the UI layer of Android app architecture
Components of the UI layer

Unidirectional data flow (UDF)

Exposing UI state

The UI is state is always changing basing on user events and also data from the internet. Therefore, we expect the data presented to the user to be changing, so we can expose UI states as an observable data stream like a LiveData or StateFlow We can create a simple data class to represent the UI state.

A common way of exposing a stream of UI state from the viewmodel is by exposing a backing mutable state flow to the UI.

var uiState by mutableStateFlow(UiState())
private set
// private set makes the setters of the state only possible
// within the scope of the view model

The view model then exposes methods that can be used to internally mutate the sate

//copy and update the the state
uiState = uiState.copy(name = "Some Name")

Related data should always be expose in a single stream in order to prevent inconsistencies in the UI for example cases like a like button changing icon without the likes count changing.

Any operation performed in the ViewModelshould be main thread safe and all intensive operations/tasks should be moved to the Domain or Data layers.

UI event decision tree

UI events can be directly handled in the UI when the event concerns modifying data in the UI layer. Like expanding a drop down button, turning a switch on and off, etc.

UI events that originate from the ViewModel should always result in updating the UI. For example once a user has been signed in, we validate and authenticate the user in the ViewModel and as soon as the user has been signed in successfully or an error occurs we want to update the UI accordingly.

// Whenever the uiState changes, check if the user is logged in.
LaunchedEffect(viewModel.uiState) {
if (viewModel.uiState.isUserLoggedIn) {
currentOnUserLogIn()
}
}

Consuming events can trigger state updates

For example, when the UI state triggers the UI to show a Snackbar indicating that there is no network connectivity and we would like to revert that state once the user dismisses/has consumed that information. In this scenario, the ViewModel Can expose some methods that can be used to reset/update the UI state appropriately.

//viewmodel
fun userMessageShown(){
uiState = uiState.copy(
userMessage = null
)
}

In this scenario, once the UI has shown the message to the user say for example in a dialog or Snackbar, then the UI can call the userMessageShown() method in order to inform the ViewModel that the UI has changed therefore the ViewModel can update the UI state accordingly.

UI Layer logic

The UI layer handles two different kinds of logic;

  1. UI Logic: This consists of logic that pertain to how to display state changes. For example navigation events, etc.
  2. Business Logic: These pertain to what to do with the state changes and usually involve updating the data for example when a user bookmarks an article, we need to notify the data layer that the user has bookmarked an article therefore we need to update the data source. This kind of logic are usually handled by the state holders e.g. a ViewModel

Handling UI States in a Lifecycle conscious way

Sometimes the State holders can be destroyed and the UI continues to listen to these UI events from an invalid State Holder. In this scenarios we would need to listen to the UI states in a lifecycle aware manner. And we can do that like this;

// In our UI e.g LoginScreen composable
val lifecylcle = LocalLifecycleOwner.current.lifecycle
val currentOnUserLogIn by rememberUpdatedState(onUserLogIn)
LaunchedEffect(viewModel, lifecycle){
snapshotFlow { viewModel.uiState }
.filter { it.isUserLoggedIn }
.flowWithLifecycle(lifecycle)
.collect { currentOnUserLogin() }
}

//the currentOnUserLogin function is a callback method that
navigates the user to the next screen

The flowWithLifecycle extension function makes the flow only active when the composable is in its started state.

Sometimes we may be using Channels and other reactive streams to inform the UI of the state changes. When the producer (ViewModel) outlives the consumer(UI — compose or views) these solutions do not guarantee the delivery and processing of those events. This can lead to inconsistent UI states which is unacceptable.

--

--

Jordan Mungujakisa

Mobile app alchemist who is trying to transmute elegant designs, into elegant code, into beautiful mobile app experiences.