Skip to main content

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 files
  • SaveFilePickerModuleImpl class stub file
  • SaveFilePickerTurboPackage class stub file

Let's start implementing!

info

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).

SaveFilePickerModuleImpl.kt

Let's start by creating a small pure Kotlin class that will be responsible for launching platform save file picker:

android/src/main/java/com/savefilepickerpackage/SaveFilePickerModuleImpl.kt
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"))
}
}
}
}
}
}
info

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.

tip

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.

tip

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:

android/src/newarch/java/com/savefilepickerpackage/SaveFilePickerModule.kt
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.

android/src/newarch/java/com/savefilepickerpackage/SaveFilePickerModule.kt
// ...

/**
* 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:

android/src/newarch/java/com/savefilepickerpackage/SaveFilePickerModule.kt
// ...

/**
* 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:

android/src/newarch/java/com/savefilepickerpackage/SaveFilePickerModule.kt
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:

android/src/newarch/java/com/savefilepickerpackage/SaveFilePickerModule.kt
// ...

/**
* 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
The implementation of old architecture module won't be visible in Android Studio when you have new architecture enabled. To handle that, you can open 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.

android/src/main/java/com/savefilepickerpackage/SaveFilePickerTurboPackage.kt
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.

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.

android/app/src/main/java/com/rnbridgingtutorial/MainActivity.kt
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
}

// ...
}

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!