androidkotlinandroid-jetpack-composeandroid-jetpack-compose-scaffold

Android Jetpack Compose (Composable) Get String resources from Coroutine


I have a Flow that wraps a sealed class, which is used for calling messages to the UI. For example to show a Snackbar. I can observe the Flow from a Composable using LaunchedEffect. But the problem is that I want to retrieve the string resources from the coroutine scope. And I don't want to use direct string value in my view model, since I want to use the Locale for different languages so I pass the String Res Id instead. So is there a good way of doing it without putting the string resources in the Application class and retrieving them from there as sujested here getString Outside of a Context or Activity

The code below:

  1. I have Scaffold for showing snackbar, when the user clicks the scaffold which fills the whole screen the showSnackbarWithScope method is called, which emits new value to the Flow.
  2. Then the change is catch through the LaunchedEffect
  3. Then it calls the showSnackbar() method with the message, but the function expects String and I have String Res Id (Int) value
  4. The problem I cannot use stringResource() method which retrieves the string from Composable scope, since I am in a coroutine
@Composable
fun HomeScreen(viewModel: CityWeatherViewModel) {
 
    val scaffoldState = rememberScaffoldState() 
    LaunchedEffect(true) {
        viewModel.eventFlow.collectLatest { event ->
            when (event) {
                is CityWeatherViewModel.UIEvent.ShowSnackbar -> {
                    
                    // THE PROBLEM IS HERE!!!, since event.messageResId is res id, and not the string resource, and I can not use stringResource from coroutine
                    scaffoldState.snackbarHostState.showSnackbar(
                        message = event.messageResId
                    )
                }
            }
        }
    } 
 
    Scaffold(
        scaffoldState = scaffoldState,
        modifier = Modifier
            .fillMaxSize()
            .clickable {
                viewModel.showSnackbarWithScope(R.string.could_not_determine_location)
            }
    ) {

    }
}


@HiltViewModel
class CityWeatherViewModel @Inject constructor(
    private val getCityWeather: GetCityWeather
) : ViewModel() {

    // to show snackbar with error
    private val _eventFlow = MutableSharedFlow<UIEvent>()
    val eventFlow = _eventFlow.asSharedFlow()

     suspend fun showSnackbar(stringResId: Int) {
         _eventFlow.emit(
            UIEvent.ShowSnackbar(stringResId)
         )
     }

     fun showSnackbarWithScope(stringResId: Int) {
         viewModelScope.launch {
            showSnackbar(stringResId)
         }
         _eventFlowSwitch.value = !_eventFlowSwitch.value
    }

    sealed class UIEvent {
        data class ShowSnackbar(val messageResId: Int) : UIEvent()
    }

    companion object {

        // time in ms, before we request new data
        const val DELAY_BEFORE_REQUEST = 1500L
    }
}

Solution

  • You can get Context in any composable using LocalContext and then use it inside LaunchedEffect to get your resources:

    val context = LocalContext.current
    LaunchedEffect(Unit) {
        context.getString(event.messageResId)
    }