Home > OS >  Jetpack Compose: How to detect when TabRow inside HorizontalPager is visible and call a ViewModel fu
Jetpack Compose: How to detect when TabRow inside HorizontalPager is visible and call a ViewModel fu

Time:07-21

I am building a simple app which has a TabLayout with two tabs. One tab is a list of items which you can click and make a note about the item. The other tab is a list of notes you made in the first tab.

I have created a TabLayout using TabRows and HorizontalPager here is the code for that section:

 @OptIn(ExperimentalPagerApi::class)
@Composable
fun HorizontalPagerTabLayout(
    modifier: Modifier
) {
    val tabData = listOf("Records" to Icons.Default.Phone, "Notes" to Icons.Default.Star)
    val pagerState = rememberPagerState(
        initialPage = 0
    )
    val tabIndex = pagerState.currentPage
    val coroutineScope = rememberCoroutineScope()

    Column {
        TabRow(
            selectedTabIndex = tabIndex,
            modifier = modifier
        ) {
            tabData.forEachIndexed { index, _ ->
                Tab(
                    icon = {
                        Icon(imageVector = tabData[index].second, contentDescription = null)
                    },
                    selected = tabIndex == index,
                    onClick = {
                        coroutineScope.launch {
                            pagerState.animateScrollToPage(index)
                        }
                    },
                    text = {
                        Text(
                            tabData[index].first,
                            color = if (pagerState.currentPage == index) Color.White else Color.LightGray
                        )
                    })
            }
        }
        HorizontalPager(state = pagerState, count = tabData.size) { page ->
            Column(
                modifier = Modifier.fillMaxSize(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                when (page) {
                    0 -> RecordsScreen(modifier = Modifier)
                    1 -> NotesScreen(modifier = Modifier)
                }
            }
        }
    }
} 

In RecordScreen I have a logic that will make a Note about the Record and store it in RoomDB. That works as expected.

In NotesScreen I have a viewModel that will pull all the notes from RoomDB and diplay them in the NotesScreen tab.

Here is the code for NotesScreen:

@Composable
fun NotesScreen(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    modifier: Modifier,
    notesViewModel: NotesViewModel = hiltViewModel()
) {

    val notesList by notesViewModel.notesStateData.collectAsState()

    // FAILED SOLUTION NUMBER 2
//    LaunchedEffect(Unit){
//        notesViewModel.getAllNotes()
//    }

    // FAILED SOLUTION NUMBER 3
//    DisposableEffect(lifecycleOwner) {
//        val observer = LifecycleEventObserver { _, event ->
//            when (event) {
//                Lifecycle.Event.ON_RESUME -> {
//                    notesViewModel.getAllNotes()
//                }
//                else -> {}
//            }
//        }
//        lifecycleOwner.lifecycle.addObserver(observer)
//        onDispose {
//            lifecycleOwner.lifecycle.removeObserver(observer)
//        }
//    }

    LazyColumn(
        contentPadding = (PaddingValues(horizontal = 16.dp, vertical = 8.dp)),
        modifier = modifier
    ) {
        items(items = notesList.orEmpty()) { note ->
            NoteItem(note)
        }
    }
}

From the code you can see that I commented out some failed solutions. My first solution is to include notesViewModel.getAllNotes() in init{} block of viewModel. But this calls the function only once, and not every time I get back to NotesScreen.

My second solution is to create a LaunchedEffect(Unit) because I want to call this viewModel function every time the NotesScreen is displayed. But it didn't work, it also calls the getAllNotes() only once.

My third solution is to create a DisposableEffect with observer that will tell me every time ON_RESUME happens, but this also didn't work.

I used debugger and logged some behaviours:

  • When I use init block, the init block happens as soon as I open the app and not when I open the screen. It seems like the HorizontalPager loads the screen immediately.

  • When I use second solution the LaunchEffect block happens when I navigate to the screen, but it doesn't get called again even though I switched tabs multiple times.

  • When I use DisposableEffect all the lifecycle states happen as soon as I open the app, ON_START, ON_CREATE, and ON_RESUME are logged as soon as app is opened even though I am not on NotesScreen yet. This means that the viewModel function got called only once, and now I can't see the new Notes I created untill I restart the app.

My desired behaviour: Every time I open NotesScreen I want to load all the notes from RoomDB, and not only once on app start. I am trying to trigger VM function that gets notes from DB automatically without refresh layouts or buttons but I can't seem to find a solution.

Any help would be heavily appreciated.

CodePudding user response:

You can use snapshotFlow for add page change callback to Pager. It Creates a Flow from observable Snapshot state. Here is a sample code

    // Page change callback
    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.currentPage }.collect { page ->
            when (page) {
                0 -> viewModel.getAllNotes() // First page
                1 -> // Second page
                else -> // Other pages
            }
        }
    }
  • Related