i have this code where i have a parent component QuranScreen and child component PagesContainer, the child component contains a horizontal pager inside it, whenever the pager state is changed (page changed), i emit an event to parent and change the currentPage state from the viewModel, this is supposed to cause a recomposition, but nothing happens..especially in the inner components (pagesContainer, benefits and Youtube screen), why is that happening?
@Composable
fun QuranScreen(
navController: NavController,
pageNum: String?,
quranViewModel: QuranViewModel = viewModel()
) {
if (pageNum != null) {
quranViewModel.currentPage = pageNum
};
val currentPage = quranViewModel.currentPage;
val page = quranViewModel.quranData.get(Integer.parseInt(currentPage) - 1)
val selectedTab = quranViewModel.selectedTab
Column {
when (selectedTab) {
"page" -> PagesContainer(quranViewModel.quranData, currentPage) { newPage ->
println("changing viewmodel page to " + newPage) //gives correct output
quranViewModel.currentPage = newPage
}
"benefits" -> Benefits(page.benefits, page.pageNum)
"yt" -> YouTube(page.ytLink.split("v=").last())
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun PagesContainer(
quranData: List<QuranPage>,
pageNum: String?,
onPageChanged: (String) -> Unit
) {
val pagerState = rememberPagerState(
initialPage = Integer.parseInt(pageNum) - 1,
initialPageOffsetFraction = 0f
) {
quranData.size
}
LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page ->
Log.d("Page change", "Page changed to $page")
onPageChanged("${page + 1}")
}
}
Box(modifier = Modifier.fillMaxSize()) {
HorizontalPager(
state = pagerState,
key = { quranData[it].pageNum },
pageSize = PageSize.Fill
) { index ->
SinglePage(quranData[index].pageNum)
}
}
}
Edit: Viewmodel code
class QuranViewModel : ViewModel() {
val quranData: List<QuranPage> = QuranStore.getQuranData();
private var _selectedTab by mutableStateOf("page")
var selectedTab:String
get() = this._selectedTab
set(value){this._selectedTab=value}
private var _currentPage by mutableStateOf("1")
var currentPage:String
get() = this._currentPage
set(value){this._currentPage=value}
}
Edit 2: i'm delving deeper into the issue and i logged the changes that happen inside my viewmodel, the recomposition does happen but the state value is then returned to the original value, i can't use remember inside viewmodel so i don't know how exactly can i fix this. there is another point i'm suspecting that is i'm sending the original pageNum in a route parameter that leads to this screen which might be getting reset with each recomposition..i don't have a way around this either
The recomposition would be triggered if a state value changes from the scope of a state hoisting Composable (in your case the QuranScreen
should be state hoisting but you are not declaring the variables are states.
You can use remember and change them to something like:
@Composable
fun QuranScreen(
navController: NavController,
pageNum: String?,
quranViewModel: QuranViewModel = viewModel()
) {
val quranData by quranViewModel.quranData.collectAsState(initial = emptyList())
val selectedTab by quranViewModel.selectedTab.collectAsState(initial = "page")
val currentPage by quranViewModel.currentPage.collectAsState(initial = "1")
when (selectedTab) {
"page" -> PagesContainer(quranData, currentPage) { newPage ->
quranViewModel.setCurrentPage(newPage)
}
"benefits" -> {
val page = quranData.getOrNull(Integer.parseInt(currentPage) - 1)
if (page != null) {
Benefits(page.benefits, page.pageNum)
}
}
"yt" -> {
val page = quranData.getOrNull(Integer.parseInt(currentPage) - 1)
if (page != null) {
YouTube(page.ytLink.split("v=").last())
}
}
}
}
But you'd need to also change the ViewModel
to expose these values as StateFlow
s to be collectedAsState
:
class QuranViewModel : ViewModel() {
private val _quranData = MutableStateFlow(QuranStore.getQuranData())
val quranData: StateFlow<List<QuranPage>> = _quranData.asStateFlow()
private val _selectedTab = MutableStateFlow("page")
val selectedTab: StateFlow<String> = _selectedTab.asStateFlow()
private val _currentPage = MutableStateFlow("1")
val currentPage: StateFlow<String> = _currentPage.asStateFlow()
fun setSelectedTab(value: String) {
_selectedTab.value = value
}
fun setCurrentPage(value: String) {
_currentPage.value = value
}
}
I'd also suggest you to try and simplify the state in just one data class since they seem to make sense to be updated together but I'm not sure it would be the best way.
Now, when the onPageChanged
lambda is invoked, the QuranViewModel
will update its state flows accordingly and the collectAsState
will be observing this new emitted value, thus triggering a new composition with the most recent values.