I'm trying to figure out how to architecture some ...
# compose
d
I'm trying to figure out how to architecture some state. I have a screen that shows a book reader with a book, a current position, and a style. The problem is that I want to store (essentially) an integer page number in the db, but have a floating point page number to represent in-progress page terms in the view model. To do this I think I need to have the source of truth be the view model, which simultaneously updates the repo. In contrast, I want a standard flow for the style where I update it in the repo and then the change flows back down to the view model and then the view. I currently combine these pieces of state into a single flow in the view model, but the different sources of updates led me to some bugs. Is there a cleaner way to do this?
t
What kinda bugs are you noticing? How are you combining the pieces of state? Generally, the data flow would look like db -> repo -> format data for UI (maybe in ViewModel) -> combine with other data -> show to UI
d
The bug is in my own code. Eg
Copy code
/** Updates when style changes (and not when position changes)
     * When style changes it will be combined with the latest position
     */
    @OptIn(ExperimentalCoroutinesApi::class)
    val book: Flow<Book?> = style.mapLatest { style ->
        val epub = epub.await()
        // TODO: This is a bad architecture. Style actually causes position update
        val position = position.await()
        if (epub != null && style != null && position != null) {
            Book(bookId, epub, style, position)
        } else {
            null
        }
    }

/** Will not cause a change to the position of the book flow */
fun updatePosition(position: BookPosition) {
        launch {
            repo.updatePosition(position)
        }
    }
I can fix the specific bug by caching the position at start, but it was starting to feel too hacky
t
have we tried to separate out the different parts that are updating? something like
Copy code
val book: Flow<Book?> = combine(epubUpdates(), positionUpdates()) { Book(...) }
just as a cleanup the repo itself should probably just define
Copy code
fun books(): Flow<Book> { /** put all the smarts here **/ }
d
@Tash The problem with that is that in my current architecture the Pages composable gets the initial position and then owns the position going forward (i.e. remember), sending events to the repo. It shouldn't be re-rendered when the repo receives that event, it already handled it
The reason that's not in the repo is some views need a flow of the position, and some views need only an initial position
t
The reason that’s not in the repo is some views need a flow of the position, and some views need only an initial position
Could you elaborate?
d
Sure. The overview screen shows a list of books with each books position, so it just consumes a flow of the position (section, character offset). To handle page turns the reader screen stores the position in a remember as (section, float page num) and has ~100loc to update this based on gestures. The initial page number is computed from the character offset. When the turn completes it fires an event to the repo so it can persist the new position.
The page number is view-dependent (screen orientation, text size, etc), which is why the repo only knows about character offsets
t
So the “overview screen” is different from the “reader screen” ?
It sounds like you might have two separate Composables expecting different kinds of state. Defining a state for each Composable, and then working from there to unify them at the ViewModel level might be what you need.
d
That's what I do. My original question was about designing the flow of state to the view models of these different components. I think I just need to play around with a few more designs. Thanks for the advice.
t
Might also be worth exploring a single "unified" state object where some fields are declared as observable fields
mutableStateOf
(or
Flow
) and others are non-observable. The entire UI still sees only one overall state but the individual Composables can hook into the observable/non-observable fields as needed.
d
Interesting. So getBook would return both initialPosition (@ time of call) and positionFlow? I could see that being nicer.