How To Hide the Android Pie Navigation Bar in React Native

Android 9 has an opinionated software Navigation Bar — and Samsung’s One UI makes it even more stubborn.

Michael Ashton
8 min readJun 12, 2019

With the release of Android 9 “Pie” and in particular Samsung’s “One UI”, mobile developers have had a new baked-in screen element to contend with.

Presumably anticipating the complete extinction of the hardware navigation buttons, Android provides a software navigation bar. It’s been around for a while as an option, but with changes in Android “Pie” and particularly with Samsung’s One UI, the navigation bar has become a bit more persistent. This makes sense, of course, as the user needs a way to navigate between apps. But what about the times when we really need a full screen experience? How can we manipulate the presence of that navigation bar in our React Native apps?

That’s what we’re here to do. But take note — removing the navigation bar is not a step to take lightly. The Android developer documentation puts it quite well:

You might be tempted to enable fullscreen mode just to maximize screen space for your app. But be mindful of how often users jump in and out of apps to check notifications, do impromptu searches, and more.

Before you remove the user’s only means of navigating in and out of your app, think twice. The docs go on to describe Fullscreen Mode not as a way to get a few more pixels of screen space, but as a way to create a truly immersive experience. For this reason, moving forward we will refer to our target UX as Immersive Mode.

There are three different ways a user can interact with Immersive Mode.

  1. “Lean back” mode exits fullscreen whenever the users touches the screen. For this, think of video playback. Your content goes fullscreen and you can lean back (get it?) and watch. Any touch on the screen will return the navigation bar.
  2. “Immersion” mode ignores user gestures except swiping up or down from the top or bottom edges of the screen. This is a good for cases where, unlike your video player, you expect the user to interact with the content displayed fullscreen.
  3. “Sticky Immersion” is similar to Immersion, but when the user swipes up or down from the edges of the screen the navigation bar is displayed temporarily. It will disappear again after a moment.

So, how do we implement these effects in React Native?

Java. That’s how.

Before you click away, don’t worry — it’s really not so bad, and this article will walk you through it step by step.

Step 1 — NativeModules Boilerplate

In order to expose native Android functionality to React Native, we’ll be utilizing the React Native NativeModules API. If you’ve never worked with it before, consider this BONUS CONTENT!

So let’s jump right in.

Open Android Studio and open android/build.gradle to kick off your gradle scripts. Do yourself a favor and ignore any warnings or suggestions to update gradle or add a graddle wrapper. Just don’t let Android Studio do anything to your React Native setup.

Once your project is open, navigate to app/java/com.<yourApp>. Cmd/Right-Click com.<yourApp>, and select new → Java Class. Let’s name it “ImmersiveMode” and under the “Superclass” field, let’s tell it to inherit from “ReactContextBaseJavaModule” — as you begin typing, Android Studio will suggest the appropriate module. Take the suggestion and it will resolve to com.facebook.react.bridge.ReactContextBaseJavaModule. Click “OK”.

Your new class has been automatically added to your classpath, and some very basic boilerplate is provided in ImmersiveMode.java. It’s gonna come in hot with an error — don’t panic! If you hover over the class declaration it’ll tell you that you need to either declare the class as abstract or implement parent’s getName() method. Since I can’t live with those squiggly red lines, let’s go ahead and do the latter before we move on.

Update ImmersiveMode.java as follows:

Here, we’re implementing getName() and defining a constructor. In just a bit we’ll instantiate our ImmersiveMode class with ReactApplicationContext, so let’s go ahead and take it as an argument and pass it to our Superclass. We’ll talk about reactContext later.

Next, let’s go ahead and finish up our NativeModules boilerplate before we start our implementations.

The next step is to expose this class to React Native. For this we will need to create our package. Create a new class, same way as before but this time let’s name it ImmersiveModePackage and leave “Superclass” blank. Click “OK”.

You should now have your ImmersiveModePackage class which extends ReactContextBaseJavaModule. Let’s just plop the code up here and briefly go over it — we don’t want to spend too much time talking about boilerplate.

Note that our new ImmersiveModePackage class implements the ReactPackage implementation. We’ve implemented two methods required by ReactPackage. These are both React Native NativeModules standard boilerplate — just be sure to include the name of our implementation class in line 22. And don’t forget to pass reactContext to its constructor…we’ll need that later.

We have just one more piece of boilerplate to set up. We need to expose our package. Open com.<appName>.MainApplication. This class itself is React Native boilerplate. Check out the getPackages()method. If you have any project dependencies that use NativeModules (ones for which you ran react-native link <depName>), you’ll see them exposed here. At the bottom of that list, add our new package. It’ll look something like this when you’re done:

And that’s our boilerplate!

Step 2 — Implement Our Functionality

Now it’s time to open up ImmersiveMode.java and start adding our desired functionality. Methods defined in this class, using a special annotation provided by React, are exposed in our Javascript. This is our Bridge between Java and React-Native!

Some background: In native Android, we work with objects called Activities. These Activities usually map to “Screens”, but not always. This can be a difficult paradigm shit for many developers, but here we only need to know the basics. According to the Android docs:

The mobile-app experience differs from its desktop counterpart in that a user’s interaction with the app doesn’t always begin in the same place. Instead, the user journey often begins non-deterministically. For instance, if you open an email app from your home screen, you might see a list of emails. By contrast, if you are using a social media app that then launches your email app, you might go directly to the email app’s screen for composing an email.

The Activity class is designed to facilitate this paradigm. When one app invokes another, the calling app invokes an activity in the other app, rather than the app as an atomic whole. In this way, the activity serves as the entry point for an app's interaction with the user.

Long quote there, but the excellent Android documentation explains it best. The point is that in order to interact with our app, we need to expose and manipulate whatever Activity is currently…active. Sounds difficult? It might be if it weren’t for our parent class ReactContextBaseJavaModule. From it our class has inherited the delightfully declarative getCurrentActivity(). I’ll let you guess what that does.

So we’re starting to form a plan here. Each time the user calls a method in our ImmersiveMode class, we’ll get the current Activity and do…something. But first, in order to keep things DRY, let’s define a private method setSystemUIFlags. We’ll work through what it does in the comments of our code below:

So now we know that for each invocation of our exposed methods, we’ll be:

  1. Getting our current “window”
  2. Manipulating the system UI from it

So let’s expose some functionality! Remember before when we talked about a special annotation provided by React to expose Java class methods? It’s time to pull those out. Somewhere at the top with your other imports, add the following import:

import android.view.View;import com.facebook.react.bridge.ReactMethod;

This is our magic React-provided annotation. So we’ll add @ReactMethod to the line before any method we want to expose in our javascript. Let’s create our first method:

don’t forget your imports!

At this point if you want to try out our conspicuously named fail() method, jump to Step 3 for the Javascript implementation. But if you hadn’t figured it out already, it’s going to fail. It’s going to fail with the following:

does it have to be bright red??

Okay. That’s hairy. What does it mean? Following the stack trace, we can at least see that our failure occurs somewhere inside our private setSystemUIVisibility method.

Here’s what this means in a nutshell:

By default, Android apps are built on a single thread, “main”. Also called the UI thread. It is from here that most draws and user interactions are executed. React Native is different. React Native uses a handful of different threads to do its cross-platform work. Briefly, they are:

  1. JS thread: all of your javascript logic runs here, the results of which are sent to the UI thread in batches every 16.67ms (because screen refresh rate and math, look it up).
  2. UI thread: the aforementioned UI thread, responsible for gesture handling and screen draws.
  3. Native Modules thread: you guessed it. Native Modules are executed on a discrete thread.

So. See where our problem is? Our decorView from above was created on the UI thread. Our code thus far is scheduled to execute on the Native Modules thread. That means we need to pull our method executions out of the Native Modules thread and execute them on the UI thread.

We’re going to do that using a Runnable (look it up). A runnable is a class that will take whatever is inside its run() method and execute it on its own thread. You can also explicitly execute on a thread of your choice via the Handler class (look it up). Long story short — we’re gonna manually grab the UI thread, and attach our methods to it via Runnables.

First thing’s first. Pull up ImmersiveMode.java. Let’s grab the UI thread. React Native very helpfully provides access to this via ReactApplicationContext. Remember our constructor from before? Our past selves helped our current selves out. We’ve passed an instance of ReactApplicationContext to the constructor of our ImmersiveMode class (via ImmersiveModePackage, if you recall).

So now it’s time for another dump of code. We’ll go over it in the comments:

Now all that’s left is to implement the rest of our methods. To review, we wanted:

  1. “Lean back”
  2. “Immersive”
  3. “Sticky Immersive”
  4. We’ll also want a method to leave fullscreen/immersive mode

We’ve kept things reasonably DRY, so the only difference should be the flags we send to our setSystemUIFlags() method. For that reason we’re going to dump a complete ImmersiveMode.java here with the rest of our methods. If you want to learn about the flags we’re using and why they work the way the work, look it up!

That should do it! We’ve created our methods, we’ve scheduled them on the UI thread, we’ve exposed them to our React Native code.

So now let’s write that React Native code.

Step 3 — Javascript Implementation

This is dead simple.

Literally all we need to do is import { NativeModules } from “react-native”; and access our package. Here’s an example component that calls one of our previously created methods:

Try it with a method other than fail…

And that’s it! You can execute any of the methods we annotated with @ReactMethod in ourImmersiveMode class through NativeModules.ImmersiveMode.

We have officially disabled our navigation bar. We have three different ways to experience fullscreen and a way to leave it. What’s missing? It would be nice if we could be notified in our Javascript when we move from one state to the other. This is a little tricky, but totally doable. It’s just out of scope for this article for now.

So congrats! We’ve done it. And we learned about NativeModules for free. Good job!

Remember that you’re removing your users’ choices. It’s your job to make sure that it’s always clear to the user how to return to a navigable view. Go forth! And use Immersive Mode with care!

--

--

Michael Ashton

Developer at Caktus Group in Durham, NC. Father and musician— focused on using the right tool for the job.