In this lesson, we will focus on the Android side. We will see the difference between Android and iOS and discuss how to provide a similar experience for our NFC game app.

The difference between iOS and Android#

Let's first run our previous work on a real Android device.

As you can see, I have pressed the START button several times, but it looks like there's nothing happened.

What's wrong here? Actually, it's nothing wrong. It's because Android provides no built-in NFC scanning UI, so even though our app is ready to scan NFC tags, there's no visual cue for our users.

So, our first step in this lesson is to build a component to mimic the behavior for iOS NFC scanning UI.

Implement the AndroidPrompt component#

Let's call it AndroidPrompt. This component will use React Native's built-in Modal component to present content above the enclosing view.

Inside the Modal, we place a View and a Text with the string Hello NFC.

Then we set both transparent and visible props to be true for the Modal component and provide some basic styling for our inner View.

After the basic setup, let's add our AndroidPrompt component into our App.js.

Okay, now our AndroidPrompt shows up on the screen.

Let's move on to style our content inside the Modal.

The building blocks are:

  • a full View component with flex: 1 as its container

  • a backdrop inside the container.

  • the actual prompt UI is rendered above the backdrop, and there are two components inside it, one hint text and one cancel button, just like on iOS.

Then, the next step is to apply our styles to our components.

Oops, it looks like there is something wrong with the backdrop. That's because we haven't applied the position: absolute part into it. To write this kind of "cover-all" style, here's a little trick: we can use the StyleSheet.absoluteFill. It's very convenient in such a case.

For now, the codes look like this:

Design the component interface#

The next step is to design the interface between our AndroidPrompt and its parent.

Normally in React, we use props passing as the primary interface between components. That's because of React's declarative nature. However, in our case, providing an imperative API set will be much easier.

The API we want to expose is something like React Native's built-in Alert module. For Alert, we simply call Alert.alert, and it will appear. From the caller's perspective, we don't need to preserve a state like isAlertVisible and pass it as props into Alert. We'd like to do something similar for our AndroidPrompt component here.

In order to accomplish this, we will use ref.

First, wrap the original AndroidPrompt component with React.forwardRef. Once a function component is wrapped with forwardRef, it will receive a second argument, which is the ref object passing from its parent.

Then, we create our internal state, visible, and hintText. Since those states are within the current component, we need to provide a method to let the parent component access them.

We can use a useEffect hook to accomplish this inside the hook. If we have a ref object passed in, we can set the current property of the ref object and put our state mutation functions inside the current object. So our parent component can use setVisible and setHintText.

And of course, we should pass our internal visible state into the Modal component and display our internal hintText state. We also need to toggle the visible state back to false and clear the hintText when the cancel button is pressed.

Now, it's time to test them. Head back to our App.js. We first call useRef hook to obtain a ref object and pass it into the AndroidPrompt component.

Then, quickly create a test button. From the test button's onPress handler, we can simply call current.setVisible(true).

As you can see, we can now toggle on our AndroidPrompt component from the parent and toggle it off by pressing its own cancel button, without tracking the visible state from the parent component.

The current code looks like this:

Animating the AndroidPrompt component#

Now, it's the fun part. We'd like to add some animation into our AndroidPrompt component.

Import the Animated module from react-native. We also need an Animated.Value, initially set to 0.

Then, we create a _visible state. I will explain this in just a second. For now, just consider that we will use this _visible state instead of the previous visible state.

Before we dive deeper, here's the thought process for creating animation: 1. What state triggers the animation? For us, that's the _visible state. 2. How to update the Animated.Value object according to prior state change? 3. How to update the style using the changing Animated.Value object?

To handle the first and second questions, we create a new useEffect hook that depends on the _visible state and the animValue object.

This hook function will be triggered when _visible value changed, and if this value is true, which means we're about to show our component, we first set the visible state to true and use Animated.timing to gradually change the animValue from 0 to 1,

On the other hand, if the _visible is false, which means we're about to hide our component, we need to call Animated.timing first and then toggle the visible state to false in the completion handler, which is the callback passed into the start function. If we use the wrong order, the Modal will disappear instantly with no animation at all. That's why we need a second _visible state to signal the beginning of a transition, and the visible state is the one actually passed into our Modal component.

After that, we change all the existing setVisible into _setVisible. Since its actual usage is to trigger the useEffect hook, we should only call setVisible inside our effect hook.

The next step is to update the style using the animValue.

First, update the backdrop with the opacity.

Test it, and it looks pretty great.

Then we can handle the animation for the prompt. The effect we want is a slide-up effect, so we do a transateY transform and let the animValue interpolate from 0 to 1.

By the way, always remember that only the Animated View can have animation-style in it. Otherwise, you'll have some trouble.

Test it again, and it's awesome.

The final code:

Using AndroidPrompt component in our game#

The next step is to use our AndroidPrompt component in our game.

First, import it and set up the ref object.

Then, when the NfcManager.registerTagEvent is called, we should set our AndroidPrompt to be visible, by calling androidPromptRef.current.setVisible(true).

When the user is playing, we can update the hint text according to the platform. If it's Android, we will call our custom setHintText function from ref; if it's iOS, we simply call setAlertMessageIOS.

Once the game is finished, we should set our AndroidPrompt to be invisible.

Okay, it seems like we're ready to test it. Before testing, you will need to check the position of the NFC antenna for your Android device because they might be in different places according to different Android manufacturers.

It looks pretty good!

The updated Game component looks like this (notice the AndroidPrompt related code):

Passing the onCancelPress callback#

Though the app seems to be working correctly, there's actually a potential bug.

That is, once the user presses the CANCEL button in our AndroidPrompt, we won't actually stop the NFC from scanning because our Game component is not aware of it.

To fix this issue, we first add the onCancelPress into our AndroidPrompt component and call it when the CANCEL button is pressed.

And then, go back to our Game component, pass the onCancelPress prop into AndroidPrompt component, and call unregisterTagEvent inside it.

Check if NFC is enabled#

Besides the system-scanning UI, there's another difference between Android and iOS. That is, NFC can be disabled in Android.

So, for the Android app, before the game starts, we will need to confirm that the NFC is enabled. Let's do it right now.

First, create an enabled state, and in our checkNfc function, we call NfcManager.isEnabled(). This API will resolve to a boolean value to indicate whether the NFC is enabled or not.

Let's use this state in our render logic: if the NFC is supported but not enabled, we show a hint to our users and provide a button for the user to jump to the NFC system setting screen by calling NfcManager.goToNfcSetting(). (Please be aware, this API only works on Android.)

You might wonder, what about iOS? Actually, our NfcManager.isEnabled() will always return true since there's no NFC switch for iOS. So, we can confirm that the runtime execution flow will never reach here.

Finally, we also provide a button to let the user re-check the NFC enable state.

It's about time to test our modifications.

We first disable NFC and then launch our app. As you can see, our UI provides a hint about our NFC state. Hit "GO TO SETTINGS" to enable NFC. Then back to our app again, re-check the state, and now the user can enter the game.

Cool! Now we have a simple but fully functional NFC tag counter game app for both Android and iOS!

Here's the modified code for App.js:

Start a new discussion. All notification go to the author.