Skip to content

Latest commit

 

History

History
646 lines (519 loc) · 21.4 KB

App.Navigation.md

File metadata and controls

646 lines (519 loc) · 21.4 KB

Step #1: App Navigation (with react native navigation)

Every app starts with navigation. In this section, we are going to build the skeleton of our app using react-native-navigation.

About react-native-navigation

React Native Navigation (RNN) is an open source project created by the Wix mobile team. It is now one of the top open source project by Wix. As opposed to other popular JS navigation solutions for react-native, RNN is a 100% native navigation solution with a simple cross-platform JS API.

This tutorial will help you learn RNN's basic functionality and feel more comfortable diving into the more complex features and API. You can view full documentation here.

Useful links

What we’re going to do

Here's the outline of what we are going to build in this section:

app skelaton

  • We will create a PostsList screen.
  • Tapping on a post will push the ViewPost screen
  • From the ViewPost screen, we can delete the post and go back to the posts list.
  • From the list, we also have an option to open a modal with the AddPost screen.
  • From the AddPost screen, we can cancel or save and go back to the posts list.

Getting Started

1. Start with a simple react-native init project

You can follow the react-native getting started guide(Choose "React Native CLI Quickstart" and not "Expo Quickstart") to make sure you have all dependencies installed. If this is not the first time you are creating a react-native project just open the terminal and run:

npx react-native init wixMobileCrashCourse
cd wixMobileCrashCourse

To run the project you will need to do:

npx react-native run-ios
npx react-native run-android

You should then see your new app running within your simulators:

iOS Android
ios android

2. Install react-native-navigation

As react-native-navigation is a native navigation library, so integrating it into your app will require editing native files. Follow the installation guides in the documentation here.

Make sure your app is still running in both simulators and that you are not getting any red screens.

❗ If you're running React Native 0.60+ make sure you install React Native Navigation v3.0.0 or later.

Adding the Screens

3. Create and Register Screens

In src/posts/screens create three screens: PostsList.js, ViewPost.js and AddPost.js. Each screen should be a very basic component that looks like this:

With Class Components
import React, {Component} from 'react';
import {View, Text, StyleSheet} from 'react-native';

class PostsList extends Component {

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>PostsList Screen</Text>
      </View>
    );
  }
}

export default PostsList;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#D3EDFF',
  },
  text: {
    fontSize: 28,
    textAlign: 'center',
    margin: 10,
  }
});
With Functional Components
import React, {Component} from 'react';
import {View, Text, StyleSheet} from 'react-native';

const PostsList = (props) => {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>PostsList Screen</Text>
      </View>
    );
}

export default PostsList;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#D3EDFF',
  },
  text: {
    fontSize: 28,
    textAlign: 'center',
    margin: 10,
  }
});

Every screen component in your app must be registered with a unique name before you are able to use it. So create a src/screens.js file and register the new screens you have just created.

Here is what your screens.js file should look like. (This file is also available in this repository):

  import {Navigation} from 'react-native-navigation';

  export function registerScreens() {

    Navigation.registerComponent('blog.PostsList', () => require('./posts/screens/PostsList').default);
    Navigation.registerComponent('blog.AddPost', () => require('./posts/screens/AddPost').default);
    Navigation.registerComponent('blog.ViewPost', () => require('./posts/screens/ViewPost').default);

  }

4. Initialize the App Layout

From your index.js file, call registerScreens and initialize the app layout that you want via the setRoot command - pick the simplest layout, which would be the one based on a single stack (a stack supports child layouts of any kind; it can be initialized with more than one screen, in which case the last screen will be presented at the top of the stack) with a single component - our PostsList screen.

The possibilities of the layout API are almost endless and you can create almost any arbitrary native layout. You can check out all of the layout types here.

Here is what your index.js should look like:

import {Navigation} from 'react-native-navigation';
import {registerScreens} from './src/screens';

registerScreens();

Navigation.events().registerAppLaunchedListener(() => {
  Navigation.setRoot({
    root: {
      stack: {
        children: [
          {
            component: {
              name: 'blog.PostsList',
              options: {
                topBar: {
                  title: {
                    text: 'Blog'
                  }
                }
              }
            }
          }
        ],
      }
    }
  });
});

You have just set the root using a single stack with the PostsList component AND the Top Bar title provided in the Options object; You can check the complete Options object format here.

When you refresh the app, you should get the blue PostsList screen:

All actions described in this section are provided in this commit.

Pushing Your First Screen

Now we want to enable the following behavior: when a user clicks on the text, the app pushes the ViewPost screen. Later on, it will be very easy to attach the same function to a list item instead of text.

5. Push a Screen into the Navigation Stack

To push a new screen into this screen’s navigation stack, we will use Navigation.push. This method expects to receive the current componentId which can be found in props.componentId.

So in PostsList.js create a pushViewPostScreen function and attach it to the onPress event of the Text item.

Here is how PostsList.js will look like:

With Class Components
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {View, Text, StyleSheet} from 'react-native';
import {Navigation} from 'react-native-navigation';

class PostsList extends Component {

  static propTypes = {
    componentId: PropTypes.string
  };

  pushViewPostScreen = () => {
    Navigation.push(this.props.componentId, {
      component: {
        name: 'blog.ViewPost',
        passProps: {
          somePropToPass: 'Some props that we are passing'
        },
        options: {
          topBar: {
            title: {
              text: 'Post1'
            }
          }
        }
      }
    });
  }


  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text} onPress={this.pushViewPostScreen}>PostsList Screen</Text>
      </View>
    );
  }
}
...
With Functional Components
import React, {useCallback} from 'react';
import {View, Text, StyleSheet} from 'react-native';
import {Navigation} from 'react-native-navigation';

const PostsList = (props) => {

  const pushViewPostScreen = useCallback(() => {
     Navigation.push(props.componentId, {
      component: {
        name: 'blog.ViewPost',
        passProps: {
          somePropToPass: 'Some props that we are passing'
        },
        options: {
          topBar: {
            title: {
              text: 'Post1'
            }
          }
        }
      }
    });
  }, [props.componentId]);

  return (
    <View style={styles.container}>
      <Text style={styles.text} onPress={pushViewPostScreen}>PostsList Screen</Text>
    </View>
  );
}
...

Several things in the code above that we didn't cover are:

  • passProps - you can pass props to screen which we are pushing.
  • options - you can style the appearance of the navigator and its behavior by passing any options via the Options object. This object can be declared in two different ways:
  1. You can declare the object dynamically when adding a screen to the layout hierarchy, as we did in the code above.
  2. You can also define the object by setting static options() on the screen component. This declaration is static and should be done for every screen. In the next section, we will explore this option.

When you refresh the app, you should now be able to push the ViewPost screen by clicking (which in real life, outside of the emulator, would be tapping) on the text:

All the steps from this section can be viewed in this commit.

Adding Buttons to the Top Bar

On the PostsLists screen, we want to have an “Add” button that opens the AddPost screen as a modal. Buttons are part of the Options object.

6. Add the “Add” Button (to PostList Screen)

Declare the button in the PostsList screen statically. Top bar buttons have multiple options for customization, but for the purposes of this course we will declare a very simple button with a title and an id.

We want the component to handle the button click, so you will need to do 2 things:

  • Add Navigation.events().bindComponent(this) to the constructor.
  • When the top bar button press event is triggered, the app need to call the navigationButtonPressed - implement that and alert or console.log the pressed button id.

Here is how your postList.js file will look like:

With Class Components
...

class PostsList extends Component {

 constructor(props) {
    super(props);

    Navigation.events().bindComponent(this);
 ...

  static options() {
    return {
      topBar: {
        rightButtons: [
          {
            id: 'addPost',
            text: 'Add'
          }
        ]
      }
    };
  }

  navigationButtonPressed({buttonId}) {
    alert(buttonId);
  }
  ...
  pushViewPostScreen() {
  ...
With Functional Components
...

const PostsList = (props) => {

  useEffect(() => {
    const subscription = Navigation.events().registerNavigationButtonPressedListener(
      ({buttonId}) => alert(buttonId),
    );
    return () => subscription.remove();
  }, []);

  ...
  const pushViewPostScreen = useCallback(() => {
  ...
  return (
  ...
  );
}, []);

PostsList.options = {
    topBar: {
      rightButtons: [
        {
          id: 'addPost',
          text: 'Add'
        }
      ]
    }
  };
...

Now you have an "Add" button and whenever you press it, you should get an alert (or a log message) with the buttonId (in our example it is addPost).

Next, instead of the just displaying the buttonId as an alert or message, let's actually write a handle for the press event and show the AddPost screen as a modal with Navigation.showModal.

All the steps from this section can be viewed in this commit.

7. Add “Cancel” and “Save” Buttons (to AddPost Screen)

Flowing the same logic we used to add the Add Button, now add the Cancel and Save buttons to the Top bar of the AddPost screen. Whenever the Cancel button is clicked, use Navigation.dismissModal to dismiss the modal and go back to the PostsList screen.

❗ Left buttons on Android only support icons, so we will add an "X" icon which you can download from the /src/icons folder.

With Class Components
...
import PropTypes from 'prop-types';
import {Navigation} from 'react-native-navigation'

class AddPost extends Component {

  static propTypes = {
    componentId: PropTypes.string
  };

  constructor(props) {
    super(props);
    Navigation.events().bindComponent(this);
  }

  static options() {
    return {
      topBar: {
        title: {
          text: 'Add Post'
        },
        rightButtons: [{
          id: 'saveBtn',
          text: 'Save'
        }],
        leftButtons: [{
          id: 'cancelBtn',
          icon: require('../../icons/x.png')
        }]
      }
    };
  }

  navigationButtonPressed({buttonId}) {
    if (buttonId === 'cancelBtn') {
      Navigation.dismissModal(this.props.componentId);
    } else if (buttonId === 'saveBtn') {
      alert('saveBtn');
    }
  }

...
With Functional Components
...
import {Navigation} from 'react-native-navigation'

const AddPost = (props) => {
  ...
  useEffect(() => {
    const subscription = Navigation.events().registerNavigationButtonPressedListener(
      ({buttonId}) => {
        if (buttonId === 'cancelBtn') {
          Navigation.dismissModal(props.componentId);
        } else if (buttonId === 'saveBtn') {
          alert('saveBtn');
        }
      },
    );
    return () => subscription.remove();
  }, []);
  ...
}

AddPost.options = {
  topBar: {
    title: {
      text: 'Add Post'
    },
    rightButtons: [{
      id: 'saveBtn',
      text: 'Save'
    }],
    leftButtons: [{
      id: 'cancelBtn',
      icon: require('../../icons/x.png')
    }]
  }
}
...

All the steps from this section can be viewed in this commit.

8. Set the Style of the “Save” Button

If you look at the GIF image showing the final stage the app will have by the end of this section (at the bottom of this page), the Save button is disabled until a user starts typing something in the TextInput. To disable the button, we can simply add enabled: false in the button option.

But how do we set styles dynamically? Glad you asked. Navigation.mergeOptions to the rescue!

You can pass any Options object in the mergeOptions method, which will dynamically change a screen style. These options are merged with the existing Options object.

Let's add a TextInput and set the Save Button dynamically.

This is how our AddPost screen will look like:

With Class Components
...
import {View, Text, TextInput, StyleSheet} from 'react-native';
...
class AddPost extends Component {

  ...

  static options() {
    return {
      topBar: {
        title: {
          text: 'Add Post'
        },
        rightButtons: [{
          id: 'saveBtn',
          text: 'Save',
          enabled: false
        }],
        leftButtons: [{
          id: 'cancelBtn',
          icon: require('../../icons/x.icon.png')
        }]
      }
    };
  }

  ...

  onChangeText = text => {
    Navigation.mergeOptions(this.props.componentId, {
      topBar: {
        rightButtons: [{
          id: 'saveBtn',
          text: 'Save',
          enabled: !!text
        }]
      }
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>AddPost Screen</Text>
        <TextInput
          placeholder="Start writing to enable the save btn"
          onChangeText={this.onChangeText}
        />
      </View>
    );
  }
}

export default AddPost;
With Functional Components
...
import {View, Text, TextInput, StyleSheet} from 'react-native';
...
const AddPost = (props) => {
  ...
  const [text, setText] = useState('');

  const onChangeText = useCallback(text => {
    setText(text);
    Navigation.mergeOptions(props.componentId, {
      topBar: {
        rightButtons: [{
          id: 'saveBtn',
          text: 'Save',
          enabled: !!text
        }]
      }
    });
  }, [props.componentId]);

  return (
    <View style={styles.container}>
      <Text style={styles.text}>AddPost Screen</Text>
      <TextInput
        placeholder="Start writing to enable the save btn"
        value={text}
        onChangeText={onChangeText}
      />
    </View>
  );
}

AddPost.options = {
  topBar: {
    title: {
      text: 'Add Post'
    },
    rightButtons: [{
      id: 'saveBtn',
      text: 'Save',
      enabled: false
    }],
    leftButtons: [{
      id: 'cancelBtn',
      icon: require('../../icons/x.icon.png')
    }]
  }
}

export default AddPost;

You can check this commit.

We're Almost Done

The app navigation backbone is almost ready and we will leave the rest to you.

9. Implement the remaining buttons press events:

  • Save - Dismiss the modal when clicking on the Save button in the same way the Cancel button does (won't actually do any saving for now).
  • Delete - In The ViewPost screen, add the Delete button. Use Navigation.pop to pop the ViewPost screen from the stack and reveal the PostsList screen underneath (again, not actually deleting anything just yet).

You can check out these commits if you need a hint: Save Button, Delete Button.

Your app should now look something like this:

Quick Recap

By this point you have:

You can view the full project in this repository. Also you can find class components version in this branch


What’s Next

Adding App Logic and State Management with Remx