I can't understand why content of collect
function in SplashViewModel
is executed after "Finish" button click.
Of course content is called when app starts, because SplashViewModel
is injected in the MainActivity
and the 'init' function from 'SplashViewModel' is called but i don't know why app is moving me to AUTH
graph after clicking Finish
button...
This is the part of code which i mean when im using a 'content' word:
val route =
if (completed == true) NavigationDestinations.Auth.GRAPH else NavigationDestinations.OnBoarding.GRAPH
splashEventChannel.send(BaseUiEvent.Navigate(route))
_isLoading.value = false
It looks like when 'saveOnBoardingState' function from 'OnBoardingViewModel' is executed then is re-emitted last flow value in SplashViewModel
or something?
Here is my code:
'DataStoreManager' File
val Context.dataStore: DataStore<Preferences> by preferencesDataStore("user_preferences")
class DataStoreManager(context: Context) {
private val dataStore = context.dataStore
fun <T> getValueFromDataStore(preferencesKey: Preferences.Key<T>) =
dataStore.data.catch {
if (it is IOException) {
emit(emptyPreferences())
} else {
throw it
}
}.map {
val value = it[preferencesKey]
value
}
suspend fun <T> storeValue(key: Preferences.Key<T>, value: T) =
dataStore.edit { it[key] = value }
}
class SplashViewModel @Inject constructor(private val dataStoreManager: DataStoreManager) :
ViewModel() {
private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow();
private val splashEventChannel = Channel<UiEvent>()
val splashScreenEvents = splashEventChannel.receiveAsFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
dataStoreManager.getValueFromDataStore(PreferenceKey.onBoardingCompleted)
.collect { completed ->
val route =
if (completed == true) NavigationDestinations.Auth.GRAPH else NavigationDestinations.OnBoarding.GRAPH
splashEventChannel.send(BaseUiEvent.Navigate(route))
_isLoading.value = false
}
}
}
}
@ExperimentalPagerApi
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var splashViewModel: SplashViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen().apply {
setKeepOnScreenCondition {
splashViewModel.isLoading.value
}
}
setContent {
MyTheme {
val navController = rememberNavController()
val scaffoldState = rememberScaffoldState()
LaunchedEffect(key1 = true) {
splashViewModel.splashScreenEvents.collect { event ->
when (event) {
is BaseUiEvent.Navigate -> {
// navController.popBackStack()
navController.navigate(event.route)
}
}
}
}
Scaffold(
scaffoldState = scaffoldState,
content = { innerPadding ->
Surface(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
SetupNavGraph(
navController = navController,
scaffoldState = scaffoldState
)
}
}
)
}
}
}
}
@ExperimentalPagerApi
@Composable
fun OnBoardingScreen(
// navController: NavHostController,
onBoardingViewModel: OnBoardingViewModel = hiltViewModel()
) {
val pages = listOf(
OnBoardingPage.OnBoardingWelcomePage,
OnBoardingPage.OnBoardingSecondPage,
OnBoardingPage.OnBoardingLastPage
)
val pagerState = rememberPagerState()
val scope = rememberCoroutineScope()
Box(
modifier = Modifier
.fillMaxSize()
.padding(vertical = 16.dp)
) {
HorizontalPager(
modifier = Modifier.fillMaxSize(),
count = pages.count(),
state = pagerState,
) { page ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
OnBoardingScreenPage(
modifier = Modifier.padding(bottom = 200.dp),
onBoardingPage = pages[page]
)
}
}
Indicators(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 80.dp),
size = pages.size,
index = pagerState.currentPage
)
Box(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.align(Alignment.BottomCenter)
) {
AnimatedVisibility(
visible = pagerState.currentPage != pagerState.pageCount - 1,
enter = slideInHorizontally(initialOffsetX = { -it }).plus(fadeIn()),
exit = slideOutHorizontally(targetOffsetX = { -it }).plus(fadeOut())
) {
TextButton(
modifier = Modifier
.fillMaxHeight()
.padding(start = 16.dp),
onClick = {
scope.launch {
pagerState.animateScrollToPage(pages.size - 1)
}
}
) {
Text(text = stringResource(id = R.string.skip))
}
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.CenterEnd),
visible = pagerState.currentPage != pagerState.pageCount - 1,
enter = slideInHorizontally(initialOffsetX = { it }).plus(fadeIn()),
exit = slideOutHorizontally(targetOffsetX = { it }).plus(fadeOut())
) {
Button(
modifier = Modifier
.fillMaxHeight()
.padding(end = 16.dp),
shape = CircleShape,
onClick = {
if (pagerState.currentPage + 1 < pages.size)
scope.launch {
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}
) {
Text(
text = stringResource(id = R.string.next)
)
Icon(
modifier = Modifier.padding(start = 8.dp),
imageVector = Icons.Rounded.ArrowForward,
contentDescription = null
)
}
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = pagerState.currentPage == pagerState.pageCount - 1,
enter = expandHorizontally(expandFrom = Alignment.CenterHorizontally) { 0 }.plus(
fadeIn()
),
exit = shrinkHorizontally(shrinkTowards = Alignment.CenterHorizontally) { 0 }.plus(
fadeOut()
),
) {
Button(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(horizontal = 32.dp),
shape = CircleShape,
onClick = {
onBoardingViewModel.saveOnBoardingState(true)
}
) {
Text(text = "Finish")
}
}
}
}
}
@HiltViewModel
class OnBoardingViewModel @Inject constructor(private val dataStoreManager: DataStoreManager) :
ViewModel() {
fun saveOnBoardingState(completed: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
dataStoreManager.storeValue(key = PreferenceKey.onBoardingCompleted, value = completed)
}
}
}
I want to the screen not to change after clicking the Finish
button and content of collect
function not been executed more than one time.
Please help me because i can't solve this problem for few days :(
I found a solution!
The screen was changing after clicking Finish
button, because the flow in SplashViewModel
never stopped collecting values. Whenever i changed "onBoardingCompleted"(Boolean) value in Data Store by calling saveOnBoardingState
function then that value was automatically emitted.
To fix that we have to use .first()
flow operator which collecting only first emitted value and cancels the flow instead of .collect()
operator.
Old code:
class SplashViewModel @Inject constructor(private val dataStoreManager: DataStoreManager) :
ViewModel() {
private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow();
private val splashEventChannel = Channel<UiEvent>()
val splashScreenEvents = splashEventChannel.receiveAsFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
dataStoreManager.getValueFromDataStore(PreferenceKey.onBoardingCompleted)
.collect { completed ->
val route =
if (completed == true) NavigationDestinations.Auth.GRAPH else NavigationDestinations.OnBoarding.GRAPH
splashEventChannel.send(BaseUiEvent.Navigate(route))
_isLoading.value = false
}
}
}
}
Working code:
class SplashViewModel @Inject constructor(private val dataStoreManager: DataStoreManager) :
ViewModel() {
private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow();
private val splashEventChannel = Channel<UiEvent>()
val splashScreenEvents = splashEventChannel.receiveAsFlow()
init {
viewModelScope.launch(Dispatchers.IO) {
val onBoardingCompleted =
dataStoreManager.getValueFromDataStore(PreferenceKey.onBoardingCompleted, false)
.first()
val route =
if (onBoardingCompleted) NavigationDestinations.Auth.GRAPH else NavigationDestinations.OnBoarding.GRAPH
splashEventChannel.send(BaseUiEvent.Navigate(route))
_isLoading.value = false
}
}
}
btw i have improved a getValueFromDataStore
function in a DataStoreManager
. Here is the code:
fun <T> getValueFromDataStore(preferencesKey: Preferences.Key<T>, default: T): Flow<T> {
return dataStore.data.catch {
if (it is IOException) {
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[preferencesKey] ?: default
}
}