How do you structure data requests? I have a compo...
# compose
c
How do you structure data requests? I have a component that looks like this:
Copy code
@Composable
fun DisplayFoo(
    fooId: String,
    getFoo: suspend (String) -> Foo,
    edit: suspend (…) -> Unit,
)
• it gets an ID of a domain object • it knows how to request the full contents • it knows how to edit the object ◦ when the object is edited, it should re-get the object to display the new value My current best attempt, which feels like a hack:
Copy code
@Composable
fun DisplayFoo(
    fooId: String,
    getFoo: suspend (String) -> Foo,
    edit: (…) -> Unit,
) {
    var foo by remember { mutableStateOf<Foo?>(null) }
    var refreshCounter by remember { mutableStateOf(0) }

    LaunchedEffect(fooId, refreshCounter) {
        delay(300) // debounce refresh requests in quick succession
        foo = getFoo(fooId)
    }

    foo?.let {
        // let's imagine there's an actual UI here
        // this should probably be another component which takes raw data as input to separate concerns
        Text(foo)
    }

    Button(onClick = {
       edit(…)
       refreshCounter++ // it was edited, please refresh the value
    }) { Text("Edit") }
}
Is this ok? Is there a simpler way? It seems weird to (ab)use an integer counter this way (though I have seen this be recommended in the React world…)
f
• it knows how to request the full contents
• it knows how to edit the object
a composable should not know how to do these things, these should be delegated to a presenter or viewmodel. A composable should simply display the UI and forward events up
c
I'm not sure I follow, this composable does nothing else than forward the events up.
f
getFoo
begs to differ
c
How would you write this example, then?
f
Copy code
@Composable
fun DisplayFoo(
    foo: Foo,
    refresh: () -> Unit,
) {
    Text(foo)

    Button(onClick = {
        refresh()
    }) { Text("Edit") }
}
but wrapped in a column or similar, you can't have 2 (or more) children as root
c
You just removed everything, that doesn't help me 😅
f
that should be in the presenter, where
refresh
and
getFoo
are implemented
l
I would try moving the foo to a viewmodel and exposing it via flow/livedata, and editFoo would change its value
Dont think your "getFoo" and "editFoo" are wrong tho, they arent actually doing anything, its just a function call
c
Somewhere higher in the composable hierarchy something must actually do the requests. That's what I'm interested in.
f
that's a presenter or viewmodel
c
It's not in your example
l
Copy code
class FooViewModel() : ViewModel() {
    private val _foo = MutableStateFlow<Foo?>(null)
    val foo: StateFlow<Foo?> = _foo

    fun loadFoo(fooId: String) {
        viewModelScope.launch {
            _foo.value = getFoo(fooId)
        }
    }

    fun editFoo(fooId: String) {
        viewModelScope.launch {
            editFoo(fooId)
            _foo.value = getFoo(fooId)
        }
    }
}

@Composable
fun DisplayFoo(fooId: String, viewModel: FooViewModel) {
    val foo by viewModel.foo.collectAsState()

    LaunchedEffect(fooId) {
        viewModel.loadFoo(fooId)
    }

    foo?.let {
        // Display the UI here.
        Text(foo)
    }

    Button(onClick = {
        viewModel.editFoo(fooId)
    }) { 
        Text("Edit") 
    }
}
f
t's not in your example
well, you just asked how I would write the composable. How to handle the logic, well, that's up to you to implement
c
@Lucas ViewModel is Android-only, right?
l
Well, android's viewmodel is android only, but thats only a pattern. As francesc said, it could be a presenter
f
what matters is the architecture, how things are called is an implementation detail
c
@Francesc there is nothing else in my example. Just the parent that gives the proper function references to do the thing, which live in the service layer.
l
I just use classes named ThingViewModel and dont actually implement any viewmodel interface
c
So you just have a composable higher in the tree that
remember { FooViewModel() }
and passes it down the tree?
l
Well it would depend on your specific use case, but something like that could work
c
Also, I think the debouncing has disappeared in your example?
l
It did, but it could be implemented in the viewmodel
Not really sure, but it could be implemented somewhere 😅
c
So as a summary: all events and state are grouped into a single class, and that class is passed down?
f
the idea is that state flow down (from your presenter to your composables), and events flow up (from the composables to the presenter). The presenter is responsible for all business logic (fetching data and massaging it in a way that the composables understand), and the composables are simply responsible for displaying the UI and forwarding events up to the presenter
c
I don't get it, that's exactly what my first example did. You can take Lucas' example, inline the class, and you get my version. The data flow is exactly the same, except that I split things into multiple parameters instead of a single class?
l
Its not the same thing because the composable lifecycle and the viewmodel lifecycle are different
Thats why you need your integer state hack
By lifecycle i mean duration, i guess
f
You can take Lucas' example, inline the class, and you get my version
by the same rationale, you could inline everything in an app and have just a
main
function that does everything, but there is a good reason we don't do that
hint: separation of concerns, maintainability, readability, reusability among others
l
Opposed to francesc, i dont think your example is necessarily bad, but i agree that there are limits Its just that you're receiving the fooId in the parameters, and it doesnt change, so it wont trigger a recomposition and you wont fetch the data again, thats why you need your refreshCounter. You could receive the foo object itself from somewhere else, and make the edit implementation change that state
If you choose to do everything in the ui, it gets ugly:
Copy code
@Composable
fun DisplayFoo(
    fooId: String,
    getFoo: suspend (String) -> Foo,
    edit: (…) -> Unit,
) {
    var foo by remember { mutableStateOf<Foo?>(null) }
    val coroutineScope = rememberCoroutineScope()
    LaunchedEffect(fooId) {
        delay(300) // debounce refresh requests in quick succession
        foo = getFoo(fooId)
    }

    foo?.let {
        // let's imagine there's an actual UI here
        // this should probably be another component which takes raw data as input to separate concerns
        Text(foo)
    }

    Button(onClick = {
        edit(…)
        coroutineScope.launch{
            foo = getFoo(fooId)
        }
    }) { Text("Edit") }
}
This does break SRP and Unidirectional Data Flow tho
c
Let's imagine all of this is in a list (it's displayed multiple times). Would you have a FooListViewModel and a FooViewModel?
l
No, i would just have the FooListViewModel, exposing the list<Foo> state, and edit(fooId:String/ foo: Foo) methods
And i would have the DisplayFoo(foo: Foo, onEditClick : (Foo(if you want)) -> Unit){ }
c
So that means FooListViewModel has a field of type
WeakMap<String, Foo>
to store the results of getFoo?
l
No
I'll write an example, gimme a sec
First, depending on how you fetch your Foo data, the initialization would bee different
Copy code
class FooListViewModel {

    private val _fooList = MutableStateFlow<List<FooItem>>(emptyList())
    val fooList: StateFlow<List<FooItem>> = _fooList

    init {
        viewModelScope.launch {
            _fooList.emit(listOf(FooItem(1, "First"), FooItem(2, "Second")))
        }
    }

    fun editFooItem(id: Int) {
        viewModelScope.launch {
            val updatedList = fooList.value.map { fooItem ->
                if (fooItem.id == id) fooItem.copy(name = "${fooItem.name} - edited")
                else fooItem
            }
            _fooList.emit(updatedList)
        }
    }
}
@Composable
fun FooList(fooViewModel: FooListViewModel) {
    val fooList  = fooViewModel.fooList.collectAsState().value

    LazyColumn {
        items(fooList) { fooItem ->
            FooItem(fooItem, fooViewModel::editFooItem)
        }
    }
}


@Composable
fun FooItem(fooItem: FooItem, onEdit: (Int) -> Unit) {
	Column{
		Text(fooItem.name)
		Button(onClick = { onEdit(fooItem.id) }) {
			Text("Edit")
		}
        }
    
}
This would be a basic implementation
The edit logic would obviously also change depending on your use case
This isnt by any means perfect, its just what i usually do, but it tends to get more complicated quickly when you need more complex actions
In android only, using room, for example, if you're fetching the Foo from a database, you could use a Flow on the DAO, and then you wouldnt event need to update the list, just update the table and Room would automatically emit a new flow for you
e
There's nothing stopping you from embedding business logic into your UI layer, but you're almost definitely going to regret it at some point in the future. What @Francesc is saying is correct. You should probably have some form of architecture that keeps things separate (MVP, MVVM, MVI, etc...). Compose takes a different mental model to make working with that straightforward, but it's definitely achievable (I have a large app using MVI with Compose that is a pleasure to work with). https://github.com/cashapp/molecule might be a good starting point
c
@eygraber There is no business logic in the example I gave. All the business logic is passed down by the parent through the callbacks, exactly as you do with an encapsulating class. Also, Molecule is a library to write business logic in Composable functions and export them as flows to the surrounding non-UI code (e.g. an HTTP backend), it's the exact opposite of the point you're making. @Lucas's examples helped me understand what you're trying to do. It seems similar to my Backbone library which I guess is some kind of view-model which does request de-duplication and error handling.
f
yes, there is business logic in the example, namely
getFoo
and the
refreshCounter
logic
if it's not "display UI" then it does not belong on the composable
e
Molecule is a library to write business logic in Composable functions and export them as flows to the surrounding non-UI code (e.g. an HTTP backend), it's the exact opposite of the point you're making
There's a strong distinction between Compose UI and Compose. Molecule uses Compose and has nothing to do with Compose UI.
c
@Francesc if the composable is not the source of the request, how do you cancel the ongoing request when the user leaves the screen? How do display loading indicators, do you have a second state in all your view models for those?
f
if your presenter/viewmodel is scoped propertly, it will be destroyed when the composable is disposed of, and there you can cancel any outstanding operations.
you could also use a
DisposableEffect
, but that's more of a workaround,
Copy code
@Composable
fun myComposable(
    onCancel: () -> Unit,
) {
    DisposableEffect(Unit) {
        // anything here that needs to be done once
        onDispose { onCancel() }
    }
}
c
How do you scope it properly, then? It's initialization/scoping is part of none of the examples above.
f
that depends on your architecture. On Android you could use Hilt and the AAC navigation to scope viewmodels to a navigation graph, but it all depends on what target you are building for
c
The question is literally about the architecture. Also, this is not Android, and the Compose authors have mentioned AAC is not recommended with Compose.
e
You can use MVI, MVVM, MVP, or anything you want. I'm using MVI with no Android dependencies whatsoever.
You can still scope it to whatever your underlying platform is