Chapter 10: Movies App

Full Stack React Native

link Movies App

Congratulations on completing unit three! Welcome to unit four, where we introduce Authentication and Associations.

Our project is a movies voting app. It will include the following features:

  • Browse a feed of movies and images.
  • Log in and sign up.
  • Vote for movies.

By the end of this lesson, developers will be able to:

  • Create a reusable Movie Poster component

Let's begin the movies app with the Expo init command. By now you should already have the expo-cli installed globally.

expo init movies-app

In the first prompt, be sure to select tabs:

? Choose a template: 
  ----- Managed workflow -----
  blank                 a minimal app as clean as an empty canvas 
  blank (TypeScript)    same as blank but with TypeScript configuration 
❯ tabs                  several example screens and tabs using react-navigation

In the second prompt, name your new movies app.

If you followed these steps successfully, you should have a new project with the following folders:

.
├── App.js
├── __tests__
│   ├── App-test.js
│   └── __snapshots__
├── app.json
├── assets
│   ├── fonts
│   └── images
├── babel.config.js
├── components
│   ├── StyledText.js
│   ├── TabBarIcon.js
│   └── __tests__
├── constants
│   ├── Colors.js
│   └── Layout.js
├── navigation
│   ├── AppNavigator.js
│   ├── BottomTabNavigator.js
│   └── useLinking.js
├── node_modules
├── package.json
├── screens
│   ├── HomeScreen.js
│   └── LinksScreen.js
├── yarn-error.log
└── yarn.lock

link Home Screen

The home screen will be a list view of movies. We will use FlatList to render movie data. Each row will be a pressable movie poster, that brings users to the movie details page.

Editing the HomeScreen.js, we remove the boilerplate code, and add a static list of movies:

Grab the full `HomeScreen.js` source code.
import React from 'react';
import {
  Image,
  Platform,
  ScrollView,
  FlatList,
  StyleSheet,
  Text,
  TouchableOpacity,
  View,
  ActivityIndicator
} from 'react-native';

const movies = [
  {
    "title": "Spider-Man: Far From Home",
    "description": "Peter Parker and his friends go on a summer trip to Europe. However, they will hardly be able to rest - Peter will have to agree to help Nick Fury uncover the mystery of creatures that cause natural disasters and destruction throughout the continent.",
    "imageUrl": "https://image.tmdb.org/t/p/w780/lcq8dVxeeOqHvvgcte707K0KVx5.jpg",
    "category": {
      "title": "Action"
    }
  },
  {
    "title": "The Lion King",
    "description": "Simba idolizes his father, King Mufasa, and takes to heart his own royal destiny. But not everyone in the kingdom celebrates the new cub's arrival. Scar, Mufasa's brother and a former heir to the throne has plans of his own.",
    "imageUrl": "https://image.tmdb.org/t/p/w780/2bXbqYdUdNVa8VIWXVfclP2ICtT.jpg",
    "category": {
      "title": "Animation"
    }
  },
  {
    "title": "Ford v Ferrari",
    "description": "American car designer Carroll Shelby and the British-born driver Ken Miles work together to battle corporate interference, the laws of physics, and their own personal demons to build a revolutionary race car for Ford Motor Company.",
    "imageUrl": "https://image.tmdb.org/t/p/w780/6ApDtO7xaWAfPqfi2IARXIzj8QS.jpg",
    "category": {
      "title": "Thriller"
    }
  },
  {
    "title": "Once Upon a Time... in Hollywood",
    "description": "A faded television actor and his stunt double strive to achieve fame and success in the film industry during the final years of Hollywood's Golden Age in 1969 Los Angeles.",
    "imageUrl": "https://image.tmdb.org/t/p/w780/8j58iEBw9pOXFD2L0nt0ZXeHviB.jpg",
    "category": {
      "title": "Western"
    }
  },
  {
    "title": "Frozen II",
    "description": "Elsa, Anna, Kristoff and Olaf are going far in the forest to know the truth about an ancient mystery of their kingdom..",
    "imageUrl": "https://image.tmdb.org/t/p/w780/qdfARIhgpgZOBh3vfNhWS4hmSo3.jpg",
    "category": {
      "title": "Animation"
    }
  },
  {
    "title": "Joker",
    "description": "During the 1980s, a failed stand-up comedian is driven insane and turns to a life of crime and chaos in Gotham City while becoming an infamous psychopathic crime figure.",
    "imageUrl": "https://image.tmdb.org/t/p/w780/udDclJoHjfjb8Ekgsd4FDteOkCU.jpg",
    "category": {
      "title": "Horror"
    }
  },
  {
    "title": "Aladdin",
    "description": "A kindhearted street urchin named Aladdin embarks on a magical adventure after finding a lamp that releases a wisecracking genie while a power-hungry Grand Vizier vies for the same lamp that has the power to make their deepest wishes come true.",
    "imageUrl": "https://image.tmdb.org/t/p/w780/3iYQTLGoy7QnjcUYRJy4YrAgGvp.jpg",
    "category": {
      "title": "Animation"
    }
  }
];

export default function HomeScreen() {
  return (
    <View style={styles.container}>
      <FlatList
        data={movies}
        keyExtractor={(itemObj, index) => {
          return `${index}`;
        }}
        numColumns={2}
        contentContainerStyle={styles.scrollContent}
        decelerationRate="fast"
        renderItem={({ item, index }) => {
          return (
            <Text>{item.title}</Text>
          )}} 
        />
    </View>
  );
}

HomeScreen.navigationOptions = {
  title: 'Now Playing',
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
  },
  scrollContent: {
    paddingTop: 10,
  },
});

By this point, you should have a list of seven simple <Text /> components. This looks okay so far, but the movie detail could use some styling.

link MoviePoster Component

Let's create a functional component called MoviePoster.js.

Add a new MoviePoster.js file into your components directory. Paste in the following snippet:

Grab the full MoviePoster source code:
import React, { Component, PropTypes } from 'react';
import {
  Dimensions,
  Image,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from 'react-native';

// Get screen dimensions
const { width, height } = Dimensions.get('window');
// How many posters we want to have in each row and column
const cols = 2, rows = 2;

export default function MoviePoster(props) {
  const { movie, movie: { title, category, imageUrl }, onPress } = props;
  return (
    <TouchableOpacity style={styles.container} onPress={() => onPress && onPress(movie)}>
      <View style={styles.imageContainer}>
        <Image source={{ uri: imageUrl }} style={styles.image} />
      </View>
      <Text style={styles.title} numberOfLines={1}>{title}</Text>
      <Text style={styles.genre} numberOfLines={1}>{category.title}</Text>
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  container: {
    marginLeft: 10,
    marginBottom: 10,
    height: (height - 20 - 20) / rows - 10,
    width: (width - 10) / cols - 10,
  },
  imageContainer: {
    flex: 1,                          // take up all available space
  },
  image: {
    borderRadius: 10,                 // rounded corners
    ...StyleSheet.absoluteFillObject, // fill up all space in a container
  },
  title: {
    fontSize: 14,
    marginTop: 4,
  },
  genre: {
    color: '#BBBBBB',
    fontSize: 12,
    lineHeight: 14,
  },
});

In order to display the movie details inside HomeScreen.js, make the following changes:
In order to display the coin inside HomeScreen.js, make the following changes:

+ import MoviePoster from '../components/MoviePoster';

export default function HomeScreen() {
  return (
    <View style={styles.container}>
      <FlatList
        data={movies}
        keyExtractor={(itemObj, index) => {
          return `${index}`;
        }}
        numColumns={2}
        contentContainerStyle={styles.scrollContent}
        decelerationRate="fast"
        renderItem={({ item, index }) => {
          return (
+           <MoviePoster movie={item} />
          )}} 
        />
    </View>
  );
}

With these changes, we see a list of seven <MoviePoster /> components. Snazzy! Nicely done.

link MovieDetail Screen

If you click any Movie, nothing happens. Let's display Movie Details with a new screen called MovieDetail.

Prior to adding the new MovieDetail screen, let's bring over the RoundedButton component from the previous unit.

Create a new file inside components and paste in the RoundedButton code.

Here's the RoundedButton source code, in case you need it.
import React from 'react';
import { Text, View, TouchableOpacity, StyleSheet } from 'react-native';

export default function RoundedButton(props) {
  const { text, icon, textColor, backgroundColor, onPress } = props;
  const color = textColor || 'white';
  return (
    <TouchableOpacity
      onPress={() => onPress && onPress()}
      style={[
        styles.wrapper,
        { backgroundColor: backgroundColor || 'transparent' },
      ]}
    >
      <View style={styles.ButtonTextWrapper}>
        {icon}
        <Text style={[{ color }, styles.buttonText]}>{text}</Text>
      </View>
    </TouchableOpacity>
  );
}

const styles = StyleSheet.create({
  wrapper: {
    padding: 15,
    display: 'flex',
    borderRadius: 40,
    borderWidth: 1,
    borderColor: 'white',
    marginBottom: 15,
    alignItems: 'center'
  },
  buttonText: {
    fontSize: 16,
    width: '100%',
    textAlign: 'center'
  },
  ButtonTextWrapper: {
    flexDirection: 'row',
    justifyContent: 'flex-end'
  }
});

In the screens directory, create a new file called MovieDetail.js. Insert the following source code:

Click here for the MovieDetail source code.
import React from 'react';
import {
  StyleSheet,
  Text,
  Image,
  TouchableOpacity,
  View,
  ScrollView,
  Dimensions
} from 'react-native';
import RoundedButton from '../components/RoundedButton';
import { Ionicons } from '@expo/vector-icons';

const { width } = Dimensions.get('window');

function MovieDetail({ route, navigation }) {
  const { params } = route;
  const { movie } = params;
  const {
    id,
    title,
    description,
    imageUrl,
    category
  } = movie;
  const isFavorite = false; 
  const primaryColor = isFavorite ? "rgba(75, 148, 214, 1)" : "#fff";
  const secondaryColor = isFavorite ? "#fff" : "rgba(75, 148, 214, 1)";
  const saveString = isFavorite ? 'Remove Vote' : 'Add Vote';
  return (
    <ScrollView style={styles.container}>
      <View style={styles.header}>
        <Image style={styles.image} source={{uri: imageUrl}} />
        <Text numberOfLines={2} style={[styles.text, {textAlign: 'center'}]}>{title}</Text>
        <RoundedButton
          text={saveString}
          textColor={primaryColor}
          backgroundColor={secondaryColor}
          onPress={() => {
            if (isFavorite) {
              console.log("pressed");
            } else {
              console.log("pressed");
            }
          }}
          icon={<Ionicons name="md-checkmark-circle" size={20} color={primaryColor} style={styles.saveIcon} />}
        />
        <View style={styles.statRow}>
          <Text style={styles.stat} numberOfLines={1}>Category</Text>
          <Text style={styles.stat} numberOfLines={1}>{category.title}</Text>
        </View>
        <View style={styles.statRow}>
          <Text style={styles.stat}>{description}</Text>
        </View>
      </View>
    </ScrollView>
  );

}

MovieDetail.navigationOptions = screenProps => ({
  title: screenProps.navigation.getParam("movie").title
});

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  header: {
    justifyContent: 'center',
    alignItems: 'center',
    padding: 10,
  },
  text: {
    fontSize: 32,
    color: '#161616',
    paddingBottom: 15,
  },
  image: {
    width: width,
    height: width,
    resizeMode: 'center',
  },
  statRow: {
    width: "100%",
    padding: 10,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  stat: {
    color: '#161616',
    fontSize: 16,
    fontWeight: '500',
  },
  saveIcon: {
    position: 'relative',
    left: 20,
    zIndex: 8
  },
  contentContainer: {
    paddingTop: 10
  },
});

export default MovieDetail;

In case you are wondering, we will add the isFavorite logic in an upcoming section. Hang tight!

link MovieDetail Navigation

In order to wire up our navigation correctly, we need to import the MovieDetail screen inside App.js:

import MovieDetail from '../screens/MovieDetail';

{/* ... */}

<NavigationContainer
  ref={containerRef}
  initialState={initialNavigationState}
>
  <Stack.Navigator>
    <Stack.Screen name="Root" component={BottomTabNavigator} />
    <Stack.Screen name="Detail" component={CoinDetail} />
  </Stack.Navigator>
</NavigationContainer>

When we tap a Movie Poster, nothing happens yet! Let's wire up the navigation to our MoviePoster component.

Inside HomeScreen.js, make the following final changes:

Click here for the final HomeScreen changes:
-export default function HomeScreen() {
+export default function HomeScreen(props) {
+  const {navigation} = props;
   return (
    <View style={styles.container}>
      <FlatList
        data={movies}
        keyExtractor={(itemObj, index) => {
          return `${index}`;
        }}
        numColumns={2}
        contentContainerStyle={styles.scrollContent}
        decelerationRate="fast"
        renderItem={({ item, index }) => {
          return (
-            <MoviePoster movie={item} />
+            <MoviePoster movie={item} onPress={() => navigation.navigate('Movie', {movie: item})} />
          )}} 
        />
    </View>
  );
}

Finally, we can navigate from the HomeScreen to the MovieDetail screen.

WELL DONE!

Wrapping Up

Today we utilized React Navigation and React Native to create the movies starter app. Up next, we take a deep dive into the world of Authentication and Associations.

format_list_bulleted
help_outline