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 native-list-package inside project-tree

The native-list-package contains 3 packages with the same name com.nativelistpackage. After expanding them, you'll notice that these contain following things:

  • code-generated Java spec files
  • AndroidNativeListViewManager view manager class stub files
  • AndroidNativeListFragment fragment class stub file
  • DataItem class stub file
  • NativeListAdapter class stub file
  • NativeListViewHolder class stub file
  • NativeListTurboPackage class stub file

Additionally, if you search for the res resources folder, you'll see the layout directory inside, which contains two layout files:

  • card_item that will contain the layout for each list item
  • fragment_list that will contain the RecyclerView element

Let's start implementing!

Add native libraries as dependencies for the package

When developing some Android native code, you often need to use some external package, whether it's from Jetpack, MaterialComponents or some 3rd party. Usually those libraries are integrated with Gradle.

tip

For more information on how to add dependencies to Android project visit Android's dedicated docs.

Each RN library that includes some Android native code is, in fact, integrated with Gradle and our native list will also need to depend on Jetpack libraries:

So let's not waste time and navigate to the build.gradle in our package.

android/build.gradle
buildscript {
// ...
}

// ...

android {
// ...
}

repositories {
// ...
}

dependencies {
// ...

// Add the dependency to the Jetpack libraries
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.recyclerview:recyclerview:1.2.1"
}

To add a dependency we need to write implementation keyword and declare the package (and its version) we want to include. After that you can invoke Gradle Sync in the Android Studio.

info

In this guide, version 1.0.0 of CardView, 2.1.4 of ConstraintLayout and 1.2.1 of RecyclerView are used. In your case these versions may be different, you can visit Jetpack's releases page and check available versions.

card_item.xml

Let's start by defining XML layouts for the list and its items. Open android/src/main/res/layout/card_item.xml file in Android Studio and paste following code:

android/src/main/res/layout/card_item.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/list_card"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:layout_marginRight="10dp"
app:cardBackgroundColor="#89CC65"
app:cardCornerRadius="10dp"
app:cardElevation="10dp">

<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/list_card_image"
android:layout_width="match_parent"
android:layout_height="70dp" />

<TextView
android:id="@+id/list_card_label"
android:textSize="10sp"
android:gravity="center"
android:textAlignment="center"
android:layout_width="match_parent"
android:layout_height="20dp" />

</LinearLayout>

</androidx.cardview.widget.CardView>

Here we are defining the CardView element that will hold an image and text views. Each of these will have android:id attribute assigned - in the code, we will be able to interact with these elements by their ids. To simplify the example, the card items will have a fixed size 100x100.

Next, let's declare the layout for the list element.

fragment_list.xml

Open android/src/main/res/layout/fragment_list.xml file in Android Studio and paste the following code:

android/src/main/res/layout/fragment_list.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Here we're wrapping RecyclerView (the native list element) inside a ConstraintLayout and make it fill parent's size. As in the card item layout, we declare android:id attribute, to reference the RecyclerView later inside the code.

DataItem.kt

After defining the layouts, let's jump to code and start by defining Kotlin data class DataItem which will be used to hold items passed from JS code:

android/src/main/java/com/nativelistpackage/DataItem.kt
package com.nativelistpackage

data class DataItem(
val imageUrl: String,
val description: String,
)

NativeListViewHolder.kt

Now, we need to define the view holder class that will be used by RecyclerView to keep reference to the UI elements inside single list item

android/src/main/java/com/nativelistpackage/NativeListViewHolder.kt
package com.nativelistpackage

import android.view.View
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView

class NativeListViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) {
var imageView: ImageView
var label: TextView

init {
imageView = itemView.findViewById(R.id.list_card_image)
label = itemView.findViewById(R.id.list_card_label)
}
}

Each class needs to extend base RecyclerView.ViewHolder class. Additionally we declare properties for image and text views, to make it easier later to interact with them.

NativeListAdapter.kt

With view holder ready, let's use it inside RecyclerView.Adapter custom class:

android/src/main/java/com/nativelistpackage/NativeListAdapter.kt
package com.nativelistpackage

import android.content.Context
import android.content.res.Resources
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView
import java.lang.reflect.Field

class NativeListAdapter(private val context: Context): RecyclerView.Adapter<NativeListViewHolder>() {
var data: List<DataItem> = emptyList()
var placeholderImage: String = ""

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NativeListViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.card_item, parent, false)
return NativeListViewHolder(itemView)
}

override fun onBindViewHolder(holder: NativeListViewHolder, position: Int) {
val drawable = ResourcesCompat.getDrawable(
context.resources,
getDrawableIdWithName(placeholderImage),
null
)
holder.imageView.setImageDrawable(drawable)
holder.label.text = data[position].description
}

override fun getItemCount(): Int {
return data.size
}

private fun getDrawableIdWithName(name: String): Int {
val appResources: Resources = context.resources
var resourceId = appResources.getIdentifier(name, "drawable", context.packageName)
if (resourceId == 0) {
// If drawable is not present in app's resources, check system's resources
resourceId = getResId(name, android.R.drawable::class.java)
}
return resourceId
}

private fun getResId(resName: String, c: Class<*>): Int {
return try {
val idField: Field = c.getDeclaredField(resName)
idField.getInt(idField)
} catch (e: Exception) {
e.printStackTrace()
0
}
}
}

Inside the custom adapter class we have to override 3 methods:

  • onCreateViewHolder - it creates the view for the item and wraps it in the instance of RecyclerView.ViewHolder class (in this case NativeListViewHolder)
  • onBindViewHolder - it binds the data for specific item to dedicated view holder instance (in this case the description text and the name of the system image to the drawable displayed in the image view)
  • getItemCount - it returns the size of the list (in this case size of data property, which represents the JS prop value)
info

For learning purposes, we only use system images/icons for the image view. After completing this guide, you can work on enhancing the experience by using remote images with e.g. Glide library.

AndroidNativeListViewFragment.kt

Let's make all RecyclerView setup in the custom Fragment class:

android/src/main/java/com/nativelistpackage/AndroidNativeListViewFragment.kt
package com.nativelistpackage

import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap

class AndroidNativeListViewFragment: Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.fragment_list, container, false)
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

val recyclerView: RecyclerView = view.findViewById(R.id.list)
recyclerView.layoutManager = GridLayoutManager(
requireContext(),
NUM_OF_COLUMNS,
GridLayoutManager.VERTICAL,
false
)
recyclerView.adapter = NativeListAdapter(requireContext())
}

fun setBackgroundColor(backgroundColor: Int?) {
requireView().setBackgroundColor(backgroundColor ?: Color.TRANSPARENT)
}

fun setData(data: ReadableArray) {
val listData = mutableListOf<DataItem>()
for (i in 0 until data.size()) {
val item = data.getMap(i)
listData.add(DataItem(item.getString("imageUrl")!!, item.getString("description")!!))
}
val recyclerView: RecyclerView = requireView().findViewById(R.id.list)
(recyclerView.adapter as NativeListAdapter).data = listData
(recyclerView.adapter as NativeListAdapter).notifyDataSetChanged()
}

fun setOptions(options: ReadableMap) {
val placeholderImage = options.getString("placeholderImage")!!
val recyclerView: RecyclerView = requireView().findViewById(R.id.list)
(recyclerView.adapter as NativeListAdapter).placeholderImage = placeholderImage
}

fun scrollToItem(index: Int) {
val recyclerView: RecyclerView = requireView().findViewById(R.id.list)
recyclerView.smoothScrollToPosition(index)
}

companion object {
const val NAME = "AndroidNativeListView"
private const val NUM_OF_COLUMNS = 3
}
}

To connect the XML layout we created before, we need to override onCreateView method. That method returns the root view of the fragment, which in our case is inflated XML layout with the RecyclerView.

In order to set the properties of the RecyclerView, we need to override onViewCreated method. It provides the instance of fragment's root view, that we can use to find the RecyclerView and attach the adapter class created before. Additionally we need to specify the type of RecyclerView.LayoutManager used by the list - in our case we want to have vertical list with multiple columns and we can achieve it with GridLayoutManager (to simplify the example, we will set 3 columns - after finishing the guide if you want, you can think how to make it dynamic and controlled from JS code).

To handle props and scroll command, we also need to declare additional methods that interact with the RecyclerView.

With the fragment ready, let's finish by connecting it to the RN's views hierarchy.

AndroidNativeListViewManager.kt

The view manager class will connect the custom fragment with our RN app. However, RN does not support Android fragments out-of-the-box. To embed a fragment inside a view, we will use FragmentContainerView - let's start by creating the boilerplate:

android/src/newarch/java/com/nativelistpackage/AndroidNativeListViewManager.kt
package com.nativelistpackage

import androidx.fragment.app.FragmentContainerView
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerDelegate
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerInterface

@ReactModule(name = AndroidNativeListViewFragment.NAME)
class AndroidNativeListViewManager : SimpleViewManager<FragmentContainerView>(), AndroidNativeListViewManagerInterface<FragmentContainerView> {
private val mDelegate: ViewManagerDelegate<FragmentContainerView> = AndroidNativeListViewManagerDelegate(this)

override fun getName() = AndroidNativeListViewFragment.NAME

override fun getDelegate() = mDelegate

override fun receiveCommand(root: FragmentContainerView, commandId: String?, args: ReadableArray?) {
mDelegate.receiveCommand(root, commandId, args)
}

override fun createViewInstance(reactContext: ThemedReactContext): FragmentContainerView {
return FragmentContainerView(reactContext)
}

@ReactProp(name = "data")
override fun setData(view: FragmentContainerView, data: ReadableArray?) {
//
}

@ReactProp(name = "options")
override fun setOptions(view: FragmentContainerView, options: ReadableMap?) {
//
}

override fun scrollToItem(view: FragmentContainerView, index: Int) {
//
}
}

We start by creating view manager class that extends SimpleViewManager base class and implements code-generated interface. The class needs to override required methods (getName, getDelegate, receiveCommand & createViewInstance) as well as prop & command handlers declared in code-generated interface.

You can take a look at the createViewInstance method - it returns already mentioned FragmentContainerView. This is specialized kind of Android UI element, that is able to encapsulate custom fragment layout and handle its lifecycle.

Now, let's define a few helper functions that will be needed when adding/removing fragment layout:

android/src/newarch/java/com/nativelistpackage/AndroidNativeListViewManager.kt
package com.nativelistpackage

import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerDelegate
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerInterface

@ReactModule(name = AndroidNativeListViewFragment.NAME)
class AndroidNativeListViewManager : SimpleViewManager<FragmentContainerView>(), AndroidNativeListViewManagerInterface<FragmentContainerView> {
private val mDelegate: ViewManagerDelegate<FragmentContainerView> = AndroidNativeListViewManagerDelegate(this)

private var mHeight: Int = 0
private var mWidth: Int = 0

override fun getName() = AndroidNativeListViewFragment.NAME

override fun getDelegate() = mDelegate

override fun receiveCommand(root: FragmentContainerView, commandId: String?, args: ReadableArray?) {
mDelegate.receiveCommand(root, commandId, args)
}

override fun createViewInstance(reactContext: ThemedReactContext): FragmentContainerView {
return FragmentContainerView(reactContext)
}

@ReactProp(name = "data")
override fun setData(view: FragmentContainerView, data: ReadableArray?) {
//
}

@ReactProp(name = "options")
override fun setOptions(view: FragmentContainerView, options: ReadableMap?) {
//
}

override fun scrollToItem(view: FragmentContainerView, index: Int) {
//
}

private fun layoutChildren(view: View) {
val width = mWidth
val height = mHeight

view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
)
view.layout(0, 0, width, height)
}

private fun findFragment(fragmentManager: FragmentManager, view: View): AndroidNativeListViewFragment? {
return fragmentManager.findFragmentByTag(getFragmentTag(view)) as? AndroidNativeListViewFragment
}

private fun getFragmentManager(view: View): FragmentManager? {
val reactContext = view.context as? ThemedReactContext ?: return null
val activity = reactContext.currentActivity as? FragmentActivity ?: return null

return activity.supportFragmentManager
}

private fun getFragmentTag(view: View) = "AndroidNativeListViewFragment-" + view.id
}

The first helper method will be used to measure and layout the FragmentContainerView instance, based on the size declared inside JS code (via style prop). Other helpers are used to simplify getting the custom fragment instance, the FragmentManager (used to perform operations with fragments) and the tag associated to custom fragment's instance.

Now let's add two functions responsible for fragment mounting and unmounting:

android/src/newarch/java/com/nativelistpackage/AndroidNativeListViewManager.kt
package com.nativelistpackage

import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerDelegate
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerInterface

@ReactModule(name = AndroidNativeListViewFragment.NAME)
class AndroidNativeListViewManager : SimpleViewManager<FragmentContainerView>(), AndroidNativeListViewManagerInterface<FragmentContainerView> {
private val mDelegate: ViewManagerDelegate<FragmentContainerView> = AndroidNativeListViewManagerDelegate(this)

private var mHeight: Int = 0
private var mWidth: Int = 0

override fun getName() = AndroidNativeListViewFragment.NAME

override fun getDelegate() = mDelegate

override fun receiveCommand(root: FragmentContainerView, commandId: String?, args: ReadableArray?) {
mDelegate.receiveCommand(root, commandId, args)
}

override fun createViewInstance(reactContext: ThemedReactContext): FragmentContainerView {
return FragmentContainerView(reactContext)
}

@ReactProp(name = "data")
override fun setData(view: FragmentContainerView, data: ReadableArray?) {
//
}

@ReactProp(name = "options")
override fun setOptions(view: FragmentContainerView, options: ReadableMap?) {
//
}

override fun scrollToItem(view: FragmentContainerView, index: Int) {
//
}

private fun mountFragment(view: FragmentContainerView) {
UiThreadUtil.assertOnUiThread()
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
val fragment = findFragment(fragmentManager, view)

if (fragment != null) {
view.post {
layoutChildren(view)
}
return
}

val newFragment = AndroidNativeListViewFragment()
view.removeAllViews()
val transaction = fragmentManager.beginTransaction()
transaction.add(newFragment, getFragmentTag(view))
transaction.runOnCommit {
view.addView(newFragment.requireView())
layoutChildren(view)
}
transaction.commitNowAllowingStateLoss()
}
}

private fun unmountFragment(view: FragmentContainerView) {
UiThreadUtil.assertOnUiThread()
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
val fragment = findFragment(fragmentManager, view)

if (fragment != null) {
val transaction = fragmentManager.beginTransaction()
transaction.remove(fragment)
transaction.commitNowAllowingStateLoss()
}
}
}

// ...
}

mountFragment & unmountFragment functions will be run on the UI thread and will be responsible for adding/removing the fragment via fragment manager's transaction.

info

To learn more, visit official guides about FragmentManager and fragment transactions

Next let's synchronize those 2 functions with the lifecycle of our view manager class:

android/src/newarch/java/com/nativelistpackage/AndroidNativeListViewManager.kt
package com.nativelistpackage

import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerDelegate
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerInterface

@ReactModule(name = AndroidNativeListViewFragment.NAME)
class AndroidNativeListViewManager : SimpleViewManager<FragmentContainerView>(), AndroidNativeListViewManagerInterface<FragmentContainerView> {
private val mDelegate: ViewManagerDelegate<FragmentContainerView> = AndroidNativeListViewManagerDelegate(this)

private var mHeight: Int = 0
private var mWidth: Int = 0

override fun getName() = AndroidNativeListViewFragment.NAME

override fun getDelegate() = mDelegate

override fun receiveCommand(root: FragmentContainerView, commandId: String?, args: ReadableArray?) {
mDelegate.receiveCommand(root, commandId, args)
}

override fun createViewInstance(reactContext: ThemedReactContext): FragmentContainerView {
return FragmentContainerView(reactContext)
}

override fun onDropViewInstance(view: FragmentContainerView) {
unmountFragment(view)

super.onDropViewInstance(view)
}

override fun addEventEmitters(reactContext: ThemedReactContext, view: FragmentContainerView) {
super.addEventEmitters(reactContext, view)
// Mount fragment here, because here the view already has reactTag set as a view.id
mountFragment(view)
}

@ReactProp(name = "data")
override fun setData(view: FragmentContainerView, data: ReadableArray?) {
//
}

@ReactProp(name = "options")
override fun setOptions(view: FragmentContainerView, options: ReadableMap?) {
//
}

override fun scrollToItem(view: FragmentContainerView, index: Int) {
//
}

// ...
}

The addEventEmitters method is called after createViewInstance with view already having its id set up. This is the time when bridged view is created and the fragment layout should be injected.

On the other hand, when the view is about to be destroyed, we should use onDropViewInstance method, to remove our fragment layout from the view hierarchy.

The last thing to do in the view manager class is to connect the prop & command handlers to the fragment instance:

android/src/newarch/java/com/nativelistpackage/AndroidNativeListViewManager.kt
package com.nativelistpackage

import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.annotations.ReactPropGroup
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerDelegate
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerInterface

@ReactModule(name = AndroidNativeListViewFragment.NAME)
class AndroidNativeListViewManager : SimpleViewManager<FragmentContainerView>(), AndroidNativeListViewManagerInterface<FragmentContainerView> {
private val mDelegate: ViewManagerDelegate<FragmentContainerView> = AndroidNativeListViewManagerDelegate(this)

private var mHeight: Int = 0
private var mWidth: Int = 0

override fun getName() = AndroidNativeListViewFragment.NAME

override fun getDelegate() = mDelegate

override fun receiveCommand(root: FragmentContainerView, commandId: String?, args: ReadableArray?) {
mDelegate.receiveCommand(root, commandId, args)
}

override fun createViewInstance(reactContext: ThemedReactContext): FragmentContainerView {
return FragmentContainerView(reactContext)
}

override fun onDropViewInstance(view: FragmentContainerView) {
unmountFragment(view)

super.onDropViewInstance(view)
}

override fun addEventEmitters(reactContext: ThemedReactContext, view: FragmentContainerView) {
super.addEventEmitters(reactContext, view)
// Mount fragment here, because here the view already has reactTag set as a view.id
mountFragment(view)
}

@ReactProp(name = "data")
override fun setData(view: FragmentContainerView, data: ReadableArray?) {
if (data == null) {
return
}
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.setData(data)
}
}

@ReactProp(name = "options")
override fun setOptions(view: FragmentContainerView, options: ReadableMap?) {
if (options == null) {
return
}
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.setOptions(options)
}
}

override fun scrollToItem(view: FragmentContainerView, index: Int) {
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.scrollToItem(index)
}
}

@ReactProp(name = "backgroundColor", customType = "Color")
fun setBackgroundColor(view: FragmentContainerView, backgroundColor: Int?) {
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.setBackgroundColor(backgroundColor)
}
}

@ReactPropGroup(names = ["width", "height"], customType = "Style")
fun setStyle(view: FragmentContainerView, index: Int, value: Dynamic?) {
if (value == null) {
return
}

if (index == 0) {
mWidth = PixelUtil.toPixelFromDIP(value.asDouble()).toInt()
}

if (index == 1) {
mHeight = PixelUtil.toPixelFromDIP(value.asDouble()).toInt()
}

view.post {
layoutChildren(view)
}
}

// ...
}

For the data & options props as well as scrollToItem command we just need to forward the input arguments to fragment instance's methods.

Additionally, we need to explicitly handle styles from style prop. In case of backgroundColor, we need to set it on the fragment instance (and not the FragmentContainerView). And for width & height values we need to trigger measuring & layout of the view (with our layoutChildren helper).

Complete AndroidNativeListViewManager.kt file
package com.nativelistpackage

import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.annotations.ReactPropGroup
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerDelegate
import com.facebook.react.viewmanagers.AndroidNativeListViewManagerInterface

@ReactModule(name = AndroidNativeListViewFragment.NAME)
class AndroidNativeListViewManager : SimpleViewManager<FragmentContainerView>(), AndroidNativeListViewManagerInterface<FragmentContainerView> {
private val mDelegate: ViewManagerDelegate<FragmentContainerView> = AndroidNativeListViewManagerDelegate(this)

private var mHeight: Int = 0
private var mWidth: Int = 0

override fun getName() = AndroidNativeListViewFragment.NAME

override fun getDelegate() = mDelegate

override fun receiveCommand(root: FragmentContainerView, commandId: String?, args: ReadableArray?) {
mDelegate.receiveCommand(root, commandId, args)
}

override fun createViewInstance(reactContext: ThemedReactContext): FragmentContainerView {
return FragmentContainerView(reactContext)
}

override fun onDropViewInstance(view: FragmentContainerView) {
unmountFragment(view)

super.onDropViewInstance(view)
}

override fun addEventEmitters(reactContext: ThemedReactContext, view: FragmentContainerView) {
super.addEventEmitters(reactContext, view)
// Mount fragment here, because here the view already has reactTag set as a view.id
mountFragment(view)
}

@ReactProp(name = "data")
override fun setData(view: FragmentContainerView, data: ReadableArray?) {
if (data == null) {
return
}
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.setData(data)
}
}

@ReactProp(name = "options")
override fun setOptions(view: FragmentContainerView, options: ReadableMap?) {
if (options == null) {
return
}
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.setOptions(options)
}
}

override fun scrollToItem(view: FragmentContainerView, index: Int) {
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.scrollToItem(index)
}
}

@ReactProp(name = "backgroundColor", customType = "Color")
fun setBackgroundColor(view: FragmentContainerView, backgroundColor: Int?) {
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.setBackgroundColor(backgroundColor)
}
}

@ReactPropGroup(names = ["width", "height"], customType = "Style")
fun setStyle(view: FragmentContainerView, index: Int, value: Dynamic?) {
if (value == null) {
return
}

if (index == 0) {
mWidth = PixelUtil.toPixelFromDIP(value.asDouble()).toInt()
}

if (index == 1) {
mHeight = PixelUtil.toPixelFromDIP(value.asDouble()).toInt()
}

view.post {
layoutChildren(view)
}
}

private fun mountFragment(view: FragmentContainerView) {
UiThreadUtil.assertOnUiThread()
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
val fragment = findFragment(fragmentManager, view)

if (fragment != null) {
view.post {
layoutChildren(view)
}
return
}

val newFragment = AndroidNativeListViewFragment()
view.removeAllViews()
val transaction = fragmentManager.beginTransaction()
transaction.add(newFragment, getFragmentTag(view))
transaction.runOnCommit {
view.addView(newFragment.requireView())
layoutChildren(view)
}
transaction.commitNowAllowingStateLoss()
}
}

private fun unmountFragment(view: FragmentContainerView) {
UiThreadUtil.assertOnUiThread()
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
val fragment = findFragment(fragmentManager, view)

if (fragment != null) {
val transaction = fragmentManager.beginTransaction()
transaction.remove(fragment)
transaction.commitNowAllowingStateLoss()
}
}
}

private fun layoutChildren(view: View) {
val width = mWidth
val height = mHeight

view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
)
view.layout(0, 0, width, height)
}

private fun findFragment(fragmentManager: FragmentManager, view: View): AndroidNativeListViewFragment? {
return fragmentManager.findFragmentByTag(getFragmentTag(view)) as? AndroidNativeListViewFragment
}

private fun getFragmentManager(view: View): FragmentManager? {
val reactContext = view.context as? ThemedReactContext ?: return null
val activity = reactContext.currentActivity as? FragmentActivity ?: return null

return activity.supportFragmentManager
}

private fun getFragmentTag(view: View) = "AndroidNativeListViewFragment-" + view.id
}
Old architecture view manager
The implementation of old architecture view manager won't be visible in Android Studio when you have new architecture enabled. To handle that, you can open android/src/oldarch/java/com/nativelistpackage/AndroidNativeListViewManager.kt at other text editor and paste following content:

package com.nativelistpackage

import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentContainerView
import androidx.fragment.app.FragmentManager
import com.facebook.react.bridge.Dynamic
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.PixelUtil
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.uimanager.annotations.ReactPropGroup

@ReactModule(name = AndroidNativeListViewFragment.NAME)
class AndroidNativeListViewManager : SimpleViewManager<FragmentContainerView>() {
private var mHeight: Int = 0
private var mWidth: Int = 0

override fun getName() = AndroidNativeListViewFragment.NAME

override fun receiveCommand(root: FragmentContainerView, commandId: String?, args: ReadableArray?) {
super.receiveCommand(root, commandId, args)

when (commandId) {
"scrollToItem" -> {
val index = args!!.getInt(0)
scrollToItem(root, index)
}
}
}

override fun createViewInstance(reactContext: ThemedReactContext): FragmentContainerView {
return FragmentContainerView(reactContext)
}

override fun onDropViewInstance(view: FragmentContainerView) {
unmountFragment(view)

super.onDropViewInstance(view)
}

override fun addEventEmitters(reactContext: ThemedReactContext, view: FragmentContainerView) {
super.addEventEmitters(reactContext, view)
// Mount fragment here, because here the view already has reactTag set as a view.id
mountFragment(view)
}

@ReactProp(name = "data")
fun setData(view: FragmentContainerView, data: ReadableArray?) {
if (data == null) {
return
}
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.setData(data)
}
}

@ReactProp(name = "options")
fun setOptions(view: FragmentContainerView, options: ReadableMap?) {
if (options == null) {
return
}
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.setOptions(options)
}
}

private fun scrollToItem(view: FragmentContainerView, index: Int) {
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.scrollToItem(index)
}
}

@ReactProp(name = "backgroundColor", customType = "Color")
fun setBackgroundColor(view: FragmentContainerView, backgroundColor: Int?) {
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
findFragment(fragmentManager, view)?.setBackgroundColor(backgroundColor)
}
}

@ReactPropGroup(names = ["width", "height"], customType = "Style")
fun setStyle(view: FragmentContainerView, index: Int, value: Dynamic?) {
if (value == null) {
return
}

if (index == 0) {
mWidth = PixelUtil.toPixelFromDIP(value.asDouble()).toInt()
}

if (index == 1) {
mHeight = PixelUtil.toPixelFromDIP(value.asDouble()).toInt()
}

view.post {
layoutChildren(view)
}
}

private fun mountFragment(view: FragmentContainerView) {
UiThreadUtil.assertOnUiThread()
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
val fragment = findFragment(fragmentManager, view)

if (fragment != null) {
view.post {
layoutChildren(view)
}
return
}

val newFragment = AndroidNativeListViewFragment()
view.removeAllViews()
val transaction = fragmentManager.beginTransaction()
transaction.add(newFragment, getFragmentTag(view))
transaction.runOnCommit {
view.addView(newFragment.requireView())
layoutChildren(view)
}
transaction.commitNowAllowingStateLoss()
}
}

private fun unmountFragment(view: FragmentContainerView) {
UiThreadUtil.assertOnUiThread()
val fragmentManager = getFragmentManager(view)

if (fragmentManager != null) {
val fragment = findFragment(fragmentManager, view)

if (fragment != null) {
val transaction = fragmentManager.beginTransaction()
transaction.remove(fragment)
transaction.commitNowAllowingStateLoss()
}
}
}

private fun layoutChildren(view: View) {
val width = mWidth
val height = mHeight

view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
)
view.layout(0, 0, width, height)
}

private fun findFragment(fragmentManager: FragmentManager, view: View): AndroidNativeListViewFragment? {
return fragmentManager.findFragmentByTag(getFragmentTag(view)) as? AndroidNativeListViewFragment
}

private fun getFragmentManager(view: View): FragmentManager? {
val reactContext = view.context as? ThemedReactContext ?: return null
val activity = reactContext.currentActivity as? FragmentActivity ?: return null

return activity.supportFragmentManager
}

private fun getFragmentTag(view: View) = "AndroidNativeListViewFragment-" + view.id
}

NativeListTurboPackage.kt

The last thing we need to do is to export AndroidNativeListViewManager in the TurboReactPackage instance. Let's go to NativeListTurboPackage.kt and add our new module.

android/src/main/java/com/nativelistpackage/NativeListTurboPackage.kt
package com.nativelistpackage

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 com.facebook.react.uimanager.ViewManager

class NativeListTurboPackage : TurboReactPackage() {
/**
* Initialize and export modules based on the name of the required module
*/
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return null
}

/**
* Declare info about exported modules
*/
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
/**
* Here declare the array of exported modules
*/
val moduleList: Array<Class<out NativeModule?>> = arrayOf(
)
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 }
}

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
/**
* Here declare the list of exported native components
*/
return listOf(AndroidNativeListViewManager())
}
}

Here the most important bit is createViewManagers method, which returns collection of view manager classes. Because our package exports only a single view, we register one-element list, with AndroidNativeListViewManager class.

You can check training repo for Kotlin implementation here and Java implementation here.

That's Android part, now let's wrap things up and see our list in action!