Don't work reCreate() activity with jetpack compose for android 13(API 30)

241 Views Asked by At

Don't work SetContent{} function when i call recreate function for change my app theme

Hi there i try to change theme in my app that developed with jetpack compose(material v1.2.0-alpha12) and android version 13(API 30) but don't work reCreate function for activity after submitted configuration i try to put Log.i in my code and debug it, that all i know is don't work setContent{} when call again after reCreate lifecycle can anybody help me?

1

There are 1 best solutions below

1
stoyan_vuchev On

After tinkering around for quite some time, I've managed to update the Locale and recreate the activity to change the app language, also working on Android 13 (API 33) by using the new localization approach.

I've included an example guide of how to achieve it, including how to change the theme in Compose.

It's important to note that I've used Preferences DataStore as a solution for storing preferences instead of Shared Preferences since it has many advantages (e.g. real-time observation without a listener), in combination with dependency injection using DaggerHilt.

I highly recommend using a single activity with a navigation for different screens since it's easier to maintain and the app doesn't get filled with activities. Here is a detailed guide of how to implement such navigation.

The structure of the example guide:

  1. A list of the available app languages.
  2. Function to set the app language by updating the locale.
  3. Custom theme mode approach.
  4. App preferences using Preferences DataStore.
  5. Setting up the dependency injection.
  6. A ViewModel containing the states and callbacks.
  7. Activity implementation.

Feel free to reach out to me if you need further assistance.


Let's start by creating a list containing the app languages:

  • It's important to note that the list must contain ONLY languages that are already a translated variant of the strings.xml.

  • I recommend using the Translations Editor to make a translated variant.

val appLanguages = listOf("en", "bg" /* More languages ... */ )  

Then, creating a function that can be called within an Activity, containing the necessary logic to set the language. I highly recommend to put this function inside a separate Kotlin file, so it's not coupled to a single Activity and then calling it within the onCreate function of an Activity in a lifecycle manner (As shown in the final example):

fun Activity.setAppLanguage(languageTag: String) {

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {

        // Change the Locale for Android version >= 13.

        val locale = getSystemService(LocaleManager::class.java).applicationLocales
        val newLocale = LocaleList(Locale.forLanguageTag(languageTag))

        if (locale != newLocale) {

            // Set the new Locale.
            getSystemService(LocaleManager::class.java).applicationLocales = newLocale

            // Recreate the Activity.
            recreate()

        }

    } else {

        // Change the Locale for Android version <= 12.

        val locale = Locale.getDefault()
        val newLocale = Locale(languageTag)

        if (locale != newLocale) {

            // Apply the new Locale to the configuration.
            val configuration = resources.configuration.apply {
                Locale.setDefault(newLocale)
                setLocale(newLocale)
            }

            // Update the configuration.
            @Suppress("Deprecation")
            resources.updateConfiguration(configuration, resources.displayMetrics)

            // Recreate the Activity.
            recreate()

        }

    }

}

Changing the theme in Compose can be done by using the following approach:

  • A custom ThemeMode enum class, containing the necessary theme modes, like: SYSTEM, LIGHT, and DARK.
  • A custom LocalThemeMode CompositionLocal key for providing and consuming the ThemeMode down the composition.
  • A custom isInDarkTheme() composable function to check if the currently applied theme is dark.
  • A preference to store the ThemeMode that can be observed in real time. (as shown later on):
/** A custom enum class, containing the necessary theme modes of the app. */
enum class ThemeMode {

    @RequiresApi(Build.VERSION_CODES.P)
    SYSTEM,

    LIGHT,

    DARK;

    companion object {

        // SYSTEM theme is available on Android 9+
        val Default = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) SYSTEM else LIGHT

    }

}

/** CompositionLocal key to provide and consume the applied theme mode down the composition. */
val LocalThemeMode = staticCompositionLocalOf { ThemeMode.Default }

/** A function to check whether the currently applied [ThemeMode] is dark. */
@Composable
fun isInDarkTheme(): Boolean = when (LocalThemeMode.current) {
    ThemeMode.DARK -> true
    ThemeMode.LIGHT -> false
    else -> isSystemInDarkTheme()
}
  • Inside the application theme composable, replace the darkTheme: Boolean parameter with themeMode: ThemeMode, and provide a LocalThemeMode key using CompositionLocalProvider, and declare a darkTheme property:
@Composable
fun MyApplicationTheme(
    themeMode: ThemeMode = ThemeMode.Default,
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {

    CompositionLocalProvider(
        LocalThemeMode provides themeMode
    ) {

        val darkTheme = isInDarkTheme()
        val colorScheme = when {

            dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {

                val context = LocalContext.current

                if (darkTheme) dynamicDarkColorScheme(context)
                else dynamicLightColorScheme(context)

            }

            darkTheme -> DarkColorScheme
            else -> LightColorScheme

        }

        MaterialTheme(
            colorScheme = colorScheme,
            typography = Typography,
            content = content
        )

    }

}

Creating the Preferences DataStore:

  • This is an interface with the app preferences (this is just an example with preferences for the language and theme, feel free to adjust it to your needs):
interface AppPreferences {

    // Language

    suspend fun setLanguage(language: String)
    fun getLanguage(): Flow<String?>

    // Theme

    suspend fun setTheme(theme: ThemeMode)
    fun getTheme(): Flow<ThemeMode?>

}
  • This is an implementation class of the AppPreferences containing the logic for saving and reading the preferences:
class AppPreferencesImpl @Inject constructor(
    private val dataStore: DataStore<Preferences>
) : AppPreferences {

    companion object {
        private val LANGUAGE_KEY = stringPreferencesKey("language")
        private val THEME_KEY = stringPreferencesKey("theme")
    }

    // Language

    override suspend fun setLanguage(language: String) {
        dataStore.edit { it[LANGUAGE_KEY] = language }
    }

    override fun getLanguage(): Flow<String?> {
        return dataStore.data.map { it[LANGUAGE_KEY] }
    }

    // Theme

    override suspend fun setTheme(theme: ThemeMode) {
        dataStore.edit { it[THEME_KEY] = theme.name }
    }

    override fun getTheme(): Flow<ThemeMode?> {
        return dataStore.data.map {
            it[THEME_KEY]?.let { theme -> ThemeMode.valueOf(theme) }
        }
    }

}

Setting up the dependency injection:

  • To inject dependencies (in this case the app preferences) using DaggerHilt, it's required to have an application class annotated with @HiltAndroidApp which is also included as a android:name attribute in the app manifest application tag:
@HiltAndroidApp
class App : Application()
<application
    android:name=".App">

    <!---->

</application>
  • Another requirement is to have a module to provide the dependencies, such as AppPreferencesModule, which provides a single instance of the AppPreferences for injection:
@Module
@InstallIn(SingletonComponent::class)
object AppPreferencesModule {

    private val Context.dataStore by preferencesDataStore("appPreferences")

    @Provides
    @Singleton
    fun provideAppPreferences(@ApplicationContext context: Context): AppPreferences {
        return AppPreferencesImpl(context.dataStore)
    }

}

A ViewModel containing the states and callbacks:

  • The ViewModel has an injected instance of the AppPreferences which are transformed into states for a real-time observation by the UI:

  • The callbacks to change the preferences can be implemented in another ViewModel (e.g. of a Settings screen, which also has the AppPreferences instance injected).

@HiltViewModel
class MainActivityViewModel @Inject constructor(
    private val appPreferences: AppPreferences
) : ViewModel() {

    val languageState = appPreferences.getLanguage().filterNotNull().stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000L),
        initialValue = Locale.getDefault().toLanguageTag().uppercase()
    )

    val themeModeState = appPreferences.getTheme().filterNotNull().stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000L),
        initialValue = ThemeMode.Default
    )

    fun setLanguage(languageTag: String) {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                appPreferences.setLanguage(languageTag)
            }
        }
    }

    fun setTheme(themeMode: ThemeMode) {
        viewModelScope.launch {
            withContext(Dispatchers.IO) {
                appPreferences.setTheme(themeMode)
            }
        }
    }

}

Finally, the Activity implementation:

  • The activity is annotated with @AndroidEntryPoint which marks an Android component class to be setup for injection.

  • The setAppLanguage() function is executed inside the collected in a lifecycle manner languageState.

  • The themeModeState is collected in a lifecycle manner as well, and it's passed as an argument to the already adapted application theme composable.

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val viewModel by viewModels<MainActivityViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Set the application language.
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.languageState.collectLatest { setAppLanguage(it) }
            }
        }

        setContent {

            val themeMode by viewModel.themeModeState.collectAsStateWithLifecycle()

            MyApplicationTheme(
                themeMode = themeMode
            ) {

                // ...
            }

        }

    }

}

And there you have it, an example way of updating the Locale and changing the theme in Compose.

Feel free to reach out to me if you need further assistance.

Happy Coding!