Screen
The Screen
is the visual representation of the feature that we are working on. It is component that renders our UI based on the given State
and emits Actions
for our Coordinator
to handle. Here are a few rules we would like our Screen
to flow.
- Easily Previewable - We should be able easily preview our screen with different states, across different device sizes and modes, light or dark
- Consise - The
Screen
component should be relatevely short, easy to read and understand. Encapsulating parts of it UI into separate components is a must. Also be aware of the number of nested blocks, having it small as possible makes the code much more readable - State less. The
Screen
should consume state but not manage this state. It doesn't allowed to reference any other component is not UI only.
State consumption
The UI of the Screen is Rendered based on the Provided state, it can a ViewModel state or it can be a composable component state. It is good practice to let you component know a little as possible. Since the Screen
is split into UI components we can make these components rely only the part of the state that is provided to the Screen
. So the deeper we go through the UI tree the less components know about our specific feature and its state
This makes these components more re-usable, agnostic to a specific ViewModel state, and causes less re-rendering wen recomposition
data class ExampleState(
val user: User,
val products: List<Products>
)
data class ExampleActions(
val onUserClicked: () -> Unit,
val onProductClick: (Product) -> Unit
)
@Composable
fun ExampleScreen(
state: ExampleState,
actions: ExampleActions
) {
LazyColumn {
item {
UserItem(state.user, actions.onUserClicked)
}
items(state.products) { product ->
ProductItem(product, actions.onProductClicked)
}
}
}
@Composable
fun UserItem(user: User, onClick: () -> Unit) { /* */ }
@Composable
fun ProductItem(user: Product, onClick: (Product) -> Unit) { /* */ }
Effects
The Compose Side Effects can also be a part of screen or a specific component. However, it is important to be aware the goal of that effects, what they are trying to achieve.
Lets look a at following screen. We have Pager State and just received a new requirement to track every time user changes the page.
data class ScreenActions(val onPageChange: (Int) -> Unit)
@Composable
fun Screen(
val pagerState: PagerState
actions: ScreenActions
) {
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }
.onEach(actions::onPageChange)
.launchIn(this)
}
ItemsPager(state = pagerState)
}
class Coordinator {
fun handlePageChange(page: Int) {
// our logic
}
}
So lets break down what we have here, once user has changed the page in the Pager
1. Our flow in LaunchedEffect
detects the change and gets triggered
2. New page number is then being passed with Actions
3. The Action
then is handled by our Route
or Coordinator
So while this actually doing what we wanted it to do, the amount interactions and component tied to this actions is a bit redundat. Since the PagerState
is external: we don't control the state, we only get notified about the changes. And it actually does nothing to the Screen
we are dealing with. All that raises the question, why would we have this effect as part of the Screen
To fix this we can just move this effect into the Coordinator
. This makes both Screen
and Actions
more consise and removes redundant Logic form them.
class Coordinator(val pagerState: PagerState, scope: CoroutineScope) {
init {
snapshotFlow { pagerState.currentPage }
.onEach { /* our track loginc */ }
.launchIn(scope)
}
}