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
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
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
Create a file called
ReactNativeBottomSheet.kt and a new
The basic methods you have to implement are
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
CoordinatorLayouts with behaviours for interacting with gestures. Add this to
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
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
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
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.
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
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
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
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.
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.
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
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.