Android implementation
Let's use Android Studio for writing Android code. Launch Android Studio and open the project under <your-project-dir>/android
path.
When the project is opened, find save-file-picker-package
inside project-tree
The save-file-picker-package
contains 3 packages with the same name com.savefilepickerpackage
. After expanding them, you'll notice that these contain following things:
- code-generated Java spec files
SaveFilePickerModule
class stub filesSaveFilePickerModuleImpl
class stub fileSaveFilePickerTurboPackage
class stub file
Let's start implementing!
The main purpose of this guide is to show you how to wrap platform APIs in asynchronous methods. So to simplify the example, the module will be sharing files only from application static local assets. After you complete this guide, you can try to refactor the module to accept the path to any file (e.g. from the documents or cache directory).
- Kotlin
- Java
SaveFilePickerModuleImpl.kt
Let's start by creating a small pure Kotlin class that will be responsible for launching platform save file picker:
package com.savefilepickerpackage
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.UiThreadUtil
/**
* Native module's shared implementation
*/
class SaveFilePickerModuleImpl(private val reactContext: ReactApplicationContext) {
fun saveFile(filename: String) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/html"
putExtra(Intent.EXTRA_TITLE, filename)
}
val launcher = activityLauncher
if (launcher == null) {
listener?.onError(Exception("Activity launcher not registered"))
return
}
UiThreadUtil.runOnUiThread {
launcher.launch(intent)
}
}
interface SaveFilePickerListener {
fun onSuccess(uri: Uri?)
fun onCancel()
fun onError(error: Exception)
}
companion object {
const val NAME = "SaveFilePickerModule"
private var activityLauncher: ActivityResultLauncher<Intent>? = null
var listener: SaveFilePickerListener? = null
fun registerActivityLauncher(activity: AppCompatActivity) {
activityLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) {
Activity.RESULT_OK -> {
listener?.onSuccess(result.data?.data)
}
Activity.RESULT_CANCELED -> {
listener?.onCancel()
}
else -> {
listener?.onError(Exception("Unknown result when saving the file"))
}
}
}
}
}
}
In this guide, we will be using generic ActivityResultContracts.StartActivityForResult
contract and raw intent action,
to showcase how to work with all possible types of actions.
However, Android provides a set of standard contracts for popular actions (e.g. launching a camera, or requesting a permission),
you can visit Android docs to learn more.
SaveFilePickerModuleImpl
is divided into 2 parts: static and non-static.
In the static part we will declare ActivityResultLauncher
variable that will be responsible for launching file activity picker and capturing picker's result.
The registerActivityLauncher
will take the activity as a parameter and invoke registerForActivityResult
on that activity. The result callback will be invoked once the file activity picker will be closed and it will contain the uri of the file's destination.
That result will be then forwarded to the SaveFilePickerListener
instance.
Registering activity launcher will need to be done when the activity is started (typically in Activity#onCreate
method), we will do that later in the next steps.
To learn more about communication between android activities, visit Android's "Getting a result from an Activity" guide.
The non-static part is a single method saveFile
that will be responsible just for invoking the ActivityResultLauncher
with the ACTION_CREATE_DOCUMENT
intent action.
The launcher needs to be used on the main/ui thread, so to do it, we will use UiThreadUtil.runOnUiThread
helper.
For more on "save file" functionality visit Android docs.
SaveFilePickerModule.kt
Now, let's move to the module that will manage function calls from the JS world:
package com.savefilepickerpackage
import android.net.Uri
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.Promise
import com.facebook.react.module.annotations.ReactModule
import java.io.File
/**
* Declare Kotlin class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
class SaveFilePickerModule(
// Each native module class consumes react application context
reactContext: ReactApplicationContext
) : NativeSaveFilePickerModuleSpec(reactContext) {
// Use shared module implementation and forward react application context
private val moduleImpl = SaveFilePickerModuleImpl(reactContext)
// Return the name of the module - it should match the name provided in JS specification
override fun getName() = SaveFilePickerModuleImpl.NAME
// Exported methods are overriden - based on the spec class
override fun saveFileWithCallback(filename: String, callback: Callback) {
//
}
override fun saveFileWithPromise(filename: String, promise: Promise) {
//
}
companion object {
const val NAME = SaveFilePickerModuleImpl.NAME
}
}
Here we declare the SaveFilePickerModule
class.
It extends codegenerated spec class and takes ReactApplicationContext
instance as constructor parameter.
Additionally, SaveFilePickerModule
is annotated with ReactModule
decorator.
Static constant NAME
matches the value declared in JS specification.
For now, we start by adding empty implementations for methods declared in codegenerated spec class.
Now, let's add private variables for callback/promise blocks - we'll need to save them, once the exported methods are invoked and use, once there's result from the file activity picker.
// ...
/**
* Declare Kotlin class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
class SaveFilePickerModule(
// Each native module class consumes react application context
reactContext: ReactApplicationContext
) : NativeSaveFilePickerModuleSpec(reactContext) {
// Use shared module implementation and forward react application context
private val moduleImpl = SaveFilePickerModuleImpl(reactContext)
private var callbackBlock: Callback? = null
private var promiseBlock: Promise? = null
private var sourceFilename: String = ""
// ...
}
Next step is to implement listener object that will use callback/promise blocks to communicate the result to the JS code.
We will do it inside init
block:
// ...
/**
* Declare Kotlin class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
class SaveFilePickerModule(
// Each native module class consumes react application context
reactContext: ReactApplicationContext
) : NativeSaveFilePickerModuleSpec(reactContext) {
// Use shared module implementation and forward react application context
private val moduleImpl = SaveFilePickerModuleImpl(reactContext)
private var callbackBlock: Callback? = null
private var promiseBlock: Promise? = null
private var sourceFilename: String = ""
init {
SaveFilePickerModuleImpl.listener = object : SaveFilePickerModuleImpl.SaveFilePickerListener {
override fun onCancel() {
val callback = callbackBlock
val promise = promiseBlock
if (callback != null) {
callback.invoke(Arguments.createMap().apply {
putBoolean("success", false)
putBoolean("cancelled", true)
})
} else if (promise != null) {
promise.resolve(false)
}
callbackBlock = null
promiseBlock = null
sourceFilename = ""
}
override fun onError(error: Exception) {
val callback = callbackBlock
val promise = promiseBlock
if (callback != null) {
callback.invoke(Arguments.createMap().apply {
putMap("error", Arguments.createMap().apply {
putInt("code", 1234)
putString("message", error.message)
})
putBoolean("success", false)
putBoolean("cancelled", false)
})
} else if (promise != null) {
promise.reject("1234", error.message)
}
callbackBlock = null
promiseBlock = null
sourceFilename = ""
}
override fun onSuccess(uri: Uri?) {
reactContext.applicationContext.assets.open(sourceFilename).use { sourceInputStream ->
uri?.let {
reactContext.contentResolver.openOutputStream(it)?.use { outputStream ->
sourceInputStream.copyTo(outputStream)
}
}
}
val callback = callbackBlock
val promise = promiseBlock
if (callback != null) {
callback.invoke(Arguments.createMap().apply {
putBoolean("success", true)
putBoolean("cancelled", false)
})
} else if (promise != null) {
promise.resolve(true)
}
callbackBlock = null
promiseBlock = null
sourceFilename = ""
}
}
}
// ...
}
For the promise block, we just resolve with boolean value or reject with an error.
For callback block, we want to create an object argument - to do that, let's use Arguments.createMap
utility helper.
The important thing and the main difference between Android & iOS implementation is that the Android's save file picker is actually only creating a new empty file instead of copying the source file like on iOS.
That means, we have to manually copy the content of the requested file into the new location.
In our case, we are doing it in the listener's onSuccess
function:
override fun onSuccess(uri: Uri?) {
reactContext.applicationContext.assets.open(sourceFilename).use { sourceInputStream ->
uri?.let {
reactContext.contentResolver.openOutputStream(it)?.use { outputStream ->
sourceInputStream.copyTo(outputStream)
}
}
}
// ...
}
That code takes specified file from application's assets directory, opens the file under the provided uri
and copies the content of the source file to the target file.
Last part is to use SaveFilePickerModuleImpl
class inside exported methods:
// ...
/**
* Declare Kotlin class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
class SaveFilePickerModule(
// Each native module class consumes react application context
reactContext: ReactApplicationContext
) : NativeSaveFilePickerModuleSpec(reactContext) {
// ...
// Return the name of the module - it should match the name provided in JS specification
override fun getName() = SaveFilePickerModuleImpl.NAME
// Exported methods are overriden - based on the spec class
override fun saveFileWithCallback(filename: String, callback: Callback) {
callbackBlock = callback
sourceFilename = filename
moduleImpl.saveFile(filename)
}
override fun saveFileWithPromise(filename: String, promise: Promise) {
promiseBlock = promise
sourceFilename = filename
moduleImpl.saveFile(filename)
}
companion object {
const val NAME = SaveFilePickerModuleImpl.NAME
}
}
In the methods body, we save the callback/promise arguments and we use SaveFilePickerModuleImpl#saveFile
method to launch file activity picker.
Complete SaveFilePickerModule.kt
file
package com.savefilepickerpackage
import android.net.Uri
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.Promise
import com.facebook.react.module.annotations.ReactModule
import java.io.File
/**
* Declare Kotlin class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
class SaveFilePickerModule(
// Each native module class consumes react application context
reactContext: ReactApplicationContext
) : NativeSaveFilePickerModuleSpec(reactContext) {
// Use shared module implementation and forward react application context
private val moduleImpl = SaveFilePickerModuleImpl(reactContext)
private var callbackBlock: Callback? = null
private var promiseBlock: Promise? = null
private var sourceFilename: String = ""
init {
SaveFilePickerModuleImpl.listener = object : SaveFilePickerModuleImpl.SaveFilePickerListener {
override fun onCancel() {
val callback = callbackBlock
val promise = promiseBlock
if (callback != null) {
callback.invoke(Arguments.createMap().apply {
putBoolean("success", false)
putBoolean("cancelled", true)
})
} else if (promise != null) {
promise.resolve(false)
}
callbackBlock = null
promiseBlock = null
sourceFilename = ""
}
override fun onError(error: Exception) {
val callback = callbackBlock
val promise = promiseBlock
if (callback != null) {
callback.invoke(Arguments.createMap().apply {
putMap("error", Arguments.createMap().apply {
putInt("code", 1234)
putString("message", error.message)
})
putBoolean("success", false)
putBoolean("cancelled", false)
})
} else if (promise != null) {
promise.reject("1234", error.message)
}
callbackBlock = null
promiseBlock = null
sourceFilename = ""
}
override fun onSuccess(uri: Uri?) {
reactContext.applicationContext.assets.open(sourceFilename).use { sourceInputStream ->
uri?.let {
reactContext.contentResolver.openOutputStream(it)?.use { outputStream ->
sourceInputStream.copyTo(outputStream)
}
}
}
val callback = callbackBlock
val promise = promiseBlock
if (callback != null) {
callback.invoke(Arguments.createMap().apply {
putBoolean("success", true)
putBoolean("cancelled", false)
})
} else if (promise != null) {
promise.resolve(true)
}
callbackBlock = null
promiseBlock = null
sourceFilename = ""
}
}
}
// Return the name of the module - it should match the name provided in JS specification
override fun getName() = SaveFilePickerModuleImpl.NAME
// Exported methods are overriden - based on the spec class
override fun saveFileWithCallback(filename: String, callback: Callback) {
callbackBlock = callback
sourceFilename = filename
moduleImpl.saveFile(filename)
}
override fun saveFileWithPromise(filename: String, promise: Promise) {
promiseBlock = promise
sourceFilename = filename
moduleImpl.saveFile(filename)
}
companion object {
const val NAME = SaveFilePickerModuleImpl.NAME
}
}
Old architecture module
android/src/oldarch/java/com/savefilepickerpackage/SaveFilePickerModule.kt
at other text editor and paste following content:package com.savefilepickerpackage
import android.net.Uri
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.Callback
import com.facebook.react.bridge.Promise
import com.facebook.react.module.annotations.ReactModule
import java.io.File
/**
* Declare Kotlin class for old arch native module implementation
*
* Each native module extends ReactContextBaseJavaModule class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
class SaveFilePickerModule(
// Each native module class consumes react application context
reactContext: ReactApplicationContext
) : ReactContextBaseJavaModule(reactContext) {
// Use shared module implementation and forward react application context
private val moduleImpl = SaveFilePickerModuleImpl(reactContext)
private var callbackBlock: Callback? = null
private var promiseBlock: Promise? = null
private var sourceFilename: String = ""
init {
SaveFilePickerModuleImpl.listener = object : SaveFilePickerModuleImpl.SaveFilePickerListener {
override fun onCancel() {
val callback = callbackBlock
val promise = promiseBlock
if (callback != null) {
callback.invoke(Arguments.createMap().apply {
putBoolean("success", false)
putBoolean("cancelled", true)
})
} else if (promise != null) {
promise.resolve(false)
}
callbackBlock = null
promiseBlock = null
sourceFilename = ""
}
override fun onError(error: Exception) {
val callback = callbackBlock
val promise = promiseBlock
if (callback != null) {
callback.invoke(Arguments.createMap().apply {
putMap("error", Arguments.createMap().apply {
putInt("code", 1234)
putString("message", error.message)
})
putBoolean("success", false)
putBoolean("cancelled", false)
})
} else if (promise != null) {
promise.reject("1234", error.message)
}
callbackBlock = null
promiseBlock = null
sourceFilename = ""
}
override fun onSuccess(uri: Uri?) {
reactContext.applicationContext.assets.open(sourceFilename).use { sourceInputStream ->
uri?.let {
reactContext.contentResolver.openOutputStream(it)?.use { outputStream ->
sourceInputStream.copyTo(outputStream)
}
}
}
val callback = callbackBlock
val promise = promiseBlock
if (callback != null) {
callback.invoke(Arguments.createMap().apply {
putBoolean("success", true)
putBoolean("cancelled", false)
})
} else if (promise != null) {
promise.resolve(true)
}
callbackBlock = null
promiseBlock = null
sourceFilename = ""
}
}
}
// Return the name of the module - it should match the name provided in JS specification
override fun getName() = SaveFilePickerModuleImpl.NAME
// Exported methods must be annotated with @ReactMethod decorator
@ReactMethod
fun saveFileWithCallback(filename: String, callback: Callback) {
callbackBlock = callback
sourceFilename = filename
moduleImpl.saveFile(filename)
}
@ReactMethod
fun saveFileWithPromise(filename: String, promise: Promise) {
promiseBlock = promise
sourceFilename = filename
moduleImpl.saveFile(filename)
}
companion object {
const val NAME = SaveFilePickerModuleImpl.NAME
}
}
Let's finalize it by exporting the module in the TurboReactPackage
instance.
SaveFilePickerTurboPackage.kt
The last thing we need to do is to export SaveFilePickerModule
in the TurboReactPackage
instance. Let's go to SaveFilePickerTurboPackage.kt
and add our new module.
package com.savefilepickerpackage
import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.turbomodule.core.interfaces.TurboModule
class SaveFilePickerTurboPackage : TurboReactPackage() {
/**
* Initialize and export modules based on the name of the required module
*/
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return when (name) {
SaveFilePickerModule.NAME -> SaveFilePickerModule(reactContext)
else -> null
}
}
/**
* Declare info about exported modules
*/
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
/**
* Here declare the array of exported modules
*/
val moduleList: Array<Class<out NativeModule?>> = arrayOf(
SaveFilePickerModule::class.java
)
val reactModuleInfoMap: MutableMap<String, ReactModuleInfo> = HashMap()
/**
* And here just iterate on that array and produce the info provider instance
*/
for (moduleClass in moduleList) {
val reactModule = moduleClass.getAnnotation(ReactModule::class.java) ?: continue
reactModuleInfoMap[reactModule.name] =
ReactModuleInfo(
reactModule.name,
moduleClass.name,
true,
reactModule.needsEagerInit,
reactModule.hasConstants,
reactModule.isCxxModule,
TurboModule::class.java.isAssignableFrom(moduleClass)
)
}
return ReactModuleInfoProvider { reactModuleInfoMap }
}
}
To export the module, as the first step, we need to return it from getModule
method inside SaveFilePickerTurboPackage
, if it's requested (the method takes name as a parameter and makes decision which module should be served).
The second step is to implement getReactModuleInfoProvider
method, where the module is injected to the info provider instance.
SaveFilePickerModuleImpl.java
Let's start by creating a small pure Java class that will be responsible for launching platform save file picker:
package com.savefilepickerpackage;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.UiThreadUtil;
/**
* Native module's shared implementation
*/
public class SaveFilePickerModuleImpl {
interface SaveFilePickerListener {
void onSuccess(@Nullable Uri uri);
void onCancel();
void onError(Exception error);
}
private final ReactApplicationContext reactContext;
public static final String NAME = "SaveFilePickerModule";
private static @Nullable ActivityResultLauncher<Intent> activityLauncher = null;
public static @Nullable SaveFilePickerListener listener = null;
public static void registerActivityLauncher(AppCompatActivity activity) {
activityLauncher = activity.registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
if (listener != null) {
Intent data = result.getData();
Uri uri = null;
if (data != null) {
uri = data.getData();
}
listener.onSuccess(uri);
}
} else if (result.getResultCode() == Activity.RESULT_CANCELED) {
if (listener != null) {
listener.onCancel();
}
} else {
if (listener != null) {
listener.onError(new Exception("Unknown result when saving the file"));
}
}
});
}
public SaveFilePickerModuleImpl(ReactApplicationContext reactContext) {
this.reactContext = reactContext;
}
public void saveFile(String filename) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("text/html");
intent.putExtra(Intent.EXTRA_TITLE, filename);
ActivityResultLauncher<Intent> launcher = activityLauncher;
if (launcher == null) {
if (listener != null) {
listener.onError(new Exception("Activity launcher not registered"));
}
return;
}
UiThreadUtil.runOnUiThread(() -> {
launcher.launch(intent);
});
}
}
In this guide, we will be using generic ActivityResultContracts.StartActivityForResult
contract and raw intent action,
to showcase, how to work with all possible types of actions.
However, Android provides a set of standard contracts for popular actions (e.g. launching a camera, or requesting a permission),
you can visit Android docs to learn more.
SaveFilePickerModuleImpl
is divided into 2 parts: static and non-static.
In the static part we will declare ActivityResultLauncher
variable that will be responsible for launching file activity picker and capturing picker's result.
The registerActivityLauncher
will take the activity as a parameter and invoke registerForActivityResult
on that activity. The result callback will be invoked once the file activity picker will be closed and it will contain the uri of the file's destination.
That result will be then forwarded to the SaveFilePickerListener
instance.
Registering activity launcher will need to be done when the activity is started (typically in Activity#onCreate
method), we will do that later in the next steps.
To learn more about communication between android activities, visit Android's "Getting a result from an Activity" guide.
The non-static part is a single method saveFile
that will be responsible just for invoking the ActivityResultLauncher
with the ACTION_CREATE_DOCUMENT
intent action.
The launcher needs to be used on the main/ui thread, so to do it, we will use UiThreadUtil.runOnUiThread
helper.
For more on "save file" functionality visit Android docs.
SaveFilePickerModule.java
Now, let's move to the module that will manage function calls from the JS world:
package com.savefilepickerpackage
import android.net.Uri;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Declare Java class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
public class SaveFilePickerModule extends NativeSaveFilePickerModuleSpec {
public static final String NAME = SaveFilePickerModuleImpl.NAME;
// Use shared module implementation and forward react application context
private final SaveFilePickerModuleImpl moduleImpl;
public SaveFilePickerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.moduleImpl = new SaveFilePickerModuleImpl(reactContext);
}
// Return the name of the module - it should match the name provided in JS specification
@Override
public String getName() {
return SaveFilePickerModuleImpl.NAME;
}
// Exported methods must be annotated with @ReactMethod decorator
@ReactMethod
public void saveFileWithCallback(String filename, Callback callback) {
//
}
@ReactMethod
public void saveFileWithPromise(String filename, Promise promise) {
//
}
}
Here we declare the SaveFilePickerModule
class.
It extends codegenerated spec class and takes ReactApplicationContext
instance as constructor parameter.
Additionally, SaveFilePickerModule
is annotated with ReactModule
decorator.
Static constant NAME
matches the value declared in JS specification.
For now, we start by adding empty implementations for methods declared in codegenerated spec class.
Now, let's add private variables for callback/promise blocks - we'll need to save them, once the exported methods are invoked and use, once there's result from the file activity picker.
// ...
/**
* Declare Java class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
public class SaveFilePickerModule extends NativeSaveFilePickerModuleSpec {
// ...
// Use shared module implementation and forward react application context
private final SaveFilePickerModuleImpl moduleImpl;
private @Nullable Callback callbackBlock = null;
private @Nullable Promise promiseBlock = null;
private String sourceFilename = "";
// ...
}
Next step is to implement listener object that will use callback/promise blocks to communicate the result to the JS code. We will do it inside constructor:
// ...
/**
* Declare Java class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
public class SaveFilePickerModule extends NativeSaveFilePickerModuleSpec {
// ...
private @Nullable Callback callbackBlock = null;
private @Nullable Promise promiseBlock = null;
private String sourceFilename = "";
public SaveFilePickerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.moduleImpl = new SaveFilePickerModuleImpl(reactContext);
SaveFilePickerModuleImpl.listener = new SaveFilePickerModuleImpl.SaveFilePickerListener() {
@Override
public void onSuccess(@Nullable Uri uri) {
try {
InputStream sourceInputStream = reactContext
.getApplicationContext()
.getAssets()
.open(sourceFilename);
if (uri != null) {
OutputStream outputStream = reactContext
.getContentResolver()
.openOutputStream(uri);
byte[] buffer = new byte[8 * 1024];
int bytes = sourceInputStream.read(buffer);
while (bytes >= 0) {
outputStream.write(buffer, 0, bytes);
bytes = sourceInputStream.read(buffer);
}
outputStream.close();
}
sourceInputStream.close();
} catch (Exception ignored) {}
Callback callback = callbackBlock;
Promise promise = promiseBlock;
if (callback != null) {
WritableMap payload = Arguments.createMap();
payload.putBoolean("success", true);
payload.putBoolean("cancelled", false);
callback.invoke(payload);
} else if (promise != null) {
promise.resolve(true);
}
callbackBlock = null;
promiseBlock = null;
sourceFilename = "";
}
@Override
public void onCancel() {
Callback callback = callbackBlock;
Promise promise = promiseBlock;
if (callback != null) {
WritableMap payload = Arguments.createMap();
payload.putBoolean("success", false);
payload.putBoolean("cancelled", true);
callback.invoke(payload);
} else if (promise != null) {
promise.resolve(false);
}
callbackBlock = null;
promiseBlock = null;
sourceFilename = "";
}
@Override
public void onError(Exception error) {
Callback callback = callbackBlock;
Promise promise = promiseBlock;
if (callback != null) {
WritableMap payload = Arguments.createMap();
WritableMap errorMap = Arguments.createMap();
errorMap.putInt("code", 1234);
errorMap.putString("message", error.getMessage());
payload.putMap("error", errorMap);
payload.putBoolean("success", false);
payload.putBoolean("cancelled", false);
callback.invoke(payload);
} else if (promise != null) {
promise.reject("1234", error.getMessage());
}
callbackBlock = null;
promiseBlock = null;
sourceFilename = "";
}
};
}
// ...
}
For the promise block, we just resolve with boolean value or reject with an error.
For callback block, we want to create an object argument - to do that, let's use Arguments.createMap
utility helper.
The important thing and the main difference between Android & iOS implementation is that the Android's save file picker is actually only creating a new empty file instead of copying the source file like on iOS.
That means, we have to manually copy the content of the requested file into the new location.
In our case, we are doing it in the listener's onSuccess
function:
@Override
public void onSuccess(@Nullable Uri uri) {
try {
InputStream sourceInputStream = reactContext
.getApplicationContext()
.getAssets()
.open(sourceFilename);
if (uri != null) {
OutputStream outputStream = reactContext
.getContentResolver()
.openOutputStream(uri);
byte[] buffer = new byte[8 * 1024];
int bytes = sourceInputStream.read(buffer);
while (bytes >= 0) {
outputStream.write(buffer, 0, bytes);
bytes = sourceInputStream.read(buffer);
}
outputStream.close();
}
sourceInputStream.close();
} catch (Exception ignored) {}
// ...
}
That code takes specified file from application's assets directory, opens the file under the provided uri
and copies the content of the source file to the target file.
Last part is to use SaveFilePickerModuleImpl
class inside exported methods:
// ...
/**
* Declare Java class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
public class SaveFilePickerModule extends NativeSaveFilePickerModuleSpec {
// ...
// Return the name of the module - it should match the name provided in JS specification
@Override
public String getName() {
return SaveFilePickerModuleImpl.NAME;
}
// Exported methods are overriden - based on the spec class
@Override
public void saveFileWithCallback(String filename, Callback callback) {
callbackBlock = callback;
sourceFilename = filename;
moduleImpl.saveFile(filename);
}
@Override
public void saveFileWithPromise(String filename, Promise promise) {
promiseBlock = promise;
sourceFilename = filename;
moduleImpl.saveFile(filename);
}
}
In the methods body, we save the callback/promise arguments and we use SaveFilePickerModuleImpl#saveFile
method to launch file activity picker.
Complete SaveFilePickerModule.java
file
package com.savefilepickerpackage;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Declare Java class for new arch native module implementation
*
* Each turbo module extends codegenerated spec class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
public class SaveFilePickerModule extends NativeSaveFilePickerModuleSpec {
public static final String NAME = SaveFilePickerModuleImpl.NAME;
// Use shared module implementation and forward react application context
private final SaveFilePickerModuleImpl moduleImpl;
private @Nullable Callback callbackBlock = null;
private @Nullable Promise promiseBlock = null;
private String sourceFilename = "";
public SaveFilePickerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.moduleImpl = new SaveFilePickerModuleImpl(reactContext);
SaveFilePickerModuleImpl.listener = new SaveFilePickerModuleImpl.SaveFilePickerListener() {
@Override
public void onSuccess(@Nullable Uri uri) {
try {
InputStream sourceInputStream = reactContext
.getApplicationContext()
.getAssets()
.open(sourceFilename);
if (uri != null) {
OutputStream outputStream = reactContext
.getContentResolver()
.openOutputStream(uri);
byte[] buffer = new byte[8 * 1024];
int bytes = sourceInputStream.read(buffer);
while (bytes >= 0) {
outputStream.write(buffer, 0, bytes);
bytes = sourceInputStream.read(buffer);
}
outputStream.close();
}
sourceInputStream.close();
} catch (Exception ignored) {}
Callback callback = callbackBlock;
Promise promise = promiseBlock;
if (callback != null) {
WritableMap payload = Arguments.createMap();
payload.putBoolean("success", true);
payload.putBoolean("cancelled", false);
callback.invoke(payload);
} else if (promise != null) {
promise.resolve(true);
}
callbackBlock = null;
promiseBlock = null;
sourceFilename = "";
}
@Override
public void onCancel() {
Callback callback = callbackBlock;
Promise promise = promiseBlock;
if (callback != null) {
WritableMap payload = Arguments.createMap();
payload.putBoolean("success", false);
payload.putBoolean("cancelled", true);
callback.invoke(payload);
} else if (promise != null) {
promise.resolve(false);
}
callbackBlock = null;
promiseBlock = null;
sourceFilename = "";
}
@Override
public void onError(Exception error) {
Callback callback = callbackBlock;
Promise promise = promiseBlock;
if (callback != null) {
WritableMap payload = Arguments.createMap();
WritableMap errorMap = Arguments.createMap();
errorMap.putInt("code", 1234);
errorMap.putString("message", error.getMessage());
payload.putMap("error", errorMap);
payload.putBoolean("success", false);
payload.putBoolean("cancelled", false);
callback.invoke(payload);
} else if (promise != null) {
promise.reject("1234", error.getMessage());
}
callbackBlock = null;
promiseBlock = null;
sourceFilename = "";
}
};
}
// Return the name of the module - it should match the name provided in JS specification
@Override
public String getName() {
return SaveFilePickerModuleImpl.NAME;
}
// Exported methods are overriden - based on the spec class
@Override
public void saveFileWithCallback(String filename, Callback callback) {
callbackBlock = callback;
sourceFilename = filename;
moduleImpl.saveFile(filename);
}
@Override
public void saveFileWithPromise(String filename, Promise promise) {
promiseBlock = promise;
sourceFilename = filename;
moduleImpl.saveFile(filename);
}
}
Old architecture module
android/src/oldarch/java/com/savefilepickerpackage/SaveFilePickerModule.java
at other text editor and paste following content:package com.savefilepickerpackage;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* Declare Java class for old arch native module implementation
*
* Each native module extends ReactContextBaseJavaModule class
*
* Class should be annotated with @ReactModule decorator
*/
@ReactModule(name = SaveFilePickerModule.NAME)
public class SaveFilePickerModule extends ReactContextBaseJavaModule {
public static final String NAME = SaveFilePickerModuleImpl.NAME;
// Use shared module implementation and forward react application context
private final SaveFilePickerModuleImpl moduleImpl;
private @Nullable Callback callbackBlock = null;
private @Nullable Promise promiseBlock = null;
private String sourceFilename = "";
public SaveFilePickerModule(ReactApplicationContext reactContext) {
super(reactContext);
this.moduleImpl = new SaveFilePickerModuleImpl(reactContext);
SaveFilePickerModuleImpl.listener = new SaveFilePickerModuleImpl.SaveFilePickerListener() {
@Override
public void onSuccess(@Nullable Uri uri) {
try {
InputStream sourceInputStream = reactContext
.getApplicationContext()
.getAssets()
.open(sourceFilename);
if (uri != null) {
OutputStream outputStream = reactContext
.getContentResolver()
.openOutputStream(uri);
byte[] buffer = new byte[8 * 1024];
int bytes = sourceInputStream.read(buffer);
while (bytes >= 0) {
outputStream.write(buffer, 0, bytes);
bytes = sourceInputStream.read(buffer);
}
outputStream.close();
}
sourceInputStream.close();
} catch (Exception ignored) {}
Callback callback = callbackBlock;
Promise promise = promiseBlock;
if (callback != null) {
WritableMap payload = Arguments.createMap();
payload.putBoolean("success", true);
payload.putBoolean("cancelled", false);
callback.invoke(payload);
} else if (promise != null) {
promise.resolve(true);
}
callbackBlock = null;
promiseBlock = null;
sourceFilename = "";
}
@Override
public void onCancel() {
Callback callback = callbackBlock;
Promise promise = promiseBlock;
if (callback != null) {
WritableMap payload = Arguments.createMap();
payload.putBoolean("success", false);
payload.putBoolean("cancelled", true);
callback.invoke(payload);
} else if (promise != null) {
promise.resolve(false);
}
callbackBlock = null;
promiseBlock = null;
sourceFilename = "";
}
@Override
public void onError(Exception error) {
Callback callback = callbackBlock;
Promise promise = promiseBlock;
if (callback != null) {
WritableMap payload = Arguments.createMap();
WritableMap errorMap = Arguments.createMap();
errorMap.putInt("code", 1234);
errorMap.putString("message", error.getMessage());
payload.putMap("error", errorMap);
payload.putBoolean("success", false);
payload.putBoolean("cancelled", false);
callback.invoke(payload);
} else if (promise != null) {
promise.reject("1234", error.getMessage());
}
callbackBlock = null;
promiseBlock = null;
sourceFilename = "";
}
};
}
// Return the name of the module - it should match the name provided in JS specification
@Override
public String getName() {
return SaveFilePickerModuleImpl.NAME;
}
// Exported methods must be annotated with @ReactMethod decorator
@ReactMethod
public void saveFileWithCallback(String filename, Callback callback) {
callbackBlock = callback;
sourceFilename = filename;
moduleImpl.saveFile(filename);
}
@ReactMethod
public void saveFileWithPromise(String filename, Promise promise) {
promiseBlock = promise;
sourceFilename = filename;
moduleImpl.saveFile(filename);
}
}
Let's finalize it by exporting the module in the TurboReactPackage
instance.
SaveFilePickerTurboPackage.java
The last thing we need to do is to export SaveFilePickerModule
in the TurboReactPackage
instance. Let's go to SaveFilePickerTurboPackage.java
and add our new module.
package com.savefilepickerpackage;
import androidx.annotation.Nullable;
import com.facebook.react.TurboReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;
import java.util.HashMap;
import java.util.Map;
public class SaveFilePickerTurboPackage extends TurboReactPackage {
/**
* Initialize and export modules based on the name of the required module
*/
@Override
@Nullable
public NativeModule getModule(String name, ReactApplicationContext reactContext) {
if (name.equals(SaveFilePickerModule.NAME)) {
return new SaveFilePickerModule(reactContext);
}
return null;
}
/**
* Declare info about exported modules
*/
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
/**
* Here declare the array of exported modules
*/
Class<? extends NativeModule>[] moduleList = new Class[] {
SaveFilePickerModule.class
};
final Map<String, ReactModuleInfo> reactModuleInfoMap = new HashMap<>();
/**
* And here just iterate on that array and produce the info provider instance
*/
for (Class<? extends NativeModule> moduleClass : moduleList) {
ReactModule reactModule = moduleClass.getAnnotation(ReactModule.class);
reactModuleInfoMap.put(
reactModule.name(),
new ReactModuleInfo(
reactModule.name(),
moduleClass.getName(),
true,
reactModule.needsEagerInit(),
reactModule.hasConstants(),
reactModule.isCxxModule(),
TurboModule.class.isAssignableFrom(moduleClass)
)
);
}
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
return reactModuleInfoMap;
}
};
}
}
To export the module, as the first step, we need to return it from getModule
method inside SaveFilePickerTurboPackage
, if it's requested (the method takes name as a parameter and makes decision which module should be served).
The second step is to implement getReactModuleInfoProvider
method, where the module is injected to the info provider instance.
Register activity launcher
We have completed the implementation of the Android part. However, there's one specific manual step that we need to do for this kind of module and that is registering the activity launcher inside the app's main activity.
- Kotlin
- Java
import com.savefilepickerpackage.SaveFilePickerModuleImpl // add this import
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String {
return "rnbridgingtutorial"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
SaveFilePickerModuleImpl.registerActivityLauncher(this) // add this line
}
// ...
}
import com.savefilepickerpackage.SaveFilePickerModuleImpl; // add this import
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
@Override
public String getMainComponentName() {
return "rnbridgingtutorial";
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SaveFilePickerModuleImpl.registerActivityLauncher(this); // add this line
}
// ...
}
In our test Android app, inside MainActivity
class we are registering the activity launcher.
Without that, our module wouldn't be able to launch another activities.
You can check training repo for Kotlin implementation here and Java implementation here.
That's Android part, now let's wrap things up and try to use save file picker module in action!