テクノクラートブログの力はAndroid開発者にあります。Vladislav Titovが、ローカリゼーションを変更するときに中断のないUIを実現する方法について説明します。
何が問題ですか
Androidでの文字列リソースの操作は、次のように構成されています。
文字列は、xmlマークアップとしてさまざまな値のフォルダに保存されます
コードで文字列を取得するには、Contextオブジェクトを参照する必要があります。
Context . , , -, Locale Locale.setDefault(), -, Context Locale attachBaseContext(). , UI. ? .
Jetpack Compose , ()
, Compose — .
:
@Composable
fun Hello() {
Text(text = "Hello Compose!")
}
@Composable
fun Hello() {
var name = "Compose"
Text(text = "Hello $name!")
TextField(
value = name,
onValueChanged = { name = it }
)
}
TextField name, . , . , . Compose , name Text. ?
State — . Compose , , . . , .
@Composable
fun Hello() {
var name by mutableStateOf("Compose")
Text(text = "Hello ${name}!")
TextField(
value = name,
onValueChanged = { name = it }
)
}
mutableStateOf — -builder Composable. MutableState, State, property value. . State , property delegate by ( ). state.value.
. State , Compose , Composable-. , . , , . , , . :
, Composable, State, . var name by mutableStateOf("Compose") . . , remember:
@Composable
fun Hello() {
val name by remember { mutableStateOf("Compose") }
Text(text = "Hello ${name}!")
TextField(
value = name,
onValueChanged = { name = it }
)
}
Composable “” , . , Composable-.
, Composable side-. - Composable side- . , Composable . , , . .
, , .
CompositionLocal
, , Composable- . . . , . , MyApp , . MyApp .
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val viewModel: MainViewModel = viewModel()
MyApp(viewModel.user)
}
}
}
@Composable
fun MyApp(user: User) {
UserWidget(user)
}
@Composable
fun UserWidget(user: User) {
Text(text = user.name)
}
, Composable, .
CompositionLocal. State, , Composable . CompositionLocal Composable. : “ CompositionLocal side-?” - . — .
val ActiveUser = compositionLocalOf<User> { error("No active user found!") }
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val viewModel: MainViewModel = viewModel()
CompositionLocalProvider(ActiveUser provides viewModel.user) {
MyApp()
}
}
}
}
@Composable
fun MyApp() {
UserWidget()
}
@Composable
fun UserWidget() {
val user = ActiveUser.current
Text(text = user.name)
}
, - compositionLocalOf CompositionLocal User ActiveUser. trailing User’, , .
- Composable ( , ) CompositionLocal. CompositionLocalProvider(ActiveUser provides viewModel.user) { //contnt }. ActiveUser , , content. user. Provider’. CompositionLocal side-.
CompositionLocal, Composable property CompositionLocal.current. State, Compose , CompositionLocal, Composable. setContent UserWidget, MyApp. , Composable , .
: Context
, , . LocalContext: CompositionLocal Context . , , Jetpack Compose. .
data class Localization(
val locale: Locale,
val strings: MutableMap<String, String> = mutableMapOf()
)
, : , , . .
, :
localizationMap — , O(1), O(n).
supportedLocales
, .
internal val defaultLocalization: Localization = Localization(Locale.ENGLISH)
private val supportedLocales: MutableSet<Locale> = mutableSetOf()
private val localizationMap = hashMapOf<Locale, Localization>()
fun registerSupportedLocales(vararg locales: Locale): Set<Locale> {
locales.filter { it != Locale.ENGLISH }
.forEach {
if (supportedLocales.add(it)) {
registerLocalizationForLocale(it)
}
}
return supportedLocales + Locale.ENGLISH
}
private fun registerLocalizationForLocale(locale: Locale) {
localizationMap[locale] = Localization(locale)
}
, , , . . , :
fun Translatable(name: String, defaultValue: String, localeToValue: () -> Map<Locale, String>): Localization.() -> String {
defaultLocalization.strings[name] = defaultValue
for ((locale, value) in localeToValue().entries) {
val localization = localizationMap[locale] ?: throw RuntimeException("There is no locale $locale")
localization.strings[name] = value
}
return fun Localization.(): String {
return this.strings[name] ?: defaultLocalization.strings[name] ?: throw RuntimeException("There is no string called $name in localization $this")
}
}
fun NonTranslatable(name: String, defaultValue: String): Localization.() -> String {
defaultLocalization.strings[name] = defaultValue
return fun Localization.(): String {
return defaultLocalization.strings[name] ?: throw RuntimeException("There is no string called $name in localization default")
}
}
( ):
name — . .
defaultValue — ,
closure localeToValue, -.
: . . extension-, Localization, — .
extension- . , . ? CompositionLocal:
val LocalLocalization = compositionLocalOf { defaultLocalization }
, LocalLocalization object ( MaterialTheme):
object Vocabulary {
@Composable
@get:ReadOnlyComposable
val localization: Localization get() = LocalLocalization.current
}
CompositionLocal:
@Composable
fun Localization(locale: Locale, content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalLocalization provides (localizationMap[locale] ?: defaultLocalization),
children = content
)
}
1.
val RUSSIAN = Locale("ru")
val TATAR = Locale("tt")
val supportedLocalesNow = registerSupportedLocales(RUSSIAN, TATAR)
2.
val hello = Translatable("hello", "Hello!") {
hashMapOf(
RUSSIAN to "!",
TATAR to "ә!"
)
}
val nonTrans = NonTranslatable("format", "%1\$d:%2\$02d")
3.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Localization(locale = TATAR /*smth from supportedLocalesNow*/) {
val localization = Vocabulary.localization
Text(text = localization.hello())
Text(text = localization.nonTrans().format(20, 9))
}
}
}
}
4. !
, Localization, Activity.
plurals, , Translatable NonTranslatable. type-safety . . Formatter, — MessageFormat, .
, , , . !
Jetpack Compose Localization - https://github.com/TechnokratosDev/jetpack-compose-localization
Jetpack Compose - https://developer.android.com/jetpack/compose/mental-model
Composable - https://medium.com/androiddevelopers/under-the-hood-of-jetpack-compose-part-2-of-2-37b2c20c6cdd
Codelab Compose - https://codelabs.developers.google.com/codelabs/jetpack-compose-basics
«». 4:28.