From d3128c3bccc0e33008c5b5ac58e5584207e246b5 Mon Sep 17 00:00:00 2001 From: Krzysztof Borowy Date: Tue, 19 Jan 2021 23:36:34 +0100 Subject: [PATCH 01/17] initial --- android/build.gradle | 42 +++++++++++++------ .../asyncstorage/AsyncStoragePackage.java | 15 +++++-- .../asyncstorage/next/StorageModule.kt | 8 ++++ example/android/build.gradle | 8 ++++ example/android/gradle.properties | 1 + 5 files changed, 59 insertions(+), 15 deletions(-) create mode 100644 android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageModule.kt diff --git a/android/build.gradle b/android/build.gradle index d1442125..7fc9fc06 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,18 +21,30 @@ def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } +def getFlagOrDefault(flagName, defaultValue) { + rootProject.hasProperty(flagName) ? rootProject.properties[flagName] == "true" : defaultValue +} + configurations { compileClasspath } buildscript { - if (project == rootProject) { - repositories { - google() - jcenter() - } - dependencies { + // kotlin version is dictated by rootProject extension or property in gradle.properties + ext.asyncStorageKtVersion = rootProject.ext.has('kotlinVersion') + ? rootProject.ext['kotlinVersion'] + : rootProject.hasProperty('AsyncStorage_kotlinVersion') + ? rootProject.properties['AsyncStorage_kotlinVersion'] + : '1.4.21' + + repositories { + google() + jcenter() + } + dependencies { + if (project == rootProject) { classpath 'com.android.tools.build:gradle:3.6.4' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$asyncStorageKtVersion" } } } @@ -42,19 +54,24 @@ buildscript { // This also protects the database from filling up the disk cache and becoming malformed. // If you really need bigger size, please keep in mind the potential consequences. long dbSizeInMB = 6L - def newDbSize = rootProject.properties['AsyncStorage_db_size_in_MB'] - if( newDbSize != null && newDbSize.isLong()) { dbSizeInMB = newDbSize.toLong() } -def useDedicatedExecutor = rootProject.hasProperty('AsyncStorage_dedicatedExecutor') - ? rootProject.properties['AsyncStorage_dedicatedExecutor'] - : false +// Instead of reusing AsyncTask thread pool, AsyncStorage can use its own executor +def useDedicatedExecutor = getFlagOrDefault('AsyncStorage_dedicatedExecutor', false) apply plugin: 'com.android.library' +// todo: decide the name +def useRoomImplementation = getFlagOrDefault("AsyncStorage_useRoomLibrary", false) + +if(useRoomImplementation) { + apply plugin: 'kotlin-android' + apply plugin: 'kotlin-kapt' +} + android { compileSdkVersion safeExtGet('compileSdkVersion', 28) buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3') @@ -62,7 +79,8 @@ android { minSdkVersion safeExtGet('minSdkVersion', 19) targetSdkVersion safeExtGet('targetSdkVersion', 28) buildConfigField "Long", "AsyncStorage_db_size", "${dbSizeInMB}L" - buildConfigField("boolean", "AsyncStorage_useDedicatedExecutor", "${useDedicatedExecutor}") + buildConfigField "boolean", "AsyncStorage_useDedicatedExecutor", "${useDedicatedExecutor}" + buildConfigField "boolean", "AsyncStorage_useRoomLibrary", "${useRoomImplementation}" } lintOptions { abortOnError false diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java index 25eb5a14..767f834d 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java @@ -12,15 +12,24 @@ import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; - -import java.util.Arrays; +import com.reactnativecommunity.asyncstorage.next.StorageModule; +import java.util.ArrayList; import java.util.Collections; import java.util.List; public class AsyncStoragePackage implements ReactPackage { @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new AsyncStorageModule(reactContext)); + + List moduleList = new ArrayList<>(1); + + if(BuildConfig.AsyncStorage_useRoomLibrary) { + moduleList.add(new StorageModule(reactContext)); + } else { + moduleList.add(new AsyncStorageModule(reactContext)); + } + + return moduleList; } // Deprecated in RN 0.47 diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageModule.kt b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageModule.kt new file mode 100644 index 00000000..b04bbb93 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageModule.kt @@ -0,0 +1,8 @@ +package com.reactnativecommunity.asyncstorage.next + +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReactContextBaseJavaModule + +class StorageModule(private val reactContext: ReactContext) : ReactContextBaseJavaModule() { + override fun getName() = "RNC_AsyncSQLiteDBStorage" +} \ No newline at end of file diff --git a/example/android/build.gradle b/example/android/build.gradle index ff15942c..7f94acfb 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -26,6 +26,13 @@ task fetchDependencies() { } buildscript { + /** + * Todo: + * In order to use this feature, the end user will have to also include + * Kotlin plugin. Add this info to docs. + */ + ext.kotlinVersion = '1.4.21' + repositories { google() jcenter() @@ -33,6 +40,7 @@ buildscript { dependencies { // Needs to match `react-native-test-app/android/app/build.gradle` classpath "com.android.tools.build:gradle:4.0.2" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 6f73824c..cf69923f 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -22,6 +22,7 @@ # Enable dedicated thread pool executor AsyncStorage_dedicatedExecutor=true +AsyncStorage_useRoomLibrary=true android.useAndroidX=true android.enableJetifier=true From 7fb5376ee86a9336bc3358ab3eefef9773c82f26 Mon Sep 17 00:00:00 2001 From: Krzysztof Borowy Date: Thu, 21 Jan 2021 09:31:07 +0100 Subject: [PATCH 02/17] room deps --- android/build.gradle | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 7fc9fc06..2408efb3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -61,12 +61,9 @@ if( newDbSize != null && newDbSize.isLong()) { // Instead of reusing AsyncTask thread pool, AsyncStorage can use its own executor def useDedicatedExecutor = getFlagOrDefault('AsyncStorage_dedicatedExecutor', false) - -apply plugin: 'com.android.library' - -// todo: decide the name def useRoomImplementation = getFlagOrDefault("AsyncStorage_useRoomLibrary", false) +apply plugin: 'com.android.library' if(useRoomImplementation) { apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' @@ -97,6 +94,16 @@ repositories { } dependencies { + + if(useRoomImplementation) { + def room_version = "2.2.6" + def coroutines_version = "1.3.9" + + implementation "androidx.room:room-runtime:$room_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + kapt "androidx.room:room-compiler:$room_version" + } + //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' // From node_modules } From 37db93eb75721e078c9d3b85fc7beb4427cf871a Mon Sep 17 00:00:00 2001 From: Krzysztof Borowy Date: Sat, 23 Jan 2021 11:19:53 +0100 Subject: [PATCH 03/17] initial implementation --- .../asyncstorage/AsyncStoragePackage.java | 25 +++- .../asyncstorage/next/StorageModule.kt | 26 +++- .../asyncstorage/next/StorageSupplier.kt | 115 ++++++++++++++++++ example/android/build.gradle | 2 + example/examples/GetSetClear.js | 4 + 5 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageSupplier.kt diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java index 767f834d..5db0d715 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java @@ -7,12 +7,13 @@ package com.reactnativecommunity.asyncstorage; +import android.util.Log; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.JavaScriptModule; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; import com.facebook.react.uimanager.ViewManager; -import com.reactnativecommunity.asyncstorage.next.StorageModule; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -23,23 +24,35 @@ public List createNativeModules(ReactApplicationContext reactConte List moduleList = new ArrayList<>(1); - if(BuildConfig.AsyncStorage_useRoomLibrary) { - moduleList.add(new StorageModule(reactContext)); + if (BuildConfig.AsyncStorage_useRoomLibrary) { + try { + Class storageClass = Class.forName("com.reactnativecommunity.asyncstorage.next.StorageModule"); + NativeModule inst = (NativeModule) storageClass.getDeclaredConstructor(new Class[]{ReactContext.class}).newInstance(reactContext); + moduleList.add(inst); + } catch (Exception e) { + // todo notify about potential issues + String message = "Something went wrong when initializing module:" + + "\n" + + e.getCause().getClass() + + "\n" + + "Cause:" + e.getCause().getLocalizedMessage(); + Log.e("AsyncStorage_Next", message); + } } else { moduleList.add(new AsyncStorageModule(reactContext)); } - return moduleList; + return moduleList; } // Deprecated in RN 0.47 public List> createJSModules() { - return Collections.emptyList(); + return Collections.emptyList(); } @Override @SuppressWarnings("rawtypes") public List createViewManagers(ReactApplicationContext reactContext) { - return Collections.emptyList(); + return Collections.emptyList(); } } \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageModule.kt b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageModule.kt index b04bbb93..2a648a04 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageModule.kt +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageModule.kt @@ -1,8 +1,32 @@ package com.reactnativecommunity.asyncstorage.next +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Callback import com.facebook.react.bridge.ReactContext import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch -class StorageModule(private val reactContext: ReactContext) : ReactContextBaseJavaModule() { +class StorageModule(reactContext: ReactContext) : ReactContextBaseJavaModule() { override fun getName() = "RNC_AsyncSQLiteDBStorage" + + private val storage: AsyncStorageAccess = StorageSupplier.getInstance(reactContext) + private val scope = CoroutineScope( + Dispatchers.IO + CoroutineName("AsyncStorageCoroutine") + SupervisorJob() + ) + + @ReactMethod + fun getAllKeys(cb: Callback) { + scope.launch { + val keys = storage.getKeys() + val result = Arguments.createArray() + keys.forEach { result.pushString(it) } + cb.invoke(result) + } + } } \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageSupplier.kt b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageSupplier.kt new file mode 100644 index 00000000..a655e98e --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/StorageSupplier.kt @@ -0,0 +1,115 @@ +package com.reactnativecommunity.asyncstorage.next + +import android.content.Context +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.Transaction +import androidx.room.Update + +private const val DATABASE_NAME = "RKStorage" +private const val DATABASE_VERSION = 1 +private const val TABLE_NAME = "catalystLocalStorage" +private const val COLUMN_KEY = "key" +private const val COLUMN_VALUE = "value" + + +@Entity(tableName = TABLE_NAME, primaryKeys = ["key"]) +data class Entry( + @PrimaryKey @ColumnInfo(name = COLUMN_KEY) val key: String, + @ColumnInfo(name = COLUMN_VALUE) val value: String +) + +@Dao +private interface StorageDao { + + // fun mergeValues() // todo + + @Transaction + @Query("SELECT * FROM $TABLE_NAME WHERE `$COLUMN_KEY` IN (:keys)") + fun getValues(keys: List): List + + @Transaction + fun setValues(entries: List) { + val insertResult = insert(entries) + with(entries.filterIndexed { i, _ -> insertResult[i] == -1L }) { + update(this) + } + } + + @Transaction + @Query("DELETE FROM $TABLE_NAME WHERE `$COLUMN_KEY` in (:keys)") + fun removeValues(keys: List) + + @Transaction + @Query("SELECT `$COLUMN_KEY` FROM $TABLE_NAME") + fun getKeys(): List + + @Transaction + @Query("DELETE FROM $TABLE_NAME") + fun clear() + + + // insert and update are components of setValues - not to be used separately + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(entries: List): List + @Update + fun update(entries: List) +} + +@Database(entities = [Entry::class], version = DATABASE_VERSION, exportSchema = false) +private abstract class StorageDb : RoomDatabase() { + abstract fun storage(): StorageDao + + companion object { + private var instance: StorageDb? = null + + fun getDatabase(context: Context): StorageDb { + var inst = instance + if (inst != null) { + return inst + } + + synchronized(this) { + inst = Room.databaseBuilder( + context, StorageDb::class.java, DATABASE_NAME + ).build() + + instance = inst + return instance!! + } + } + } +} + +interface AsyncStorageAccess { + suspend fun getValue(keys: List): List + suspend fun setValues(entries: List) + suspend fun removeValues(keys: List) + suspend fun getKeys(): List + suspend fun clear() + // suspend fun mergeValues() // todo +} + +class StorageSupplier private constructor(db: StorageDb) : AsyncStorageAccess { + companion object { + fun getInstance(ctx: Context): AsyncStorageAccess { + return StorageSupplier(StorageDb.getDatabase(ctx)) + } + } + + private val access = db.storage() + + override suspend fun getValue(keys: List) = access.getValues(keys) + override suspend fun setValues(entries: List) = access.setValues(entries) + override suspend fun removeValues(keys: List) = access.removeValues(keys) + override suspend fun getKeys() = access.getKeys() + override suspend fun clear() = access.clear() +} \ No newline at end of file diff --git a/example/android/build.gradle b/example/android/build.gradle index 7f94acfb..2b4b2955 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -40,6 +40,8 @@ buildscript { dependencies { // Needs to match `react-native-test-app/android/app/build.gradle` classpath "com.android.tools.build:gradle:4.0.2" + + // todo: remember to document this as well classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" } } diff --git a/example/examples/GetSetClear.js b/example/examples/GetSetClear.js index 6cc1853c..fe1904d0 100644 --- a/example/examples/GetSetClear.js +++ b/example/examples/GetSetClear.js @@ -16,6 +16,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; export default function GetSet() { const [storedNumber, setStoredNumber] = React.useState(''); const [needsRestart, setNeedsRestart] = React.useState(false); + const [keys, setKeys] = React.useState([]); React.useEffect(() => { AsyncStorage.getItem(STORAGE_KEY).then((value) => { @@ -23,6 +24,7 @@ export default function GetSet() { setStoredNumber(value); } }); + AsyncStorage.getAllKeys().then(setKeys); }, []); const increaseByTen = React.useCallback(async () => { @@ -54,6 +56,8 @@ export default function GetSet() {