BLOG

【簡単ReactNative】映画検索アプリ開発#4/コンポーネント化と検索機能をつくる!

2023-02-24

■過去回はこちら
#1環境設定と画面を作ろう編
#2APIを使って映画一覧画面を作ろう編
#3ライブラリを使って画面遷移編
Youtubeでお送りしているReactNativeを使った映画情報アプリを作ろう!
4回目の今回は映画検索画面の作成と検索機能を作ります!機能と画面のイメージはこちら↓そして今回は新たにコンポーネント化について学び実践していきます。
コンポーネント化とは共通して使っている要素をまとめることです。コンポーネントと言われる処理のひな形を作っておくことで使うときには必要な値を渡すだけで使えるようになるので同じ処理を何度も書かなくてよくなります。
今回作っているアプリでいうと前回までは公開中の映画や人気の映画など表示させる時に同じような処理をかいていて、コードが長くなってしまいました。今回のコンポーネント化をすると以下のようにすっきりさせることができます!コードが見やすくなり、エラーが見つけやすかったり新しい機能がつけやすくなったりとメリットばかりだからぜひ身に付けましょう!

動画で紹介した参考リンクなどは以下から!

01:22 Native Stack Navigator
https://reactnavigation.org/docs/native-stack-navigator/
01:36 検索アイコン/vector-icon
https://icons.expo.fyi/Feather/search
02:50 コピーペしたCSS
const styles = StyleSheet.create({
    container: {
        flex: 1, 
        backgroundColor: '#202328',
        alignItems: 'center' 
    },
    searchForm: {
        flexDirection: 'row',
        marginTop: 10
    },
    input:{
        width: '45%',
        height:30,
        fontSize: 18,
        color: '#ccc',
        marginLeft: 5,
        padding: 5,
        borderColor: 'gray'
    },
    movieContainer: {
        width: 110,
        marginHorizontal: 5
    },
    movieTitle: {
        color: '#ccc',
        fontSize: 14
    },
    movieReleaseDate: {
        color: '#ccc',
        marginBottom: 10
    }

03:05 TextInput ReactNative公式
https://reactnative.dev/docs/textinput
03:38 TextInputの設定参考
placeholder, placeholderTextColor → placeholderの設定
keyboardAppearance → キーボードをダークモードに変更できる
borderBottomWidth → 下線の設定
autoFocus → 次のTextInputに自動で飛べる設定
onSubmitEditing → 送信またはテキスト入力終わり後のイベントを設定できる

04:18 TMDB/Search Movies
https://developers.themoviedb.org/3/search/search-movies
10:24 ポスター画像のアイコン
https://icons.expo.fyi/Ionicons/image-outline
11:06 ポスター情報のコンポーネント化でコードを変更する際の参考
★変更箇所
MovieFlatList.js
MovieDetail.js
SearchMovie.js

★流れ
①Poster.jsをimport
②propsで渡す情報を設定する
③不要なmovieImageスタイルを削除

★渡す情報
poster_path → Imageタグのuriに渡している情報
imageWidth → Imageタグのuriの「w」の後の数値
imageHeight → movieImageスタイルのheight

12:28 評価情報のコンポーネント化でコードを変更する際の参考
★変更箇所
MovieDetail.js
SearchMovie.js

★流れ
①Vote.jsをimport
②propsで渡す情報を設定する
③不要なvoteスタイルを削除

★渡す情報
vote_average → Starタグのdefault
vote_count → vote_count

今回書いたコードは以下からご参考ください!

まとめてダウンロードは以下のリンクからどうぞ!
movieApp.zip
https://drive.google.com/file/d/1RPI-EcSYjp360dU8edVZgdj5dl7nth9U/view?usp=sharing&usp=embed_facebook

★App.js
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import MovieList from './screens/MovieList';
import MovieDetail from './screens/MovieDetail';
import Ionicons from "@expo/vector-icons/Ionicons";
import { TouchableOpacity } from 'react-native';
import SearchMovie from './screens/SearchMovie';

const Stack = createNativeStackNavigator();
export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="MovieList" component={MovieList} options={({navigation}) => ({
          title: "映画一覧",
          headerStyle: {
            backgroundColor: '#202328',
          },
          headerTintColor: '#fff',
          headerRight: () => (
            <TouchableOpacity onPress={() => navigation.navigate('SearchMovie')}>
              <Ionicons name="search" size={30} color="#ccc" />
            </TouchableOpacity>
          )
        })}/>
        <Stack.Screen name="MovieDetail" component={MovieDetail} options={{
          title: "映画詳細",
          headerStyle: {
            backgroundColor: '#202328',
          },
          headerTintColor: '#fff'
        }}/>
        <Stack.Screen name="SearchMovie" component={SearchMovie} options={{
          title: "映画検索",
          headerStyle: {
            backgroundColor: '#202328',
          },
          headerTintColor: '#fff'
        }}/>
      </Stack.Navigator>
    </NavigationContainer>
  )
};
★SearchMovie.js
import { StyleSheet,View,TextInput,FlatList,TouchableOpacity,Text } from "react-native"
import { useState } from "react";
import Ionicons from "@expo/vector-icons/Ionicons";
import { requests } from '../request';
import axios from 'axios';
import Poster from "../components/Poster";
import Vote from "../components/Vote";

export default function SearchMovie({navigation}) {
    const [text, onChangeText] = useState({});
    const [movies, setSearchMovies] = useState({});
    const numColumns = 3;

    async function searchMovies() {
        try {
            const results = await axios.get(requests.SEARCH + text);
            setSearchMovies(results.data.results);
        } catch (error) {
            console.log(error);
        }
    }

    return (
        <View style={styles.container}>
            <View style={styles.searchForm}>
            <Ionicons name="search" size={30} color="#ccc" />
            <TextInput
                style={styles.input}
                onChangeText={text => onChangeText(text)}
                value={text}
                placeholder='映画名'
                placeholderTextColor={'#ccc'}
                keyboardAppearance='dark'
                borderBottomWidth='1'
                autoFocus={true}
                onSubmitEditing={() => searchMovies()}
            />
            </View>
            <FlatList
                data={movies}
                keyExtractor={item => item.id}
                numColumns={numColumns}
                flashScrollIndicators
                renderItem={({ item }) => (
                <TouchableOpacity onPress={() => navigation.navigate("MovieDetail", {movie: item})}>
                    <View style={styles.movieContainer}>
                    <Poster posterPath={item.poster_path} imageWidth={300} imageHeight={180}></Poster>
                    <Text numberOfLines={1} style={styles.movieTitle}>{item.title}</Text>
                    <Vote vote_average={item.vote_average/2} vote_count={item.vote_count}></Vote>
                    <Text style={styles.movieReleaseDate}>{item.release_date}</Text>
                </View>
                </TouchableOpacity>
                )}>
            </FlatList>
        </View>
    )
}

const styles = StyleSheet.create({
    container: {
        flex: 1, 
        backgroundColor: '#202328',
        alignItems: 'center' 
    },
    searchForm: {
        flexDirection: 'row',
        marginTop: 10
    },
    input:{
        width: '45%',
        height:30,
        fontSize: 18,
        color: '#ccc',
        marginLeft: 5,
        padding: 5,
        borderColor: 'gray'
    },
    movieContainer: {
        width: 110,
        marginHorizontal: 5
    },
    movieTitle: {
        color: '#ccc',
        fontSize: 14
    },
    movieReleaseDate: {
        color: '#ccc',
        marginBottom: 10
    }
})
★request.js
const BASE_URL = 'https://api.themoviedb.org/3';
const API_KEY = '6e044104eab08fa1afbc1bce908316a1';

export const requests = {
    NOW_PLAYING :`${BASE_URL}/movie/now_playing?api_key=${API_KEY}&language=ja&page=1`,
    COMMING_SOON:`${BASE_URL}/movie/upcoming?api_key=${API_KEY}&language=ja&page=1`,
    POPULARS:`${BASE_URL}/movie/popular?api_key=${API_KEY}&language=ja&page=1`,
    TOP_RATED:`${BASE_URL}/movie/top_rated?api_key=${API_KEY}&language=ja&page=1`,
    SEARCH:`${BASE_URL}/search/movie?api_key=${API_KEY}&language=ja&page=1&include_adult=false&query=`
}
★MovieList.js
import { StyleSheet, Text, View, ScrollView, FlatList, Image, TouchableOpacity } from 'react-native';
import { requests } from '../request';
import axios from 'axios';
import { useState, useEffect } from 'react';
import MovieFlatList from '../components/MovieFlatList';

export default function MovieList({ navigation }) {
  const [picupMovies, setPicupMovies] = useState({});

  useEffect(() => {
    async function getPickUpMovie() {
      try {
        const result = await axios.get(requests.NOW_PLAYING);
        const number = Math.floor(Math.random() * (result.data.results.length - 1) + 1);
        setPicupMovies(result.data.results[number]);
      } catch (error) {
        console.log(error);
      }
    }
    getPickUpMovie();
  }, []);
  return (
    <ScrollView style={styles.container}>
    <TouchableOpacity onPress={() => navigation.navigate("MovieDetail", {movie: picupMovies})}>
      <View style={styles.pickupContainer}>
        <Image style={styles.pickupImage} source={{uri: `https://image.tmdb.org/t/p/w780${picupMovies.poster_path}`}}></Image>
        <Text style={styles.pickupTitle}>{picupMovies.title}</Text>
      </View>
    </TouchableOpacity>
      <MovieFlatList url={requests.NOW_PLAYING} listName={'公開中の映画'} navigation={navigation}></MovieFlatList>
      <MovieFlatList url={requests.COMMING_SOON} listName={'公開予定の映画'} navigation={navigation}></MovieFlatList>
      <MovieFlatList url={requests.POPULARS} listName={'人気の映画'} navigation={navigation}></MovieFlatList>
      <MovieFlatList url={requests.TOP_RATED} listName={'高評価の映画'} navigation={navigation}></MovieFlatList>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#202328'
  },
  pickupContainer: {
    width: '100%', 
    flexDirection: 'row', 
    alignItems: 'center', 
    justifyContent: 'center'
  },
  pickupImage: {
    height: 350, 
    width: '45%',
    resizeMode: 'contain'
  },
  pickupTitle: {
    color: '#fff', 
    fontSize: 24, 
    fontWeight: 'bold', 
    width: '45%', 
    marginLeft: 5
  },
  listName: {
    color: '#fff', 
    fontSize: 18, 
    fontWeight: 'bold',
  },
  movieContainer: {
    width: 130,
    marginBottom:30
  },
  movieImage: {
    height: 200,
    marginRight: 10,
    resizeMode: 'contain'
  },
  movieTitle: {
    color: '#ccc', 
    fontSize: 14
  }
});
★MovieFlatList.js
import { StyleSheet, Text, View, FlatList, Image, TouchableOpacity } from 'react-native';
import axios from 'axios';
import { useState, useEffect } from 'react';
import Poster from './Poster';

export default function MovieFlatList(props) {
    const url = props.url;
    const listName = props.listName;
    const navigation = props.navigation;

    const [movies, setMovies] = useState({});

  useEffect(() => {
    async function getMovies() {
      try {
        const results = await axios.get(url);
        setMovies(results.data.results);
      } catch (error) {
        console.log(error);
      }
    }
    getMovies();
  }, []);
  return (
    <View>
      <Text style={styles.listName}>{listName}</Text>

      <FlatList
        data={movies}
        keyExtractor={item => item.id}
        horizontal={true}
        flashScrollIndicators
        renderItem={({ item }) => (
        <TouchableOpacity onPress={() => navigation.navigate("MovieDetail", {movie: item})}>
            <View style={styles.movieContainer}>
                <Poster posterPath={item.poster_path} imageWidth={300} imageHeight={200}></Poster>
            <Text numberOfLines={1} style={styles.movieTitle}>{item.title}</Text>
            </View>
        </TouchableOpacity>
        )}>
      </FlatList>

    </View>
  );
}

const styles = StyleSheet.create({
  listName: {
    color: '#fff', 
    fontSize: 18, 
    fontWeight: 'bold',
  },
  movieContainer: {
    width: 130,
    marginBottom:30
  },
  movieTitle: {
    color: '#ccc', 
    fontSize: 14
  }
});
★Poster.js
import Ionicons from "@expo/vector-icons/Ionicons";
import { Image,View } from "react-native";

export default function Poster(props) {
    let posterPath = props.posterPath;
    const imageWidth = props.imageWidth;
    const imageHeight = props.imageHeight;

    if (posterPath === null) {
        return (
            <View style={{height: imageHeight, alignItems: 'center', justifyContent: 'center'}}>
                <Ionicons name="image-outline" size={24} color="#ccc" />
            </View>
        )
    } else {
        return (
            <Image style={{height: imageHeight, resizeMode: 'contain'}} source={{uri: `https://image.tmdb.org/t/p/w${imageWidth}${posterPath}`}}></Image>
        )
    }
}
★MovieDetail.js
import { Text, View, ScrollView, StyleSheet } from "react-native";
import Poster from "../components/Poster";
import Vote from "../components/Vote";

export default function MovieDetail(props) {
    const { movie } = props.route.params;
    return (
        <ScrollView style={styles.container} >
            <Poster posterPath={movie.poster_path} imageWidth={780} imageHeight={480}></Poster>
            <View>
                <Text style={styles.title}>{movie.title}</Text>
                <Vote vote_average={movie.vote_average/2} vote_count={movie.vote_count}></Vote>
                <Text style={styles.movieReleaseDate}>{movie.release_date}</Text>
                <Text style={styles.overview}>{movie.overview}</Text>
            </View>
        </ScrollView>
    );
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#202328'
    },
    textBox: {
        paddingHorizontal:30,
        paddingVertical: 5
    },
    title: {
        color: '#fff',
        fontSize: 26,
        fontWeight: 'bold'
    },
    movieReleaseDate: {
        color: '#ccc',
        marginBottom: 10
    },
    overview: {
        color: '#fff',
        fontSize: 18
    }
})
★Vote.js
import { Text, View, StyleSheet } from "react-native";
import Star from "react-native-stars";
import Ionicons from "@expo/vector-icons/Ionicons";

export default function Vote(props) {
    const vote_average = props.vote_average;
    const vote_count = props.vote_count;
    
    return (
        <View style={styles.vote}>
            <Star
                default={(vote_average/2)}
                count={5}
                half={true}
                fullStar={<Ionicons name="star-sharp" style={styles.star}></Ionicons>}
                emptyStar={<Ionicons name="star-outline" style={styles.star}></Ionicons>}
                halfStar={<Ionicons name="star-half-sharp" style={styles.star}></Ionicons>}
                >
            </Star>
        <Text style={styles.voteCount}>{vote_count}</Text>
        </View>
    )
}

const styles = StyleSheet.create({
    vote: {
        flexDirection: 'row',
        marginTop: 10
    },
    voteCount: {
        color: '#ccc',
        marginLeft: 3
    },
    star: {
        color: 'yellow',
        backgroundColor: 'transparent',
        textShadowColor: 'black',
        textShadowOffset: {width: 1, height: 1},
        textShadowRadius: 2,
    }
});

株式会社ロックシステム

「ブラック企業をやっつけろ!!」を企業理念にエンジニアが働きやすい環境をつきつめる大阪のシステム開発会社。2014年会社設立以来、残業時間ほぼゼロを達成し、高い従業員還元率でエンジニアファーストな会社としてIT業界に蔓延るブラックなイメージをホワイトに変えられる起爆剤となるべく日々活動中!絶賛エンジニア募集中。