Skip to content

Instantly share code, notes, and snippets.

@ericluong
Created October 8, 2019 12:12
Show Gist options
  • Save ericluong/8176413b6f02bdc97b97dc0f92aa1f69 to your computer and use it in GitHub Desktop.
Save ericluong/8176413b6f02bdc97b97dc0f92aa1f69 to your computer and use it in GitHub Desktop.
Migrating AsyncStorage after ejecting from Expo (SDK 34)
package com.company.project;
import java.io.File;
import java.net.URLEncoder;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;
import com.facebook.react.modules.storage.ReactDatabaseSupplier;
public class AsyncStorageMigration {
static final String LOG_TAG = "expo_storage_migration";
static final String PERSIST_KEY = "persist:root";
static final String TABLE_CATALYST = "catalystLocalStorage";
static final String KEY_COLUMN = "key";
static final String VALUE_COLUMN = "value";
private static Context mContext;
private static String expoDatabaseName;
public static void migrate(Context context) {
mContext = context;
expoDatabaseName = getExpoDatabaseName();
if (expoDatabaseName.length() == 0)
return;
Boolean expoDatabaseExists = checkExpoDatabase();
if (!expoDatabaseExists) {
Log.v(LOG_TAG, "Expo AsyncStorage was previously migrated. Exiting migration...");
return;
}
String data = getOldData();
if (data.length() == 0) {
Log.v(LOG_TAG, "Could not get old data. Exiting migration...");
return;
}
if (!deleteOldDatabase()) {
Log.v(LOG_TAG, "Could not delete old database. Exiting migration...");
return;
}
populateNewStore(data);
Log.v(LOG_TAG, "Migration done!");
}
private static String getExpoDatabaseName() {
String databaseName = "";
String experienceId = getExperienceId();
if (experienceId == null || experienceId.length() == 0) {
Log.v(LOG_TAG, "No Experience ID. Exiting migration...");
} else {
try {
String experienceIdEncoded = URLEncoder.encode(experienceId, "UTF-8");
databaseName = "RKStorage-scoped-experience-" + experienceIdEncoded;
} catch (Exception e) {
Log.e(LOG_TAG, "Could not get Expo database name");
}
}
return databaseName;
}
private static String getExperienceId() {
return BuildConfig.LEGACY_EXPO_EXPERIENCE_ID;
}
private static boolean checkExpoDatabase() {
File dbFile = mContext.getDatabasePath(expoDatabaseName);
return dbFile.exists();
}
private static String getOldData() {
String data = "";
SQLiteDatabase readableDatabase = null;
Cursor cursor = null;
try {
String databasePath = mContext.getDatabasePath(expoDatabaseName).getPath();
readableDatabase = SQLiteDatabase.openDatabase(databasePath, null, SQLiteDatabase.OPEN_READONLY);
cursor = readableDatabase.query(TABLE_CATALYST, new String[]{VALUE_COLUMN}, KEY_COLUMN + " = ?", new String[]{PERSIST_KEY}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
data = cursor.getString(cursor.getColumnIndex(VALUE_COLUMN));
}
} catch (Exception e) {
Log.e(LOG_TAG, "Get old data error: " + e.getMessage());
} finally {
if (readableDatabase != null) {
readableDatabase.close();
}
}
return data;
}
private static Boolean deleteOldDatabase() {
return mContext.deleteDatabase(expoDatabaseName);
}
private static void populateNewStore(String value) {
SQLiteDatabase readableDatabase = null;
try {
readableDatabase = ReactDatabaseSupplier.getInstance(mContext).getReadableDatabase();
// Check if entry exists
Cursor cursor = readableDatabase.query(TABLE_CATALYST, new String[]{VALUE_COLUMN}, KEY_COLUMN + " = ?", new String[]{PERSIST_KEY}, null, null, null);
boolean entryExists = cursor.getCount() != 0;
cursor.close();
ContentValues args = new ContentValues();
args.put(KEY_COLUMN, PERSIST_KEY);
args.put(VALUE_COLUMN, value);
// Update value
if (entryExists) {
Log.v(LOG_TAG, "Entry already exists, ready for update...");
readableDatabase.update(TABLE_CATALYST, args, KEY_COLUMN + " = ?", new String[]{PERSIST_KEY});
}
// Insert value
else {
Log.v(LOG_TAG, "Entry does not exists, ready for insertion...");
readableDatabase.insert(TABLE_CATALYST, null, args);
}
} catch (Exception e) {
Log.e(LOG_TAG, "Populate error: " + e.getMessage());
} finally {
if (readableDatabase != null) {
readableDatabase.close();
}
}
}
}
@ericluong
Copy link
Author

This is how I migrated my AsyncStorage data on Android after ejecting from Expo 😁

If your Experience ID is @project-owner/project, Expo database name will be RKStorage-scoped-experience-%40project-owner%2Fproject.

Because I'm using Redux Persist on my project, I only need to copy the entry with the persist:root key from Expo database to the React Native one.

MainApplication.java

 @Override
 public void onCreate() {
        super.onCreate();
+       AsyncStorageMigration.migrate(getApplicationContext());
        SoLoader.init(this, /* native exopackage */ false);
 }

build.gradle

 buildTypes {
        debug {
             ...
+            buildConfigField "String", "LEGACY_EXPO_EXPERIENCE_ID", "\"@project-owner/project\""
        }
}

@yaroslavnikiforov
Copy link

Hi @ericluong, thanks for providing this solution. Can you say where did you put AsyncStorageMigration.java file in android folder?

@ericluong
Copy link
Author

Hello @yaroslavnikiforov, this file is located next to the MainApplication.java and MainActivity.java files in android/app/src/main/java/project/path

@yaroslavnikiforov
Copy link

Thanks, @ericluong, this solution works great

@ericluong
Copy link
Author

@yaroslavnikiforov You're welcome! Migrating out of Expo was painful (especially with an app in production) but it has been worth it

@sammahfoud
Copy link

Thank you for the solution, if you are not using Redux this can be useful too: https://gist.github.com/luizjr92/db091719f09617abc5909ba64485e395

@Zakrevskijj
Copy link

Guys, I just want to say thank you for this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment