Using design and navigation libraries to implement Clean Architecture and MVI patterns
The core architecture defines the project structure, layers, and conventions. The design and navigation libraries provide concrete implementations for the presentation layer, reducing boilerplate and enforcing architectural rules at the code level.
The design library provides the building blocks for the MVI presentation layer:
ScreenState, ScreenController, and the state builder DSL functions
(screen, loading, error).
ScreenState is the primary state model used across most screens. It encapsulates all UI-relevant
state and is constructed using a DSL-like builder pattern.
ScreenBuilderViewModel is a convenience base class in the project's
core:presentation module. It implements ScreenController from
design and provides navigation support. ViewModels inherit from it and use the builder
DSL functions to construct ScreenState:
@KoinViewModel
class FeatureScreenViewModel(
@InjectedParam private val destination: FeatureDestination,
@Provided private val getDetailsUseCase: GetFeatureDetailsUseCase,
) : ScreenBuilderViewModel() {
override val state: StateFlow<ScreenState> = _state
private val _state = MutableStateFlow<ScreenState>(
loading { message(TextViewData.Res(Res.string.generic_loading)) }
)
init {
viewModelScope.launch {
val result = getDetailsUseCase()
_state.value = screen(header = header(showBackButton = true)) {
title(TextViewData.Raw(result.title))
text(TextViewData.Raw(result.description ?: ""))
}
}
}
}
The builder functions produce sealed ScreenState values: loading { }
for loading indicators, error { } for error views, and screen { }
for content. The ScreenComposable UI component subscribes to the state flow and
renders the correct state automatically.
Beyond reducing boilerplate, the builder DSL enforces consistent UX across every screen.
Padding, spacing, typography, and window-inset handling are baked into each component — a
media() or title() call produces the same visual result everywhere.
Loading and error states are also uniform, since they are rendered by
ScreenComposable from the sealed ScreenState rather than hand-crafted
per screen.
See the Builder API for the full list of available content scope builders.
When not using ScreenBuilderViewModel, follow the stateless UI pattern with
ScreenEvents and separate ScreenUi composables. The design library's
components are compatible with both approaches.
The navigation library provides a type-safe, serializable navigation system with animated transitions and multi-backstack support.
Each screen is associated with a specific AppDestination subclass defined in the
feature's :api module. Destinations are @Serializable, carry typed
parameters, and manage backstack position via Destination.Args:
@Serializable
data class FeatureDestination(
val itemId: String,
val backstackIndex: Int = 0,
) : AppDestination(
args = Destination.Args(
backstackIndex = backstackIndex,
)
)
@Composable
fun FeatureScreen(
destination: FeatureDestination,
viewModel: FeatureScreenViewModel = koinViewModel(parameters = { parametersOf(destination) })
) {
ScreenComposable(viewModel)
}
ViewModels inheriting from ScreenBuilderViewModel can trigger navigation using the
navigate(destination) function, which accepts any AppDestination:
class FeatureScreenViewModel(...) : ScreenBuilderViewModel() {
fun onItemSelected(id: String) {
navigate(DetailDestination(itemId = id))
}
}
The combination of design and navigation provides a complete implementation of the patterns described in the core architecture:
screen { }, loading { }, error { }) and ScreenComposable rendererAppDestination with backstack management and animated transitionsScreenBuilderViewModel that wire ScreenController, navigation, and DI togetherTogether, these libraries let you focus on feature logic while the framework handles the architectural plumbing.