Does anyone have a sample app with runtime locale ...
# compose
v
Does anyone have a sample app with runtime locale changing at hand?
v
I have but it's not a sample and not public 😅 what do you need to know?
v
I need to set locale to my language and instruct the app to recompose if it changes
Although I use moko-resources and likely will need to find how it done there. Seems like moko-res will not react on System locale change
v
you will probably need to figure out how moko-resources discovers the active locale
on plain compose the default
stringResource
implementation is a composable function that will trigger recomposition of the parent when
LocalConfiguration.current
changes
👌 1
p
Maybe also check out my thread, on a similar topic. See last response. https://kotlinlang.slack.com/archives/CJLTWPH7S/p1702397330578879
v
Okay, for MOKO it is as simple as
Copy code
StringDesc.localeType = StringDesc.LocaleType.Custom("en")
It does change selection of the localizations it fetches. But seems like it doesn't actually change the locale returned via
Copy code
Locale.current.language
Wild that is not KMP thread unfortunately. I am not seeing setters in the KMP Locale
Now question is how I force recomposition of the whole tree after I change Moko locale. The top of the tree is recomposed, but deeper is not
v
what is triggering the top level recomposition now?
and is it so that moko resource getters are not composable?
v
Simple
Copy code
AppTheme {

    var currentLanguage: String by mutableStateOf(
        ""
    )
   App()
}
I mean, they are at some extent
v
what does the
localized()
function look like?
v
I don't think it is moko at this point. I need to force recomposition of whole tree when some state field changes
v
there's a way to do it but it's not really the correct solution here
localized()
seems to be composable, so what does it read? If it would read the current locale from a state or composition local internally, you would trigger recomposition for all places you need to
v
You think I should connect my state field to the composable which actually fetches a string so it recompose itself?
v
that's how vanilla compose does it
Screenshot 2023-12-19 at 13.00.32.png
Screenshot 2023-12-19 at 13.00.43.png
v
I mean I have my own abstraction for the Moko
Copy code
@Composable
override fun getString(stringToken: StringTokens): String {
    return translator?.translate(stringToken.translationKey)
        ?: stringResource(stringToken.stringResource)
}
So technically every label delivered through my own composable which delegates the call to the Moko
v
I don't know how LocalConfiguration behaves for multiplatform, but just reading the value of it in your own abstraction should work?
v
So it would be compose way to add the "My custom locale" into LocalComposition, then connect my all use function to LocalMyCustomLocal?
And I don't even need to actually read anything from it, I just want to force of the recomposition when it changes. And I can change it whenever I want
v
yes
If you take a look at the compose implementation, that's exactly what is done.
LocalConfiguration.current
is read in the
resources()
function, but it's not actually used
That will narrow down the recomposition scope a bit to only recompose the places where you use the locale instead of the whole UI tree
(If you really really want to invalidate the whole tree, you can create a new
staticCompositionLocalOf
and change its value.)
🤔 1
v
That is smelly 😄
Copy code
var currentLanguageState: String by mutableStateOf(
    ""
)

    override fun updateLocale(language: String) {
        currentLanguageState = language
    }

@Composable
override fun getString(stringToken: StringTokens): String {

    currentLanguageState

    return stringResource(stringToken.stringResource)
}
Yes, basically doing even something as simple as this do the thing. Now I can even easily connect flow from the database which will be changing the language state every time it changed. This is not pure compose way, because I added OOP abstraction for my resources provider Thank you.
a
You might be running into https://issuetracker.google.com/issues/240191036 if you are using
Locale.current
- they aren’t snapshot state aware if the current locale changes.
1
s
The linked issue seems to imply that using
LocalConfiguration.current
should be the right approach to get a recomposition when the locale changes. I got a function like this
Copy code
@Composable
@ReadOnlyComposable
fun getLocale(): Locale {
  val configuration = LocalConfiguration.current
  SideEffect { Log.d("", "configuration changed") }
  return ConfigurationCompat.getLocales(configuration).get(0) ?: LocaleListCompat.getAdjustedDefault()[0]!!
}
But it seems like this is not properly giving me the latest response in all Android API versions, with all other things being the same. The log just does not run at all, and I do not get the latest value back from this composable On API 33 for example changing the locale by calling
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags("some language tag here"))
I get back the latest locale from this composable and the log triggers too. Doing the same on API 24, 28, 29, 30, 31 and 32 (what I've tested so far) I do not get the new value back in composition from my
getLocale()
here, but my app does seem to properly change language. I have no idea how exactly this is happening 🫣 All this with
android:configChanges="locale"
being there of course. Without it you simply get a activity recreation and all this works, but I'd like to not have to do this if possible.
b
Do your
Activity
inherits from
AppCompatActivity
?
a
It’s because
AppCompatDelegate.setApplicationLocales
force recreates the activity below API 33, which is necessary to change app locales without framework API.
andorid:configChanges
only controls config changes initiated by system and it doesn’t prevent
Acitivity.recreate()
calls.
s
Yes, it does indeed inherit from
AppCompatActivity
So Albert, if it forces activity recreation, but I am not getting the new value, how does this interaction exactly play out? What should I be doing instead?
a
That doesn't make sense. Can you confirm the activity is recreated?
a
Hmm there might be a bug here… there are two distinct paths that configuration changes get reported (in the case where the
Activity
isn’t recreated) Through
Activity.onConfigurationChanged
, and
View.onConfigurationChanged
. Notably, they are independently called by the platform (one doesn’t directly depend on the other). Also notably, Compose updates through the
View.onConfigurationChanged
path. It might be the case that
AppCompactDelegate
only calls through the
Activity.onConfigurationChanged
, which leaves Compose out of date. What happens if you call
View.dispatchConfigurationChanged
on your root view, from your
Activity.onConfigurationChanged
method? That might force any attached
ComposeView
to be notified about the new
Configuration
s
So: I added
|locale
into my
configChanges
of my activity In my activity, I got my aforementioned
getLocale()
and painting it on the center of the screen for debug purposes
Copy code
setContent {
  val locale = getLocale()
  Box {
    App()
    Text(text = locale.toLanguageTag(), textAlign = TextAlign.Center, modifier = Modifier.align(Alignment.Center))
  }
}
"before_dispatching.mov" video shows how changing the language (I call
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(language.toString()))
on click there) Now the weird thing is that my app does in fact return the right strings afterwards. Perhaps since
stringResource
tries to get those from
LocalContext.resources.getString
that one is updated properly, while configuration doesn't know about this change? Then the second video, "after_dispatching.mov", shows after I've applied your suggestion Alex like this:
Copy code
override fun onConfigurationChanged(newConfig: Configuration) {
  super.onConfigurationChanged(newConfig)
  val composeView = window.decorView
    .findViewById<ViewGroup>(android.R.id.content)
    .getChildAt(0) as? ComposeView
  composeView?.dispatchConfigurationChanged(newConfig)
}
that I do in fact get the new configuration this way. So this absolutely must be a bug. I should probably file a new bug report right?
Unfortunately using
configChanges
as suggested here has made me shoot myself in the foot more than once, last time that I remember it was with m3 bottom sheets here, I think there was some other case too, and now this. I think it's a good idea to discourage people from using it in general when it seems like there's a lot of issues here and there. I think for now I'd rather just not adding
locale
to the list of those changes and just let it recreate the activity, which at least I now know will work correctly.
150 Views