diff --git a/.circleci/config.yml b/.circleci/config.yml index 2bf563fa..e2082237 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -137,6 +137,14 @@ jobs: name: Flow check command: yarn test:flow + "Test: units": + <<: *js_defaults + steps: + - *addWorkspace + - run: + name: Unit tests + command: yarn test:unit + "Test: iOS e2e": <<: *macos_defaults steps: @@ -306,14 +314,19 @@ workflows: - "Test: flow": requires: - "Setup environment" + - "Test: units": + requires: + - "Setup environment" - "Test: iOS e2e": requires: - "Test: lint" - "Test: flow" + - "Test: units" - "Build: Android release apk": requires: - "Test: lint" - "Test: flow" + - "Test: units" # - "Test: Android e2e": # requires: # - "Test: lint" diff --git a/android/build.gradle b/android/build.gradle index 4dc43dc8..57dcfd5e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,19 +2,43 @@ def safeExtGet(prop, fallback) { rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback } +def useDedicatedExecutor = rootProject.hasProperty('AsyncStorage_dedicatedExecutor') + ? rootProject.properties['AsyncStorage_dedicatedExecutor'] + : false + + buildscript { - // The Android Gradle plugin is only required when opening the android folder stand-alone. - // This avoids unnecessary downloads and potential conflicts when the library is included as a - // module dependency in an application project. - if (project == rootProject) { - repositories { - google() + def isRootProject = project == rootProject + + ext.coroutinesVersion = "1.3.8" + ext.roomVersion = "2.2.5" + + // kotlin version can be set by setting ext.kotlinVersion or having it in gradle.properties in root + ext.asyncStorageKtVersion = rootProject.ext.has('kotlinVersion') + ? rootProject.ext.get('kotlinVersion') + : properties['kotlinVersion'] + ? properties['kotlinVersion'] + : "1.3.72" + + repositories { + if (isRootProject) { jcenter() } - dependencies { + google() + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$asyncStorageKtVersion" + if (isRootProject) { + // The Android Gradle plugin is only required when opening the android folder stand-alone. + // This avoids unnecessary downloads and potential conflicts when the library is included as a + // module dependency in an application project. classpath 'com.android.tools.build:gradle:3.5.0' } } + + } // AsyncStorage has default size of 6MB. @@ -24,16 +48,13 @@ buildscript { long dbSizeInMB = 6L def newDbSize = rootProject.properties['AsyncStorage_db_size_in_MB'] - -if( newDbSize != null && newDbSize.isLong()) { +if (newDbSize != null && newDbSize.isLong()) { dbSizeInMB = newDbSize.toLong() } -def useDedicatedExecutor = rootProject.hasProperty('AsyncStorage_dedicatedExecutor') - ? rootProject.properties['AsyncStorage_dedicatedExecutor'] - : false - apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' android { compileSdkVersion safeExtGet('compileSdkVersion', 28) @@ -62,4 +83,11 @@ repositories { dependencies { //noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' // From node_modules + //noinspection GradleDependency + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$asyncStorageKtVersion" + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" + implementation "androidx.room:room-runtime:$roomVersion" + implementation "androidx.room:room-ktx:$roomVersion" + kapt "androidx.room:room-compiler:$roomVersion" } diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java index 25eb5a14..f83efdea 100644 --- a/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/AsyncStoragePackage.java @@ -16,11 +16,15 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import com.reactnativecommunity.asyncstorage.next.AsyncStorageModuleNext; public class AsyncStoragePackage implements ReactPackage { @Override public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new AsyncStorageModule(reactContext)); + return Arrays.asList( + new AsyncStorageModule(reactContext), + new AsyncStorageModuleNext(reactContext) + ); } // Deprecated in RN 0.47 diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/next/AsyncStorageModule.kt b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/AsyncStorageModule.kt new file mode 100644 index 00000000..3a3ccf80 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/AsyncStorageModule.kt @@ -0,0 +1,107 @@ +/** + * Copyright (c) Krzysztof Borowy + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.reactnativecommunity.asyncstorage.next + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlin.coroutines.CoroutineContext + +class AsyncStorageModuleNext(reactAppContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactAppContext), CoroutineScope { + companion object { + const val MODULE_NAME = "RNC_AsyncStorageNext" + } + + override val coroutineContext: CoroutineContext + get() = Dispatchers.IO + CoroutineName("AsyncStorageCoroutine") + + private val db: ASDao + get() = AsyncStorageDB.getDatabase(reactApplicationContext).getASDao() + + override fun getName() = MODULE_NAME + + @ReactMethod + fun getSingle(key: String, promise: Promise) { + launch { + val value = db.get(key) + promise.resolve(value) + } + } + + @ReactMethod + fun setSingle(key: String, value: String?, promise: Promise) { + val entry = AsyncStorageEntry(key, value) + launch { + db.set(entry) + promise.resolve(true) + } + } + + @ReactMethod + fun deleteSingle(key: String, promise: Promise) { + val entry = AsyncStorageEntry(key) + launch { + db.delete(entry) + promise.resolve(true) + } + } + + @ReactMethod + fun getMany(keys: ReadableArray, promise: Promise) { + val queryKeys = keys.toKeyList() + + launch { + val entries = db.getMany(queryKeys).toReadableMap() + promise.resolve(entries) + } + } + + @ReactMethod + fun setMany(entries: ReadableMap, promise: Promise) { + val entryList = entries.toAsyncStorageEntries() + + launch { + db.setMany(entryList) + promise.resolve(true) + } + } + + @ReactMethod + fun deleteMany(keys: ReadableArray, promise: Promise) { + val keysToDelete = keys.toKeyList() + + launch { + db.deleteMany(keysToDelete) + promise.resolve(true) + } + } + + @ReactMethod + fun getAllKeys(promise: Promise) { + launch { + val keys = db.keys().toReadableArray() + promise.resolve(keys) + } + } + + @ReactMethod + fun dropDatabase(promise: Promise) { + launch { + db.dropDatabase() + promise.resolve(true) + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/next/DatabaseSchema.kt b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/DatabaseSchema.kt new file mode 100644 index 00000000..20b9bd32 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/DatabaseSchema.kt @@ -0,0 +1,99 @@ +/** + * Copyright (c) Krzysztof Borowy + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.reactnativecommunity.asyncstorage.next + +import android.content.Context +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Delete +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 + +typealias KeyType = String +typealias ValueType = String? + +// todo: change those to use old DB +// todo: unit tests +const val DB_NAME = "AsyncStorageNextDB" +const val DB_VERSION = 1 +const val TABLE_NAME = "as_table" +const val KEY_COLUMN = "as_keys" +const val VALUE_COLUMN = "as_value" + +// The only table in this DB + +@Entity(tableName = TABLE_NAME) +data class AsyncStorageEntry( + @PrimaryKey + @ColumnInfo(name = KEY_COLUMN) + val key: String, + @ColumnInfo(name = VALUE_COLUMN) + val value: String? = null +) + +@Dao +interface ASDao { + + @Query("SELECT $VALUE_COLUMN FROM $TABLE_NAME WHERE $KEY_COLUMN = :key") + suspend fun get(key: KeyType): ValueType + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun set(entry: AsyncStorageEntry) + + @Delete + suspend fun delete(entry: AsyncStorageEntry) + + @Query("SELECT $KEY_COLUMN FROM $TABLE_NAME") + suspend fun keys(): List + + @Query("DELETE from $TABLE_NAME") + suspend fun dropDatabase() + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun setMany(entries: List) + + @Query("SELECT * FROM $TABLE_NAME WHERE $KEY_COLUMN in (:keys)") + suspend fun getMany(keys: List): List + + @Query("DELETE FROM $TABLE_NAME WHERE $KEY_COLUMN in (:keys)") + suspend fun deleteMany(keys: List) +} + +@Database(entities = [AsyncStorageEntry::class], version = DB_VERSION) +abstract class AsyncStorageDB : RoomDatabase() { + + companion object { + private var instance: AsyncStorageDB? = null + + fun getDatabase(context: Context): AsyncStorageDB { + + var inst = instance + + if (inst != null) { + return inst + } + synchronized(this) { + inst = Room + .databaseBuilder(context, AsyncStorageDB::class.java, DB_NAME) + .build() + + instance = inst + return instance!! + } + } + } + + abstract fun getASDao(): ASDao +} + diff --git a/android/src/main/java/com/reactnativecommunity/asyncstorage/next/Utils.kt b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/Utils.kt new file mode 100644 index 00000000..dab0b548 --- /dev/null +++ b/android/src/main/java/com/reactnativecommunity/asyncstorage/next/Utils.kt @@ -0,0 +1,50 @@ +/** + * Copyright (c) Krzysztof Borowy + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.reactnativecommunity.asyncstorage.next + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap + +fun ReadableArray.toKeyList(): List { + val keys: MutableList = mutableListOf() + for (index in 0 until this.size()) { + val key = getString(index) + if (key != null) { + keys.add(key) + } + } + return keys +} + +fun ReadableMap.toAsyncStorageEntries(): List { + val entryList = mutableListOf() + val keyIterator = keySetIterator() + while (keyIterator.hasNextKey()) { + val key = keyIterator.nextKey() + val value = getString(key) + entryList.add(AsyncStorageEntry(key, value)) + } + return entryList +} + +fun List.toReadableArray(): ReadableArray { + val keyArray = Arguments.createArray() + forEach { key -> + keyArray.pushString(key) + } + return keyArray +} + +fun List.toReadableMap(): ReadableMap { + val result = Arguments.createMap() + forEach { + result.putString(it.key, it.value) + } + return result +} \ No newline at end of file diff --git a/example/App.js b/example/App.js index a4464835..74027dc7 100644 --- a/example/App.js +++ b/example/App.js @@ -21,6 +21,7 @@ import { import GetSetClear from './examples/GetSetClear'; import MergeItem from './examples/MergeItem'; +import AsyncStorageNext from './examples/Next'; const TESTS = { GetSetClear: { @@ -39,6 +40,14 @@ const TESTS = { return ; }, }, + Next: { + title: 'Next AsyncStorage', + testId: 'as-next', + description: 'Use Next version of AsyncStorage', + render() { + return ; + }, + }, }; type Props = {}; @@ -87,6 +96,11 @@ export default class App extends Component { title="Merge Item" onPress={() => this._changeTest('MergeItem')} /> +