{"id":13611362,"url":"https://github.com/ryanw-mobile/FusedUserPreferences","last_synced_at":"2025-04-13T04:34:07.039Z","repository":{"id":227695667,"uuid":"772026792","full_name":"ryanw-mobile/FusedUserPreferences","owner":"ryanw-mobile","description":"Template data source for both SharedPreferences and DataStore implementation","archived":false,"fork":false,"pushed_at":"2025-04-09T22:08:36.000Z","size":1671,"stargazers_count":2,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-09T23:20:47.963Z","etag":null,"topics":["android","android-datastore","jetpack-android","jetpack-compose","kotlin-android"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ryanw-mobile.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-03-14T11:54:29.000Z","updated_at":"2025-04-09T22:08:17.000Z","dependencies_parsed_at":"2024-03-22T19:34:38.901Z","dependency_job_id":"ac263a27-189b-4a79-aa66-9f7609b7939c","html_url":"https://github.com/ryanw-mobile/FusedUserPreferences","commit_stats":null,"previous_names":["ryanw-mobile/fuseduserpreferences"],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanw-mobile%2FFusedUserPreferences","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanw-mobile%2FFusedUserPreferences/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanw-mobile%2FFusedUserPreferences/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanw-mobile%2FFusedUserPreferences/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ryanw-mobile","download_url":"https://codeload.github.com/ryanw-mobile/FusedUserPreferences/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248664926,"owners_count":21142070,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","android-datastore","jetpack-android","jetpack-compose","kotlin-android"],"created_at":"2024-08-01T19:01:54.564Z","updated_at":"2025-04-13T04:34:06.645Z","avatar_url":"https://github.com/ryanw-mobile.png","language":"Kotlin","funding_links":[],"categories":["Kotlin"],"sub_categories":[],"readme":"# Fused User Preferences\u003cbr /\u003e\n![Gradle Build](https://github.com/ryanw-mobile/FusedUserPreferences/actions/workflows/main_build.yml/badge.svg) [![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com/)\n\n\u003cp\u003e\u003cimg src=\"hero.jpg\" style=\"width: 100%; max-width: 1000px; height: auto;\" alt=\"cover image\" style=\"width: 100%; max-width: 1000px; height: auto;\"\u003e\u003c/p\u003e\n\nThis is an experimental project that tries to put the legacy SharedPreferences and the new Jetpack\nPreferences Datastore side by side by means of dependency inversion to see how it would look if it\nwere to provide the same preferences storage at the data layer.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"Screenshot_1.png\" width=\"200\" /\u003e\n  \u003cimg src=\"Screenshot_2.png\" width=\"200\" /\u003e\n\u003c/p\u003e\n\n## Background\n\nIn my first few Android apps, which date back to 2010, we did not have any architecture to follow.\nWe did not have fragments, so we mostly allocated an activity class for each screen. There, if we\nwanted to deal with user preferences, it was so easy that we could have placed the code under\nonResume or even onCreated. SharedPreferences is non-blocking, so it works quickly and simply for\nmost small use cases when it does not break.\n\n- Later, people suggested that SharedPreferences being synchronous can be a problem. That is\n  sensible when developers abuse SharedPreferences by storing a massive amount of key pairs.\n- Later, people came up with more different architectures, so we are not simply accessing user\n  preferences right from the activity class.\n\nEventually, if we want to access user preferences, we can have many more boilerplate codes before\nexecuting the few lines that do the work.\n\nNow we have Jetpack Preference Datastore. It is asynchronous, which means that when we want to\nretrieve user preferences, the result is not immediately available. Up to this moment, if we want to\nobserve preference changes, there is a known limitation: we are not being told which key pair has\nchanged. We know only _something_ has changed, so we probably have to propagate _all_ the keys we\nare interested in, even if we know that only _one_ has changed.\n\n## The minimum code to make SharedPreferences work\n\n```\nprivate val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)\n\nprivate val onPreferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPref, key -\u003e\n    when (key) {\n        prefKeyString -\u003e {\n            _stringPreference.value = sharedPref.getString(prefKeyString, null) ?: stringPreferenceDefault\n        }\n\n        prefKeyBoolean -\u003e {\n            _booleanPreference.value = sharedPref.getBoolean(prefKeyBoolean, booleanPreferenceDefault)\n        }\n    }\n}\n\ninit {\n    // If we want to keep track of the changes\n    sharedPref.registerOnSharedPreferenceChangeListener(onPreferenceChangeListener)\n}\n\nfun getStringPreference() = sharedPref.getString(prefKeyString, null) ?: \"default-value\"\n\nfun updateStringPreference(newValue: String) {\n  try {\n      sharedPref.edit()\n          .putString(prefKeyString, newValue)\n          .apply()\n  } catch (e: Throwable) {\n      // Not likely to produce exception though\n  }\n}\n\n```\n\n## The minimum code to make Jetpack Preferences Data Store work\n\n```\nprivate val Context.dataStore: DataStore\u003cPreferences\u003e by preferencesDataStore(name = \"preferences\")\nprivate val prefKeyStrings:Preferences.Key\u003cString\u003e = stringPreferencesKey(\"some-key-name\")\n    \ninit {\n    // assume caller passing in Context.dataStore as DataStore\u003cPreferences\u003e\n    externalCoroutineScope.launch(dispatcher) {\n        dataStore.data.catch { exception -\u003e\n            _preferenceErrors.emit(exception)\n        }\n            .collect { prefs -\u003e\n                // or use map \n                _stringPreference.value = prefs[prefKeyString] ?: stringPreferenceDefault\n                _booleanPreference.value = prefs[prefKeyBoolean] ?: booleanPreferenceDefault\n                _intPreference.value = prefs[prefKeyInt] ?: intPreferenceDefault\n            }\n    }\n}\n\nsuspend fun updateStringPreference(newValue: String) {\n    withContext(dispatcher) {\n        try {\n            dataStore.edit { mutablePreferences -\u003e\n                mutablePreferences[prefKeyString] = newValue\n            }\n        } catch (e: Throwable) {\n            _preferenceErrors.emit(e)\n        }\n    }\n}\n```\n\n## Approach\n\nWhether for SharedPreferences or Jetpack Preferences Datastore, even if the core is about 10 lines\nof code, this code project tries to put them in the right place when following the MVVM and Clean\narchitecture. That means the UI will talk to the ViewModel, which will then connect to a repository\nthat invisibly talks to either the SharedPreferences or the Jetpack Preferences Datastore data\nstore. Dependency inversion with Dagger Hilt allows injecting different data sources (\nSharedPreferences and Jetpack Preferences Data Store) into the same repository. Usually in\nproduction apps it is not likely that we have a need to use both sources interchangeably.\n\n## Let's download and run it!\n\nThis project was configured to build using Android Studio Iguana | 2023.2.1. You will need to have\nJava 17 to build the project.\n\nAlternatively, you can find the ready-to-install APKs and App Bundles under\nthe [release section](https://github.com/ryanw-mobile/FusedUserPreferences/releases).\n\n## Technical details\n\n### Dependencies\n\n* [JUnit](https://junit.org/junit5/) - EPL 2.0 - A simple framework to write repeatable tests\n* [AndroidX Test Ext JUnit](https://developer.android.com/jetpack/androidx/releases/test) - Apache 2.0 - Extensions for Android testing\n* [AndroidX Espresso](https://developer.android.com/training/testing/espresso) - Apache 2.0 - UI testing framework\n* [AndroidX Lifecycle](https://developer.android.com/jetpack/androidx/releases/lifecycle) - Apache 2.0 - Lifecycles-aware components\n* [AndroidX Activity Compose](https://developer.android.com/jetpack/androidx/releases/activity) - Apache 2.0 - Jetpack Compose integration with Activity\n* [Jetpack Compose BOM](https://developer.android.com/jetpack/compose/bom) - Apache 2.0 - Bill of Materials for Jetpack Compose\n* [AndroidX Core KTX](https://developer.android.com/jetpack/androidx/releases/core) - Apache 2.0 - Extensions to Java APIs for Android development\n* [AndroidX Compose UI](https://developer.android.com/jetpack/androidx/releases/compose-ui) - Apache 2.0 - UI components for Jetpack Compose\n* [AndroidX Material3](https://developer.android.com/jetpack/androidx/releases/compose-material3) - Apache 2.0 - Material Design components for Jetpack Compose\n* [JUnit Vintage Engine](https://junit.org/junit5/docs/current/user-guide/#vintage) - EPL 2.0 - Support for running JUnit 3 and JUnit 4 tests\n* [Kotlinx Coroutines Test](https://github.com/Kotlin/kotlinx.coroutines) - Apache 2.0 - Testing libraries for Kotlin coroutines\n* [AndroidX DataStore Preferences](https://developer.android.com/jetpack/androidx/releases/datastore) - Apache 2.0 - Data storage solution\n* [MockK](https://mockk.io/) - Apache 2.0 - Mocking library for Kotlin\n* [Robolectric](http://robolectric.org/) - MIT - A framework that brings fast, reliable unit tests to Android\n* [Timber](https://github.com/JakeWharton/timber) - Apache 2.0 - A logger with a small, extensible API\n* [Hilt](https://dagger.dev/hilt/) - Apache 2.0 - A dependency injection library for Android that reduces the boilerplate of doing manual dependency injection\n\n### Plugins\n\n* [Android Application Plugin](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration) - Google - Plugin for building Android applications\n* [Jetbrains Kotlin Android Plugin](https://kotlinlang.org/docs/gradle.html) - JetBrains - Plugin for Kotlin Android projects\n* [Compose Compiler Plugin](https://developer.android.com/jetpack/compose) - JetBrains - Plugin for Jetpack Compose\n* [Ktlint Plugin](https://github.com/JLLeitschuh/ktlint-gradle) - JLLeitschuh - Plugin for Kotlin linter\n* [Hilt Android Plugin](https://dagger.dev/hilt/gradle-setup.html) - Google - Plugin for Hilt dependency injection\n* [Google DevTools KSP](https://github.com/google/ksp) - Google - Kotlin Symbol Processing API plugin\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanw-mobile%2FFusedUserPreferences","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fryanw-mobile%2FFusedUserPreferences","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanw-mobile%2FFusedUserPreferences/lists"}