I am a bit confused. Can we pass viewmodel to another composable function? if not then what could be a good approach for accessing any viewmodel to another function? I am giving code snippets here so you can understand better. I am passing viewmodel for accessing one of room database insert function after click on "save" button in dialog box.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun homeScreen(modifier: Modifier, todoViewModel: todoViewModel = viewModel()) {
val dataList by todoViewModel.getAllTodo.collectAsState()
val scrollBehavior = TopAppBarDefaults
.pinnedScrollBehavior(rememberTopAppBarState())
val openDialog = remember { mutableStateOf(true) }
Scaffold(modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
CenterAlignedTopAppBar(
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Black,
titleContentColor = Color.White
),
title = {
Text(
"TO DO LISTS",
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
actions = {
IconButton(onClick = { dialogBox(todoViewModel,openDialog) }){
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "You have to add new todo work",
tint = Color.White
)
}
},
scrollBehavior = scrollBehavior
)
}
){
Column(modifier= modifier.padding(it)) {
LazyColumn {
items(dataList){list ->
todoItems( data = list)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun dialogBox(todoViewModel: todoViewModel, openDialog: MutableState<Boolean>){
var title by rememberSaveable{mutableStateOf("")}
var description by rememberSaveable{mutableStateOf("")}
Dialog(onDismissRequest = {openDialog.value = false}){
Card(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.padding(16.dp),
shape = RoundedCornerShape(16.dp)
){
Column(modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally) {
OutlinedTextField(value = title,
onValueChange = { title = it },
label = { Text("Title") })
Spacer(modifier = Modifier.padding(vertical = 12.dp))
OutlinedTextField(value = description,
onValueChange = { description = it },
label = { Text("Description") })
Row(modifier = Modifier
.fillMaxWidth(),
horizontalArrangement = Arrangement.Center) {
TextButton(onClick = {
todoViewModel.insertTodo(todoEntity(title = title, desciription = description))},
modifier = Modifier.padding(8.dp)){
Text("Save")
}
TextButton(onClick = {}, modifier = Modifier.padding(8.dp)){
Text("Dismiss")
}
}
}
}
}
}
I have searched the articles about it but have not gotten enough articles that can tell in a better way.
By passing an entire ViewModel
object into your reusable composables, you are tightly coupling your reusable composable to the ViewModel
object, which prevents you from easily using it elsewhere, or for say testing it without requiring a stubbed ViewModel
to be used.
This is also noted in the ViewModel section of the "Compose and other libraries" docs:
Note: Due to their lifecycle and scoping, you should access and call ViewModel instances at screen-level composables, that is, close to a root composable called from an activity, fragment, or destination of a Navigation graph. You should never pass down ViewModel instances to other composables, pass only the data they need and functions that perform the required logic as parameters.
As such, only pass what you need from the ViewModel down to your dialogBox
composable (you should also preferably use PascalCase
for method names, as a minor nitpick). From what I can tell, you're only using the insertTodo
method from your ViewModel
- this can be reduced down to a onSaveClick
lambda, which you could then call insertTodo
in where you're using this DialogBox
composable.
There's also no need to pass a state directly to handle the conditional rendering when you could just do it in the parent composable:
@Composable
fun DialogBox(..., openDialog: MutableState<Boolean>) { ... }
// VS
@Composable
fun DialogBox(...) { ... }
@Composable
fun HomeScreen(...) {
if (openDialog.value) {
DialogBox(...)
}
}
Anyways, here's what the updated code would look like:
// "dialogBox" is too vague, I would probably name it something more
// specific like "NewTodoDialog"
@Composable
fun NewTodoDialog(
onDismissRequest: () -> Unit,
// Pass back the data here...
// You could maybe make the data be contained in a data class,
// something like "TodoItem" or similar
onSaveClick: (title: String, description: String) -> Unit
) {
var title by rememberSaveable { mutableStateOf(...) }
var description by rememberSaveable { mutableStateOf(...) }
Dialog(onDismissRequest = onDismissRequest) {
// Dialog content...
// ...
// Dialog actions:
Row(/* ... */) {
TextButton(
// You can pass back the data to be added here:
onClick = { onSaveClick(title, description) },
modifier = Modifier.padding(8.dp)
) {
Text("Save")
}
// I've also updated the onClick of your dismiss button to _actually_
// do something, like say request for the dialog to be dismissed
TextButton(
onClick = onDismissRequest,
modifier = Modifier.padding(8.dp)
) {
Text("Dismiss")
}
}
}
}
// In the parent composable:
@Composable
fun HomeScreen(viewModel: YourViewModel, /* ... */) {
var isDialogShown by rememberSaveable { mutableStateOf(false) }
// Then you can conditionally render the dialog
if (isDialogShown) {
NewTodoDialog(
// This would actually dismiss the dialog
onDismissRequest = { isDialogShown = false },
// Method references are more ideal here if the method + lambda
// definitions are the same.
// Otherwise, you'll have to call it manually in a lambda
onSaveClick = viewModel::insertTodo
)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With