24
22

More than 3 years have passed since last update.

React Native + Expoでtodoアプリを作る

Last updated at Posted at 2020-03-09

最近React Nativeでアプリを作っていて、そのときに勉強したことのメモが散らばっていたのでまとめていこうと思う。

とりあえず今回の記事ではTodoを簡単に作っていく。
このTodoにReact Elements, React Natvigation, Redux sagaを追加したやつの記事も書いていく予定。

スクリーンショット 2020-03-09 10.55.45.png

今回のソースは以下にあげてある
https://github.com/megaya/react_native_expo_todo_redux/tree/feature/todo-v1

今回はデザインは度外視している。次回にReact Elementsを導入して整えるので気にしない方向で

Expoとは

React NativeはJsでiOSやAndroidアプリが作成できるフレームワークなのだけれど、ネイティブの知識がある程度は必要になってくる。アプリへのビルドなども自前で行う必要がある。

Expoはそういったことをすべて自動でやってくれるサービスだ。ネイティブコードを一切書かずに本当の意味でjsの知識だけでiOS/Androidアプリが作成できる。さらにAppleの審査なしにリリースができるという特徴もある。

Expoでできないこと

難点としてはexpoでネイティブのコードを固定しているので、できることが限らているという点だ。ネイティブ機能をふんだんに使ったアプリをExpoで作るのは難しい。例えばお財布ケータイなどのデバイス特有の機能はExpoで使うことはできない(厳密にはやろうと思えばできる)。あと現状だとGoogle Analyticsも使えない。GAについてはissueやプルリクが出されているので、そのうちマージされるはず。

あとReactのライブラリもExpoだと使用できないものがあるので注意が必要。

Eject

ただしexpoで使えない機能を使うようにすることもできる。Ejectという機能だ。Expo プロジェクトを通常の React Native プロジェクトに変換し、 Xcode や Android Studio を使ってビルドできるようになるのだ。

以前はEjectするためにExpoから抜け出さないといけなかった。しかし、2019年7月よりExpo Bare Workflowという機能が追加されて、一度EjectしてもExpo clinetで動かせるようになった。

うーん、便利だ。ネイティブを知らない人はEjectをなるべく使わなければいいし、ネイティブ知識がある人は機能が足りなくなったときにEjectすればいいと思う。

Expoでプロジェクトを作ってアプリで起動する

$ npm install --global expo-cli

$ expo init expo_todo_redux
=> blankを選択

$ cd expo_todo_redux

$ yarn start

スクリーンショット 2020-03-09 10.54.08.png
(プロジェクトをブランクで作成すると上記のような画面が表示される)

yarn startするとQRコードが表示される。
iOSかAndroidにExpoアプリをダウンロードして、このQRコードを読み込むとアプリが起動できる。
しかもホットリロードなのでソースコードを変更するだけで勝手にアプリが更新される。

タスクを追加する

スクリーンショット 2020-03-09 10.58.12.png

App.js
function Item({ text }) {
  return (
    <View style={styles.item}>
      <Text style={styles.title}>{text}</Text>
    </View>
  );
}

export default function App() {
  const [text, onChangeText] = React.useState("");
  const [todos, setTodos] = React.useState([]);
  const [id, setID] = React.useState(1);

  return (
    <SafeAreaView style={styles.container}>
      <TextInput
        style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
        onChangeText={t => onChangeText(t)}
        value={text}
      />
      <Button
        title="Press me"
        onPress={() => {
          onChangeText("")
          setTodos(oldTodos =>[...oldTodos, { id: id, text: text }])
          setID(id + 1)
        }}
      />
      <FlatList
        data={todos}
        renderItem={({ item }) => <Item text={item.text} />}
        keyExtractor={item => item.id}
      />
    </SafeAreaView>
  );
}

まずはテキストボックスで入れた文字を一覧で表示するところまで。
<Button>や`などはReact Nativeで用意されているコンポーネントだ。
公式サイトから使いたい画面の部品を探していろいろと使ってみるが早いと思う。
https://reactnative.dev/docs/components-and-apis.html

hooks(React.useState)について

export default function App() {
  const [value, onChangeText] = React.useState('Hello World');

  return (
    <View style={styles.container}>
      <TextInput
        style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
        onChangeText={text => onChangeText(text)}
        value={value}
      />
      <Text>{ value }</Text>
    </View>
  );
}

const [value, onChangeText] = React.useState('hogehoge')という風に書くと、stateのような振る舞いを持つことができる。

  • useState('hogehoge')のhogehogeの値は初期値で好きなものを入れられる
    • 配列やオブジェクトなど好きな値が入れられる
  • 戻り値でuseStateでセットした値(value)と、valueをセットするための関数が作成される
    • onChangeTextはsetStateと同じような振る舞いをするため、値を変更するとDOMが更新されるようになる
export default class App extends React.Component {

  constructor(props) {
    super(props);
    this.state = { value: "Hello World" };
  }

  onChangeText(text) {
    this.setState({ value: text });
  }

  render() {
    return (
      <View style={styles.container}>
        <TextInput
          style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
          onChangeText={text => this.onChangeText(text)}
          value={this.state.value}
        />
        <Text>{ this.state.value }</Text>
      </View>
    );
  }
}

hooksを使用しないで同じようなコードを書くと上記のようになる。
classとしてコンポーネントを作成してstateで状態を保持する必要がある。これでもいいのだけれどアプリが巨大になってくるとコードが複雑になっていく。hooksを使っておけば再利用もしやすいし、コードを薄く保つことできる。

ただ何でもかんでもhooksでやればいいってもんじゃないから、そのあたりは使い分けを考えたほうがいい。

hooks自体はReactの機能なので詳しくは公式サイトを見てください
フック早わかり – React

配列をループさせてリストを表示する

function Item({ text }) {
  return (
    <View style={styles.item}>
      <Text style={styles.title}>{text}</Text>
    </View>
  );
}

// ~省略~

  <FlatList
    data={todos}
    renderItem={({ item }) => <Item text={item.text} />}
    keyExtractor={item => item.id}
  />

FlatListでtodosをループさせてItemで中身を表示させている。

FlatList · React Native

  render() {
    return (
      <View>
        {
          todos.map((item, i) => {
            return(
              <Text key={i}>
                {item.text}
              </Text>
            )
          })
        }
      </View>
    );
   }

他の配列を表示させる方法としてはmapでループさせる方法もある。

タスクを追加する処理

setTodos(oldTodos =>[...oldTodos, { id: id, text: text }])

タスクを追加するための処理は上記のようになっている。
既存の配列を...oldTodosで展開して、配列のうしろに新しいオブジェクトを追加しているだけ。

単純にtodosにpushなどはしてはいけない。setTodosを使用することで、Reactは変更があったことを検知して画面に描画している。

タスクを削除する処理の追加

function Item({ text, onPressDelete }) {
  return (
    <View style={styles.item}>
      <Text style={styles.title}>{text}</Text>
      <Button
        title="削除"
        onPress={onPressDelete}
      />
    </View>
  );
}


// ~~省略~~

<FlatList
  data={todos}
  renderItem={({ item }) => <Item text={item.text} onPressDelete={() => setTodos(oldTodos => oldTodos.filter((oldTodo) => oldTodo.id !== item.id) ) } />}
  keyExtractor={item => item .id}
/>

タスクの削除の処理は追加の場合とほとんどと同じ。

setTodos(oldTodos => oldTodos.filter((oldTodo) => oldTodo.id !== item.id))

削除ボタンが押されたtodoのidと一致しないものを配列としてセットしているだけ

コンポーネントに分割する

スクリーンショット 2020-03-09 11.07.59.png

今のままだとApp.jsファイルにすべてを書いてしまっているので、コンポーネントごとに分割していく。今のjsのフレームワークは基本的にコンポーネントごとにわける設計が中心になっていて、「ボタン」「フォーム」などのWebのパーツごとにファイルをわけるようになっている。

あとReact Nativeでは画面ごとにscreenというファイルを作り、それにコンポーネントを追加していく設計が多い。今回もそれに従っていいる。最終的なディレクトリは以下のようになる。

App.js
src/
├── components
│   ├── AddTodo.js
│   ├── TodoList.js
│   └── TodoListItem.js
└── screens
    └── TodoScreen.js

screen

import React from 'react';
import { StyleSheet, SafeAreaView } from 'react-native';
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'

export default function TotoList() {
  const [todos, setTodos] = React.useState([]);
  const [id, setID] = React.useState(1);

  return (
    <SafeAreaView style={styles.container}>
      <AddTodo setTodos={(text) => {
        setTodos(oldTodos => [...oldTodos, { id: id, text: text }])
        setID(id + 1)
      }}
      />
      <TodoList todos={todos} setTodos={setTodos} />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

compornent

./components/TodoList.js

import React from 'react';
import { FlatList } from 'react-native';
import TodoListItem from './TodoListItem'

export default function TotoList({todos, setTodos}) {
  return (
    <FlatList
      data={todos}
      renderItem={({ item }) => <TodoListItem text={item.text} onPressDelete={() => setTodos(oldTodos => oldTodos.filter((oldTodo) => oldTodo.id !== item.id) ) } />}
      keyExtractor={item => item .id}
    />
  );
}

./components/TodoItem.js

import React from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';

export default function TodoListItem({ text, onPressDelete }) {
  return (
    <View style={styles.item}>
      <Text>{text}</Text>
      <Button
        title="削除"
        onPress={onPressDelete}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
});

./components/AddTodo.js

import React from 'react';
import { View, TextInput, Button } from 'react-native';

export default function AddTodo({setTodos}) {
  const [text, onChangeText] = React.useState("");

  return (
    <View>
      <TextInput
        style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
        onChangeText={t => onChangeText(t)}
        value={text}
      />
      <Button
        title="Press me"
        onPress={() => { setTodos(text); onChangeText("") }}
      />
    </View>
  );
}

終わり

次はReact Elementsを導入して見た目を整えたものを記事にする予定

24
22
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
22