CLOVIS
05/24/2023, 4:09 PM@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…)Francesc
05/24/2023, 4:13 PM• it knows how to request the full contents
• it knows how to edit the objecta 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
CLOVIS
05/24/2023, 4:14 PMFrancesc
05/24/2023, 4:14 PMgetFoo
begs to differCLOVIS
05/24/2023, 4:15 PMFrancesc
05/24/2023, 4:16 PM@Composable
fun DisplayFoo(
foo: Foo,
refresh: () -> Unit,
) {
Text(foo)
Button(onClick = {
refresh()
}) { Text("Edit") }
}
CLOVIS
05/24/2023, 4:18 PMFrancesc
05/24/2023, 4:18 PMrefresh
and getFoo
are implementedLucas
05/24/2023, 4:18 PMCLOVIS
05/24/2023, 4:19 PMFrancesc
05/24/2023, 4:20 PMCLOVIS
05/24/2023, 4:20 PMLucas
05/24/2023, 4:20 PMclass 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")
}
}
Francesc
05/24/2023, 4:21 PMt's not in your examplewell, you just asked how I would write the composable. How to handle the logic, well, that's up to you to implement
CLOVIS
05/24/2023, 4:22 PMLucas
05/24/2023, 4:23 PMFrancesc
05/24/2023, 4:23 PMCLOVIS
05/24/2023, 4:23 PMLucas
05/24/2023, 4:23 PMCLOVIS
05/24/2023, 4:25 PMremember { FooViewModel() }
and passes it down the tree?Lucas
05/24/2023, 4:25 PMCLOVIS
05/24/2023, 4:26 PMLucas
05/24/2023, 4:26 PMCLOVIS
05/24/2023, 4:27 PMFrancesc
05/24/2023, 4:28 PMCLOVIS
05/24/2023, 4:30 PMLucas
05/24/2023, 4:30 PMFrancesc
05/24/2023, 4:32 PMYou can take Lucas' example, inline the class, and you get my versionby 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 thatLucas
05/24/2023, 4:35 PM@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") }
}
CLOVIS
05/24/2023, 4:41 PMLucas
05/24/2023, 4:42 PMCLOVIS
05/24/2023, 4:44 PMWeakMap<String, Foo>
to store the results of getFoo?Lucas
05/24/2023, 4:44 PMclass 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 implementationeygraber
05/24/2023, 8:45 PMCLOVIS
05/24/2023, 9:34 PMFrancesc
05/24/2023, 9:37 PMgetFoo
and the refreshCounter
logiceygraber
05/24/2023, 9:38 PMMolecule 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 makingThere's a strong distinction between Compose UI and Compose. Molecule uses Compose and has nothing to do with Compose UI.
CLOVIS
05/24/2023, 9:51 PMFrancesc
05/24/2023, 9:58 PMDisposableEffect
, but that's more of a workaround,
@Composable
fun myComposable(
onCancel: () -> Unit,
) {
DisposableEffect(Unit) {
// anything here that needs to be done once
onDispose { onCancel() }
}
}
CLOVIS
05/25/2023, 6:41 AMFrancesc
05/25/2023, 2:51 PMCLOVIS
05/25/2023, 4:18 PMeygraber
05/25/2023, 4:38 PM