Search code examples
androidandroid-jetpack-composeandroid-viewmodel

Android Compose - How to handle ViewModel clear focus event in JetPackCompose?


How to handle ViewModel clear focus event in JetPackCompose?

I have a coroutines channel that sometimes notify my screen to clear the TextField focus

How is the best way to notify my composable to clear focus?

I tried to create a mutableStateFlow, but is there a better way to do it?

@Composable
fun HomeScreen(
    viewModel: MainViewModel = hiltViewModel()
) {

    val clearFocus by viewModel.clearFocus.collectAsStateWithLifecycle()

    AppTheme {
            HomeScreenContent(
                clearFocus
            )
    }
}

@HiltViewModel
class MainViewModel @Inject constructor() : ViewModel() {
    val clearFocus = MutableStateFlow(false)

    init {
        viewModelScope.launch {
            delay(3000)
            clearFocus.value = true
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreenContent(
    clearFocus: Boolean
) {
    val focusRequester = remember { FocusRequester() }
    val focusManager = LocalFocusManager.current
    var value by rememberSaveable { mutableStateOf("initial value") }
    TextField(
        value = value,
        onValueChange = {
            value = it
        }
    )
    if(clearFocus) {
        focusManager.clearFocus()
    }
}

When a coroutine channel notifies the ViewModel, I want to clear the TextField focus, how is the best way to achieve that?


Solution

  • Instead of delegating to the HomeScreenContent the duty of clearing the focus you could do it in HomeScreen.

    You should not use a stateFlow if you want to do an action that does not affect the compose tree. Instead of using StateFlow use a SharedFlow when you want to trigger an Event.

    Using a SharedFlow

    @HiltViewModel
    class MainViewModel @Inject constructor() : ViewModel() {
        val clearFocusEvent = MutableSharedFlow<Unit>()
    
        init {
            viewModelScope.launch {
                delay(3000)
                clearFocusEvent.emit(Unit)
            }
        }
    }
    
    @Composable
    fun HomeScreen(
        viewModel: MainViewModel = hiltViewModel()
    ) {
    
       
        val focusManager = LocalFocusManager.current
    
        LaunchedEffect(Unit) {
            viewModel.clearFocusEvent.collectLatest {
                focusManager.clearFocus()
            }
        }
    
        AppTheme {
                HomeScreenContent()
        }
    }
    

    Using a sealed interface as event

    If you want to have more events between your VM and Composable or just a cleaner code, you can make a sealed interface that will represent the events

    @HiltViewModel
    class MainViewModel @Inject constructor() : ViewModel() {
        val homeScreenEvent = MutableSharedFlow<HomeScreenEvent>()
    
        init {
            viewModelScope.launch {
                delay(3000)
                homeScreenEvent.emit(HomeScreenEvent.ClearFocus)
            }
        }
    }
    
    sealed interface HomeScreenEvent {
        object ClearFocus: HomeScreenEvent
    }
    
    @Composable
    fun HomeScreen(
        viewModel: MainViewModel = hiltViewModel()
    ) {
        val focusManager = LocalFocusManager.current
    
        LaunchedEffect(Unit) {
            viewModel.homeScreenEvent.collectLatest {
                when(it) {
                    HomeScreenEvent.ClearFocus ->  focusManager.clearFocus()
                }
            }
        }
    
        AppTheme {
            HomeScreenContent()
        }
    }
    

    Now when you'll add an event you just have to handle the new case in the when