挑戦って大事
Ionic+Angularでスマホアプリはプロジェクトでやってますが、それ以外で作る方法ってFlutterを少しかじったぐらいでして。。。
存在は知ってましたが、食わず嫌いで全然手つかずで来てしまったReactNativeを少し勉強がてらにいじってみた。
とりあえず簡単なタスク管理という名のTODOアプリを作ってみた
作りたいものはこんな感じ
- とりあえず一覧画面と登録画面の2画面
- 登録データはローカルストレージに登録
※今回は削除とか編集とかはせず、あくまで登録だけ出来るやつを作る
開発PCはこんなん使ってるよ
- windows 11
- Node(nvm) 21
とりあえずNodeにGlobalにインストール
- Expo CLI
npm install -g expo-cli
じゃあプロジェクトを作ってみるよ
expo init TaskManagerApp
必要なライブラリとかを追加
一覧と登録画面と画面遷移が必要になるため navigation関係を追加、データをStorageに入れるためにreact-native-async-storageを追加
# 1行で書いてもいいよ
npm install @react-navigation/native
npm install @react-navigation/stack
npm install @react-native-async-storage/async-storage
npm install react-native-screens
npm install react-native-safe-area-context
npm install react-native-gesture-handler
npm install react-native-reanimated
ちなみにファイル構造
TaskManagerApp/
│
├── App.tsx
├── types.ts
├── components/
│ ├── TaskItem.tsx
│ └── TaskList.tsx
├── screens/
│ ├── HomeScreen.tsx
│ └── TaskScreen.tsx
├── navigation/
│ └── AppNavigator.tsx
└── storage/
└── StorageHelper.tsx
共通の型を定義ファイル
export interface Task {
id: string;
title: string;
}
ストレージ関連のヘルパーファイル
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Task } from '../types';
const TASKS_STORAGE_KEY = 'tasks';
export const saveTasks = async (tasks: Task[]): Promise<void> => {
try {
const jsonValue = JSON.stringify(tasks);
await AsyncStorage.setItem(TASKS_STORAGE_KEY, jsonValue);
} catch (e) {
console.error('[Error]登録エラー', e);
}
};
export const loadTasks = async (): Promise<Task[]> => {
try {
const jsonValue = await AsyncStorage.getItem(TASKS_STORAGE_KEY);
return jsonValue != null ? JSON.parse(jsonValue) : [];
} catch (e) {
console.error('[Error]読み込みエラー', e);
return [];
}
};
コンポーネント郡
一覧のタスク1行分のコンポーネント
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Task } from '../types';
const TaskItem: React.FC<{ task: Task }> = ({ task }) => {
return (
<View style={styles.item}>
<Text>{task.title}</Text>
</View>
);
};
const styles = StyleSheet.create({
item: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
});
export default TaskItem;
一覧部分のコンポーネント
import React from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { Task } from '../types';
import TaskItem from './TaskItem';
interface TaskListProps {
tasks: Task[];
}
const TaskList: React.FC<TaskListProps> = ({ tasks }) => {
return (
<View style={styles.container}>
<FlatList
data={tasks}
renderItem={({ item }) => <TaskItem task={item} />}
keyExtractor={item => item.id}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
width: '100%',
},
});
export default TaskList;
タスク一覧(ホーム)画面
import React, { useState, useEffect } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import TaskList from '../components/TaskList';
import { Task } from '../types';
import { loadTasks, saveTasks } from '../storage/StorageHelper';
const HomeScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
const [tasks, setTasks] = useState<Task[]>([]);
useEffect(() => {
const loadStoredTasks = async () => {
const storedTasks = await loadTasks();
setTasks(storedTasks);
};
loadStoredTasks();
}, []);
useEffect(() => {
saveTasks(tasks);
}, [tasks]);
const addTask = (newTask: Task) => {
setTasks(prevTasks => [...prevTasks, newTask]);
};
return (
<View style={styles.container}>
<TaskList tasks={tasks} />
<Button title="Add Task" onPress={() => navigation.navigate('Task', { addTask })} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});
export default HomeScreen;
登録画面
import React, { useState } from 'react';
import { View, TextInput, Button, StyleSheet } from 'react-native';
import { Task } from '../types';
const TaskScreen: React.FC<{ navigation: any; route: any }> = ({ navigation, route }) => {
const [task, setTask] = useState('');
const addTask = () => {
if (task.trim()) {
const newTask: Task = { id: Math.random().toString(), title: task.trim() };
route.params.addTask(newTask);
navigation.goBack();
}
};
return (
<View style={styles.container}>
<TextInput
placeholder="New Task"
value={task}
onChangeText={setTask}
style={styles.input}
/>
<Button title="Add Task" onPress={addTask} />
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
input: {
width: '80%',
padding: 10,
marginBottom: 20,
borderWidth: 1,
borderColor: '#ccc',
},
});
export default TaskScreen;
ナビゲーション部分(画面遷移を制御する部分)
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';
import TaskScreen from '../screens/TaskScreen';
type RootStackParamList = {
Home: undefined;
Task: undefined;
};
const Stack = createStackNavigator<RootStackParamList>();
function AppNavigator() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Task" component={TaskScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
export default AppNavigator;
mainとなるのファイル
import React from 'react';
import AppNavigator from './navigation/AppNavigator';
export default function App() {
return <AppNavigator />;
}
実行の仕方
- AndroidでExpo Goをインストールする
- npm run start で実行すると、QRコードがコンソールに表示される
- Expo GoでQRを読み取り動作を確認する
画面イメージ
一覧画面
登録画面
ね、簡単でしょ?
だいたい半日ぐらいでこんな感じで出来上がります。
とりあえず今回は【勉強がてらReactNativeとExpoを触ってみた】って言うレベルなので、細かい機能とか設定などについては特に何もやってません。
またExpoの機能なのか、実機で即確認ができるのがとてもありがたいですね。
マジでIonicとかFlutterみたいにAndroidStudioとか用意しなくても簡単に環境出来るし、アプリを入れなければいけない制約はありますが実機でそのまま確認が出来るのは本当にありがたい限りです。
ただ。。。ずっとAngularやVueばかりイジってきた自分からすると、Reactの書き方(コードのReturnがTagになってるやつ)が全然違和感ありまくりで気持ち悪いですね。。。
早く慣れることを願いますが。。。
ちな、ソースを
gitにアカウント作って公開してみました。
え~すけさんのGitHub