dave08
12/13/2022, 9:51 AMCasey Brooks
12/13/2022, 3:43 PMmutableStateOf()
variables in Compose (because they’re both based on the principle of Unidirectional Data Flow), but just lifted to the root of the screen so you can easily describe the contract for the entire state and possible interactions of the screen in one place. As documented in the Basic Workflow, it’s easiest to start by defining your Contract, and then applying that contract to the UI becomes a very straightforward matter.
With pagination, it will be useful to create the State with variables in its constructor that you need to directly change, and then set all the computed properties into the class body so they are recomputed automatically with each change. Here’s an example of how a screen with pagination might look:
object ListScreenContract {
data class State(
val loading: Boolean = false,
val listItems: List<ListItem> = emptyList(),
val page: Int = 0,
val itemsPerPage: Int = 25,
) {
// please double-check all the math here, I haven't actually run this code and my math isn't always very good
val numberOfItems: Int = listItems.size
val numberOfPages: Int = numberOfItems / itemsPerPage
val startingPageIndex: Int = page * itemsPerPage
val endingPageIndex: Int = min((page + 1) * itemsPerPage, numberOfItems)
val currentPageItems: List<ListItem> = listItems.subList(startingPageIndex, endingPageIndex)
val canMoveForward: Boolean = endingPageIndex != numberOfItems
val canMoveBackward: Boolean = startingPageIndex != 0
}
sealed class Inputs {
object RequestLoadingListItems : Inputs()
data class SetItemsPerPage(val itemsPerPage: Int) : Inputs()
object GoToNextPage : Inputs()
object GoToPreviousPage : Inputs()
data class GoToPage(val page: Int) : Inputs()
data class ListItemAction1(val selectedListItem: ListItem) : Inputs()
data class ListItemAction2(val selectedListItem: ListItem) : Inputs()
data class ListItemAction3(val selectedListItem: ListItem) : Inputs()
}
sealed class Events { }
}
class ListScreenInputHandler(
private val listItemRepository: ListItemRepository,
) : InputHandler<ListScreenContract.Inputs, ListScreenContract.Events, ListScreenContract.State> {
override suspend fun InputHandlerScope<ListScreenContract.Inputs, ListScreenContract.Events, ListScreenContract.State>.handleInput(
input: ListScreenContract.Inputs
) = when (input) {
is ListScreenContract.Inputs.RequestLoadingListItems -> {
updateState { it.copy(loading = true) }
val listItems = listItemRepository.fetchAllListItems()
updateState { it.copy(loading = false, listItems = listItems) }
}
is ListScreenContract.Inputs.SetItemsPerPage -> {
updateState { it.copy(itemsPerPage = input.itemsPerPage) }
}
is ListScreenContract.Inputs.GoToNextPage -> {
val currentState = getCurrentState()
if(currentState.canMoveForward) {
updateState { it.copy(page = it.page + 1) }
} else {
noOp()
}
}
is ListScreenContract.Inputs.GoToPreviousPage -> {
val currentState = getCurrentState()
if(currentState.canMoveBackward) {
updateState { it.copy(page = it.page - 1) }
} else {
noOp()
}
}
is ListScreenContract.Inputs.GoToPage -> {
val currentState = getCurrentState()
if(input.page < currentState.numberOfPages) {
updateState { it.copy(page = input.page) }
} else {
noOp()
}
}
is ListScreenContract.Inputs.ListItemAction1 -> {
// do something with the item that was clicked
}
is ListScreenContract.Inputs.ListItemAction2 -> {
// do something with the item that was clicked
}
is ListScreenContract.Inputs.ListItemAction3 -> {
// do something with the item that was clicked
}
}
}
@Composable
fun PagedListItems(
state: ListScreenContract.State,
postInput: (ListScreenContract.Inputs)->Unit,
) {
// just read the values in the state and apply them as you would any other Compose variables
}
Casey Brooks
12/13/2022, 3:52 PMdave08
12/13/2022, 4:02 PMdave08
12/13/2022, 4:03 PMCasey Brooks
12/13/2022, 4:08 PMFlow<PagingData>
, which could be collected into Ballast instead of directly into the UI, but I’m not sure how useful that would be.
But you don’t necessarily need to exclusively use one library or the other. For example, you could use Paging to fetch and display the data in the UI, and then Ballast for handling the list item actions as shown in the above snippetdave08
12/13/2022, 4:13 PMitems { }
has one form that takes paging3's output and manages pagination for you... But I need to control the contents of that initial list and manage the states that Pager returns (whether loading error or loaded)... pagination is such a common use-case that it would be a headache to force each user to manage by themselves...
It's not really page1, page2, etc... it's just dynamically loading the continuation of the list as the user scrolls down...Casey Brooks
12/13/2022, 6:50 PMpaging-compose
artifact, and what is there is not able to be used outside of Compose (so it cannot be used directly in Ballast).
However, I think it’s possible to basically reverse-engineer the paging-compose
library into a Ballast workflow, since it does effectively boil down to collecting from Flow<PagingData<ListItem>>
. You’d use this workflow as a replacement for the paging-compose
library. Here’s something I threw together that might help get you started (I basically took this article) and just moved around the logic from the paging-compose library’s source into a Ballast MVI workflow). It’s missing a few pieces, but might help you wrap your head around how Ballast work with things like thisCasey Brooks
12/13/2022, 6:50 PMdave08
12/15/2022, 10:33 AMCasey Brooks
12/15/2022, 2:07 PMpaging-compose
library’s sources, but there might be better ways of handling it if you know more about the Paging APIs and how they’re supposed to be useddave08
04/02/2023, 8:47 AMdave08
04/03/2023, 8:38 AMCasey Brooks
04/03/2023, 2:43 PMremember { mutableStateOf() }
, and when to lift it to the ViewModel.
For this case of pagination, you can absolutely load all the data into your root ViewModel, then create a UI component to implement front-end paging, and this is probably what I would do here, too. You definitely can implement this fully in the Compose UI state mechanisms, but I would probably make a dedicated Ballast ViewModel scoped to that UI component (not the screen root) to help manage the complexity, since paging has a lot of edge cases that could be tricky to solve properly in normal Compose state management