https://kotlinlang.org logo
Title
c

CLOVIS

05/24/2023, 4:09 PM
How do you structure data requests? I have a component that looks like this:
@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:
@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

Francesc

05/24/2023, 4:13 PM
• 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

CLOVIS

05/24/2023, 4:14 PM
I'm not sure I follow, this composable does nothing else than forward the events up.
f

Francesc

05/24/2023, 4:14 PM
getFoo
begs to differ
c

CLOVIS

05/24/2023, 4:15 PM
How would you write this example, then?
f

Francesc

05/24/2023, 4:16 PM
@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

CLOVIS

05/24/2023, 4:18 PM
You just removed everything, that doesn't help me 😅
f

Francesc

05/24/2023, 4:18 PM
that should be in the presenter, where
refresh
and
getFoo
are implemented
l

Lucas

05/24/2023, 4:18 PM
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

CLOVIS

05/24/2023, 4:19 PM
Somewhere higher in the composable hierarchy something must actually do the requests. That's what I'm interested in.
f

Francesc

05/24/2023, 4:20 PM
that's a presenter or viewmodel
c

CLOVIS

05/24/2023, 4:20 PM
It's not in your example
l

Lucas

05/24/2023, 4:20 PM
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

Francesc

05/24/2023, 4:21 PM
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

CLOVIS

05/24/2023, 4:22 PM
@Lucas ViewModel is Android-only, right?
l

Lucas

05/24/2023, 4:23 PM
Well, android's viewmodel is android only, but thats only a pattern. As francesc said, it could be a presenter
f

Francesc

05/24/2023, 4:23 PM
what matters is the architecture, how things are called is an implementation detail
c

CLOVIS

05/24/2023, 4:23 PM
@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

Lucas

05/24/2023, 4:23 PM
I just use classes named ThingViewModel and dont actually implement any viewmodel interface
c

CLOVIS

05/24/2023, 4:25 PM
So you just have a composable higher in the tree that
remember { FooViewModel() }
and passes it down the tree?
l

Lucas

05/24/2023, 4:25 PM
Well it would depend on your specific use case, but something like that could work
c

CLOVIS

05/24/2023, 4:26 PM
Also, I think the debouncing has disappeared in your example?
l

Lucas

05/24/2023, 4:26 PM
It did, but it could be implemented in the viewmodel
Not really sure, but it could be implemented somewhere 😅
c

CLOVIS

05/24/2023, 4:27 PM
So as a summary: all events and state are grouped into a single class, and that class is passed down?
f

Francesc

05/24/2023, 4:28 PM
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

CLOVIS

05/24/2023, 4:30 PM
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

Lucas

05/24/2023, 4:30 PM
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

Francesc

05/24/2023, 4:32 PM
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

Lucas

05/24/2023, 4:35 PM
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:
@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

CLOVIS

05/24/2023, 4:41 PM
Let's imagine all of this is in a list (it's displayed multiple times). Would you have a FooListViewModel and a FooViewModel?
l

Lucas

05/24/2023, 4:42 PM
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

CLOVIS

05/24/2023, 4:44 PM
So that means FooListViewModel has a field of type
WeakMap<String, Foo>
to store the results of getFoo?
l

Lucas

05/24/2023, 4:44 PM
No
I'll write an example, gimme a sec
First, depending on how you fetch your Foo data, the initialization would bee different
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

eygraber

05/24/2023, 8:45 PM
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

CLOVIS

05/24/2023, 9:34 PM
@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

Francesc

05/24/2023, 9:37 PM
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

eygraber

05/24/2023, 9:38 PM
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

CLOVIS

05/24/2023, 9:51 PM
@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

Francesc

05/24/2023, 9:58 PM
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,
@Composable
fun myComposable(
    onCancel: () -> Unit,
) {
    DisposableEffect(Unit) {
        // anything here that needs to be done once
        onDispose { onCancel() }
    }
}
c

CLOVIS

05/25/2023, 6:41 AM
How do you scope it properly, then? It's initialization/scoping is part of none of the examples above.
f

Francesc

05/25/2023, 2:51 PM
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

CLOVIS

05/25/2023, 4:18 PM
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

eygraber

05/25/2023, 4:38 PM
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