@@ -27,11 +27,28 @@ import kotlinx.coroutines.flow.firstOrNull
2727import kotlinx.coroutines.runBlocking
2828
2929/* *
30- * Don't use this unless you're bridging Java code Use your own [dataStore] directly in Kotlin code,
31- * or if you're writing new code.
30+ * Wrapper around [DataStore] for easier migration from `SharedPreferences` in Java code.
31+ *
32+ * Automatically migrates data from any `SharedPreferences` that share the same context and name.
33+ *
34+ * There should only ever be _one_ instance of this class per context and name variant.
35+ *
36+ * > Do **NOT** use this _unless_ you're bridging Java code. If you're writing new code, or your >
37+ * code is in Kotlin, then you should create your own singleton that uses [DataStore] directly.
38+ *
39+ * Example:
40+ * ```java
41+ * DataStorage heartBeatStorage = new DataStorage(applicationContext, "FirebaseHeartBeat");
42+ * ```
43+ *
44+ * @property context The [Context] that this data will be saved under.
45+ * @property name What the storage file should be named.
3246 */
3347class DataStorage (val context : Context , val name : String ) {
34- private val transforming = ThreadLocal <Boolean >()
48+ /* *
49+ * Used to ensure that there's only ever one call to [editSync] per thread; as to avoid deadlocks.
50+ */
51+ private val editLock = ThreadLocal <Boolean >()
3552
3653 private val Context .dataStore: DataStore <Preferences > by
3754 preferencesDataStore(
@@ -41,26 +58,134 @@ class DataStorage(val context: Context, val name: String) {
4158
4259 private val dataStore = context.dataStore
4360
61+ /* *
62+ * Get data from the datastore _synchronously_.
63+ *
64+ * Note that if the key is _not_ in the datastore, while the [defaultValue] will be returned
65+ * instead- it will **not** be saved to the datastore; you'll have to manually do that.
66+ *
67+ * Blocks on the currently running thread.
68+ *
69+ * Example:
70+ * ```java
71+ * Preferences.Key<Long> fireCountKey = PreferencesKeys.longKey("fire-count");
72+ * assert dataStore.get(fireCountKey, 0L) == 0L;
73+ *
74+ * dataStore.putSync(fireCountKey, 102L);
75+ * assert dataStore.get(fireCountKey, 0L) == 102L;
76+ * ```
77+ *
78+ * @param key The typed key of the entry to get data for.
79+ * @param defaultValue A value to default to, if the key isn't found.
80+ *
81+ * @see Preferences.getOrDefault
82+ */
4483 fun <T > getSync (key : Preferences .Key <T >, defaultValue : T ): T = runBlocking {
4584 dataStore.data.firstOrNull()?.get(key) ? : defaultValue
4685 }
4786
87+ /* *
88+ * Checks if a key is present in the datastore _synchronously_.
89+ *
90+ * Blocks on the currently running thread.
91+ *
92+ * Example:
93+ * ```java
94+ * Preferences.Key<Long> fireCountKey = PreferencesKeys.longKey("fire-count");
95+ * assert !dataStore.contains(fireCountKey);
96+ *
97+ * dataStore.putSync(fireCountKey, 102L);
98+ * assert dataStore.contains(fireCountKey);
99+ * ```
100+ *
101+ * @param key The typed key of the entry to find.
102+ */
48103 fun <T > contains (key : Preferences .Key <T >): Boolean = runBlocking {
49104 dataStore.data.firstOrNull()?.contains(key) ? : false
50105 }
51106
107+ /* *
108+ * Sets and saves data in the datastore _synchronously_.
109+ *
110+ * Existing values will be overwritten.
111+ *
112+ * Blocks on the currently running thread.
113+ *
114+ * Example:
115+ * ```java
116+ * dataStore.putSync(PreferencesKeys.longKey("fire-count"), 102L);
117+ * ```
118+ *
119+ * @param key The typed key of the entry to save the data under.
120+ * @param value The data to save.
121+ *
122+ * @return The [Preferences] object that the data was saved under.
123+ */
52124 fun <T > putSync (key : Preferences .Key <T >, value : T ): Preferences = runBlocking {
53125 dataStore.edit { it[key] = value }
54126 }
55127
56- /* * Do not modify the returned map (should be obvious, since it's immutable though) */
128+ /* *
129+ * Gets all data in the datastore _synchronously_.
130+ *
131+ * Blocks on the currently running thread.
132+ *
133+ * Example:
134+ * ```java
135+ * ArrayList<String> allDates = new ArrayList<>();
136+ *
137+ * for (Map.Entry<Preferences.Key<?>, Object> entry : dataStore.getAllSync().entrySet()) {
138+ * if (entry.getValue() instanceof Set) {
139+ * Set<String> dates = new HashSet<>((Set<String>) entry.getValue());
140+ * if (!dates.isEmpty()) {
141+ * allDates.add(new ArrayList<>(dates));
142+ * }
143+ * }
144+ * }
145+ * ```
146+ *
147+ * @return An _immutable_ map of data currently present in the datastore.
148+ */
57149 fun getAllSync (): Map <Preferences .Key <* >, Any> = runBlocking {
58150 dataStore.data.firstOrNull()?.asMap() ? : emptyMap()
59151 }
60152
61- /* * Edit calls should not be called within edit calls. Is there a way to prevent this? */
153+ /* *
154+ * Transactionally edit data in the datastore _synchronously_.
155+ *
156+ * Edits made within the [transform] callback will be saved (committed) all at once once the
157+ * [transform] block exits.
158+ *
159+ * Because of the blocking nature of this function, you should _never_ call [editSync] within an
160+ * already running [transform] block. Since this can cause a deadlock, [editSync] will instead
161+ * throw an exception if it's caught.
162+ *
163+ * Blocks on the currently running thread.
164+ *
165+ * Example:
166+ * ```java
167+ * dataStore.editSync((pref) -> {
168+ * Long heartBeatCount = pref.get(HEART_BEAT_COUNT_TAG);
169+ * if (heartBeatCount == null || heartBeatCount > 30) {
170+ * heartBeatCount = 0L;
171+ * }
172+ * pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount);
173+ * pref.set(LAST_STORED_DATE, "1970-0-1");
174+ *
175+ * return null;
176+ * });
177+ * ```
178+ *
179+ * @param transform A callback to invoke with the [MutablePreferences] object.
180+ *
181+ * @return The [Preferences] object that the data was saved under.
182+ * @throws IllegalStateException If you attempt to call [editSync] within another [transform]
183+ * block.
184+ *
185+ * @see Preferences.getOrDefault
186+ */
62187 fun editSync (transform : (MutablePreferences ) -> Unit ): Preferences = runBlocking {
63- if (transforming .get() == true ) {
188+ if (editLock .get() == true ) {
64189 throw IllegalStateException (
65190 """
66191 Don't call DataStorage.edit() from within an existing edit() callback.
@@ -70,15 +195,35 @@ class DataStorage(val context: Context, val name: String) {
70195 .trimIndent()
71196 )
72197 }
73- transforming .set(true )
198+ editLock .set(true )
74199 try {
75200 dataStore.edit { transform(it) }
76201 } finally {
77- transforming .set(false )
202+ editLock .set(false )
78203 }
79204 }
80205}
81206
82- /* * Helper for Java code */
207+ /* *
208+ * Helper method for getting the value out of a [Preferences] object if it exists, else falling back
209+ * to the default value.
210+ *
211+ * This is primarily useful when working with an instance of [MutablePreferences]
212+ * - like when working within an [DataStorage.editSync] callback.
213+ *
214+ * Example:
215+ * ```java
216+ * dataStore.editSync((pref) -> {
217+ * long heartBeatCount = DataStoreKt.getOrDefault(pref, HEART_BEAT_COUNT_TAG, 0L);
218+ * heartBeatCount+=1;
219+ * pref.set(HEART_BEAT_COUNT_TAG, heartBeatCount);
220+ *
221+ * return null;
222+ * });
223+ * ```
224+ *
225+ * @param key The typed key of the entry to get data for.
226+ * @param defaultValue A value to default to, if the key isn't found.
227+ */
83228fun <T > Preferences.getOrDefault (key : Preferences .Key <T >, defaultValue : T ) =
84229 get(key) ? : defaultValue
0 commit comments