Skip to content

Commit 5d2e64f

Browse files
author
Krzysztof Borowy
committed
implement storage functionality
Functionality to persist data, new example, error handling
1 parent 37db93e commit 5d2e64f

File tree

7 files changed

+328
-21
lines changed

7 files changed

+328
-21
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.reactnativecommunity.asyncstorage.next
2+
3+
import com.facebook.react.bridge.Arguments
4+
import com.facebook.react.bridge.ReadableArray
5+
6+
fun ReadableArray.toEntryList(): List<Entry> {
7+
val list = mutableListOf<Entry>()
8+
for (keyValue in this.toArrayList()) {
9+
if (keyValue !is ArrayList<*>) {
10+
throw AsyncStorageError.invalidKeyValueFormat()
11+
}
12+
val key = keyValue[0]
13+
val value = keyValue[1]
14+
15+
if (key == null || key !is String) {
16+
throw AsyncStorageError.keyIsNull()
17+
}
18+
19+
if (value !is String) {
20+
throw AsyncStorageError.valueNotString(key)
21+
}
22+
23+
list.add(Entry(key, value))
24+
}
25+
return list
26+
}
27+
28+
fun ReadableArray.toKeyList(): List<String> {
29+
val list = this.toArrayList().toList()
30+
31+
for (item in list) {
32+
if (item !is String) {
33+
throw AsyncStorageError.keyIsNull()
34+
}
35+
}
36+
return list as List<String>
37+
}
38+
39+
fun List<Entry>.toKeyValueArgument(): ReadableArray {
40+
val args = Arguments.createArray()
41+
42+
for (entry in this) {
43+
val keyValue = Arguments.createArray()
44+
keyValue.pushString(entry.key)
45+
keyValue.pushString(entry.value)
46+
args.pushArray(keyValue)
47+
}
48+
49+
return args
50+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.reactnativecommunity.asyncstorage.next
2+
3+
import com.facebook.react.bridge.Arguments
4+
import com.facebook.react.bridge.Callback
5+
import kotlinx.coroutines.CoroutineExceptionHandler
6+
7+
internal fun createExceptionHandler(cb: Callback): CoroutineExceptionHandler {
8+
return CoroutineExceptionHandler { _, throwable ->
9+
val error = Arguments.createMap()
10+
if (throwable !is AsyncStorageError) {
11+
error.putString(
12+
"message", "Unexpected AsyncStorage error: ${throwable.localizedMessage}"
13+
)
14+
} else {
15+
error.putString("message", throwable.errorMessage)
16+
}
17+
18+
cb(error)
19+
}
20+
}
21+
22+
internal class AsyncStorageError private constructor(val errorMessage: String) :
23+
Throwable(errorMessage) {
24+
25+
companion object {
26+
fun keyIsNull() = AsyncStorageError("Key cannot be null")
27+
28+
fun invalidKeyValueFormat() =
29+
AsyncStorageError("Invalid key-value format. Expected a list of [key, value] list.")
30+
31+
fun valueNotString(key: String?): AsyncStorageError {
32+
val detail = if (key == null) "Provided value" else "Value for key \"$key\""
33+
return AsyncStorageError("$detail is not of type String")
34+
}
35+
}
36+
}

android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageModule.kt

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,62 @@ import kotlinx.coroutines.Dispatchers
1212
import kotlinx.coroutines.SupervisorJob
1313
import kotlinx.coroutines.launch
1414

15-
class StorageModule(reactContext: ReactContext) : ReactContextBaseJavaModule() {
15+
class StorageModule(reactContext: ReactContext) : ReactContextBaseJavaModule(), CoroutineScope {
1616
override fun getName() = "RNC_AsyncSQLiteDBStorage"
1717

18-
private val storage: AsyncStorageAccess = StorageSupplier.getInstance(reactContext)
19-
private val scope = CoroutineScope(
20-
Dispatchers.IO + CoroutineName("AsyncStorageCoroutine") + SupervisorJob()
21-
)
18+
override val coroutineContext =
19+
Dispatchers.IO + CoroutineName("AsyncStorageScope") + SupervisorJob()
20+
21+
private val storage = StorageSupplier.getInstance(reactContext)
22+
23+
/**
24+
* Todo:
25+
* - MultiMerge
26+
*/
27+
28+
@ReactMethod
29+
fun multiGet(keys: ReadableArray, cb: Callback) {
30+
launch(createExceptionHandler(cb)) {
31+
val entries = storage.getValues(keys.toKeyList())
32+
cb(null, entries.toKeyValueArgument())
33+
}
34+
}
35+
36+
@ReactMethod
37+
fun multiSet(keyValueArray: ReadableArray, cb: Callback) {
38+
launch(createExceptionHandler(cb)) {
39+
val entries = keyValueArray.toEntryList()
40+
storage.setValues(entries)
41+
cb(null)
42+
}
43+
}
44+
45+
@ReactMethod
46+
fun multiRemove(keys: ReadableArray, cb: Callback) {
47+
launch(createExceptionHandler(cb)) {
48+
storage.removeValues(keys.toKeyList())
49+
cb(null)
50+
}
51+
}
52+
53+
// @ReactMethod
54+
// fun multiMerge(val keyValueArray: ReadableArray, cb: Callback) {}
2255

2356
@ReactMethod
2457
fun getAllKeys(cb: Callback) {
25-
scope.launch {
58+
launch(createExceptionHandler(cb)) {
2659
val keys = storage.getKeys()
2760
val result = Arguments.createArray()
2861
keys.forEach { result.pushString(it) }
29-
cb.invoke(result)
62+
cb.invoke(null, result)
63+
}
64+
}
65+
66+
@ReactMethod
67+
fun clear(cb: Callback) {
68+
launch(createExceptionHandler(cb)) {
69+
storage.clear()
70+
cb(null)
3071
}
3172
}
3273
}

android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageSupplier.kt

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,20 @@ import androidx.room.Room
1313
import androidx.room.RoomDatabase
1414
import androidx.room.Transaction
1515
import androidx.room.Update
16+
import androidx.room.migration.Migration
17+
import androidx.sqlite.db.SupportSQLiteDatabase
1618

17-
private const val DATABASE_NAME = "RKStorage"
18-
private const val DATABASE_VERSION = 1
19-
private const val TABLE_NAME = "catalystLocalStorage"
19+
private const val DATABASE_VERSION = 2
20+
private const val DATABASE_NAME = "AsyncStorage"
21+
private const val TABLE_NAME = "Storage"
2022
private const val COLUMN_KEY = "key"
2123
private const val COLUMN_VALUE = "value"
2224

2325

24-
@Entity(tableName = TABLE_NAME, primaryKeys = ["key"])
26+
@Entity(tableName = TABLE_NAME)
2527
data class Entry(
2628
@PrimaryKey @ColumnInfo(name = COLUMN_KEY) val key: String,
27-
@ColumnInfo(name = COLUMN_VALUE) val value: String
29+
@ColumnInfo(name = COLUMN_VALUE) val value: String?
2830
)
2931

3032
@Dao
@@ -60,11 +62,45 @@ private interface StorageDao {
6062
// insert and update are components of setValues - not to be used separately
6163
@Insert(onConflict = OnConflictStrategy.IGNORE)
6264
fun insert(entries: List<Entry>): List<Long>
65+
6366
@Update
6467
fun update(entries: List<Entry>)
6568
}
6669

67-
@Database(entities = [Entry::class], version = DATABASE_VERSION, exportSchema = false)
70+
71+
/**
72+
* Previous version of AsyncStorage is violating the SQL standard (based on bug in SQLite),
73+
* where PrimaryKey ('key' column) should never be null (https://www.sqlite.org/lang_createtable.html#the_primary_key).
74+
* Because of that, we cannot reuse the old DB, because ROOM is guarded against that case (won't compile).
75+
*
76+
* In order to work around this, two steps are necessary:
77+
* - Room DB pre-population from the old database file (https://developer.android.com/training/data-storage/room/prepopulate#from-asset)
78+
* - Version migration, so that we can mark 'key' column as NOT-NULL
79+
*
80+
* This migration will happens only once, when developer enable this feature (when DB is still not created).
81+
*/
82+
@Suppress("ClassName")
83+
private object MIGRATION_TO_NEXT : Migration(1, 2) {
84+
override fun migrate(database: SupportSQLiteDatabase) {
85+
val oldTableName = "catalystLocalStorage" // from ReactDatabaseSupplier
86+
87+
database.execSQL("CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`${COLUMN_KEY}` TEXT NOT NULL, `${COLUMN_VALUE}` TEXT, PRIMARY KEY(`${COLUMN_KEY}`));")
88+
89+
// even if the old AsyncStorage has checks for not nullable keys
90+
// make sure we don't copy any, to not fail migration
91+
database.execSQL("DELETE FROM $oldTableName WHERE `${COLUMN_KEY}` IS NULL")
92+
93+
database.execSQL(
94+
"""
95+
INSERT INTO $TABLE_NAME (`${COLUMN_KEY}`, `${COLUMN_VALUE}`)
96+
SELECT `${COLUMN_KEY}`, `${COLUMN_VALUE}`
97+
FROM $oldTableName;
98+
""".trimIndent()
99+
)
100+
}
101+
}
102+
103+
@Database(entities = [Entry::class], version = DATABASE_VERSION, exportSchema = true)
68104
private abstract class StorageDb : RoomDatabase() {
69105
abstract fun storage(): StorageDao
70106

@@ -78,10 +114,17 @@ private abstract class StorageDb : RoomDatabase() {
78114
}
79115

80116
synchronized(this) {
81-
inst = Room.databaseBuilder(
117+
val oldDbFile = context.getDatabasePath("RKStorage")
118+
val db = Room.databaseBuilder(
82119
context, StorageDb::class.java, DATABASE_NAME
83-
).build()
120+
)
121+
122+
if (oldDbFile.exists()) {
123+
// migrate data from old database, if it exists
124+
db.createFromFile(oldDbFile).addMigrations(MIGRATION_TO_NEXT)
125+
}
84126

127+
inst = db.build()
85128
instance = inst
86129
return instance!!
87130
}
@@ -90,7 +133,7 @@ private abstract class StorageDb : RoomDatabase() {
90133
}
91134

92135
interface AsyncStorageAccess {
93-
suspend fun getValue(keys: List<String>): List<Entry>
136+
suspend fun getValues(keys: List<String>): List<Entry>
94137
suspend fun setValues(entries: List<Entry>)
95138
suspend fun removeValues(keys: List<String>)
96139
suspend fun getKeys(): List<String>
@@ -107,7 +150,7 @@ class StorageSupplier private constructor(db: StorageDb) : AsyncStorageAccess {
107150

108151
private val access = db.storage()
109152

110-
override suspend fun getValue(keys: List<String>) = access.getValues(keys)
153+
override suspend fun getValues(keys: List<String>) = access.getValues(keys)
111154
override suspend fun setValues(entries: List<Entry>) = access.setValues(entries)
112155
override suspend fun removeValues(keys: List<String>) = access.removeValues(keys)
113156
override suspend fun getKeys() = access.getKeys()

example/App.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121

2222
import GetSetClear from './examples/GetSetClear';
2323
import MergeItem from './examples/MergeItem';
24+
import BasicExample from './examples/Basic';
2425

2526
const TESTS = {
2627
GetSetClear: {
@@ -39,6 +40,14 @@ const TESTS = {
3940
return <MergeItem />;
4041
},
4142
},
43+
Basic: {
44+
title: 'Basic',
45+
testId: 'basic',
46+
description: 'Basic functionality test',
47+
render() {
48+
return <BasicExample />;
49+
},
50+
},
4251
};
4352

4453
type Props = {};
@@ -87,6 +96,10 @@ export default class App extends Component<Props, State> {
8796
title="Merge Item"
8897
onPress={() => this._changeTest('MergeItem')}
8998
/>
99+
<Button
100+
title={TESTS.Basic.title}
101+
onPress={() => this._changeTest('Basic')}
102+
/>
90103
</View>
91104

92105
{restarting ? null : (

0 commit comments

Comments
 (0)