CountDownTimer in Android ViewModel executes onFinish twice - android

I tried to implement a timer in a ViewModel that after ten seconds returns to the previous view, but it doesn't work at all. I wonder if CountDownTimer can even be used in a ViewModel.
class PairingScreenViewModel(
private val routing: NavController,
) : ViewModel() {
var content = mutableStateOf(10)
var counter: CountDownTimer? = null
init {
viewModelScope.launch {
delay(1000L)
counter = object : CountDownTimer(9000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
content.value = content.value.dec()
}
override fun onFinish() {
routing.popBackStack() // Go back twice. WHY?
}
}.start()
}
}
override fun onCleared() {
super.onCleared() // It seems that it never runs.
counter?.cancel()
}
fun onButtonClicked() = viewModelScope.launch {
counter?.cancel() // It doesn't cancel the timer.
routing.navigate("next-route")
}
}
EDIT: This is my MainActivity.kt, I am probably doing something wrong with NavHostController.
This is the first time I use androidx.navigation:navigation-compose.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val routing = rememberNavController()
val heading = "MyApplication"
AndroidNavigationDemoTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(heading, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth()) }
)
},
content = {
Router(routing)
}
)
}
}
}
}
}
#Composable
fun Router(routing: NavHostController) {
NavHost(
navController = routing,
startDestination = "welcome"
) {
composable("welcome") {
WelcomeScreen(routing)
}
composable("hisense") {
HisenseScreen(HisenseScreenViewModel(routing))
}
composable("pairing") {
PairingScreen(PairingScreenViewModel(routing))
}
// ...
}
}

I have to remove ViewModel from the NavHost.
#Composable
fun Router(routing: NavHostController) {
NavHost(
navController = routing,
startDestination = "welcome"
) {
composable("welcome") {
WelcomeScreen(routing)
}
// ...
composable("pairing") {
PairingScreen(routing)
}
// ..
}
}
And set the ViewModel from the composable.
class PairingScreenViewModelFactory(private val routing: NavController) :
ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T = PairingScreenViewModel(routing) as T
}
#Composable
fun PairingScreen(routing: NavController) {
val viewModel: PairingScreenViewModel = viewModel(factory = PairingScreenViewModelFactory(routing))
// ...
}
And everything is working like a charm.

Related

Datastore does not update mutableStateOf to update the view

I'm using DataStore to hold a boolean and depending on the value the view will change. The value does change and works sort of, it is just the app requires a restart for the change to take. Is this because of the runBlocking?
This is the suspend fun I'm calling
suspend fun isOnBoardingCompleted(): Boolean {
return context
.dataStore
.data
.map { preferences -> preferences[ON_BOARDING_COMPLETED] ?: false }
.first()
}
class MainActivity : ComponentActivity() {
private val projectViewModel: ProjectRoomViewModel by viewModels()
#OptIn(ExperimentalPagerApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val projectRoomViewModel: ProjectRoomViewModel by viewModels()
setContent {
val preferences = DataStoreViewModel(this)
val isOnBoardingCompleted = remember {
mutableStateOf(
runBlocking {
preferences.isOnBoardingCompleted()
}
)
}
AppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
if (isOnBoardingCompleted.value) {
RootScreen( preferences = preferences, projectRoomViewModel = projectRoomViewModel)
} else {
PagerView(navController = rememberNavController()) {
mutableStateOf(
runBlocking{
preferences.disableOnboarding()
}
)
}
}
}
}
}
}
}

Compose navigation lose state after pop screen (navigate for network success)

I am using compose navigation with single activity and no fragments.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MobileComposePlaygroundTheme {
Surface(color = MaterialTheme.colors.background) {
val navController = rememberNavController()
NavHost(navController, startDestination = "main") {
composable("main") { MainScreen(navController) }
composable("helloScreen/{data}") { HelloScreen() }
}
}
}
}
}
}
#Composable
private fun MainScreen(navController: NavHostController) {
val viewModel = viewModel()
val loginState by viewModel.loginState
LaunchedEffect(loginState) {
if(loginState is State.Success){
navController.navigate("helloScreen/data")
}
}
Column {
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { viewModel.login() }, //viewmodel will change loginState
modifier = Modifier.padding(8.dp)
) {
Text(text = "Go To HelloScreen")
}
}
}
#Composable
fun HelloScreen() {
Log.d("TAG", "HelloScreen")
Text("Hello Screen")
}
This post is hidden. You deleted this post 7 mins ago.
i have some problem
MainScreen(loginState)-> LaunchedEffect(loginState) -> HelloScreen -> back button -> MainScreen
when i pop HelloScreen
MainScreen will be recompose for
loginState, LaunchedEffect(netWorkState) will navigate(HelloScreen) again
how can i change code right of navigate
Use Disposable effect
DisposableEffect(loginState) {
if(loginState is State.Success){
navController.navigate("helloScreen/data")
}
onDispose {
loginState = State.Idle // Reset back your state here
}
}

Calling methods of InputMethodService from Jetpack Compose

Trying to make a custom Keyboard using Jetpack Compose. Cannot figure out how call currentInputConnection or other methods from Composable.
#Composable
fun CustomKeyboard() {
var inputVal by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
Spacer(modifier = Modifier.height(50.dp))
Text("Last key pressed: $inputVal")
Row(modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically)
{
MyButton(mText = "A") { inputVal = it}
MyButton(mText = "B") { inputVal = it}
MyButton(mText = "C") { inputVal = it}
}
}
}
#Composable
private fun MyButton(
mText: String,
onPressed: (String) -> Unit
) {
OutlinedButton(
onClick = {
onPressed(mText)
},
modifier = Modifier.padding(4.dp)
) {
Text(text = mText, fontSize = 30.sp, color = Color.White)
}
}
And the InputMethodService class here...
class ComposeKeyboardView(context: Context) : AbstractComposeView(context) {
#Composable
override fun Content() {
CustomKeyboard()
}
}
class IMEService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner,
SavedStateRegistryOwner {
override fun onCreateInputView(): View {
val view = ComposeKeyboardView(this)
window!!.window!!.decorView.let { decorView ->
ViewTreeLifecycleOwner.set(decorView, this)
ViewTreeViewModelStoreOwner.set(decorView, this)
ViewTreeSavedStateRegistryOwner.set(decorView, this)
}
view.let {
ViewTreeLifecycleOwner.set(it, this)
ViewTreeViewModelStoreOwner.set(it, this)
ViewTreeSavedStateRegistryOwner.set(it, this)
}
return view
}
fun doSomethingWith(mData: String) {
currentInputConnection?.commitText(mData, 1)
}
//Lifecylce Methods
private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this)
override fun getLifecycle(): Lifecycle {
return lifecycleRegistry
}
private fun handleLifecycleEvent(event: Lifecycle.Event) =
lifecycleRegistry.handleLifecycleEvent(event)
override fun onCreate() {
super.onCreate()
savedStateRegistry.performRestore(null)
handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun onDestroy() {
super.onDestroy()
handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
//ViewModelStore Methods
private val store = ViewModelStore()
override fun getViewModelStore(): ViewModelStore = store
//SaveStateRegestry Methods
private val savedStateRegistry = SavedStateRegistryController.create(this)
override fun getSavedStateRegistry(): SavedStateRegistry = savedStateRegistry.savedStateRegistry
}
If you just need to call a single function, i.e. doSomethingWith(mData: String), or a few of them, then you can pass them into your composables and call them when you want to. This approach would be more loosely coupled and easier to #Preview the CustomKeyboard composable.
#Composable
fun CustomKeyboard(onKeyPressed: (String) -> Unit) {
//...
MyButton(mText = "A") {
inputVal = it
onKeyPressed(it)
}
// ...
}
class ComposeKeyboardView(
context: Context,
private val onKeyPressed: (String) -> Unit,
) : AbstractComposeView(context) {
#Composable
override fun Content() {
CustomKeyboard(onKeyPressed)
}
}
class IMEService : InputMethodService() {
override fun onCreateInputView(): View {
val view = ComposeKeyboardView(this, onKeyPressed = this::doSomethingWith)
// ...
return view
}
private fun doSomethingWith(mData: String) {
currentInputConnection?.commitText(mData, 1)
}
}
If you plan to add many more functions to the IMEService that you will have to also call, then you can just pass the IMEService (or some interface that IMEService implements) into your composables and then call its members normally. Using an interface over an actual class would make it possible to #Preview the CustomKeyboard composable.
#Composable
fun CustomKeyboard(imeService: IMEService) {
//...
MyButton(mText = "A") {
inputVal = it
imeService.doSomethingWith(it)
}
// ...
}
class ComposeKeyboardView(private val imeService: IMEService) : AbstractComposeView(imeService) {
#Composable
override fun Content() {
CustomKeyboard(imeService)
}
}
class IMEService : InputMethodService() {
override fun onCreateInputView(): View {
val view = ComposeKeyboardView(this)
// ...
return view
}
fun doSomethingWith(mData: String) {
currentInputConnection?.commitText(mData, 1)
}
}

swipe to refresh using accompanist

I'm using accompanist library for swipe to refresh.
And I adopt it sample code for testing, however, it didn't work.
I search for adopt it, but I couldn't find.
Is there anything wrong in my code?
I want to swipe when user needs to refresh
class MyViewModel : ViewModel() {
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing: StateFlow<Boolean>
get() = _isRefreshing.asStateFlow()
fun refresh() {
// This doesn't handle multiple 'refreshing' tasks, don't use this
viewModelScope.launch {
// A fake 2 second 'refresh'
_isRefreshing.emit(true)
delay(2000)
_isRefreshing.emit(false)
}
}
}
#Composable
fun SwipeRefreshSample() {
val viewModel: MyViewModel = viewModel()
val isRefreshing by viewModel.isRefreshing.collectAsState()
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() },
) {
LazyColumn {
items(30) { index ->
// TODO: list items
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
setContent {
TestTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
}
}
}
}
}
Your list doesn't take up the full screen width and you should include the state parameter:
SwipeRefresh(
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = { viewModel.refresh() },
) {
LazyColumn(state = rememberLazyListState(), modifier = Modifier.fillMaxSize()) {
items(100) { index ->
Text(index.toString())
}
}
}
or with Column:
Column(modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())) {
repeat(100) { index ->
Text(index.toString())
}
}

How could i run my logic code inside composable function just 1 time?

I'm doing my test project using ViewModel and ComposeView.
My architecture include: one Activity and multi ComposeView, using navigation like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainView()
}
}
}
#Composable
fun MainView() {
TestComposeTheme {
val navController = rememberNavController()
Surface(color = MaterialTheme.colors.background) {
NavHost(
navController = navController,
startDestination = KYCScreen.getName(),
) {
navigation(startDestination = KYCScreen.Preload.name, route = KYCScreen.getName()) {
composable(KYCScreen.Preload.name) { PreloadView(navHostController = navController) }
composable(KYCScreen.StartKYC.name) { StartView(navHostController = navController) }
composable(KYCScreen.Login.name) { LoginView(navHostController = navController) }
}
navigation(startDestination = HomeScreen.Home.name, route = HomeScreen.getName()) {
composable(HomeScreen.Home.name) { HomeView(navHostController = navController) }
}
}
}
}
}
The problem happens when I include logic that executes when a Composable function is executed, my logic code was looped many times. Here my code:
#Composable
fun PreloadView(navHostController: NavHostController) {
val preloadViewModel: PreloadViewModel = viewModel()
preloadViewModel.getSyncData()
val syncState by preloadViewModel.uiStateSync.observeAsState()
syncState?.let { PreloadContent(navHostController = navHostController, it) }
}
#Composable
fun PreloadContent(navHostController: NavHostController, uiState: UiState<SyncResponse>) {
when (uiState.state) {
RequestState.SUCCESS -> {
navHostController.navigate(KYCScreen.StartKYC.name)
}
RequestState.FAIL -> {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
) {
Text(text = "Error", color = Color.Black)
}
}
RequestState.NON -> {
}
}
}
Anyone have a solution to help me with that architecture?
Your composable function should be side-effects free.
If you want to run something just once. You can do something like the following
LaunchedEffect(Unit){
preloadViewModel.getSyncData()
}
or in your case, if it is mandatory to Sync Data when ViewModel initializes you can call this function in the init block inside of your ViewModel.
Check the official doc https://developer.android.com/jetpack/compose/side-effects

Categories

Resources