Fonament Presentation
The fonament presentation contains base blocks to build a UI Node (Screen, Dialog, ...).
FonamentUI<FonamentUIState>: Represents the base class for the UI Node. It contains aFonamentContentinstance that is rendered on theinvokeand that receives theFonamentUIStateinstance from theFonamentViewModel.FonamentViewModel: handles the events triggered by thecontentand updates the state ofuiStateinstance. These events are received on thehandleEvent(event: FonamentEvent)function.FonamentContent: contains the content of our UI Node. It receives auiState,contentState,modifierandonEventparameters oninvokefunction. The UI state responsibility is shared between theuiStateandcontentStatevariables, following the Android Developers definition for the types of UI state. ThecontentState: FonamentContentStateis the state holder for the UI Elements on this content. It is based on the State hoisting practice of having a plain state holder class.
Defining the FonamentUI class
data object EditTaskListScreen : FonamentUI<EditTaskListUIState>() {
override val content: FonamentContent<EditTaskListUIState, *> = EditTaskListContent
}
@Immutable
data class EditTaskListUIState(
val isLoading: Boolean = false,
) : FonamentUIState
Defining the FonamentViewModel class
class EditTaskListViewModel(
private val updateTaskListUseCase: UpdateTaskListUseCase,
) : FonamentViewModel<EditTaskListUIState>(
initialUIState = EditTaskListUIState(isLoading = true),
) {
override fun handleEvent(event: FonamentEvent) {
when (event) {
is EditTaskListEvent.UpdateTaskList -> updateTaskList(event)
}
}
// ...
}
FonamentEvents
sealed interface EditTaskListEvent : FonamentEvent {
data class UpdateTaskList(val name: String) : EditTaskListEvent
}
Defining the FonamentContent class
FonamentContent Code
data object EditTaskListContent : FonamentContent<EditTaskListUIState, EditTaskListContentState>() {
@Composable
override fun createContentState(
uiState: EditTaskListUIState,
): EditTaskListContentState = rememberEditTaskListContentState(
taskListName = uiState.taskList?.name ?: "",
)
@Composable
override fun Content(
uiState: EditTaskListUIState,
contentState: EditTaskListContentState,
modifier: Modifier,
) {
when {
!uiState.isLoading -> {
Scaffold(
topBar = {
EditTaskListTopBar(
onSaveButtonClick = {
onEvent(
EditTaskListEvent.UpdateTaskList(
contentState.nameTextFieldValue,
),
)
},
)
},
content = { paddingValues ->
EditTaskListContent(
nameTextFieldValue = contentState.nameTextFieldValue,
modifier = Modifier.padding(paddingValues),
)
},
)
}
}
}
Info
The events can be triggered on FonamentContent using the onEvent function.
FonamentContentState Code
data class EditTaskListContentState internal constructor(
private val taskListName: String,
) : FonamentContentState {
var nameTextFieldValue: String by mutableStateOf(taskListName)
private set
override fun handleEvent(event: FonamentEvent) {
when (event) {
is EditTaskListEvent.NameTextFieldValueChange -> {
nameTextFieldValue = event.value
}
}
}
companion object {
internal fun saver(): Saver<EditTaskListContentState, String> = Saver(
save = {
it.nameTextFieldValue
},
restore = {
EditTaskListContentState(
taskListName = it,
)
},
)
}
}
Abstract
For this implementation we are implementing a custom Saver to avoid losing nameTextFieldValue between configuration changes. Also, it is important to remember the FonamentContentState instantiation to be reused across compositions.
📲 Display a Fonament UI Node
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun App() {
EditTaskListScreen.invoke(
viewModel = viewModel<EditTaskListViewModel>(),
onEvent = {},
)
}
Info
We are using the androidx.lifecycle.viewmodel.compose.viewModel function to create the ViewModel on this base example, but we can also Koin and Hilt. Check the examples on demos directory.
🧠Navigation
Navigation is handled with a specific type of FonamentEvent, the FonamentNavigationEvent. To make a FonamentUI node navigable we should use the FonamentUI<U>.NavigationNode extension function. This function receives a FonamentNavigationEventHandler and the viewModel. The FonamentNavigationEventHandler is an interface that handles FonamentNavigationEvent.
sealed interface TaskDetailsNavigationEvent : FonamentNavigationEvent {
data object NavigateBack : TaskDetailsNavigationEvent
data object NavigateToEditTask : TaskDetailsNavigationEvent
}
fun taskDetailsNavigationEventHandler(
navigateBack: () -> Unit,
navigateToEditTask: () -> Unit,
): FonamentNavigationEventHandler<TaskDetailsNavigationEvent> = FonamentNavigationEventHandler {
when (it) {
TaskDetailsNavigationEvent.NavigateBack -> navigateBack()
TaskDetailsNavigationEvent.NavigateToEditTask -> navigateToEditTask()
}
}
Then, in our Navigation graph (in this example, NavGraph from NavigationCompose):
private fun NavGraphBuilder.taskDetailsNode(
navigateBack: () -> Unit,
navigateToEditTask: (String) -> Unit,
) {
composable(...) { navBackStackEntry ->
val taskId = ...
val taskDetailsNavigationEventHandler = taskDetailsNavigationEventHandler(
navigateBack = navigateBack,
navigateToEditTask = {
navigateToEditTask.invoke(taskId)
},
)
TaskDetailsScreen.NavigationNode(
navigationEventHandler = taskDetailsNavigationEventHandler,
viewModel = ... // DI viewModel with taskId parameter.
)
}
}
Launch the event on content
@Composable
private fun TaskDetailsTopBar() {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
onEvent(TaskDetailsNavigationEvent.NavigateBack)
},
) {
📸 Preview/Test UI content
This separation of concerns between FonamentContent and FonamentViewModel makes it easier to test and preview because we don't need a FonamentViewModel instance to test/preview our content.
@PreviewLocales
@PreviewLightDark
@PreviewLandscape
@Composable
fun EditTaskListContentPreview() {
EditTaskListContent(
uiState = EditTaskListUIState(
taskList = taskListSample,
),
contentState = rememberEditTaskListContentState(
taskListName = taskListSample.name,
),
)
}