JetpackComposeのCompositionLocal。それは何であり、それを使用してアプリケーションのリアクティブローカリゼーションを実装する方法

テクノクラートブログの力は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.





(MutableState), . , , , view. , ViewModel, var name by mutableStateOf("Compose") val name by viewModel.nameLiveData.observeAsState(""), onValueChanged = { name = it } onValueChanged = { viewModel.setName(it) }. , var name val name. .





. 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





P.S. , .





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.





Telegram- « », .








All Articles