Implementing right architecture for your app in android

Mubarak Native
8 min readAug 29, 2024

--

In this article, we are going to learn how to architect our app to scale better, be easy to maintain, and for testing. It includes state management and UDF (Unidirection data flow) and also learning why we need to properly architect our app, its benefits, and lots more. Let’s begin.

Note: This article is primarily focused on architecting composed-based android apps, but the rest of the pattern is also applicable for all types of UI framework including Views.

Why do we need to invest in architect our app ?

By adopting the best architecture for the app, we can ensure our app can easily scale better by adding new features easily in our app, and it is easy to test our functions or classes in isolation by replacing it with a fake version for testing and also there is no single architecture that is best suited for all app.

How to architect our app ?

In some old codebases, all the business logic, and UI interactions happen in an Activity or a Fragment (App components). These classes are only meant to show our app data, and a primary entry point for user interaction. We never own this class. This is provided by the android framework itself. By making these classes what they meant to be, we can achieve separation of concern and better scalability and testability in our app.

As I already mentioned, there is no single architecture that is better suited for all types of app. Some apps might have an advanced UI design with simple business logic considered. Some of them consider both logic and UI, but whatever the app requirements and features it has, the core architecture and best practise remains the same, such as: Single Responsibility, Unidirectional data Flow (data (state) flows down the event trigger up), Here is the list some of architectural pattern that every app must satisfy

  • AAC (Android Architecture component) Viewmodel to provide screen UI state and handling business logic.
  • Reactive programming by using Kotlin Flows.
  • Coroutines for asynchronous operations.
  • Dependency Injection.

Android recommended app architecture

Android recommends each application should have at least two layers.

  • UI Layer that displays app data on the screen, and is the primary entry point for user interactions.
  • Data Layer that contains core app data that we need to show on screen and contains business logic.
App Architecture

Let’s explore this layers in more detail.

UI Layer:

The UI Layer

The main role of the UI layer is to display the application data on the screen and is the primary entry point for user interaction. The UI layer should be reactive. This means when the underlying data changes, the UI should update to the latest current value.

The UI layer is made up of two things:

  • UI elements that show the data on the screen using its UI elements:Text, Switch,etc. You build these elements using Views or Jetpack Compose.
  • State holders AAC (such as ViewModel classes) that hold data, expose it to the UI, typically the data (Screen UI state), and handle logic.

Note: Here, ViewModel is the android recommended implementation of managing screen level UI state with access to the data layer. It also survives process recreation automatically (Configuration changes) common in android apps. Usually, the event produced by users through UI elements is handled on ViewModel. According to the event, it produces the UI state.

The UI Layer consists of UI elements and StateHolders

As I described in the image above, about the UDF pattern, here the UI Layer consists of UI elements and a state holder (ViewModel) UI element. It can be built on Compose or Views. When the user interacts in our app, such as clicking the button or toggling the switch, the events that are triggered are handled by the ViewModel. According to the event, it performs logic and creates the screen UI state, then it feeds back to the UI.

In this pattern, as you can see clearly, the state flows down, and events are up. This is called UDF (Unidirectional data flow). This will ensure our app remains in a consistent state.

This is the example of compose code that implement UDF pattern

@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }

HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") })
}
}

/**As you can see here where (name) parameter in HelloContent STATE goes
DOWN from HelloScreen to HelloContent and the (onNameChange) lambda EVENT UP*/

Note: Another important thing to consider ViewModel should not contain: Android framework classes such as: Activity and Context, because as I already mentioned, it also survives configuration changes, meaning its lives longer than the Activity, So it creates a memork leaks.

How to create a UI State or Screen UI State ?

Before we create a UI state, we need to ensure some of its properties must be satisfied. Here is the list of properties of the UI state that need to be satisfied

  • Immutable in nature: The UI state must be immutable, because modifying the UI state in UI will lead to an inconsistent app state. The Kotlin data class is the best suite for this
  • The UI State will never modify into UI itself unless the UI itself is the sole source of its data. Only the owner that will produce the data should be responsible for updating the data they expose.

Let’s see the example of defining Screen UI State:

data class HomeNoteUiState(
val notes: List<Note> = emptyList(),
val message: Int? = null
)

This is a very basic example of defining the Screen UI state in ViewModel

Expose UI state

As I already mentioned, UI should be reactive. It means it collects or observes the data (UI State). The data must be a streams of data. Whenever the data changes, it immediately notifies its subscriber, instead of sending a single value, which is the case in the suspend function. Also, the UI does not directly pull data from the ViewModel, instead it collects it Kotlin Flows is perfect for this example for such observable data holders such as, LiveData or StateFlow.

In a Compose app, we can use a compose observable state api for exposing the UI state. Any type of observable data holder such as StateFlow or LiveData that I mention here can be easily consumed in Compose by using the following extension function.

  • For LiveData → observeAsState()
  • For Flow → collectAsState() or collectAsStateWithLifecycle()
  • For RxJava2 → Observable.subscribeAsState()

Here is an example of both the Views and Compose

Jetpack Compose

data class HomeNoteUiState(
val notes: List<Note> = emptyList(),
val message: Int? = null
)
class HomeNoteViewModel(
private val osbRepository:OsbRepository
){

val uiState by mutableStateOf(HomeNoteUiState)
private set // only update the UiState on ViewModel exposing immutable version
// for UI
}

Views

data class HomeNoteUiState(
val notes: List<Note> = emptyList(),
val message: Int? = null
)

class HomeNoteViewModel(
/* ... */
){
private val _uiState = MutableStateFlow(HomeNoteUiState())
val uiState: StateFlow<HomeNoteUiState> = _uiState.asStateFlow()
// only update the UiState on ViewModel exposing immutable version for UI
}

Updating the UI State in ViewModel

Here is the simplified example of Updating the compose observable type UI State in ViewModel.

data class SearchNoteUiState( // UI State
val notes: List<Note> = emptyList()
)

@HiltViewModel
class SearchNoteViewModel @Inject constructor(
private val osbRepository: OsbRepository
) : ViewModel() {

var uiState by mutableStateOf(SearchNoteUiState())
private set

fun searchNote(searchQuery: String) {
viewModelScope.launch {
osbRepository.getNoteBySearch(searchQuery).collect { notes ->
uiState = uiState.copy(
notes = notes
)
}
}
}
}

Domain Layer

The domain layer is an optional layer that sits between the UI layer and the data layer.

The Domain Layer

As I mentioned earlier, the domain layer is optional. It is only required to encapsulate complex business logic that is reused by multiple ViewModels. The Domain layer consists of Usecases or interactors that depend on data layer repositories.

There are some ideal use cases when the domain layer is best suited in your app architecture.

  • If your Viewmodel class is filled with business logic and functions, you want to separate out and reuse them. In this case, introducing a domain layer in your app architecture can be beneficial, so it avoids code duplication on multiple viewmodels.
  • Testing is easy and improves code readability.

Here, is the android platform demo

class FormatDateUseCase(userRepository: UserRepository) {

private val formatter = SimpleDateFormat(
userRepository.getPreferredDateFormat(),
userRepository.getPreferredLocale()
)

operator fun invoke(date: Date): String {
return formatter.format(date)
}
}

Data Layer

The data layer contains application data and business logic.

The Data Layer

The Data Layer consists of repositories and concrete data stores. It can be local (Room) or remote. The other component in the data layer is repository classes. They are responsible for the following tasks

  • Exposing data containing methods to the rest of the app.
  • Resolving conflicts between multiple data sources.
  • Abstracting data sources from the rest of the app (so that we can swap the implementation easily, ex: switching from Retrofit to Ktor http client),
  • Containing business logic

Note: When we declare a repository we should abstract it by using an interface, so that it can be easily tested with fake implementation. Also, it helps to swap the implementation easier and, by using Dependency Injection, we don’t have to construct the object for the implementation manually, it is, constructed automatically. We only need to tell you how it’s going to be constructed for more info. See: Dependency Injection on Android.

Here is the example of my OnScriber App Repository

Conclusion

In this article, we overview the modern app architecture that Android recommends, and its benefits, use cases and more. I hope this article helps you to provide a good overview of each topic. If you want to dig deeper into these, I would recommend reading the Guide to app architecture section in developer docs. If you want to see more details in each layer, provide your feedback on this page. I will try to create those articles for you. I hope you will like this article. If so, don’t forget to share this with your friends and family and I will see you in the next upcoming article.

--

--

Mubarak Native
Mubarak Native

Written by Mubarak Native

Mubarak Basha a.k.a (Mubarak Native) Mobile App Developer, Embedded Systems Developer, Analog,... driven by curiosity and not afraid to explore new worlds.

No responses yet