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.
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?
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.