0

Trying to implement accompanist pager with tabs to achieve something like instagram's page displaying followers, following and subscription - 3 tab menu with pager basically. This is the code I am using.

fun UsersPager(
    myDBViewModel: MyDBViewModel
) {
    val tabData = listOf(
        "FOLLOWING" to Icons.Filled.PermIdentity,
        "ALLUSERS" to Icons.Filled.PersonOutline,
        "FOLLOWERS" to Icons.Filled.PersonOutline
    )
    val pagerState = rememberPagerState(
        0
    )
    val tabIndex = pagerState.currentPage
    val coroutineScope = rememberCoroutineScope()
    Column {
        TabRow(
            selectedTabIndex = tabIndex,
            indicator = { tabPositions ->
                TabRowDefaults.Indicator(
                    Modifier.pagerTabIndicatorOffset(pagerState, tabPositions)
                )
            }
        ) {
            tabData.forEachIndexed { index, pair ->
                Tab(
                    selected = tabIndex == index,
                    onClick = {
                        coroutineScope.launch {
                            Log.d("MP18", "click on Tab num: $index")
                            pagerState.animateScrollToPage(index)
                        }
                    },
                    text = {
                        Text(text = pair.first)
                    },
                    icon = {
                        Icon(imageVector = pair.second, contentDescription = null)
                    })
            }
        }
        HorizontalPager(
            state = pagerState,
            itemSpacing = 1.dp,
            modifier = Modifier
                .weight(1f),
            count = tabData.size
        ) { index ->
            Column(
                modifier = Modifier.fillMaxHeight(),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                when (index) {
                    1 -> ShowMyFollowees(myDBViewModel = myDBViewModel)
                    2 -> ShowMyUsers(myDBViewModel = myDBViewModel)
                    3 -> ShowMyFollowers(myDBViewModel = myDBViewModel)
                }
            }
        }
    }
}

Then 3 composables follow this pattern to fetch data from API and display them:

@Composable
fun ShowMyUsers(
    myDBViewModel: MyDBViewModel,
) {
    val pageLoadedTimes by myDBViewModel.pageLoadedTimes.observeAsState(initial = null)
    val myUsersList by myDBViewModel.myUsersList.observeAsState(initial = emptyList())
    val loading by myDBViewModel.loading.observeAsState(initial = myDBViewModel.loading.value)

    if (myUsersList.isNullOrEmpty() && pageLoadedTimes == 0 && !loading!!) {
        LaunchedEffect(key1 = Unit, block = {
            Log.d("MP18", "launchedEffect in ScreenMyAccount.ShowMyUsers")
            myDBViewModel.getFirstPageUsers()
        })
    }
    ListMyUsers(myUsers = myUsersList, myDBViewModel = myDBViewModel)
}
@Composable
fun ListMyUsers(
    myUsers: List<MyUser>,
    myDBViewModel: MyDBViewModel
) {
    val pageLoadedTimes by myDBViewModel.pageLoadedTimes.observeAsState(initial = myDBViewModel.pageLoadedTimes.value)
    val loading by myDBViewModel.loading.observeAsState(initial = myDBViewModel.loading.value)
    Log.d(
        "MP18",
        "comp ShowMyUsers and pageLoadedTimes is: $pageLoadedTimes and loading is: $loading"
    )

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.Red)
    ) {
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            contentPadding = PaddingValues(16.dp)
        ) {
            itemsIndexed(
                items = myUsers
            ) { index, user ->
                myDBViewModel.onChangeProductScrollPosition(index)
                val numRec = pageLoadedTimes?.times(PAGE_SIZE)

                Log.d(
                    "MP188",
                    "in composable, page: $pageLoadedTimes, index: $index, loading: $loading, numRec: $numRec"
                )
                //we should query and display next page if this is true:
                if ((index + 1) >= (pageLoadedTimes?.times(PAGE_SIZE)!!) && !loading!!) {
                    myDBViewModel.getNextPageUsers()
                }
                ShowSingleUser(
                    index = index,
                    pageLoadedTimes = pageLoadedTimes!!,
                    user = user,
                    myDBViewModel = myDBViewModel
                )
            }
        }
    }
}

In composables that are available, there's an API call (through ViewModel) which gets data from backend in order to populate some vars in viewModel. The problem I have is that when first tab is clicked, also the neighbouring composable gets composed and thus I'am making 2 API calls and "preparing" second tab data even if the user might never click on that tab. This is not what I want. I'd like to fetch data from tab2 and later tab3 only when there's a click on them. I hope I am clear in what's bothering me.

Gregor Sotošek
  • 357
  • 1
  • 4
  • 22

2 Answers2

2

This is the expected behavior of the pager as the pager has been implemented by using LazyRow in accompanist pager. Basically, pager loads the second page before you scroll to it as LazyLayout is implemented in that way. If you want to cancel that you can do something like this, which I use in my code also:

// In anywhere of your composable
SideEffect {
        if(currentShownItemIndex == pagerState.currentPage) {
            // Make api call...
        }
    }

This should ensure that you are making your api call if and only if you are on the correct index

Edit: You can use Launched Effect if you want, I used SideEffect as it is easier to write and does not rely on any key and I needed a coroutine scope simply :d

Finally, this does not prevent the composition of the page in index+1 however prevents the unnecessary api call made by pager.

Subfly
  • 492
  • 2
  • 13
0

I found the solution for this. I added another variable in viewModel:

private val _pageInPager = MutableLiveData(0)
val pageInPager: LiveData<Int> = _pageInPager
fun setPageInPager(pageNum: Int) {
    Log.d("MP188", "setPageInPager to: $pageNum")
    _pageInPager.value = pageNum
}

Then in composable: if user clicks on tab:

                   onClick = {
                    coroutineScope.launch {
                        Log.d("MP18", "click on Tab num: $index")
                        pagerState.animateScrollToPage(index)
                        myDBViewModel.setPageInPager(index)
                    }
                },

or move the pager(slider):

myDBViewModel.setPageInPager(pagerState.currentPage)

I have the exact page in the variable: myDBViewModel.pageInPager, so I can add checker in LaunchedEffect before making an API call:

if (myUsersList.isNullOrEmpty() && pageLoadedTimes == 0 && !loading!! && pageInPager == 1) {
    LaunchedEffect(key1 = Unit, block = {
        Log.d("MP18", "launchedEffect in ScreenMyAccount.ShowMyUsers")
        myDBViewModel.getFirstPageUsers()
    })

I think this works ok now. Thank you @Subfly.

Gregor Sotošek
  • 357
  • 1
  • 4
  • 22
  • you're welcome :d. however, rather than counting the pageLoadedTimes, you can remember the value of the current index and rather than using pageLoadedTimes == 0, you can easily check currentPage == pagerState.currentPage. Additionally, you can call coroutineScope.launch { launch{pagerState.animateScrollToPage(index)} launch{myDBViewModel.setPageInPager(index)} } as each line in the scope works in the same thread. by using two launch{} statements, you create two different threads in the coroutine scope which will increase the performance of the app as they both work simultaneously – Subfly Oct 10 '22 at 15:10
  • pageLoadedTimes is a variable I use for infinite scroll - I'am fetching data in batches of 10 (from Firestore DB) and this is needed for correctly implementing logic relating to infinite scroll . . – Gregor Sotošek Oct 10 '22 at 18:45
  • oh ok, I thought it was counting how many times you have swiped between pager items. sorry. – Subfly Oct 11 '22 at 07:07