Giter Club home page Giter Club logo

fuseduserpreferences's Introduction

Fused User Preferences

cover image

This is an experimental project that tries to put the legacy SharedPreferences and the new Jetpack Preferences Datastore side by side by means of dependency inversion to see how it would look if it were to provide the same preferences storage at the data layer.

Background

In my first few Android apps, which date back to 2010, we did not have any architecture to follow. We did not have fragments, so we mostly allocated an activity class for each screen. There, if we wanted to deal with user preferences, it was so easy that we could have placed the code under onResume or even onCreated. SharedPreferences is non-blocking, so it works quickly and simply for most small use cases when it does not break.

  • Later, people suggested that SharedPreferences being synchronous can be a problem. That is sensible when developers abuse SharedPreferences by storing a massive amount of key pairs.
  • Later, people came up with more different architectures, so we are not simply accessing user preferences right from the activity class.

Eventually, if we want to access user preferences, we can have many more boilerplate codes before executing the few lines that do the work.

Now we have Jetpack Preference Datastore. It is asynchronous, which means that when we want to retrieve user preferences, the result is not immediately available. Up to this moment, if we want to observe preference changes, there is a known limitation: we are not being told which key pair has changed. We know only something has changed, so we probably have to propagate all the keys we are interested in, even if we know that only one has changed.

The minimum code to make SharedPreferences work

private val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)

private val onPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPref, key ->
    when (key) {
        prefKeyString -> {
            _stringPreference.value = sharedPref.getString(prefKeyString, null) ?: stringPreferenceDefault
        }

        prefKeyBoolean -> {
            _booleanPreference.value = sharedPref.getBoolean(prefKeyBoolean, booleanPreferenceDefault)
        }
    }
}

init {
    // If we want to keep track of the changes
    sharedPref.registerOnSharedPreferenceChangeListener(onPreferenceChangeListener)
}

fun getStringPreference() = sharedPref.getString(prefKeyString, null) ?: "default-value"

fun updateStringPreference(newValue: String) {
  try {
      sharedPref.edit()
          .putString(prefKeyString, newValue)
          .apply()
  } catch (e: Throwable) {
      // Not likely to produce exception though
  }
}

The minimum code to make Jetpack Preferences Data Store work

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "preferences")
private val prefKeyStrings:Preferences.Key<String> = stringPreferencesKey("some-key-name")
    
init {
    // assume caller passing in Context.dataStore as DataStore<Preferences>
    externalCoroutineScope.launch(dispatcher) {
        dataStore.data.catch { exception ->
            _preferenceErrors.emit(exception)
        }
            .collect { prefs ->
                // or use map 
                _stringPreference.value = prefs[prefKeyString] ?: stringPreferenceDefault
                _booleanPreference.value = prefs[prefKeyBoolean] ?: booleanPreferenceDefault
                _intPreference.value = prefs[prefKeyInt] ?: intPreferenceDefault
            }
    }
}

suspend fun updateStringPreference(newValue: String) {
    withContext(dispatcher) {
        try {
            dataStore.edit { mutablePreferences ->
                mutablePreferences[prefKeyString] = newValue
            }
        } catch (e: Throwable) {
            _preferenceErrors.emit(e)
        }
    }
}

Approach

Whether for SharedPreferences or Jetpack Preferences Datastore, even if the core is about 10 lines of code, this code project tries to put them in the right place when following the MVVM and Clean architecture. That means the UI will talk to the ViewModel, which will then connect to a repository that invisibly talks to either the SharedPreferences or the Jetpack Preferences Datastore data store. Dependency inversion with Dagger Hilt allows injecting different data sources ( SharedPreferences and Jetpack Preferences Data Store) into the same repository. Usually in production apps it is not likely that we have a need to use both sources interchangeably.

Let's download and run it!

This project was configured to build using Android Studio Iguana | 2023.2.1. You will need to have Java 17 to build the project.

Alternatively, you can find the ready-to-install APKs and App Bundles under the release section.

Technical details

Dependencies

Plugins

fuseduserpreferences's People

Contributors

ryanw-mobile avatar renovate[bot] avatar

Stargazers

jherrero avatar

Watchers

 avatar

fuseduserpreferences's Issues

ditch data source interface

In shared preferences concrete implementation, it does not return errors, nor does it require coroutines to run.

Interface at repository level makes sense as we need dependency inversion.

That means we are splitting the repository into two, and our data source won't share the same interface.

It should be sensible to tightly couple a repository implementation to a particular data source while the repository is loosely coupled to the viewmodel/use case.

If at any time in the future we need to unit test repository, we will mock the data source to avoid the redundant data source interface.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/dependency-submission.yml
  • actions/checkout v4
  • actions/setup-java v4
  • gradle/actions v3
.github/workflows/main_build.yml
  • timheuer/base64-to-file v1.2
  • actions/checkout v4
  • actions/setup-java v4
.github/workflows/renovate_check.yml
  • timheuer/base64-to-file v1.2
  • actions/checkout v4
  • actions/setup-java v4
.github/workflows/tag_create_release.yml
  • timheuer/base64-to-file v1.2
  • actions/checkout v4
  • actions/setup-java v4
  • actions/create-release v1
  • actions/upload-release-asset v1
  • actions/upload-release-asset v1
gradle
gradle.properties
settings.gradle.kts
build.gradle.kts
app/build.gradle.kts
gradle/libs.versions.toml
  • junit:junit 4.13.2
  • androidx.test.ext:junit 1.1.5
  • androidx.test.espresso:espresso-core 3.5.1
  • androidx.lifecycle:lifecycle-runtime-ktx 2.7.0
  • androidx.lifecycle:lifecycle-runtime-compose 2.7.0
  • androidx.activity:activity-compose 1.9.0
  • androidx.compose:compose-bom 2024.05.00
  • androidx.test.ext:junit-ktx 1.1.5
  • androidx.core:core-ktx 1.13.1
  • org.junit.vintage:junit-vintage-engine 5.10.2
  • org.jetbrains.kotlinx:kotlinx-coroutines-test 1.8.1
  • androidx.datastore:datastore-preferences 1.1.1
  • io.mockk:mockk 1.13.11
  • io.mockk:mockk-agent-jvm 1.13.11
  • io.mockk:mockk-android 1.13.11
  • org.robolectric:robolectric 4.12.2
  • com.jakewharton.timber:timber 5.0.1
  • com.google.dagger:hilt-android 2.51.1
  • com.google.dagger:hilt-compiler 2.51.1
  • com.google.dagger:hilt-android-compiler 2.51.1
  • com.google.dagger:hilt-android-testing 2.51.1
  • androidx.hilt:hilt-navigation-compose 1.2.0
  • com.android.application 8.4.2
  • org.jetbrains.kotlin.android 2.0.0
  • org.jetbrains.kotlin.plugin.compose 2.0.0
  • org.jlleitschuh.gradle.ktlint 12.1.1
  • com.google.dagger.hilt.android 2.51.1
  • com.google.devtools.ksp 2.0.0-1.0.22
gradle-wrapper
gradle/wrapper/gradle-wrapper.properties
  • gradle 8.8

  • Check this box to trigger a request for Renovate to run again on this repository

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.