I'm finding myself passing my `ViewModel` object i...
# compose
v
I'm finding myself passing my
ViewModel
object into loads of composable functions, and that feels like a code smell. For instance, if a button has an onClick action which needs to call a viewModel function, but that button is a child of a child of a child....
👀 1
m
Did you try passing your method as a lambda parameter instead?
f
As Morgane said, why not pass the lambda up to the latest composable that hoists your viewmodel
v
I do, but sometimes that means several lambda being passed up and up to the "root" composable.
f
That's not an issue, make sure to extract your inner composables as separete functions and add that lambda as a parameter, try to read on stateless composables
y
Doing this makes the code so much more testable and reusable.
a
Pass click function lambda
c
You can also try passing content to your composables instead of nesting them so deeply. Consider something like a “user card” which shows information about another user in a system:
Copy code
@Composable
fun UserCard(
  name: String,
  photoUri: Uri,
  onContactClick: () -> Unit,
  onProfileClick: () -> Unit,
) {
  Row {
    Image(photoUri)
    
    Column {
      Text(name)
      Row {
        Button(onContactClick) { Text("Contact this user") }
        Button(onProfileClick) { Text("Open user profile") }
      }
    }
  }
}
This only passes lambdas two levels down (into UserCard and then into Button). But it sounds like in your example you’re passing many more levels down. You could try this instead:
Copy code
@Composable
fun UserCard(
  name: String,
  photoUri: Uri,
  buttons: @Composable () -> Unit,
) {
  Row {
    Image(photoUri)
    
    Column {
      Text(name)
      Row {
        buttons()
      }
    }
  }
}
and you use it like this:
Copy code
UserCard(
  name = ...,
  photoUri = ...,
  buttons = {
    Button(
      onClick = {
        // Handle contact user
      },
    ) { Text("Contact this user") }
    Button(
      onClick = {
        // Handle open user profile 
      },
    ) { Text("Open user profile") }
  },
)
Now your lambdas are hoisted to the top. And, this way your
UserCard
is focused on layout, instead of the details of interactions. If you want, you could wrap the UserCard call site in a container that supplies the viewModel:
Copy code
UserCardWrapper(
  viewModel: MyViewModel = viewModel()
) {
  ...
}
f
If you want to learn more about this ☝️ approach, google "Compose slot API"
c
The pattern I do to help wrangle this complexity is to use a single State object to hold all the data for a screen and a sealed class to capture the Inputs and pass them back up the Compose hierarchy to the root, where the ViewModel lives. This allows me to have complex data and interactions on the screen, but not have to pass a bunch of parameters and lambdas around, while also not being directly coupled to a ViewModel object. You can add as many actions or state properties as you need, and only ever need to pass around 2 parameters: the State, and the
postInput
callback Here’s a rough idea of what I typically do:
Copy code
kotlin
object Screen1Contract { 
    data class State(
        val stringValue: String,
        val intValue: Int,
    )
    
   sealed class Inputs {
        data class UpdateStringValue(val newStringValue: String) : Inputs()
        data class UpdateIntValue(val newIntValue: String) : Inputs()
    }   
}

@Composable
fun MainContent() { 
    var uiState by remember { mutableStateOf(Screen1Contract.State()) }
    MainContent(uiState) { input ->
        when(input) { 
            is Screen1Contract.Inputs.UpdateStringValue -> {
                uiState = uiState.copy(stringValue = input.newStringValue)
            } 
            is Screen1Contract.Inputs.UpdateIntValue ->  {
                uiState = uiState.copy(intValue = input.newIntValue)
            }
        }
    }
}

@Composable
fun MainContent(
    uiState: Screen1Contract.State,
    postInput: (Screen1Contract.Inputs)->Unit,
) {
    // ...
}
This is just the general idea, in practice I use my Ballast MVI library to handle the actual processing of Inputs and emitting the state. But this is the core concept behind that library
102 Views