Skip to content

feat(android): Use Room library for persistent storage #528

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Apr 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 37 additions & 14 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ cache keys:
brew ios: &key_brew_ios cache-brew-ios-v4-{{ arch }}
brew android: &key_brew_android cache-brew-android-v4-{{ arch }}
yarn: &key_yarn cache-yarn-{{ checksum "package.json" }}-{{ arch }}
gradle: &key_gradle cache-gradle-{{ checksum "example/android/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "package.json" }}-{{ arch }}
gradle: &key_gradle cache-gradle-v1-{{ checksum "example/android/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "package.json" }}-{{ arch }}
pods: &key_pods cache-pods-v1-{{ checksum "example/ios/Podfile" }}-{{ checksum "package.json" }}-{{ arch }}

cache:
Expand Down Expand Up @@ -151,6 +151,26 @@ jobs:
name: Flow check
command: yarn test:flow

"Test: Android unit":
<<: *android_defaults
steps:
- *addWorkspace
- restore-cache: *cache_restore_yarn
- run:
name: Installing Yarn dependencies
command: yarn --pure-lockfile --non-interactive --cache-folder ~/.cache/yarn
- save-cache: *cache_save_yarn
- restore-cache: *cache_restore_gradle
- run:
name: Downloading Gradle dependencies
working_directory: example/android
command: ./gradlew --max-workers 2 fetchDependencies
- save-cache: *cache_save_gradle
- run:
name: Next storage tests
working_directory: example/android
command: ./gradlew react-native-async-storage_async-storage:test

"Test: iOS e2e":
<<: *macos_defaults
steps:
Expand Down Expand Up @@ -192,23 +212,16 @@ jobs:
<<: *android_defaults
steps:
- *addWorkspace
- restore-cache: *cache_restore_yarn
- run:
name: Installing Yarn dependencies
command: yarn --pure-lockfile --non-interactive --cache-folder ~/.cache/yarn
- save-cache: *cache_save_yarn
- restore-cache: *cache_restore_gradle
- run:
name: Downloading Gradle dependencies
working_directory: example/android
command: ./gradlew --max-workers 2 fetchDependencies
- save-cache: *cache_save_gradle
- run:
name: Bundle JS
command: yarn bundle:android --dev false
- run:
name: Build APK
name: Build APKs
command: yarn build:e2e:android
- run:
name: Build APK with Next storage
working_directory: example/android
command: ./gradlew assembleNext --max-workers 2
- persist_to_workspace:
root: ~/async_storage
paths:
Expand Down Expand Up @@ -272,7 +285,7 @@ jobs:
-no-snapshot \
-no-boot-anim \
-no-window \
-logcat '*:W' | grep -i "ReactNative"
-logcat '*:E ReactNative:W ReactNativeJS:*'
- run:
name: Wait for emulator to boot
command: ./scripts/android_e2e.sh 'wait_for_emulator'
Expand All @@ -283,6 +296,12 @@ jobs:
- run:
name: Run e2e tests
command: yarn test:e2e:android
- run:
name: Clear previous app from device
command: adb uninstall com.microsoft.reacttestapp
- run:
name: Run e2e tests for Next storage
command: yarn detox test -c android.emu.release.next --maxConcurrency 1

Release:
<<: *js_defaults
Expand All @@ -306,6 +325,9 @@ workflows:
- "Test: flow":
requires:
- "Setup environment"
- "Test: Android unit":
requires:
- "Setup environment"
- "Test: iOS e2e":
requires:
- "Test: lint"
Expand All @@ -314,6 +336,7 @@ workflows:
requires:
- "Test: lint"
- "Test: flow"
- "Test: Android unit"
- "Test: Android e2e":
requires:
- "Build: Android release apk"
Expand Down
74 changes: 61 additions & 13 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
Expand All @@ -42,18 +54,23 @@ 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()) {
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)

// Use next storage implementation
def useNextStorage = getFlagOrDefault("AsyncStorage_useNextStorage", false)

apply plugin: 'com.android.library'
if (useNextStorage) {
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply from: './testresults.gradle'
}

android {
compileSdkVersion safeExtGet('compileSdkVersion', 28)
Expand All @@ -62,11 +79,18 @@ 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_useNextStorage", "${useNextStorage}"
}
lintOptions {
abortOnError false
}

if (useNextStorage) {
testOptions {
unitTests.returnDefaultValues = true
}
}
}

repositories {
Expand All @@ -79,6 +103,30 @@ repositories {
}

dependencies {

if (useNextStorage) {
def room_version = "2.2.6"
def coroutines_version = "1.4.2"
def junit_version = "4.12"
def robolectric_version = "4.5.1"
def truth_version = "1.1.2"
def androidxtest_version = "1.1.0"
def coroutinesTest_version = "1.4.2"

implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
kapt "androidx.room:room-compiler:$room_version"

testImplementation "junit:junit:$junit_version"
testImplementation "androidx.test:runner:$androidxtest_version"
testImplementation "androidx.test:rules:$androidxtest_version"
testImplementation "androidx.test.ext:junit:$androidxtest_version"
testImplementation "org.robolectric:robolectric:$robolectric_version"
testImplementation "com.google.truth:truth:$truth_version"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesTest_version"
}

//noinspection GradleDynamicVersion
implementation 'com.facebook.react:react-native:+' // From node_modules
}
1 change: 1 addition & 0 deletions android/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
android.useAndroidX=true
Original file line number Diff line number Diff line change
Expand Up @@ -46,37 +46,6 @@ public final class AsyncStorageModule
private ReactDatabaseSupplier mReactDatabaseSupplier;
private boolean mShuttingDown = false;

// Adapted from https://android.googlesource.com/platform/frameworks/base.git/+/1488a3a19d4681a41fb45570c15e14d99db1cb66/core/java/android/os/AsyncTask.java#237
private class SerialExecutor implements Executor {
private final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
private Runnable mActive;
private final Executor executor;

SerialExecutor(Executor executor) {
this.executor = executor;
}

public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
executor.execute(mActive);
}
}
}

private final SerialExecutor executor;

public AsyncStorageModule(ReactApplicationContext reactContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,51 @@

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 java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class AsyncStoragePackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Arrays.<NativeModule>asList(new AsyncStorageModule(reactContext));

List<NativeModule> moduleList = new ArrayList<>(1);

if (BuildConfig.AsyncStorage_useNextStorage) {
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) {
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;
}

// Deprecated in RN 0.47
public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
return Collections.emptyList();
}

@Override
@SuppressWarnings("rawtypes")
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
return Collections.emptyList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.reactnativecommunity.asyncstorage;

import java.util.ArrayDeque;
import java.util.concurrent.Executor;

/**
* Detox is using this implementation detail in its environment setup,
* so in order for Next storage to work, this class has been made public
*
* Adapted from https://android.googlesource.com/platform/frameworks/base.git/+/1488a3a19d4681a41fb45570c15e14d99db1cb66/core/java/android/os/AsyncTask.java#237
*/
public class SerialExecutor implements Executor {
private final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
private Runnable mActive;
private final Executor executor;

public SerialExecutor(Executor executor) {
this.executor = executor;
}

public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
executor.execute(mActive);
}
}
}
Loading