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 aFonamentContent
instance that is rendered on theinvoke
and that receives theFonamentUIState
instance from theFonamentViewModel
.FonamentViewModel
: handles the events triggered by thecontent
and updates the state ofuiState
instance. These events are received on thehandleEvent(event: FonamentEvent)
function.FonamentContent
: contains the content of our UI Node. It receives auiState
,contentState
,modifier
andonEvent
parameters oninvoke
function. The UI state responsibility is shared between theuiState
andcontentState
variables, following the Android Developers definition for the types of UI state. ThecontentState: FonamentContentState
is 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)
}
}
// ...
}
FonamentEvent
s
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,
),
)
}