久しぶりにReact Nativeに触れたらとても進歩していた(1/2)の続きです。3年前にReact Nativeで作ったアプリを最新のReact Nativeで動かしてみました。
アプリは下の画像のようなスマフォとジャンケンができるアプリです。画面はジャンケンの記録と成績をタブ・バーで切り替えて表示できます。
アプリが動くまで
3年前のアプリはコンパイルエラーになりました、主な原因はListViewとTabBarIOSコンポーネントが無くなっている事でした。
ListView
ListViewは同等のFlatListがあり、置き換える事ができました。
ただし、1点ハマりました。Reactではリストのように同一コンポーネントが並ぶ場合はkey属性を指定しないといけません。
しかしFlatListでは子コンポーネントにkey属性を指定するのではなく、keyExtractorにkeyの値を作る無名関数を指定する必要がありました。
TabBarIOS
TabBarIOSはiOS専用のコンポーネントで、最新版では画面遷移を扱うライブラリーReact NavigationのcreateBottomTabNavigatorに置き換える事になりました。
この部分は大枠で以下のようになります。React Routerに似ていますね。
<NavigationContainer>
<Tab.Navigator>
<Tab.Screen name="score" component={ScoreScreen} />
<Tab.Screen name="status" component={StatusScreen} />
</Tab.Navigator>
</NavigationContainer>
TabBarIOSではタブの状態はStateで管理しTabBarIOS.Itemの引数に指定していましたが、React Navigationは独自に状態を管理しているようです。
また、React Navigationを使うには以下をインストールする必要があります。
npm install @react-navigation/native @react-navigation/bottom-tabs react-native-screens
react-native-screensはコードからはimportされていませんがインストールしないエラーになってしまいます。@react-navigation/native等の依存ライブラリーになっていないのでしょうか?
Webでデバッグ出来るのは楽
現在のReact Nativeではブラウザー上でも実行できます。console.log()
が使えたりstyleの調整もしやすいので、開発の最初の段階ではブラウザーで開発・デバックを行うことで開発がかなりはかどります。
まとめ
- React Nativeは3年前にくらべ生産性が高くなっていることを感じました
- iOS/Androidに依存したコンポーネントがへり、たぶん両スマフォへの対応も楽になったのでは?
- 私はSwiftでiOSアプリを開発ができますが、それでもReact Nativeをもっと知りたくなりました!
コード
import React, { useState, useMemo } from 'react'
import { StyleSheet, Text, View, Button, FlatList, SafeAreaView, Dimensions }
from 'react-native'
import { NavigationContainer } from '@react-navigation/native'
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'
import Ionicons from 'react-native-vector-icons/Ionicons'
import Jyanken, { Statuses, Score, Te, Judgment } from './Jyanken'
const windowWidth = Dimensions.get('window').width
const Tab = createBottomTabNavigator()
export default function App() {
const [scores, setScores] = useState<Score[]>([])
const [status, setStatus] = useState<Statuses>({draw: 0, win: 0, lose: 0})
const jyanken = useMemo(() => new Jyanken(), [])
const pon = (te: number) => {
jyanken.pon(te)
setScores(jyanken.getScores())
setStatus(jyanken.getStatuses())
}
const ScoreScreen = () => (
<SafeAreaView style={styles.container}>
<Header />
<JyankenBox action={pon} />
<ScoreList scores={scores} />
</SafeAreaView>
)
const StatusScreen = () => (
<SafeAreaView style={styles.container}>
<Header />
<JyankenBox action={pon} />
<StatusList status={status} />
</SafeAreaView>
)
const icons = (routeName: string) =>
routeName == "score" ? "ios-list" : "ios-speedometer"
return (
<NavigationContainer>
<Tab.Navigator screenOptions={({ route }) => ({tabBarIcon:
({ color, size }) =>
<Ionicons name={icons(route.name)} size={size} color={color} /> }) }>
<Tab.Screen name="score" component={ScoreScreen} />
<Tab.Screen name="status" component={StatusScreen} />
</Tab.Navigator>
</NavigationContainer>
);
}
const Header: React.FC = () => (
<Text style={styles.header}> じゃんけん ポン! </Text>
)
type JyankenBoxProps = {
action: (te: number) => void
}
const JyankenBox: React.FC<JyankenBoxProps> = ({action}) => (
<View style={styles.buttons}>
<Button title="グー" onPress={() => action(0)} />
<Button title="チョキ" onPress={() => action(1)} />
<Button title="パー" onPress={() => action(2)} />
</View>
)
type ScoreListProps = {
scores: Score[]
}
const ScoreList: React.FC<ScoreListProps> = ({scores}) => (
<FlatList data={scores} style={styles.list}
ListHeaderComponent={<ScoreHeader />}
keyExtractor={(_item, index) => String(index)}
renderItem={({item}) => <ScoreRow score={item}/>} />
)
const ScoreHeader: React.FC = () => (
<View style={styles.listScoreHeader}>
<Text style={styles.head}>あなた</Text>
<Text style={styles.head}>コンピュタ</Text>
<Text style={styles.head}>勝敗</Text>
</View>
)
const judgmentColor = (judge: Judgment) => ["#333", "#2979FF", "#FF1744"][judge]
type ScoreRowProps = {
score: Score
}
const ScoreRow: React.FC<ScoreRowProps> = ({score}) => {
const teString = (te: Te) => ["グー","チョキ", "パー"][te]
const judgmentString = (judge: Judgment) => ["引き分け","勝ち", "負け"][judge]
return (
<View style={styles.listContainer}>
<Text style={styles.te}>{teString(score.human)}</Text>
<Text style={styles.te}>{teString(score.computer)}</Text>
<Text style={ {...styles.judge, color: judgmentColor(score.judgment) } }>
{judgmentString(score.judgment)}
</Text>
</View>
)
}
type StatusData = {
title: string
value: number
color: string
}
type StatusListProps = {
status: Statuses
}
const StatusList: React.FC<StatusListProps> = ({status}) => {
const statuses: StatusData[] = [
{title: '勝ち', value: status.win, color: judgmentColor(Judgment.Win)},
{title: '負け', value: status.lose, color: judgmentColor(Judgment.Lose)},
{title: '引き分け', value: status.draw, color: judgmentColor(Judgment.Draw)}]
return (<FlatList data={statuses} style={styles.list}
keyExtractor={(_item, index) => String(index)}
renderItem={({item, index}) => <StatusRow status={item} key={index} />} />)
}
type StatusRowProps = {
status: StatusData
}
const StatusRow: React.FC<StatusRowProps> = ({status}) => (
<View style={styles.listContainer}>
<Text style={styles.te}>{status.title}</Text>
<Text style={{...styles.judge, color: status.color}}>{status.value}</Text>
</View>
)
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
},
header: {
fontSize: 26,
textAlign: 'center',
},
list: {
width: windowWidth * 0.95,
},
listContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
height: 81,
borderBottomColor: '#bbb',
borderBottomWidth: 1,
},
listScoreHeader: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
height: 41,
backgroundColor: '#f7f7f7',
},
listStatusHeader: {
borderBottomColor: '#bbb',
borderBottomWidth: 2,
},
head: {
flex: 1,
textAlign: 'center',
fontWeight: "bold",
},
te: {
flex: 1,
textAlign: 'center',
padding: 10,
},
judge: {
flex: 2,
fontSize: 18,
textAlign: 'center',
},
buttons: {
flexDirection: 'row',
padding: 20,
justifyContent: 'space-around',
width: windowWidth,
},
})