I have switched my app from using SharedPreferences to PreferencesDataStore. I have also implemented a dark mode and several themes inside my app. For theming, I basically rely in each activity on this code:
val themePreferences = getSharedPreferences("THEME_PREFERENCES", MODE_PRIVATE)
val settingsPreferences = getSharedPreferences("SETTINGS_PREFERENCES", MODE_PRIVATE)
darkMode = settingsPreferences.getBoolean("darkMode", false)
setTheme(when (darkMode) {
true -> themePreferences.getInt("themeDark", R.style.AppThemeDark)
false -> themePreferences.getInt("themeLight", R.style.AppThemeLight)
})
The selected theme gets stored as an Integer, each once for the light mode as well as the dark mode. Now, I also want to adopt this section of my code. I get my darkMode boolean from my dataStore repository like this:
viewModel.storedDarkMode.observe(this) { darkMode = it }
If I work inside the observe(this) { ... } of the LiveData object it won't work.
Now, how can I change my code snippet shown above to PreferencesDataStore? Or is it actually better e.g. to make a separate class in order to observe the values from there? If yes, how could something like that look like? Or do you know some good example code following Android Architecture including custom themes with a dark mode where I can look at?
I am still learning a lot, any help for better understanding this is much appreciated!
Best regards, Markus
Edit:
runBlocking {
val darkMode = viewModel.darkModeFlow.first()
setTheme(when (darkMode) {
true -> viewModel.themeDarkFlow.first()
false -> viewModel.themeLightFlow.first()
})
}
I don't know if it is best practice or not, but I use runBlocking to get theme data from dataStore. It is always recommended that, we should never use runBlocking in our production code.
val preferences = runBlocking {
mainActivityViewModel.preferencesFlow.first()
}
setAppTheme(preferences.currentTheme)
binding = ActivityMainBinding.inflate(layoutInflater)
In setAppTheme method
private fun setAppTheme(theme: Boolean) {
mainActivityViewModel.darkMode = theme
//set your theme here
}
Now observe preferencesFlow for any change in theme value, and if the theme is changed then recreate()
mainActivityViewModel.preferencesLiveData.observe(this) {
if (it.currentTheme != mainActivityViewModel.selectedTheme) {
recreate()
}
}
As we can't load our UI, without getting the theme. It seemed right to use runBlocking.
Related
I am using a file picker inside a HorizontalPager in jetpack compose. When the corresponding screen is loaded while tapping the button, the launcher is triggered 2 times.
Code snippet
var openFileManager by remember {
mutableStateOf(false)
}
if (openFileManager) {
launcher.launch("*/*")
}
Button(text = "Upload",
onClick = {
openFileManager = true
})
Edited: First of all Ian's point is valid why not just launch it in the onClick directly? I also assumed that maybe you want to do something more with your true false value. If you want nothing but launch then all these are useless.
The screen can draw multiple times when you click and make openFileManager true so using only condition won't prevent it from calling multiple times.
You can wrap your code with LaunchedEffect with openFileManager as a key. The LaunchedEffect block will run only when your openFileManager change.
if (openFileManager) {
LaunchedEffect(openFileManager) {
launcher.launch("*/*")
}
}
You should NEVER store such important state inside a #Composable. Such important business logic is meant to be stored in a more robust holder like the ViewModel.
ViewModel{
var launch by mutableStateOf (false)
private set
fun updateLaunchValue(newValue: Boolean){
launch = newValue
}
}
Pass these to the Composable from the main activity
MyComposable(
launchValue = viewModel.launch
updateLaunchValue = viewModel::updateLaunchValue
)
Create the parameters in the Composable as necessary
#Comoosable
fun Uploader(launchValue: Boolean, onUpdateLaunchValue: (Boolean) -> Unit){
LaunchedEffect (launchValue){
if (launchValue)
launcher.launch(...)
}
Button { // This is onClick
onUpdateLaunchValue(true) // makes the value true in the vm, updating state
}
}
If you think it is overcomplicated, you're in the wrong paradigm. This is the recommended AND CORRECT way of handling state in Compose, or any declarative paradigm, really afaik. This keeps the code clean, while completely separating UI and data layers, allowing controlled interaction between UI and state to achieve just the perfect behaviour for the app.
In Jetpack Compose can I use state hoisting and pass down state / setState on multiple composables down the hierarchy (with different packages) ?
I have the following issue:
#Composable
fun JetpackComposeApp() {
var theme by rememberSaveable {
mutableStateOf(ThemeState(isDarkTheme = isDarkTheme))
}
...
NavHost(...) {
composable(route = MainNavRoutes.Settings) {
SettingsScreen(hiltViewModel(it), theme) { newTheme ->
theme = newTheme
}
}
}
}
The above works. SettingsScreen updates whole theme correctly.
But if I refactor Navhost to a different package with arguments (theme, setTheme), setter works but SettingsScreen doesn't get the updated theme. Theme is always the old one.
Is state hoisting limited to something ? Like package / composables optimization ?
Or I am missing something here ?
The above can be found in repo: link, files: MainActivity, SettingsScreen.
updating/observing state value should work even with deep call stacks, if I understand correctly your problem you should wrap setTheme in a rememberUpdatedState() within your SettingsScreen and access/hoist this updated value
I had previously replaced SharedPreferences in my app with the new DataStore, as recommended by Google in the docs, to reap some of the obvious benefits. Then it came time to add a settings screen, and I found the Preferences Library. The confusion came when I saw the library uses SharedPreferences by default with no option to switch to DataStore. You can use setPreferenceDataStore to provide a custom storage implementation, but DataStore does not implement the PreferenceDataStore interface, leaving it up to the developer. And yes this naming is also extremely confusing. I became more confused when I found no articles or questions talking about using DataStore with the Preferences Library, so I feel like I'm missing something. Are people using both of these storage solutions side by side? Or one or the other? If I were to implement PreferenceDataStore in DataStore, are there any gotchas/pitfalls I should be looking out for?
For anyone reading the question and thinking about the setPreferenceDataStore-solution. Implementing your own PreferencesDataStore with the DataStore instead of SharedPreferences is straight forward at a glance.
class SettingsDataStore(private val dataStore: DataStore<Preferences>): PreferenceDataStore() {
override fun putString(key: String, value: String?) {
CoroutineScope(Dispatchers.IO).launch {
dataStore.edit { it[stringPreferencesKey(key)] = value!! }
}
}
override fun getString(key: String, defValue: String?): String {
return runBlocking { dataStore.data.map { it[stringPreferencesKey(key)] ?: defValue!! }.first() }
}
...
}
And then setting the datastore in your fragment
#AndroidEntryPoint
class AppSettingsFragment : PreferenceFragmentCompat() {
#Inject
lateinit var dataStore: DataStore<Preferences>
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.preferenceDataStore = SettingsDataStore(dataStore)
setPreferencesFromResource(R.xml.app_preferences, rootKey)
}
}
But there are a few issues with this solution. According to the documentation runBlocking with first() to synchronously read values is the preferred way, but should be used with caution.
Make sure to setpreferenceDataStore before calling setPreferencesFromResource to avoid loading issues where the default implementation (sharedPreferences) will be used for initial loading.
A couple weeks ago on my initial try to implement the PreferenceDataStore, I had troubles with type long keys. My settings screen was correctly showing and saving numeric values for an EditTextPreference but the flows did not emit any values for these keys. There might be an issue with EditTextPreference saving numbers as strings because setting an inputType in the xml seems to have no effect (at least not on the input keyboard). While saving numbers as strings might work, this also requires reading numbers as strings. Therefore you lose the type-safety for primitive types.
Maybe with one or two updates on the settings and datastore libs there might be an official working solution for this case.
I have run into the same issue using DataStore. Not only does DataStore not implement PreferenceDataStore, but I believe it is impossible to write an adapter to bridge the two, because the DataStore uses Kotlin Flows and is asynchronous, whereas PreferenceDataStore assumes that both get and put operations to be synchronous.
My solution to this is to write the preference screen manually using a recycler view. Fortunately, ConcatAdapter made it much easier, as I can basically create one adapter for each preference item, and then combine them into one adapter using ConcatAdapter.
What I ended up with is a PreferenceItemAdapter that has mutable title, summary, visible, and enabled properties that mimics the behavior of the preference library, and also a Jetpack Compose inspired API that looks like this:
preferenceGroup {
preference {
title("Name")
summary(datastore.data.map { it.name })
onClick {
showDialog {
val text = editText(datastore.data.first().name)
negativeButton()
positiveButton()
.onEach { dataStore.edit { settings -> settings.name = text.first } }.launchIn(lifecycleScope)
}
}
}
preference {
title("Date")
summary(datastore.data.map { it.parsedDate?.format(dateFormatter) ?: "Not configured" })
onClick {
showDatePickerDialog(datastore.data.first().parsedDate ?: LocalDate.now()) { newDate ->
lifecycleScope.launch { dataStore.edit { settings -> settings.date = newDate } }
}
}
}
}
There is more manual code in this approach, but I find it easier than trying to bend the preference library to my will, and gives me the flexibility I needed for my project (which also stores some of the preferences in Firebase).
I'll add my own strategy I went with for working around the incompatibility in case it's useful to some:
I stuck with the preference library and added android:persistent="false" to all my editable preferences so they wouldn't use SharedPreferences at all. Then I was free to just save and load the preference values reactively. Storing them through click/change listeners → view model → repository, and reflecting them back with observers.
Definitely messier than a good custom solution, but it worked well for my small app.
I'm working on an Android app and I want to implement the MVVM pattern, which is pretty much the standard pushed by Google, however, I'd like to avoid using Android Data Bindings library if possible, since I hate autogenerated XML magic.
I've tried to implement something essentially akin to databinding in RxJava (Kotlin) using Jake Wharton's data binding library, plus some helpful extension methods.
My question is, is this the right way to go about things? Is this good enough to use in production? Are there potential problems I'm not seeing with this approach that will pop up later?
Essentially, I've implemented it like this:
I have a MvvmFragment (there is a similar class for activities) which takes care of setting up and managing the lifecycle of a CompositeDisposable object.
Then, in my ViewModel (part of the android Arch ViewModel package) I have all of the fields that will be bound to declared like this:
var displayName = BindableVar("")
var email = BindableVar("")
var signInProvider = BindableVar<AuthProvider>(defaultValue = AuthProvider.PASSWORD)
(Side note - Since Rx doesn't allow null values, I'm not sure how to handle the case of defaults for objects where the concept of a default doesn't really make sense, such as the AuthProvider above)
The BindableVar class is implemented like this:
class BindableVar<T>(defaultValue: T) {
var value: T = defaultValue
set(value) {
field = value
observable.onNext(value)
}
var observable = BehaviorSubject.createDefault(value)!!
}
Using Jake Wharton's RxBindings library, I have created some helpful extension methods on top of that, such as:
fun Disposable.addTo(compositeDisposable: CompositeDisposable): Disposable {
compositeDisposable.add(this)
return this
}
fun TextView.bindTextTo(string: BindableVar<String>): Disposable {
return string.observable.subscribe(this.text())
}
fun View.bindVisibilityTo(visibility: Int) {
// ... not shown
}
fun ImageView.bindImageUriTo(
src: BindableVar<Uri>, #DrawableRes placeholder: Int? = null
): Disposable {
return if (placeholder == null) {
src.observable.subscribe {
GlideApp.with(context).load(it).into(this)
}
} else {
src.observable.subscribe {
GlideApp.with(context).load(it).placeholder(placeholder).into(this)
}
}
}
Using these extension methods, I then obtain the ViewModel instance on Fragment initialization, and call a method initBindings(), which looks something like this:
item_display_name_value.bindTextTo(viewModel.displayName).addTo(bindings)
item_email_address_value.bindTextTo(viewModel.email).addTo(bindings)
item_profile_picture_view.bindImageUrlTo(viewModel.avatarUrl).addTo(bindings)
I want to avoid getting a week into fleshing out this architecture and then suddenly realizing there is some critical problem that can't be solved easily, or some other hidden gotcha. Should I just go with XML based data binding? I've heard a lot of complaints about the difficulty of unit-testing it, and the difficulty of reusing code with it.
Based on my theme I use different layouts in my app, so I cant use DataBinding in my use case because this would clutter my whole code with functions like following:
fun getCVAnswer() : CardView {
return when (ThemeManager.themeStyle()) {
ThemeManager.Style.Modern -> return (binding as MviGameActivityModern).cvAnswer1
ThemeManager.Style.Round -> return (binding as MviGameActivityRound).cvAnswer1
...
}
}
So I want to use an alternative solution, all my styles have the same layouts and same ids, so I can do following that works in all styles:
private val cvAnswer1 by lazy { findViewById<CardView>(R.id.cvAnswer1) }
Question
After screen rotation, I will leak the activity and cvAnswer1 has the wrong view in it (the one from my activity before rotation). Any ideas how to solve this?