Author Archives: fullstackdev

index=4 count=5 addInArray AddViewInner error React Native maps

React Native apps will fail in Android if you use the react-native-maps module improperly.

The problem is documented:

Components that aren’t declared by this library (Ex: Markers, Polyline) must not be children of the MapView component due to MapView’s unique rendering methodology. Have your custom components / views outside the MapView component and position absolute to ensure they only re-render as needed. Example: Bad:

  <View style={StyleSheet.absoluteFillObject}>
    <MapView style={StyleSheet.absoluteFillObject}>
      <View style={{ position: 'absolute', top: 100, left: 50 }}/>
    </MapView>
  </View>

Good:

  <View style={StyleSheet.absoluteFillObject}>
    <MapView style={StyleSheet.absoluteFillObject} />
    <View style={{ position: 'absolute', top: 100, left: 50 }}/>
  </View>

When you do things the wrong way, you’ll get a RedBox error. The error can have different formats depending on what your code looks like. It might say

index=4 count=3
addInArray
ViewGroup.java

addViewInner
ViewGroup.java

addView
ViewGroup.java

addFeature
AirMapView.java

manageChildren
NativeViewHierarchyManager
...

The initial error message may also say “The specified child already has a parent. You must call removeView() on the child’s parent first”. The stack trace will be similar, but not identical.

The documentation explains how to fix this problem: simply move the improper components outside of MapView. I’ve got a YouTube video that shows the issue, and a simple project at github that demos the error and the fix. I hope this blog post helps you fix your React Native bug!

I’ve got a working React Native app checked into github that demos the issue and the fix. There’s also a YouTube video.

I hope this blog post helps you fix your React Native bug!

If you found this interesting, click the subscribe button below! I write a new post about once a week.

instant run button gone in Android Studio

I’ve been following a slightly out-of-date Android course in order to polish my Android skills. In the course, the instructor discusses the “instant run” button to hot swap code while debugging your app. This button has a lightning bolt icon in the menu.

That didn’t look familiar! When I tried to follow along, I found that my version of Android Studio (3.6) doesn’t have that lightning bolt. I don’t know why they got rid of that lightning bolt, since I think that was a good, easy to recall icon. However, the feature itself is not gone. Now, it looks like a stacked set of horizontal lines. This says nothing to me (it looks kind of like a formatting button – a sheet of paper with lines of text, perhaps).

Anyway, when you hover over it, you’ll see the text “Apply Code Changes”. Here’s a screenshot with the old version on top, and the newer version at the bottom:

Android Studio 3.6 Apply Code Changes button
Apply Code Changes in Android Studio (aka Hot Code Swap)

If you found this interesting, click the subscribe button below! I write a new post about once a week.

Missing Generated Signed Bundle APK menu item in Android Studio

If you’ve just switched to the latest version of Android Studio (3.6), and you don’t see any option to generate a signed APK, you should take a look at the “Build: Sync” window in the IDE. Let’s look at such a case, here:

https://www.fullstackoasis.com/images/2020-03-23-android-studio-build-error-clouddoorbell.png

Here we have an Android Studio project, but we cannot see the “Generate Signed Bundle / APK…” menu item. How did this happen?

In this case, I opened Android Studio, and closed all projects. Then, I clicked the link to “Import an Android code sample”. I chose Cloud Doorbell, and when the project opened, I saw a number of errors. You can see those errors in the screenshot above. I wanted to build an APK, but when I clicked the “Build” menu, that menu item was missing. It was frustrating because there was no mention in the menu about what was wrong.

However, under the “Build: Sync” window, I saw:

ERROR: Failed to find Build Tools revision 26.0.2
Install Build Tools 26.0.2 and sync project
Upgrade plugin to version 3.6.1 and sync project

Those two lines were links, and when I clicked the first one, the latest Build Tools were installed. I believe it also did a gradle sync on its own.

So, if you run into this problem, take a look at that “Build: Sync” window. If it’s not open already, you can open it by clicking on the “Build” tab at the bottom of the Android Studio IDE. If you see errors there, fix them before trying to do anything else. Otherwise, you may wind up banging your head against a wall… 🙂

This problem happened to me with the Cloud Doorbell project, but it can happen in other types of projects. Someone on Reddit had the same problem after importing a project from Buildbox.

If you found this interesting, click the subscribe button below! I write a new post about once a week.

How to play an Android audio file in React Native

Here’s a quick tutorial to get you started playing an mp3 file in an Android app that was built using React Native. This tutorial was developed using Linux (Ubuntu 16.04), React Native 0.61.5, and the React Native Sound package 0.11.0.

First, make your project directory. Let’s call it AudioTmp. In a terminal window, do this:

npx react-native init AudioTmp
cd AudioTmp

All of the following commands assume you’ve got your terminal window open, and you’re standing in the root directory of your project (in AudioTmp, where App.js is located).

You’ll need some extra packages, so run these install commands:

npm install typescript@
npm install eslint@^5.0.0
npm install react-native-sound

Next, as described in the React Native Sound documentation, use the link command:

react-native link react-native-sound

This “links” the library where needed into the android and ios directories.

Now add an mp3 file to your Android project:

mkdir android/app/src/main/res/raw
cp john_bartmann_04_earning_happiness.mp3 android/app/src/main/res/raw

You can download a copy of John Bartmann’s “Earning Happiness” mp3 file here. It has a public domain license; let’s thank John Bartmann for making his cool “Earning Happiness” music open source (original source at Free Music Archive)!

Next, make the utils directory under your project root:

mkdir utils

Add a new file called SoundPlayer.js in the utils directory. Here’s the source for that:

/**
 * Really simple demo of https://www.npmjs.com/package/react-native-sound
 */
const Sound = require('react-native-sound');
/**
 * Audio credit:
 * https://freemusicarchive.org/music/John_Bartmann/Public_Domain_Soundtrack_Music_Album_One/earning-happiness
 * "Earning Happiness" by John Bartmann licensed under a CC0 1.0 Universal License
 * https://creativecommons.org/publicdomain/zero/1.0/
 */
const FILENAME = 'john_bartmann_04_earning_happiness.mp3';

class SoundPlayer {
    constructor(props) {
        console.log("SoundPlayer constructor");
    };
    stopSound() {
        console.log('SoundPlayer.stopSound');
        this.whoosh.stop();
        this.whoosh.release();
    }
    playSound() {
        console.log('SoundPlayer.playSound');
        let me = this;
        me.whoosh = new Sound(FILENAME, Sound.MAIN_BUNDLE, error => {
            if (error) {
                console.log('failed to load the sound', error);
                return;
            }
            // loaded successfully
            console.log(
                'duration in seconds: ' +
                me.whoosh.getDuration() +
                'number of channels: ' +
                me.whoosh.getNumberOfChannels()
            );
            me.whoosh.play(success => {
                if (success) {
                console.log('successfully finished playing');
                } else {
                console.log('playback failed due to audio decoding errors');
                }
            });
        });
    }
}
export default SoundPlayer;

Replace the code in App.js with this code:

/**
 * Simple demo of https://www.npmjs.com/package/react-native-sound
 * The app plays an included mp3 file when the user clicks on 'Play' button.
 * It stops the playback when the user clicks on the 'Stop' button.
 */
import React from 'react';
import {
  StyleSheet,
  View,
    Text,
  Button
} from 'react-native';

import {
  Colors
} from 'react-native/Libraries/NewAppScreen';

/** Keep all sound functionality in separate utils area */
import SoundPlayer from './utils/SoundPlayer';

class App extends React.Component {
    constructor(props) {
        super(props);
        this.soundPlayer = new SoundPlayer();
    }

    onPlayPress() {
        console.log('onPlayPress method');
        this.soundPlayer.playSound();
    }

    onStopPress() {
        console.log('onPlayPress method');
        this.soundPlayer.stopSound();
    }

    render() {
        return (
        <View style={styles.body}>
            <Text style={styles.sectionTitle}>Click button to Play</Text>
            <View style={styles.buttonStyle}>
                <Button style={styles.buttonStyle}
                    onPress={this.onPlayPress.bind(this)}
                    title="Play"
                />
            </View>
            <Text style={styles.sectionTitle}>Click button to Stop</Text>
            <View style={styles.buttonStyle}>
                <Button buttonStyle={styles.buttonStyle}
                    onPress={this.onStopPress.bind(this)}
                    title="Stop"
                />
            </View>
        </View>
        );
    }
};

const styles = StyleSheet.create({
    body: {
        backgroundColor: Colors.white,
        flex: 1,
        flexDirection: 'column',
        justifyContent: 'center',
        backgroundColor: 'gainsboro'
    },
    sectionTitle: {
        fontSize: 24,
        fontWeight: '600',
        color: Colors.black,
        padding: 20,
        backgroundColor: 'gainsboro' /* See https://reactnative.dev/docs/colors */,
    },
    buttonStyle: {
        padding: 20,
        backgroundColor: 'gainsboro' /* See https://reactnative.dev/docs/colors */,
    }
});

export default App;

Run the Android Emulator:

emulator -no-snapshot -avd Galaxy_Nexus_API_28

Start your Metro server:

npx react-native start

Install the app in your Android Emulator:

npx react-native run-android

I see these warnings at my command line, but everything seems to work okay:

error React Native CLI uses autolinking for native dependencies, but the following modules are linked manually: 
  - react-native-sound (to unlink run: "react-native unlink react-native-sound")
This is likely happening when upgrading React Native from below 0.60 to 0.60 or above. Going forward, you can unlink this dependency via "react-native unlink <dependency>" and it will be included in your app automatically. If a library isn't compatible with autolinking, disregard this message and notify the library maintainers.
Read more about autolinking: https://github.com/react-native-community/cli/blob/master/docs/autolinking.md
info Running jetifier to migrate libraries to AndroidX. You can disable it using "--no-jetifier" flag.
Jetifier found 875 file(s) to forward-jetify. Using 8 workers...

Then it just works! You can hit the Play and Stop buttons to play the audio.

The source code for this project is available at github.

One remark: when I initially created this project, I didn’t use the link command, because the React Native documentation indicated it was not needed for version 0.60. However, that turned out to be untrue! You can save yourself time by doing it the way I’ve done it in this tutorial. When using link, the demo worked immediately, and I didn’t have to make any changes to gradle files.

Here’s a view of the demo app:

Audio Player using React Native

If you found this interesting, click the subscribe button below! I write a new post about once a week.

How to solve Postman “Could not get any response” error

Trying to build a REST API can be really frustrating. You install Express.js with mongoose or mongodb, and then create a schema, just following the instructions, and you think everything should work. You set up Postman to test the API, hit the API, and Postman stays at “sending request..” for a while. You wait… and wait… and then eventually you see the message “Could not get any response”.

Postman Could not get any response
Postman Could not get any response

Sometimes, it works fine to set everything up all at once. But, it can often happen that there are just too many moving parts. When that happens, and something breaks, it can be very difficult to figure out the point of failure.

As an example, suppose you have an Express router which handles a POST request like this using a mongoose model:

const router = require('express').Router();
// ... mongoose code here
router.post('/registerKitten', async (req, res) => {
    const fluffy = new Kitten({ name: 'fluffy' });
    try {
        await fluffy.save();
        res.json(fluffy);
    } catch(err) {
        res.status(400).send(err);
    }
})

If Postman times out when hitting the API, you can’t know whether the problem is in Express.js or Mongoose.

The first, simple thing you can do is add some console logging to see if your route is executed:

const router = require('express').Router();
// ... mongoose code here
router.post('/registerKitten', async (req, res) => {
    console.log("registerKitten");
    const fluffy = new Kitten({ name: 'fluffy' });
    try {
        console.log("awaiting save...");
        await fluffy.save();
        console.log("save is finished");
        res.json(fluffy);
    } catch(err) {
        res.status(400).send(err);
    }
})

That won’t tell you the whole story, however. If you see “awaiting save…” but don’t see “save is finished”, then doing this has helped you locate the problem – mongoose is hanging.

At this point, you may be better off writing a small Node.js test script which will just exercise the code related to mongoose, independently of Express. That way, you can debug the problem without having to deal with Express code or restart your Express server.

Here’s some sample code. You’ll have to adjust this to your own specific case, but the idea is to put this code into a script (test.js) and run it from the command line with node test.js:

"use strict";
const mongoose = require('mongoose');
const Kitten = require('./models/Kitten');

mongoose.connect(process.env.MONGO_DB_URL, { useNewUrlParser: true });
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));

// Set up some mock user data
const fluffy = new Kitten({ name: 'fluffy' });

// When the db is open, 'save' this data.
db.once('open', async function () {
    // we're connected!
    console.log("db opened... going to save the kitten");
    await fluffy.save();
    console.log("printing fluffy");
    console.log(fluffy);
    db.close();
});

// Print that db connection has been closed.
db.once('close', function () {
    console.log('close');
});

Notice that the code references an environment variable, process.env.MONGO_DB_URL. You’ll need to set that in your terminal. In Linux, you can do that by using the export command: export MONGO_DB_URL=mongodb://127.0.0.1:27017/myappdb.

If you found this interesting, click the subscribe button below! I write a new post about once a week.

MySQL Cannot delete or update a parent row: a foreign key constraint fails

TL;DR: If you can’t quickly see why your DROP TABLE statement is failing, check for typos in your table names.

In MySQL, foreign key constraints can stop you from dropping a table. But that’s not the only thing that can go wrong.

Let’s create a simple example using MySQL, in order to demo the problem. As the root user, sign in to MySQL using your favorite client. I like to use the command line mysql client, like this: mysql -u root -p.

Next, create a new test database, CREATE DATABASE hamsters;

Then:

USE hamsters;
create table toys (
   toy_id INT NOT NULL AUTO_INCREMENT,
   name VARCHAR(50) NOT NULL,
   PRIMARY KEY ( toy_id )
);
create table toy_makers (
   toy_maker_id INT NOT NULL AUTO_INCREMENT,
   toy_id INT,
   name VARCHAR(25),
   FOREIGN KEY ( toy_id ) REFERENCES toys ( toy_id ),
   PRIMARY KEY ( toy_maker_id )
);

After doing this, verify the tables exist.

mysql> show tables;
+--------------------+
| Tables_in_hamsters |
+--------------------+
| toy_makers         |
| toys               |
+--------------------+
2 rows in set (0.00 sec)

When experimenting with MySQL, you’ll often want to be running CREATE and DROP scripts. Here is my DROP script:

USE hamsters;
DROP TABLE IF EXISTS toys;
DROP TABLE IF EXISTS toy_makes;

Let’s look at the output:

mysql> USE hamsters;
Database changed
mysql> DROP TABLE IF EXISTS toys;
ERROR 1217 (23000): Cannot delete or update a parent row: a foreign key constraint fails
mysql> DROP TABLE IF EXISTS toy_makes;
Query OK, 0 rows affected, 1 warning (0.00 sec)

Well, that’s a bit frustrating, but it’s actually a pretty simple problem. The primary key toy_id in table toys is referenced as a foreign key in table toy_makers. You cannot drop toys first, even though there’s no data in either table! You’ll have to drop any tables that reference toys.toy_id as a foreign key prior to dropping toys. In my case, the solution is simple. Just do a switcheroo on our DROP statements, and DROP the toys table last:

USE hamsters;
DROP TABLE IF EXISTS toy_makes;
DROP TABLE IF EXISTS toys;

Here’s the output:

mysql> USE hamsters;
Database changed
mysql> DROP TABLE IF EXISTS toy_makes;
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> DROP TABLE IF EXISTS toys;
ERROR 1217 (23000): Cannot delete or update a parent row: a foreign key constraint fails

If you’re looking at this, you might be confused. I switched the order in which the tables were dropped, but, hm, I’m still getting the same complaint! Let’s look at our database by using the SHOW TABLES command:

mysql> SHOW TABLES;
+--------------------+
| Tables_in_hamsters |
+--------------------+
| toy_makers         |
| toys               |
+--------------------+
2 rows in set (0.00 sec)

At this point, I hope you’ve noticed the problem! There was a typo in my DROP TABLE script. The command DROP TABLE IF EXISTS toy_makes; ran without an error because it has the option IF EXISTS in it. There’s no table called toy_makes here – the table is called toy_makers.

What if we just run DROP TABLE toy_makes? That does produce an error: ERROR 1051 (42S02): Unknown table 'hamsters.toy_makes'.

Let’s fix my script:

USE hamsters;
DROP TABLE IF EXISTS toy_makers;
DROP TABLE IF EXISTS toys;

Both lines run successfully now, with output like Query OK, 0 rows affected (0.97 sec).

If you found this interesting, click the subscribe button below! I write a new post about once a week.

Adding a map to React Native

This is part of a series on building a movie locator app in React Native. The first post is available here.

In my last post, I’d given the user the ability to navigate to a new page in the app. In this post, I’ll add a map on that new page. It will display the location of the theater that the user picked.

This is the first part of my app build which requires that you do something different depending on whether you’re working with Android or iOS. The installation instructions are here (as of 2020/03/23). I will be following the build configuration on Android, because I’m using an Android emulator (and a Linux OS, not a Mac, for my builds).

There are 6 steps given in the “build config for Android” section. I went through each of them one by one. I created a separate page which describes exactly what I did to prepare to add a map to my React Native app. Take a look there if you want to know about it in more detail.

In my previous post, I listed the code in MapsScreen.js, which is the page where my map should be displayed:

import React, { Component } from 'react';
import { Text, View } from 'react-native';

export default class MapsScreen extends Component {
    render() {
        return (<View style={[{ flex: 1, justifyContent: 'center' },
        { flexDirection: 'row', justifyContent: 'space-around', padding: 10 }]}>
            <Text>A map goes here</Text>
        </View>);
    }
}

But there’s no map there, yet! I need to add the code that will display a map.

Fortunately, the React Native maps project has an example app on GitHub. I took the time to download this example project, and create an entirely separate React Native project which uses its code. This helped me get my feet wet, and I knew where to look for the code that I needed to add a map: StaticMap.js is a page which displays a simple map. I’m going to take some of that code and add it to my own, as follows:

import React, { Component } from 'react';
import { View, Text } from 'react-native';
import MapView from 'react-native-maps';

export default class MapsScreen extends Component {
    render() {
        return (<View style={[{ flex: 1, justifyContent: 'center' },
        { flexDirection: 'row', justifyContent: 'space-around', padding: 10 }]}>
            <MapView
                initialRegion={{
                    latitude: 37.78825,
                    longitude: -122.4324,
                    latitudeDelta: 0.0922,
                    longitudeDelta: 0.0421,
                }}
            />
        </View>);
    }
}

This is almost the same as my previous render method. The only difference is that I’ve added a MapView component. It includes a mocked initialRegion property (latitude and longitude are static). I just want to get maps working, before adding a real initialRegion.

Unfortunately, when I tested this, I just saw an empty display, aside from the Maps title bar. Ugh! No error messages – just nothing! It was frustrating! Since there were no error messages, I wondered if there was something wrong with the display. The StaticMap example uses a MapView that is styled. So I copied that style, which just specified a width and height, and pasted it into my MapView, like this:

<MapView
    style={{width: 250, height: 250}}
    initialRegion={{
        ...

It worked!! I saw a map, but it looked cramped. I changed the style to {{flex:1}}, and the map filled the page! Excellent! Here’s the current behavior:

map of San Francisco in React Native app

The source code for this example – App.js, MoviesScreen.js, and MapsScreen.js – is available online.

If you found this interesting, click the subscribe button below! I write a new post about once a week.

Adding a simple navigation system to React Native – Part II

In my last post, I started the process of adding a navigation system to my app. The idea is to make it easy for the developer to add more pages to the app, and easy for the user to move around from page to page. I’m using the React Navigation community project. I’ve got the right packages installed and imported into my app. It’s clear to me from the documentation that I need to add a new component as a new page.

So I add a rudimentary, blank component. I’ve decided that this page will show a map of the theater that the user picks on the first page, so I name the file MapsScreen.js, with this source:

import React, { Component } from 'react';
import { Text, View } from 'react-native';

export default class MapsScreen extends Component {
    render() {
        return (<View style={[{ flex: 1, justifyContent: 'center' },
        { flexDirection: 'row', justifyContent: 'space-around', padding: 10 }]}>
            <Text>A map goes here</Text>
        </View>);
    }
}

Then I edit App.js to include a route to this component. It looks like this:

import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import MoviesScreen from './MoviesScreen';
import MapsScreen from './MapsScreen';

const MainNavigator = createStackNavigator({
    Movies: { screen: MoviesScreen },
    Maps: { screen: MapsScreen }
});

const App = createAppContainer(MainNavigator);

export default App;

It was more trouble than I’d like to add the React Navigation packages, but now that they’re in place, it’s super easy to add a new page. According to the documentation, I just need to call a function called navigate in order for a button click to take me to a new page.

I’ve already attached a method handleShowTheaterClick to my “GO” button. Here’s the short navigation code that needs to be added:

handleShowTheaterClick() {
    const { navigate } = this.props.navigation;
    navigate('Maps', {});
}

This is all it takes to make the “GO” button functional. I click the button, and I’m taken to my “Maps” screen, which looks like this:

current app functionality selecting movie and theater

Notice there’s a back arrow in the title bar. I can click that to get back to the previous screen. I hardly needed to do any work at all to get built-in navigation functionality. This is the beauty of code reuse.

The source code for this example – App.js, MoviesScreen.js, and MapsScreen.js – is available online.

If you found this interesting, click the subscribe button below! I write a new post about once a week.

Adding a simple navigation system to React Native – Part I

In previous posts (start here), I’ve written about building a movie app that a person can use to pick a movie from a list, and then click a button to display theaters where the movie is being shown.

Let’s add some code which allows our user to select a specific theater. Upon doing that, a button will appear which will take them to a new page in the app.

I only want the new button to appear when the list of theaters is populated. Here’s how I ensure that:

render() {
    if (this.state && !this.state.isLoading) {
        ...
        var theatersPicker = null;
        var goButton = null;
        if (this.state.nearbyTheaters) {
            ...
            theatersPicker = <Picker>{theaters}</Picker>
            goButton = <Button onPress={this.handleShowTheaterClick.bind(this)} title="Go"></Button>;
        }
        return <View>
        ...
            <Button onPress={this.handleClick.bind(this)} title="Find Movie Near Me"></Button>
            {theatersPicker}
            {goButton}
        </View>;

The goButton has been initialized to null. It will only be set to a Button if the theatersPicker has a list of theaters. I also added a new, empty method called handleShowTheaterClick (not shown here). The new button will call handleShowTheaterClick when clicked, but since it’s empty, it won’t do anything yet. We’ll get to that later.

You’ll find that the {goButton}line doesn’t show anything if goButton is null. That’s the UX that we want.

At this point, I want handleShowTheaterClick to cause the app to display a new page. This behavior is new! So far, everything has been happening in a single page. We’re getting into navigation, also known as “routing”. It’s how users get around the app from one page to another.

Navigation is not built into the default React Native install. You have to install it as a separate package. I followed the instructions for doing this as given in the documentation, but I got a lot of warnings. It took me about 30 minutes to resolve them, ugh! I kept trying and retrying… One of the errors that I saw in the Metro server terminal window looked like this:

error: bundling failed: Error: Unable to resolve module `react-navigation-stack` from `App.js`: react-navigation-stack could not be found within the project.

If you are sure the module exists, try these steps:
 1. Clear watchman watches: watchman watch-del-all
 2. Delete node_modules: rm -rf node_modules and run yarn install
 3. Reset Metro's cache: yarn start --reset-cache
 4. Remove the cache: rm -rf /tmp/metro-*

This turned out to be misleading. I thought that I need to use yarn, so I installed it, and started using it according the instructions. It could be that this works for some setups, but I’m running Ubuntu 16.04, and I’ve been using npm throughout this exercise. I should have stuck with npm. I learned that if a message mentioned yarn, I should just use npm instead. I suggest you do the same, unless you’re already using yarn to run other commands.

When I finally switched back to using npm, I found there were some warnings about “peer dependencies”. Here is one example:

...
npm WARN @typescript-eslint/eslint-plugin@1.13.0 requires a peer of eslint@^5.0.0 but none is installed. You must install peer dependencies yourself.

I wound up installing each peer dependency when I saw that one was missing. Then I had to go back and install the package which required the dependency again (so it seemed). In the end, I did all these installs in my project root (where App.js lives), in the order given:

npm install eslint@^5.0.0
npm install typescript@>=3.7.0-dev
npm install react-native-gesture-handler@*
npm install @react-native-community/masked-view@^0.1.1
npm install react-native-safe-area-context@^0.6.0
npm install react-navigation-stack
npm install react-navigation

Installing all that stuff was kind of a headache, and I was happy when it was finally done.

A quick read of the navigation documentation told me that I should only have navigation code in my App.js file. My existing application logic should go into a separate “screen” page. I’ll call this new page MoviesScreen.

To do this, I copied all the source of App.js into a new file, which I named MoviesScreen.js. Then I replaced HelloWorldApp in that source with MoviesScreen. Really, just replace

export default class HelloWorldApp extends Component {

with

export default class MoviesScreen extends Component {

Next, I replaced all the code in App.js with this simple navigation code:

import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import MoviesScreen from './MoviesScreen';

const MainNavigator = createStackNavigator({
    Movies: { screen: MoviesScreen }
});

const App = createAppContainer(MainNavigator);

export default App;

After doing this, I ran npx react-native run-android in my project root. I’ve learned that running npx react-native run-android sometimes clears up errors, although I’m not exactly sure when or if it’s required (might be a superstition type thing!).

The net result was that my app looked almost exactly the same as it did before! That’s expected, and good, because I only made infrastructure changes, and no functional ones. The only difference now is that there’s a title bar at the top of the display. It reads “Movies”. It’s not beautiful, but I’ll figure out how to deal with that later.

In testing the latest version of my app, I notice there’s one problem: when I select a theater from my “theaters” Picker, the value doesn’t stay selected. I know how to fix that – the same way that I did it for the “movies” Picker. I add similar code for that in MoviesScreen‘s render method:

...
theatersPicker = <Picker selectedValue={this.state.theater}
    onValueChange={(itemValue, itemIndex) => this.setState({ theater: itemValue })
    }>{theaters}</Picker>
current app functionality selecting movie and theater

Now, my app just works, except for one little problem. The GO button does nothing! Clicking on it should take the user to a new page. I’ll work that out in my next post.

The source code for this new version of App.js and for MoviesScreen are available online.

If you found this interesting, click the subscribe button below! I write a new post about once a week.

How to handle button clicks in React Native – Part II

Now that my Button responds to clicks (see previous post), I can program my React Native app to do something interesting in response to the click. Remember that my user is going to select a movie title from a dropdown list of movies, and then click a button to get a list of nearby movie theaters where the movie is showing.

There are REST APIs that will return the information we want. However, I’m not going to use them – at least not yet. Why not? Because I don’t want to go down a rabbit hole researching all the possible REST APIs. Even if it wouldn’t take very long, I don’t want to get sidetracked right now.

It’s important to note that if you’re building an app for business purposes, you should not lift one finger to code up your app until you’ve figured out what 3rd party services you need, and whether there is one available which will let you access it within your budget constraints. Suppose you’ve decided that you want to build an app which supplies turn-by-turn directions to every independent coffee shop across the world. This sounds kind of like a niche app, “Google Maps for coffee”. Well, just because the Google Maps app can do something like this, doesn’t mean that a REST service exists which will be available to you. Don’t make that kind of assumption! And even if the service does exist, it may not be affordable. Suppose it exists but there’s a charge for each API call… will your business model be profitable if you take into account the cost?

Since we’re not building a real app, but just messing around to become familiar with React Native, we don’t have to worry about the above business realities. Instead, I’m going to build a very simple REST API which returns a list of movie locations. By mocking the API, I can keep my flow of building the app working, and worry about third-party features later.

I can easily mock the API because I’ve got a domain name and a shared server. All I have to do is upload a PHP file with the following content:

<?php
$theaters = array();
$theaters[] = array( "name" => "Paris Theatre", "phone" => "(212)688-3800", "address1" => "4 West 58th Street", "city" => "New York", "state" => "NY", "zip" => "10019");
$theaters[] = array( "name" => "French Institute Alliance Francaise", "phone" => "(212)355-6160", "address1" => "55 East 59th Street", "city" => "New York", "state" => "NY", "zip" => "10022");
$theaters[] = array( "name" => "Roxy Cinema Tribeca", "phone" => "(212)519-6820", "address1" => "2 Avenue of the Americas", "city" => "New York", "state" => "NY", "zip" => "10013");

$out = json_encode($theaters);
echo $out;
?>

The list of movie theaters gives this static output:

[{"name":"Paris Theatre","phone":"(212)688-3800","address1":"4 West 58th Street","city":"New York","state":"NY","zip":"10019"},
{"name":"French Institute Alliance Francaise","phone":"(212)355-6160","address1":"55 East 59th Street","city":"New York","state":"NY","zip":"10022"},
{"name":"Roxy Cinema Tribeca","phone":"(212)519-6820","address1":"2 Avenue of the Americas","city":"New York","state":"NY","zip":"10013"}]

You can do that with a static HTML page if you want, it doesn’t matter. If you don’t have a server and don’t want to spend time setting one up, just reuse my URL.

I’m going to do a little copy/paste here. People rant against copying and pasting code, and there are reasons not to do it, but it’s actually pretty reasonable to do this in rapid application development of prototypes. The idea is “let’s just get this done, since the code might be thrown out tomorrow.” Yes, doing this will lead to technical debt. However, you may never have to pay off the debt, if the application is just an experiment. Further, you’re not at a stage where refactoring for reusability makes sense, yet. So I just copy the code that was used to fetch in componentDidMount, paste it, and make some small changes. Ditto for my setMovieState and handleMoviesResponse methods. Here’s what I wind up with:

...
setNearbyTheatersState(theaters) {
    this.setState({
        isLoading: false,
        nearbyTheaters: theaters,
    });
}
...
handleNearbyTheatersResponse(theaters, delay) {
    if (delay && delay > 0) {
        const timer = setTimeout(function () {
            this.setNearbyTheatersState(theaters);
        }.bind(this), delay);
    } else {
        this.setNearbyTheatersState(theaters);
    }
}
...
handleClick() {
    console.log("handleClick");
    return fetch('https://www.fullstackoasis.com/rnb/theaters.php')
        .then((response) => response.json())
        .then((responseJson) => {
            // TODO FIXME handle timeout / delay
            this.handleNearbyTheatersResponse(responseJson);
        })
        .catch((error) => {
            // TODO FIXME replace the red screen with something informative.
            console.error(error);
        });
}
...

This is pretty much a repeat of the REST call to get the list of movies, only now I’m calling my own mocked API, and setting a state parameter named “nearbyTheaters”. Looks good! But when I click the button, I see a RedBox which reads "TypeError: _this2.handleNearbyTheatersResponse is not a function. (In '_this2.handleNearbyTheatersResponse(responseJson)', '_this2.handleNearbyTheatersResponse' is undefined)".

You might think that copy/paste didn’t work so well for us… but I’d disagree. It got us to an error faster than if we wrote all the code from scratch. Now it’s just a matter of figuring out what’s gone wrong.

To help debug this problem, I add a comment just before the call to this.handleNearbyTheatersResponse:

handleClick() {
    console.log("handleClick");
    return fetch('https://www.fullstackoasis.com/rnb/theaters.php')
        .then((response) => response.json())
        .then((responseJson) => {
            // TODO FIXME handle timeout / delay
            console.log(this);
            this.handleNearbyTheatersResponse(responseJson);
        })
        .catch((error) => {
            // TODO FIXME replace the red screen with something informative.
            console.error(error);
        });
}

Then I click the button again. I see the following logged in my terminal:

{"accessibilityLabel": undefined, "accessibilityRole": "button", "accessibilityStates": [], "background": {"attribute": "selectableItemBackground", "type": "ThemeAttrAndroid"}, "children": <RCTView style={[[Object]]}><ForwardRef(Text) style={[Array]}>FIND MOVIE NEAR ME</ForwardRef(Text)></RCTView>, "disabled": undefined, "hasTVPreferredFocus": undefined, "nextFocusDown": undefined, "nextFocusForward": undefined, "nextFocusLeft": undefined, "nextFocusRight": undefined, "nextFocusUp": undefined, "onPress": [Function handleClick], "testID": undefined, "touchSoundDisabled": undefined}

At this point I was a little puzzled; it looked to me like this was a reference to my Button, and not my app. I expected the latter. Here’s the relevant part of the render method, again:

<Button onPress={this.handleClick} title="Find Movie Near Me"></Button>

In fact, JavaScript’s correct behavior is a little hazy to me. I could find discussions that made claims about the standard behavior, but no references to the specification, and I didn’t go hunting for the official answer. Let’s not waste time tracking this down right now. Let’s assume this refers to the Button being pressed. That may be useful in some cases, but in my case, I want this to reference the app itself so that I can call handleNearbyTheatersResponse within the handleClick method.

I took a stab at fixing the problem by binding the Button‘s onPress method to this:

<Button onPress={this.handleClick.bind(this)} title="Find Movie Near Me"></Button>

That worked! Once I do that, clicking the button does not produce an error. The call to setState in setNearbyTheatersState is made. I verified this by adding a new Picker which is populated with theater listings if this.state.nearbyTheaters exists.

Here’s the new code in my render method:

if (this.state && !this.state.isLoading) {
    let items = [];
    var length = this.state.dataSource.length;
    for (var i = 0; i < length; i++) {
        var item = this.state.dataSource[i];
        // Really, really important to not put quotes around these braces:
        items.push(<Picker.Item label={item.title} value={item.title} key={item.id} />);
    }
    var theatersPicker = null;
    if (this.state.nearbyTheaters) {
        let theaters = this.state.nearbyTheaters.map(function (t, i) {
            return <Picker.Item label={t.name} value={t.name} key={i} />;
        });
        theatersPicker = <Picker>{theaters}</Picker>
    }
    return <View>
        <Picker selectedValue={this.state.movie}
            onValueChange={(itemValue, itemIndex) =>
                this.setState({ movie: itemValue })
            }>
            {items}
        </Picker>
        <Button onPress={this.handleClick.bind(this)} title="Find Movie Near Me"></Button>
        {theatersPicker}
    </View>;
    ...

The source code for this experiment is available online.

If you found this interesting, click the subscribe button below! I write a new post about once a week.