Search code examples
androidkotlinandroid-jetpack-composeandroid-jetpack-compose-text

How to trigger recomposition after updateConfiguration?


I want to change my app language dynamically, without the need to restart the Activity for the results to take effect. What I am doing now is to add a mutable Boolean state that is switch and is used by all Text elements.

enter image description here

To change the language I call the following code inside the clickable callback (I use the box as a dummy object, just to test):

val configuration = LocalConfiguration.current
val resources = LocalContext.current.resources
Box( 
    modifier = Modifier
        .fillMaxWidth()
        .height(0.2.dw)
        .background(Color.Red)
        .clickable {
            
            // to change the language
            val locale = Locale("bg")
            configuration.setLocale(locale)
            resources.updateConfiguration(configuration, resources.displayMetrics)
            viewModel.updateLanguage()
        }
) {
}

Then it switches the language value using the updateLanguage() method

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

    private val _languageSwitch = mutableStateOf(true)
    var languageSwitch: State<Boolean> = _languageSwitch

    fun updateLanguage() {
        _languageSwitch.value = !_languageSwitch.value
    }
}

The problem is that in order to update each Text composable, I need to pass the viewmodel to all descendant that use Text and then use some bad logic to force update each time, some changes in the view model occur.

@Composable
fun SomeChildDeepInTheHierarchy(viewModel: CityWeatherViewModel, @StringRes textResId: Int) {
 
    Text(
        text = stringResource(id = if (viewModel.languageSwitch.value) textResId else textResId),
        color = Color.White,
        fontSize = 2.sp,
        fontWeight = FontWeight.Light,
        fontFamily = RobotoFont
    )
}

It works, but that is some really BAD logic, and the code is very ugly! Is there a standard way of changing the Locale using Jetpack Compose dynamically?


Solution

  • The easiest solution is to recreate the activity after configuration change:

    val context = LocalContext.current
    Button({
        // ...
        resources.updateConfiguration(configuration, resources.displayMetrics)
        context.findActivity()?.recreate()
    }) {
        Text(stringResource(R.string.some_string))
    }
    

    findActivity:

    fun Context.findActivity(): Activity? = when (this) {
        is Activity -> this
        is ContextWrapper -> baseContext.findActivity()
        else -> null
    }
    

    If for some reason you don't wanna do that, you can override LocalContext with the new configuration like this:

    MainActivity:

    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            setContent {
                val context = LocalContext.current
                CompositionLocalProvider(
                    LocalMutableContext provides remember { mutableStateOf(context) },
                ) {
                    CompositionLocalProvider(
                        LocalContext provides LocalMutableContext.current.value,
                    ) {
                        // your app
                    }
                }
            }
        }
    }
    
    val LocalMutableContext = staticCompositionLocalOf<MutableState<Context>> {
        error("LocalMutableContext not provided")
    }
    

    In your view:

    val configuration = LocalConfiguration.current
    val context = LocalContext.current
    val mutableContext = LocalMutableContext.current
    Button(onClick = {
        val locale = Locale(if (configuration.locale.toLanguageTag() == "bg") "en_US" else "bg")
        configuration.setLocale(locale)
        mutableContext.value = context.createConfigurationContext(configuration)
    }) {
        Text(stringResource(R.string.some_string))
    }
    

    Note that remember will not live through system configuration change, e.g. screen rotation, you probably need to store the selected locale somewhere, e.g. in DataStore, and provide the needed configuration instead of my initial context when providing LocalMutableContext.

    p.s. in both cases you don't need a flag in the view model, if you have resources placed according to documentation, e.g. in values-bg/strings.xml, and so on, stringResource is gonna work out of the box.