React Native adoption has been steadily growing since its release in 2015, especially with its ability to quickly create cross-platform apps. A very strong open-source community has formed, producing great libraries like Reanimated and Gesture Handler that allow you to achieve native performance for animations and gestures while writing exclusively React Native code. At Shopify we are using React Native for many different types of applications, and are committed to giving back to the community.
However, sometimes there is a native component you made for another app, or already exists on the platform, which you want to quickly port to React Native and aren’t able to build cross-platform using exclusively React Native. The documentation for React Native has good examples of how to create a native module which exposes native methods or components, but what should you do if you want to use a component you already have and render React Native views inside of it? In this guide, I’ll show you how to make a native component which provides bottom sheet functionality to React Native and lets you render React views inside of it.
A simple example is the bottom sheet pattern from Google’s Material Design. It’s a draggable view which peeks up from the bottom of the screen and is able to expand to take up the full screen. It renders subviews inside of the sheet, which can be interacted with when the sheet is expanded.
This guide only focuses on an Android native implementation and assumes a basic knowledge of Kotlin. When creating an application, it’s best to make sure all platforms have the same feature parity.
Bottom sheet functionality
Table of Contents
Setting Up Your Project
If you already have a React Native project set up for Android with Kotlin and TypeScript you’re ready to begin. If not, you can run react-native init NativeComponents —template react-native-template-typescript
in your terminal to generate a project that is ready to go.
As part of the initial setup, you’ll need to add some Gradle dependencies to your project.
Modify the root build.gradle (android/build.gradle
) to include these lines:
This will add all of the required libraries for the code used in the rest of this guide.
You should use fixed version numbers instead of + for actual development.
Creating a New Package Exposing the Native Component
To start, you need to create a new package that will expose the native component. Create a file called NativeComponentsReactPackage.kt
.
Right now this doesn’t actually expose anything new, but you’ll add to the list of View Managers
soon. After creating the new package, go to your Application
class and add it to the list of packages.
Creating The Main View
A ViewGroupManager<T>
can be thought of as a React Native version of ViewGroup
from Android. It accepts any number of children provided, laying them out according to the constraints of the type T
specified on the ViewGroupManager
.
Create a file called ReactNativeBottomSheet.kt
and a new
The basic methods you have to implement are getName()
and createViewInstance()
.
name
is what you’ll use to reference the native class from React Native.
createViewInstance
is used to instantiate the native view and do initial setup.
Inflating Layouts Using XML
Before you create a real view to return, you need to set up a layout to inflate. You can set this up programmatically, but it’s much easier to inflate from an XML layout.
Here’s a fairly basic layout file that sets up some CoordinatorLayout
s with behaviours for interacting with gestures. Add this to android/app/src/main/res/layout/bottom_sheet.xml
.
The first child is where you’ll put all of the main content for the screen, and the second is where you’ll put the views you want inside BottomSheet. The behaviour is defined so that the second child can translate up from the bottom to cover the first child, making it appear like a bottom sheet.
Now that there is a layout created, you can go back to the createViewInstance
method in ReactNativeBottomSheet.kt
.
Referencing The New XML File
First, inflate the layout using the context provided from React Native. Then save references to the children for later use.
If you aren’t using Kotlin Synthetic Properties, you can do the same thing with container = findViewById(R.id.container)
.
For now, this is all you need to initialize the view and have a fully functional bottom sheet.
The only thing left to do in this class is to manage how the views passed from React Native are actually handled.
Handling Views Passed from React Native To Native Android
By overriding addView
you can change where the views are placed in the native layout. The default implementation is to add any views provided as children to the main CoordinatorLayout
. However, that won’t have the effect expected, as they’ll be siblings to the bottom sheet (the second child) you made in the layout.
Instead, don’t make use of super.addView(parent, child, index)
(the default implementation), but manually add the views to the layout’s children by using the references stored earlier.
The basic idea followed is that the first child passed in is expected to be the main content of the screen, and the second child is the content that’s rendered inside of the bottom sheet. Do this by simply checking the current number of children on the container
. If you already added a child, add the next child to the bottomSheet
.
The way this logic is written, any views passed after the first one will be added to the bottom sheet. You’re designing this class to only accept two children, so you’ll make some modifications later.
This is all you need for the first version of our bottom sheet. At this point, you can run react-native run-android
, successfully compile the APK, and install it.
Referencing the New Native Component in React Native
To use the new native component in React Native you need to require it and export a normal React component. Also set up the props here, so it will properly accept a style and children.
Create a new component called BottomSheet.tsx
in your React Native project and add the following:
Now you can update your basic App.tsx
to include the new component.
This is all the code that is required to use the new native component. Notice that you're passing it two children. The first child is the content used for the main part of the screen, and the second child is rendered inside of our new native bottom sheet.
Adding Gestures
Now there's a working native component that renders subviews from React Native, you can add some more functionality.
Being able to interact with the bottom sheet through gestures is our main use case for this component, but what if you want to programmatically collapse/expand the bottom sheet?
Since you’re using a CoordinatorLayout
with behaviour to make the bottom sheet in native code, you can make use of BottomSheetBehaviour
. Going back to ReactNativeBottomSheet.kt
, we will update the createViewInstance()
method.
By creating a BottomSheetBehaviour
you can make more customizations to how the bottom sheet functions and when you’re informed about state changes.
First, add a native method which specifies what the expanded state of the bottom sheet should be when it renders.
This adds a prop to our component called sheetState
which takes a string and sets the collapsed/expanded state of the bottom sheet based on the value sent. The string sent should be either collapsed
or expanded
.
We can adapt our TypeScript to accept this new prop like so:
Now, when you include the component, you can change whether it’s collapsed or expanded without touching it. Here’s an example of updating your App.tsx
to add a button that updates the bottom sheet state.
Now, when pressing the button, it expands the bottom sheet. However, when it’s expanded, the button disappears. If you drag the bottom sheet back down to a collapsed state, you'll notice that the button isn't updating its text. So you can set the state programmatically from React Native, but interacting with the native component isn't propagating the value of the bottom sheet's state back into React. To fix this you will add more to the *BottomSheetBehaviour* you created earlier.
This code adds a state change listener to the bottom sheet, so that when its collapsed/expanded state changes, you emit a React Native event that you listen to in the React component. The event is called "BottomSheetStateChange
” and has the same value as the states accepted in setSheetState()
.
Back in the React component, you listen to the emitted event and call an optional listener prop to notify the parent that our state has changed due to a native interaction.
https://gist.github.com/josephmbeveridge/38c218bc960cfd96300c6d63543654ca
Updating the App.tsx again
…
Now when you drag the bottom sheet, the state of the button updates with its collapsed/expanded state.
Native Code And Cross Platform Components
When creating components in React Native our goal is always to make cross-platform components that don’t require native code to perform well, but sometimes that isn’t possible or easy to do. By creating ViewGroupManager
classes, we are able to extend the functionality of our native components so that we can take full advantage of React Native’s flexible layouts, with very little code required.
Additional Information
All the code included in the guide can be found at the react-native-bottom-sheet-example repo.
This guide is just an example of how to create native views that accept React Native subviews as children. If you want a complete implementation for bottom sheets on Android, check out the react-native wrapper for android BottomSheetBehavior.
You can follow the Android guideline for CoordinatorLayout
and BottomSheetBehaviour
to better understand what is going on. You’re essentially creating a container with two children.
If this sounds like the kind of problems you want to solve, we're always on the lookout for talent and we’d love to hear from you. Visit our Engineering career page to find out about our open positions.