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 filesAndroidNativeListFragment
fragment class stub fileDataItem
class stub fileNativeListAdapter
class stub fileNativeListViewHolder
class stub fileNativeListTurboPackage
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 itemfragment_list
that will contain theRecyclerView
element
Let's start implementing!
- Kotlin
- Java
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.
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.
- Groovy script (build.gradle)
- Kotlin script (build.gradle.kts)
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"
}
buildscript {
// ...
}
// ...
plugins {
// ...
}
// ...
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.
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:
<?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:
<?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:
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
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:
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 ofRecyclerView.ViewHolder
class (in this caseNativeListViewHolder
)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 ofdata
property, which represents the JS prop value)
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:
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:
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:
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:
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.
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:
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:
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
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.
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.
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.
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.
- Groovy script (build.gradle)
- Kotlin script (build.gradle.kts)
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"
}
buildscript {
// ...
}
// ...
plugins {
// ...
}
// ...
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.
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:
<?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:
<?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.java
After defining the layouts, let's jump to code and start by defining Java class DataItem
which will be used to hold items passed from JS code:
package com.nativelistpackage;
public class DataItem {
public String imageUrl;
public String description;
public DataItem(String imageUrl, String description) {
this.imageUrl = imageUrl;
this.description = description;
}
}
NativeListViewHolder.java
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
package com.nativelistpackage;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
public class NativeListViewHolder extends RecyclerView.ViewHolder {
public ImageView imageView;
public TextView label;
public NativeListViewHolder(View itemView) {
super(itemView);
this.imageView = itemView.findViewById(R.id.list_card_image);
this.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.java
With view holder ready, let's use it inside RecyclerView.Adapter
custom class:
package com.nativelistpackage;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.core.content.res.ResourcesCompat;
import androidx.recyclerview.widget.RecyclerView;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
public class NativeListAdapter extends RecyclerView.Adapter<NativeListViewHolder> {
public List<DataItem> data = Collections.emptyList();
public String placeholderImage = "";
private final Context context;
public NativeListAdapter(Context context) {
super();
this.context = context;
}
@Override
public NativeListViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_item, parent, false);
return new NativeListViewHolder(itemView);
}
@Override
public void onBindViewHolder(NativeListViewHolder holder, int position) {
Drawable drawable = ResourcesCompat.getDrawable(
context.getResources(),
getDrawableIdWithName(placeholderImage),
null
);
holder.imageView.setImageDrawable(drawable);
holder.label.setText(data.get(position).description);
}
@Override
public int getItemCount() {
return data.size();
}
private int getDrawableIdWithName(String name) {
Resources appResources = context.getResources();
int resourceId = appResources.getIdentifier(name, "drawable", context.getPackageName());
if (resourceId == 0) {
// If drawable is not present in app's resources, check system's resources
resourceId = getResId(name, android.R.drawable.class);
}
return resourceId;
}
private int getResId(String resName, Class c) {
try {
Field idField = c.getDeclaredField(resName);
return idField.getInt(idField);
} catch (Exception e) {
e.printStackTrace();
return 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 ofRecyclerView.ViewHolder
class (in this caseNativeListViewHolder
)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 ofdata
property, which represents the JS prop value)
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.java
Let's make all RecyclerView
setup in the custom Fragment
class:
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.annotation.NonNull;
import androidx.annotation.Nullable;
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;
import java.util.ArrayList;
import java.util.List;
public class AndroidNativeListViewFragment extends Fragment {
public static final String NAME = "AndroidNativeListView";
private static final int NUM_OF_COLUMNS = 3;
@Override
public View onCreateView(
LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState
) {
return inflater.inflate(R.layout.fragment_list, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
RecyclerView recyclerView = view.findViewById(R.id.list);
recyclerView.setLayoutManager(new GridLayoutManager(
requireContext(),
NUM_OF_COLUMNS,
GridLayoutManager.VERTICAL,
false
));
recyclerView.setAdapter(new NativeListAdapter(requireContext()));
}
public void setBackgroundColor(@Nullable Integer backgroundColor) {
Integer color = backgroundColor;
if (color == null) {
color = Color.TRANSPARENT;
}
requireView().setBackgroundColor(color);
}
public void setData(ReadableArray data) {
List<DataItem> listData = new ArrayList<>();
for (int i = 0; i < data.size(); i++) {
ReadableMap item = data.getMap(i);
DataItem dataItem = new DataItem(item.getString("imageUrl"), item.getString("description"));
listData.add(dataItem);
}
RecyclerView recyclerView = requireView().findViewById(R.id.list);
((NativeListAdapter)recyclerView.getAdapter()).data = listData;
}
public void setOptions(ReadableMap options) {
String placeholderImage = options.getString("placeholderImage");
RecyclerView recyclerView = requireView().findViewById(R.id.list);
((NativeListAdapter)recyclerView.getAdapter()).placeholderImage = placeholderImage;
}
public void scrollToItem(int index) {
RecyclerView recyclerView = requireView().findViewById(R.id.list);
recyclerView.smoothScrollToPosition(index);
}
}
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.java
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:
package com.nativelistpackage;
import android.view.View;
import androidx.annotation.Nullable;
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)
public class AndroidNativeListViewManager extends SimpleViewManager<FragmentContainerView> implements AndroidNativeListViewManagerInterface<FragmentContainerView> {
private final ViewManagerDelegate<FragmentContainerView> mDelegate = new AndroidNativeListViewManagerDelegate(this);
@Override
public String getName() {
return AndroidNativeListViewFragment.NAME;
}
@Override
public ViewManagerDelegate<FragmentContainerView> getDelegate() {
return mDelegate;
}
@Override
public void receiveCommand(FragmentContainerView root, String commandId, @Nullable ReadableArray args) {
mDelegate.receiveCommand(root, commandId, args);
}
@Override
public FragmentContainerView createViewInstance(ThemedReactContext reactContext) {
return new FragmentContainerView(reactContext);
}
@Override
@ReactProp(name = "data")
public void setData(FragmentContainerView view, @Nullable ReadableArray data) {
//
}
@Override
@ReactProp(name = "options")
public void setOptions(FragmentContainerView view, @Nullable ReadableMap options) {
//
}
@Override
public void scrollToItem(FragmentContainerView view, int index) {
//
}
}
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:
package com.nativelistpackage;
import android.view.View;
import androidx.annotation.Nullable;
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)
public class AndroidNativeListViewManager extends SimpleViewManager<FragmentContainerView> implements AndroidNativeListViewManagerInterface<FragmentContainerView> {
private final ViewManagerDelegate<FragmentContainerView> mDelegate = new AndroidNativeListViewManagerDelegate(this);
private int mHeight = 0;
private int mWidth = 0;
@Override
public String getName() {
return AndroidNativeListViewFragment.NAME;
}
@Override
public ViewManagerDelegate<FragmentContainerView> getDelegate() {
return mDelegate;
}
@Override
public void receiveCommand(FragmentContainerView root, String commandId, @Nullable ReadableArray args) {
mDelegate.receiveCommand(root, commandId, args);
}
@Override
public FragmentContainerView createViewInstance(ThemedReactContext reactContext) {
return new FragmentContainerView(reactContext);
}
@Override
@ReactProp(name = "data")
public void setData(FragmentContainerView view, @Nullable ReadableArray data) {
//
}
@Override
@ReactProp(name = "options")
public void setOptions(FragmentContainerView view, @Nullable ReadableMap options) {
//
}
@Override
public void scrollToItem(FragmentContainerView view, int index) {
//
}
private void layoutChildren(View view) {
final int width = mWidth;
final int 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 AndroidNativeListViewFragment findFragment(FragmentManager fragmentManager, View view) {
return (AndroidNativeListViewFragment)fragmentManager.findFragmentByTag(getFragmentTag(view));
}
@Nullable
private FragmentManager getFragmentManager(View view) {
final ThemedReactContext reactContext = (ThemedReactContext) view.getContext();
if (reactContext == null) {
return null;
}
final FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
if (activity == null) {
return null;
}
return activity.getSupportFragmentManager();
}
private String getFragmentTag(View view) {
return "AndroidNativeListViewFragment-" + view.getId();
}
}
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:
package com.nativelistpackage;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
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)
public class AndroidNativeListViewManager extends SimpleViewManager<FragmentContainerView> implements AndroidNativeListViewManagerInterface<FragmentContainerView> {
private final ViewManagerDelegate<FragmentContainerView> mDelegate = new AndroidNativeListViewManagerDelegate(this);
private int mHeight = 0;
private int mWidth = 0;
@Override
public String getName() {
return AndroidNativeListViewFragment.NAME;
}
@Override
public ViewManagerDelegate<FragmentContainerView> getDelegate() {
return mDelegate;
}
@Override
public void receiveCommand(FragmentContainerView root, String commandId, @Nullable ReadableArray args) {
mDelegate.receiveCommand(root, commandId, args);
}
@Override
public FragmentContainerView createViewInstance(ThemedReactContext reactContext) {
return new FragmentContainerView(reactContext);
}
@Override
@ReactProp(name = "data")
public void setData(FragmentContainerView view, @Nullable ReadableArray data) {
//
}
@Override
@ReactProp(name = "options")
public void setOptions(FragmentContainerView view, @Nullable ReadableMap options) {
//
}
@Override
public void scrollToItem(FragmentContainerView view, int index) {
//
}
private void mountFragment(FragmentContainerView view) {
UiThreadUtil.assertOnUiThread();
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
view.post(() -> layoutChildren(view));
return;
}
final AndroidNativeListViewFragment newFragment = new AndroidNativeListViewFragment();
view.removeAllViews();
final FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(newFragment, getFragmentTag(view));
transaction.runOnCommit(() -> {
view.addView(newFragment.requireView());
layoutChildren(view);
});
transaction.commitNowAllowingStateLoss();
}
}
private void unmountFragment(FragmentContainerView view) {
UiThreadUtil.assertOnUiThread();
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
final FragmentTransaction 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.
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:
package com.nativelistpackage;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
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)
public class AndroidNativeListViewManager extends SimpleViewManager<FragmentContainerView> implements AndroidNativeListViewManagerInterface<FragmentContainerView> {
private final ViewManagerDelegate<FragmentContainerView> mDelegate = new AndroidNativeListViewManagerDelegate(this);
private int mHeight = 0;
private int mWidth = 0;
@Override
public String getName() {
return AndroidNativeListViewFragment.NAME;
}
@Override
public ViewManagerDelegate<FragmentContainerView> getDelegate() {
return mDelegate;
}
@Override
public void receiveCommand(FragmentContainerView root, String commandId, @Nullable ReadableArray args) {
mDelegate.receiveCommand(root, commandId, args);
}
@Override
public FragmentContainerView createViewInstance(ThemedReactContext reactContext) {
return new FragmentContainerView(reactContext);
}
@Override
public void onDropViewInstance(FragmentContainerView view) {
unmountFragment(view);
super.onDropViewInstance(view);
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, FragmentContainerView view) {
super.addEventEmitters(reactContext, view);
// Mount fragment here, because here the view already has reactTag set as a view.id
mountFragment(view);
}
@Override
@ReactProp(name = "data")
public void setData(FragmentContainerView view, @Nullable ReadableArray data) {
//
}
@Override
@ReactProp(name = "options")
public void setOptions(FragmentContainerView view, @Nullable ReadableMap options) {
//
}
@Override
public void scrollToItem(FragmentContainerView view, int index) {
//
}
// ...
}
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:
package com.nativelistpackage;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
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)
public class AndroidNativeListViewManager extends SimpleViewManager<FragmentContainerView> implements AndroidNativeListViewManagerInterface<FragmentContainerView> {
private final ViewManagerDelegate<FragmentContainerView> mDelegate = new AndroidNativeListViewManagerDelegate(this);
private int mHeight = 0;
private int mWidth = 0;
@Override
public String getName() {
return AndroidNativeListViewFragment.NAME;
}
@Override
public ViewManagerDelegate<FragmentContainerView> getDelegate() {
return mDelegate;
}
@Override
public void receiveCommand(FragmentContainerView root, String commandId, @Nullable ReadableArray args) {
mDelegate.receiveCommand(root, commandId, args);
}
@Override
public FragmentContainerView createViewInstance(ThemedReactContext reactContext) {
return new FragmentContainerView(reactContext);
}
@Override
public void onDropViewInstance(FragmentContainerView view) {
unmountFragment(view);
super.onDropViewInstance(view);
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, FragmentContainerView view) {
super.addEventEmitters(reactContext, view);
// Mount fragment here, because here the view already has reactTag set as a view.id
mountFragment(view);
}
@Override
@ReactProp(name = "data")
public void setData(FragmentContainerView view, @Nullable ReadableArray data) {
if (data == null) {
return;
}
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.setData(data);
}
}
}
@Override
@ReactProp(name = "options")
public void setOptions(FragmentContainerView view, @Nullable ReadableMap options) {
if (options == null) {
return;
}
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.setOptions(options);
}
}
}
@Override
public void scrollToItem(FragmentContainerView view, int index) {
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.scrollToItem(index);
}
}
}
@ReactProp(name = "backgroundColor", customType = "Color")
public void setBackgroundColor(FragmentContainerView view, @Nullable Integer backgroundColor) {
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.setBackgroundColor(backgroundColor);
}
}
}
@ReactPropGroup(names = {"width", "height"}, customType = "Style")
public void setStyle(FragmentContainerView view, int index, Dynamic value) {
if (value == null) {
return;
}
if (index == 0) {
mWidth = (int)PixelUtil.toPixelFromDIP(value.asDouble());
}
if (index == 1) {
mHeight = (int)PixelUtil.toPixelFromDIP(value.asDouble());
}
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.java
file
package com.nativelistpackage;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
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)
public class AndroidNativeListViewManager extends SimpleViewManager<FragmentContainerView> implements AndroidNativeListViewManagerInterface<FragmentContainerView> {
private final ViewManagerDelegate<FragmentContainerView> mDelegate = new AndroidNativeListViewManagerDelegate(this);
private int mHeight = 0;
private int mWidth = 0;
@Override
public String getName() {
return AndroidNativeListViewFragment.NAME;
}
@Override
public ViewManagerDelegate<FragmentContainerView> getDelegate() {
return mDelegate;
}
@Override
public void receiveCommand(FragmentContainerView root, String commandId, @Nullable ReadableArray args) {
mDelegate.receiveCommand(root, commandId, args);
}
@Override
public FragmentContainerView createViewInstance(ThemedReactContext reactContext) {
return new FragmentContainerView(reactContext);
}
@Override
public void onDropViewInstance(FragmentContainerView view) {
unmountFragment(view);
super.onDropViewInstance(view);
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, FragmentContainerView view) {
super.addEventEmitters(reactContext, view);
// Mount fragment here, because here the view already has reactTag set as a view.id
mountFragment(view);
}
@Override
@ReactProp(name = "data")
public void setData(FragmentContainerView view, @Nullable ReadableArray data) {
if (data == null) {
return;
}
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.setData(data);
}
}
}
@Override
@ReactProp(name = "options")
public void setOptions(FragmentContainerView view, @Nullable ReadableMap options) {
if (options == null) {
return;
}
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.setOptions(options);
}
}
}
@Override
public void scrollToItem(FragmentContainerView view, int index) {
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.scrollToItem(index);
}
}
}
@ReactProp(name = "backgroundColor", customType = "Color")
public void setBackgroundColor(FragmentContainerView view, @Nullable Integer backgroundColor) {
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.setBackgroundColor(backgroundColor);
}
}
}
@ReactPropGroup(names = {"width", "height"}, customType = "Style")
public void setStyle(FragmentContainerView view, int index, Dynamic value) {
if (value == null) {
return;
}
if (index == 0) {
mWidth = (int)PixelUtil.toPixelFromDIP(value.asDouble());
}
if (index == 1) {
mHeight = (int)PixelUtil.toPixelFromDIP(value.asDouble());
}
view.post(() -> layoutChildren(view));
}
private void mountFragment(FragmentContainerView view) {
UiThreadUtil.assertOnUiThread();
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
view.post(() -> layoutChildren(view));
return;
}
final AndroidNativeListViewFragment newFragment = new AndroidNativeListViewFragment();
view.removeAllViews();
final FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(newFragment, getFragmentTag(view));
transaction.runOnCommit(() -> {
view.addView(newFragment.requireView());
layoutChildren(view);
});
transaction.commitNowAllowingStateLoss();
}
}
private void unmountFragment(FragmentContainerView view) {
UiThreadUtil.assertOnUiThread();
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
final FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.remove(fragment);
transaction.commitNowAllowingStateLoss();
}
}
}
private void layoutChildren(View view) {
final int width = mWidth;
final int 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 AndroidNativeListViewFragment findFragment(FragmentManager fragmentManager, View view) {
return (AndroidNativeListViewFragment)fragmentManager.findFragmentByTag(getFragmentTag(view));
}
@Nullable
private FragmentManager getFragmentManager(View view) {
final ThemedReactContext reactContext = (ThemedReactContext) view.getContext();
if (reactContext == null) {
return null;
}
final FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
if (activity == null) {
return null;
}
return activity.getSupportFragmentManager();
}
private String getFragmentTag(View view) {
return "AndroidNativeListViewFragment-" + view.getId();
}
}
Old architecture view manager
android/src/oldarch/java/com/nativelistpackage/AndroidNativeListViewManager.java
at other text editor and paste following content:package com.nativelistpackage;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentContainerView;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
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)
public class AndroidNativeListViewManager extends SimpleViewManager<FragmentContainerView> {
private int mHeight = 0;
private int mWidth = 0;
@Override
public String getName() {
return AndroidNativeListViewFragment.NAME;
}
@Override
public void receiveCommand(FragmentContainerView root, String commandId, @Nullable ReadableArray args) {
super.receiveCommand(root, commandId, args);
switch (commandId) {
case "scrollToItem":
final int index = args.getInt(0);
scrollToItem(root, index);
break;
}
}
@Override
public FragmentContainerView createViewInstance(ThemedReactContext reactContext) {
return new FragmentContainerView(reactContext);
}
@Override
public void onDropViewInstance(FragmentContainerView view) {
unmountFragment(view);
super.onDropViewInstance(view);
}
@Override
protected void addEventEmitters(ThemedReactContext reactContext, FragmentContainerView view) {
super.addEventEmitters(reactContext, view);
// Mount fragment here, because here the view already has reactTag set as a view.id
mountFragment(view);
}
@ReactProp(name = "data")
public void setData(FragmentContainerView view, @Nullable ReadableArray data) {
if (data == null) {
return;
}
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.setData(data);
}
}
}
@ReactProp(name = "options")
public void setOptions(FragmentContainerView view, @Nullable ReadableMap options) {
if (options == null) {
return;
}
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.setOptions(options);
}
}
}
private void scrollToItem(FragmentContainerView view, int index) {
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.scrollToItem(index);
}
}
}
@ReactProp(name = "backgroundColor", customType = "Color")
public void setBackgroundColor(FragmentContainerView view, @Nullable Integer backgroundColor) {
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
fragment.setBackgroundColor(backgroundColor);
}
}
}
@ReactPropGroup(names = {"width", "height"}, customType = "Style")
public void setStyle(FragmentContainerView view, int index, Dynamic value) {
if (value == null) {
return;
}
if (index == 0) {
mWidth = (int)PixelUtil.toPixelFromDIP(value.asDouble());
}
if (index == 1) {
mHeight = (int)PixelUtil.toPixelFromDIP(value.asDouble());
}
view.post(() -> layoutChildren(view));
}
private void mountFragment(FragmentContainerView view) {
UiThreadUtil.assertOnUiThread();
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
view.post(() -> layoutChildren(view));
return;
}
final AndroidNativeListViewFragment newFragment = new AndroidNativeListViewFragment();
view.removeAllViews();
final FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.add(newFragment, getFragmentTag(view));
transaction.runOnCommit(() -> {
view.addView(newFragment.requireView());
layoutChildren(view);
});
transaction.commitNowAllowingStateLoss();
}
}
private void unmountFragment(FragmentContainerView view) {
UiThreadUtil.assertOnUiThread();
final FragmentManager fragmentManager = getFragmentManager(view);
if (fragmentManager != null) {
final AndroidNativeListViewFragment fragment = findFragment(fragmentManager, view);
if (fragment != null) {
final FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.remove(fragment);
transaction.commitNowAllowingStateLoss();
}
}
}
private void layoutChildren(View view) {
final int width = mWidth;
final int 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 AndroidNativeListViewFragment findFragment(FragmentManager fragmentManager, View view) {
return (AndroidNativeListViewFragment)fragmentManager.findFragmentByTag(getFragmentTag(view));
}
@Nullable
private FragmentManager getFragmentManager(View view) {
final ThemedReactContext reactContext = (ThemedReactContext) view.getContext();
if (reactContext == null) {
return null;
}
final FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
if (activity == null) {
return null;
}
return activity.getSupportFragmentManager();
}
private String getFragmentTag(View view) {
return "AndroidNativeListViewFragment-" + view.getId();
}
}
NativeListTurboPackage.java
The last thing we need to do is to export AndroidNativeListViewManager
in the TurboReactPackage
instance. Let's go to NativeListTurboPackage.java
and add our new module.
package com.nativelistpackage;
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 com.facebook.react.uimanager.ViewManager;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class NativeListTurboPackage extends TurboReactPackage {
/**
* Initialize and export modules based on the name of the required module
*/
@Override
@Nullable
public NativeModule getModule(String name, ReactApplicationContext 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[] {
};
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;
}
};
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
/**
* Here declare the list of exported native components
*/
return Arrays.<ViewManager>asList(new 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!