0

I am trying to update the title of the TopAppBar based on a live data in the ViewModel, which I update on different screens. It looks like the live data is getting updated properly, but the update is not getting reflected on the title of the TopAppBar. Here is the code:

class MainActivity : ComponentActivity() {
    @ExperimentalFoundationApi
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            LazyVerticalGridActivityScreen()
        }
    }
}

@ExperimentalFoundationApi
@Composable
fun LazyVerticalGridActivityScreen(destinationViewModel: DestinationViewModel = viewModel()) {
    val navController = rememberNavController()
    var canPop by remember { mutableStateOf(false) }

    // getting the latest title value from the view model
    val title: String by destinationViewModel.title.observeAsState("")
    Log.d("MainActivity_title", title) // not getting called

    navController.addOnDestinationChangedListener { controller, _, _ ->
        canPop = controller.previousBackStackEntry != null
    }

    val navigationIcon: (@Composable () -> Unit)? =
        if (canPop) {
            {
                IconButton(onClick = { navController.popBackStack() }) {
                    Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = null)
                }
            }
        } else {
            null
        }

    Scaffold(
        topBar = {
            TopAppBar(title = { Text(title) }, navigationIcon = navigationIcon) // updating the title
        },
        content = {
            NavHost(navController = navController, startDestination = "home") {
                composable("home") { HomeScreen(navController) }
                composable("details/{listId}") { backStackEntry ->
                    backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController) }
                }
            }
        }
    )
}
@ExperimentalFoundationApi
@Composable
fun HomeScreen(navController: NavHostController, destinationViewModel: DestinationViewModel = viewModel()) {
    val destinations = DestinationDataSource().loadData()

    // updating the title in the view model
    destinationViewModel.setTitle("Lazy Grid Layout")

    LazyVerticalGrid(
        cells = GridCells.Adaptive(minSize = 140.dp),
        contentPadding = PaddingValues(8.dp)
    ) {
        itemsIndexed(destinations) { index, destination ->
            Row(Modifier.padding(8.dp)) {
                ItemLayout(destination, index, navController)
            }
        }
    }
}

@Composable
fun ItemLayout(
    destination: Destination,
    index: Int,
    navController: NavHostController
) {
    Column(
        verticalArrangement = Arrangement.Center,
        modifier = Modifier
            .background(MaterialTheme.colors.primaryVariant)
            .fillMaxWidth()
            .clickable {
                navController.navigate("details/$index")
            }
    ) {
        Image(
            painter = painterResource(destination.photoId),
            contentDescription = stringResource(destination.nameId),
            modifier = Modifier.fillMaxWidth(),
            contentScale = ContentScale.Crop
        )
        Text(
            text = stringResource(destination.nameId),
            color = Color.White,
            fontSize = 14.sp,
            modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp)
        )
    }
}

@Composable
fun DetailsScreen(
    index: String,
    navController: NavController,
    destinationViewModel: DestinationViewModel = viewModel()
) {
    val dataSource = DestinationDataSource().loadData()

    val destination = dataSource[index.toInt()]
    val destinationName = stringResource(destination.nameId)
    val destinationDesc = stringResource(destination.descriptionId)
    val destinationImage = painterResource(destination.photoId)

    // updating the title in the view model
    destinationViewModel.setTitle("Destination Details")

    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth()
            .verticalScroll(rememberScrollState())
    ) {
        Image(
            painter = destinationImage,
            contentDescription = destinationName,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxWidth()
        )
        Column(modifier = Modifier.padding(horizontal = 16.dp)) {
            Text(
                text = destinationName,
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold,
                modifier = Modifier.padding(vertical = 16.dp)
            )
            Text(text = destinationDesc, lineHeight = 24.sp)
            Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
                OutlinedButton(
                    onClick = {
                        navController.navigate("home") {
                            popUpTo("home") { inclusive = true }
                        }
                    },
                    modifier = Modifier.padding(top = 24.dp)
                ) {
                    Image(
                        imageVector = Icons.Filled.ArrowBack,
                        contentDescription = null,
                        colorFilter = ColorFilter.tint(MaterialTheme.colors.primaryVariant),
                        modifier = Modifier.size(20.dp)
                    )
                    Text("Back to Destinations", modifier = Modifier.padding(start = 16.dp))
                }
            }
        }
    }
}

EDIT: The ViewModel

class DestinationViewModel : ViewModel() {

    private var _title = MutableLiveData("")
    val title: LiveData<String>
        get() = _title

    fun setTitle(newTitle: String) {
        _title.value = newTitle
        Log.d("ViewModel_title", _title.value.toString())
        Log.d("ViewModelTitle", title.value.toString())
    }
}

Can anyone please help to find the bug? Thanks!

Edit:

Here is the GitHub link of the project: https://github.com/rawhasan/compose-exercise-lazy-vertical-grid

Raw Hasan
  • 1,096
  • 1
  • 9
  • 25

2 Answers2

2

The reason it's not working is because those are different objects created in different scopes.

When you're using a navController, each destination will have it's own scope for viewModel() creation. By the design you may have a view model for each destination, like HomeViewModel, DestinationViewModel, etc

You can't access an other destination view model from current destination scope, as well as you can't access view model from the outer scope(which you're trying to do)

What you can do, is instead of trying to retrieve it with viewModel(), you can pass outer scope view model to your composable:

composable("details/{listId}") { backStackEntry ->
    backStackEntry.arguments?.getString("listId")?.let { DetailsScreen(it, navController, destinationViewModel) }
}

Check out more details about viewModel() in the documentation


Another problem with your code is that you're calling destinationViewModel.setTitle("Lazy Grid Layout") inside composable function. So this code will be called many times, which may lead to recomposition.

To call any actions inside composable, you need to use side-effects. LaunchedEffect in this case:

LaunchedEffect(Unit) {
    destinationViewModel.setTitle("Destination Details")
}

This will be called only once after view appear. If you need to call it more frequently, you need to specify key instead of Unit, so it'll be recalled each time when the key changes

Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • The issue indeed was on the scope of the view model. I've now shared one outer view model to all the screens, and it resolved the issue. Thanks a lot! – Raw Hasan Aug 09 '21 at 06:16
  • Can you please also tell me, is this the recommended way to change the title inside a navigation? Or there is a better way? – Raw Hasan Aug 09 '21 at 06:31
  • 1
    @Hasan yes, I forget to add it to my answer. Updated it, check it out – Phil Dukhov Aug 09 '21 at 06:33
  • Perfect! Thanks a lot! :) I was going to ask a separate question for that. By the way, is this the recommended way to change the navigation title (via view model), or there is a better way? – Raw Hasan Aug 09 '21 at 07:27
  • 1
    @Hasan If you have different navigation title for each navigation screen, I think it's better to just declare it inside each navigation destination, eg move it with `Scaffold`. You can create your own composable wrapper which will make navigation bar with passed title and content, it should be much cleaner – Phil Dukhov Aug 09 '21 at 08:00
0

You might wanna have a look here https://stackoverflow.com/a/68671477/15880865

Also, you do not need to paste all the code in the question. Just the necessary bit. We shall ask for it if something is missing in the question. Just change your LiveData to mutableStateOf and then let me know if that fixed your problem. Also, you do not need to call observeAsState after modifying the type. Just refer to the link it contains all the info.

Thanks

Richard Onslow Roper
  • 5,477
  • 2
  • 11
  • 42
  • Thanks for the answer! I've initially used `var title = mutableStateOf("") private set` on the view model, which was not getting updated, so used LiveData instead. The issue was on the scope of the view model, as the accepted answer by @Philip. Change it back to mutableStateOf now, as that is the recommended way in Compose. – Raw Hasan Aug 09 '21 at 06:22