Using the UI Toolkit


React Native exposes plain iOS and Android native components that you can use, but there’s usually much work left to do just to make them look beautiful. Instead, you can use @shoutem/ui, a set of customizable UI components. There are plenty of components that you can use out of the box.

Creating a Restaurants List

Let’s create a list of restaurants. Start by importing UI components from the toolkit.

#file: app/screens/List.js
import React, {
  Component
} from 'react';

import {
  StyleSheet
} from 'react-native';

import {
  Image,
  ListView,
  Tile,
  Title,
  Subtitle,
  Overlay,
  Screen
} from '@shoutem/ui';

import { NavigationBar } from '@shoutem/ui/navigation';

Notice that you didn’t need to install the @shoutem/ui package into the app folder of your extension. That’s because this package will be installed in the extension by the app into which your extension is bundled. All packages installed by the app by default can be found in peerDependencies of app/package.json. Also, we removed View and Text from the react-native import.

We prepared some mockup restaurants data for you. Download this compressed file, extract it and copy the extracted assets folder into your app folder. The assets folder contains static restaurants data in restaurants.json.

Define a method in your List class that returns an array of restaurants.

#file: app/screens/List.js
export default class List extends Component {

  getRestaurants() {
    return require('../assets/restaurants.json');
  }

Implement a render method that will use a ListView component. ListView accepts data in the form of an array to be shown in the list and renderRow is a callback function that defines how each row in the list should look.

Add the renderRow method and change the implementation of the render method:

#file: app/screens/List.js
  getRestaurants() {...}

  // defines the UI of each row in the list
  renderRow(restaurant) {
    return (
      <Image styleName="large-banner" source={{ uri: restaurant.image &&
        restaurant.image.url ? restaurant.image.url : undefined  }}>
        <Tile>
          <Title>{restaurant.name}</Title>
          <Subtitle>{restaurant.address}</Subtitle>
        </Tile>
      </Image>
    );
  }

  render() {
    return (
      <Screen>
        <NavigationBar title="RESTAURANTS" />
        <ListView
          data={this.getRestaurants()}
          renderRow={restaurant => this.renderRow(restaurant)}
        />
      </Screen>
    );
  }

Since we’ve only changed the app code now, we don’t need to upload the extension. However, in case you’re checking the changes in the Builder, do:

$ shoutem push
Uploading `Restaurants` extension to Shoutem...
Success!

The app preview will be shown after Shoutem bundles the new app. List is now showing the list of restaurants.

This looks exactly how we wanted.

Try clicking on a one of the restaurants. Nothing happens! We want to open up the screen with a restaurant’s details when the user touches a row in the list.

Creating a Details Screen

When a restaurant in the list is touched, we will open the details screen for that restaurant. To make components respond to touches, use the TouchableOpacity component from React Native. We’ll also import Shoutem’s navigateTo action creator to navigate to another screen and the ext function for the name of screen we’re navigating to.

Let’s import these things (find the complete code below):

#file: app/screens/List.js
import {
  TouchableOpacity
} from 'react-native';
import { navigateTo } from '@shoutem/core/navigation';
import { ext } from '../extension';

Connect navigateTo action creator to redux store.

#file: app/screens/List.js
import { connect } from 'react-redux';

export class List extends Component {
  constructor(props) {
    super(props);

    // bind renderRow function to get the correct props
    this.renderRow = this.renderRow.bind(this);
  }

  getRestaurants() {...}
}

// connect screen to redux store
export default connect(
  undefined,
  { navigateTo }
)(List);

Note

Make sure that you remove the default from export default class List extends Component because there can only be one default export and we want export default connect to be it.

Now create the Details screen:

$ shoutem screen add Details
File `app/screens/Details.js` is created.
File `app/extension.js` was modified.
File `extension.json` was modified.

We didn’t create a shortcut since this screen isn’t the starting screen your extension.

Open the restaurants details screen in the renderRow function. The navigateTo action creator accepts Shoutem route object as the only argument with screen (full name of screen to navigate to) and props (passed to screen) properties. To get the full name of the screen, we’ll use the ext function, which returns the full name of the extension part passed as its first argument (e.g. returns tom.restaurants.Details for Details) or the full extension name (e.g. tom.restaurants) if no argument is passed.

#file: app/screens/List.js
  renderRow(restaurant) {
    const { navigateTo } = this.props;

    return (
      <TouchableOpacity onPress={() => navigateTo({
        screen: ext('Details'),
        props: { restaurant }
      })}>
        <Image styleName="large-banner" source={{ uri: restaurant.image &&
        restaurant.image.url ? restaurant.image.url : undefined }}>
          <Tile>
            <Title>{restaurant.name}</Title>
            <Subtitle>{restaurant.address}</Subtitle>
          </Tile>
        </Image>
      </TouchableOpacity>
    );
  }

This is what you should end up with in app/screens/List.js:

#file: app/screens/List.js
import React, {
  Component
} from 'react';

import {
  TouchableOpacity,
} from 'react-native';

import {
  Image,
  ListView,
  Tile,
  Title,
  Subtitle,
  Overlay,
  Screen
} from '@shoutem/ui';

import { NavigationBar } from '@shoutem/ui/navigation';
import { navigateTo } from '@shoutem/core/navigation';
import { ext } from '../extension';
import { connect } from 'react-redux';

export class List extends Component {
  constructor(props) {
    super(props);

    // bind renderRow function to get the correct props
    this.renderRow = this.renderRow.bind(this);
  }

  getRestaurants() {
    return require('../assets/restaurants.json');
  }

  // defines the UI of each row in the list
  renderRow(restaurant) {
    const { navigateTo } = this.props;

    return (
      <TouchableOpacity onPress={() => navigateTo({
        screen: ext('Details'),
        props: { restaurant }
      })}>
        <Image styleName="large-banner" source={{ uri: restaurant.image &&
        restaurant.image.url ? restaurant.image.url : undefined }}>
          <Tile>
            <Title>{restaurant.name}</Title>
            <Subtitle>{restaurant.address}</Subtitle>
          </Tile>
        </Image>
      </TouchableOpacity>
    );
  }

  render() {
    return (
      <Screen>
        <NavigationBar title="RESTAURANTS" />
        <ListView
          data={this.getRestaurants()}
          renderRow={restaurant => this.renderRow(restaurant)}
        />
      </Screen>
    );
  }
}

// connect screen to redux store
export default connect(
  undefined,
  { navigateTo }
)(List);

For the Details screen just copy the following code. We’re not introducing any new concept here, just using some additional components.

#file: app/screens/Details.js
import React, {
  Component
} from 'react';

import {
  ScrollView,
} from 'react-native';

import {
  Icon,
  Row,
  Subtitle,
  Text,
  Title,
  View,
  Image,
  Divider,
  Overlay,
  Tile,
} from '@shoutem/ui';

export default class Details extends Component {
  render() {
    const { restaurant } = this.props;

    return (
      <ScrollView style = {{marginTop:-70}}>
        <Image styleName="large-portrait" source={{ uri: restaurant.image &&
        restaurant.image.url ? restaurant.image.url : undefined }}>
          <Overlay styleName="fill-parent">
            <Title>{restaurant.name}</Title>
            <Subtitle>{restaurant.address}</Subtitle>
          </Overlay>
        </Image>

        <Row>
          <Text>{restaurant.description}</Text>
        </Row>

        <Divider styleName="line" />

        <Row>
          <Icon name="laptop" />
          <View styleName="vertical">
            <Subtitle>Visit webpage</Subtitle>
            <Text>{restaurant.url}</Text>
          </View>
          <Icon name="right-arrow" />
        </Row>

        <Divider styleName="line" />

        <Row>
          <Icon name="pin" />
          <View styleName="vertical">
            <Subtitle>Address</Subtitle>
            <Text>{restaurant.address}</Text>
          </View>
          <Icon name="right-arrow" />
        </Row>

        <Divider styleName="line" />

        <Row>
          <Icon name="email" />
          <View styleName="vertical">
            <Subtitle>Email</Subtitle>
            <Text>{restaurant.mail}</Text>
          </View>
        </Row>

        <Divider styleName="line" />
      </ScrollView>
    );
  }
}

Upload the extension:

$ shoutem push
Uploading `Restaurants` extension to Shoutem...
Success!

Now when you preview the app and click on a restaurant in the list, this is what you get:

That’s exactly what we wanted! However, your app is using static, local data. Let’s connect it to the Shoutem Cloud.