Assisted Project Architecture

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.

Design System

The design library provides the building blocks for the MVI presentation layer: ScreenState, ScreenController, and the state builder DSL functions (screen, loading, error).

ScreenState

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

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.

Stateless UI Integration

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.

Navigation

The navigation library provides a type-safe, serializable navigation system with animated transitions and multi-backstack support.

AppDestination

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,
    )
)

Screen Entry Point

@Composable
fun FeatureScreen(
    destination: FeatureDestination,
    viewModel: FeatureScreenViewModel = koinViewModel(parameters = { parametersOf(destination) })
) {
    ScreenComposable(viewModel)
}

Outgoing Navigation

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))
    }
}

Putting It Together

The combination of design and navigation provides a complete implementation of the patterns described in the core architecture:

Together, these libraries let you focus on feature logic while the framework handles the architectural plumbing.

← Back