我們今天來使用react native的路由套件,我們使用React Navigation。
安裝@react-navigation/native react-native-screens react-native-safe-area-context @react-navigation/bottom-tabs @react-navigation/native-stack
。請參考安裝步驟
react-native-screens套件需要一個額外的設定步驟才能在 Android 裝置上正常運作,我們添加引入和方法。添加android.os.Bundle和onCreate,然後再重新執行build
// android\app\src\main\java\com\mytest\MainActivity.kt
package com.mytest
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
import android.os.Bundle
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "MyTest"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(null)
}
}
在App添加container
// App.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useColorScheme } from 'react-native';
import HomePage from './src/pages/home.page';
import AddTaskPage from './src/pages/add-task.page';
import StatisticsPage from './src/pages/statistics-page';
import UserPage from './src/pages/user.page';
import LoginPage from './src/pages/login.page';
import SettingsPage from './src/pages/settings.page';
import TaskDetailsPage from './src/pages/task-detail.page';
const Tab = createBottomTabNavigator();
const Stack = createNativeStackNavigator();
function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen name="HomeList" component={HomePage} options={{ title: '任務列表' }} />
<Stack.Screen name="TaskDetails" component={TaskDetailsPage} options={{ title: '任務詳情' }} />
</Stack.Navigator>
);
}
function UserStack() {
return (
<Stack.Navigator>
<Stack.Screen name="UserProfile" component={UserPage} options={{ title: '用戶資料' }} />
<Stack.Screen name="Settings" component={SettingsPage} options={{ title: '設置' }} />
<Stack.Screen name="Login" component={LoginPage} options={{ title: '登入' }} />
</Stack.Navigator>
);
}
function MainTabs() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeStack} options={{ title: '主頁' }} />
<Tab.Screen name="AddTask" component={AddTaskPage} options={{ title: '新增' }} />
<Tab.Screen name="Statistics" component={StatisticsPage} options={{ title: '統計' }} />
<Tab.Screen name="User" component={UserStack} options={{ title: '用戶' }} />
</Tab.Navigator>
);
}
function App(): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<SafeAreaProvider>
<NavigationContainer>
<MainTabs />
</NavigationContainer>
</SafeAreaProvider>
</GestureHandlerRootView>
);
}
export default App;
接著調整其他的頁面
type HomePageProps = {
navigation: any;
};
type HomePageProps = {
navigation: any;
};
如下:
// src\pages\home.page.tsx
navigation: any;
};
const HomePage: React.FC<HomePageProps> = ({navigation}) => {
const [tasks, setTasks] = useAtom(tasksAtom);
useEffect(() => {
const loadedTasks = TaskService.getTasks();
setTasks(loadedTasks);
}, [setTasks]);
const toggleTaskStatus = useCallback((id: string) => {
const updatedTasks = tasks.map(task =>
task.id === id ? { ...task, isDone: !task.isDone } : task
);
setTasks(updatedTasks);
TaskService.saveTasks(updatedTasks);
}, [tasks, setTasks]);
const deleteTask = useCallback((id: string) => {
const updatedTasks = tasks.filter(task => task.id !== id);
setTasks(updatedTasks);
TaskService.deleteTask(id);
}, [tasks, setTasks]);
const renderRightActions = useCallback((id: string) => {
return (
<TouchableOpacity
style={styles.deleteButton}
onPress={() => deleteTask(id)}
>
<Text style={styles.deleteButtonText}>Delete</Text>
</TouchableOpacity>
);
}, [deleteTask]);
const renderTask = useCallback(({ item }: { item: Task }) => (
<Swipeable renderRightActions={() => renderRightActions(item.id)}>
<TouchableOpacity onPress={() => navigation.navigate('TaskDetails', { taskId: item.id })}>
<View
style={[
styles.taskItem,
{ backgroundColor: item.isDone ? '#E8F5E9' : 'transparent' },
]}
>
<TouchableOpacity onPress={() => toggleTaskStatus(item.id)}>
<View style={styles.checkbox}>
{item.isDone && <View style={styles.checkboxInner} />}
</View>
</TouchableOpacity>
<View style={styles.taskContent}>
<Text style={styles.taskTitle}>{item.title}</Text>
<Text style={styles.taskDates}>
{item.startDate} - {item.endDate}
</Text>
<Text style={styles.taskDescription}>{item.description}</Text>
<View style={styles.taskTags}>
{item.tags.map((tag, index) => (
<View key={index} style={styles.tag}>
<Text style={styles.tagText}>{tag.title}</Text>
</View>
))}
</View>
</View>
</View>
</TouchableOpacity>
</Swipeable>
), [renderRightActions, navigation, toggleTaskStatus]);
return (
<View style={styles.container}>
<Text style={styles.title}>任務列表</Text>
<TextInput
style={styles.searchBar}
placeholder="搜尋欄"
placeholderTextColor={Colors.dark}
/>
<FlatList
style={styles.taskList}
data={tasks}
renderItem={renderTask}
keyExtractor={item => item.id}
/>
</View>
);
};
type AddTaskPageProps = {
navigation: any;
};
navigation.goBack();
如下:
// src\pages\add-task.page.tsx
type AddTaskPageProps = {
navigation: any;
};
const AddTaskPage: React.FC<AddTaskPageProps> = ({ navigation }) => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(null);
const [openStartDate, setOpenStartDate] = useState(false);
const [openEndDate, setOpenEndDate] = useState(false);
const [tags, setTags] = useState<Tags[]>([]);
const [currentTag, setCurrentTag] = useState('');
const setTasks = useSetAtom(tasksAtom);
const validateTask = (task: Task): string[] => {
const errors: string[] = [];
if (!task.title.trim()) {
errors.push('Title is required');
}
if (task.startDate && task.endDate && new Date(task.startDate) > new Date(task.endDate)) {
errors.push('End date should be after start date');
}
return errors;
};
const handleSubmit = () => {
const newTask: Task = {
id: Date.now().toString(),
title,
startDate: startDate ? startDate.toISOString().split('T')[0] : null,
endDate: endDate ? endDate.toISOString().split('T')[0] : null,
description,
isDone: false,
tags,
subTasks: [],
};
const validationErrors = validateTask(newTask);
if (validationErrors.length > 0) {
Alert.alert('Validation Error', validationErrors.join('\n'));
return;
}
TaskService.addTask(newTask);
setTasks(prevTasks => [...prevTasks, newTask]);
// Reset form fields
setTitle('');
setDescription('');
setStartDate(null);
setEndDate(null);
setTags([]);
Alert.alert('Success', 'Task added successfully');
navigation.goBack();
};
const handleAddTag = () => {
if (currentTag.trim()) {
setTags([...tags, { title: currentTag.trim() }]);
setCurrentTag('');
}
};
const handleRemoveTag = (index: number) => {
setTags(tags.filter((_, i) => i !== index));
};
修改一下路由名稱,如下
// src\pages\user.page.tsx
type UserPageProps = {
navigation: any;
};
const UserPage: React.FC<UserPageProps> = ({ navigation }) => {
const [user, setUser] = useAtom(userAtom);
const [isLoggedIn, setIsLoggedIn] = useAtom(isLoggedInAtom);
const handleLogout = () => {
UserService.logout();
setUser(null);
setIsLoggedIn(false);
};
if (!isLoggedIn) {
return (
<View style={styles.container}>
<Text style={styles.title}>請先登入</Text>
<TouchableOpacity
style={styles.loginButton}
onPress={() => navigation.navigate('Login')}
>
<Text style={styles.loginButtonText}>登入</Text>
</TouchableOpacity>
</View>
);
}
return (
<ScrollView style={styles.container}>
<View style={styles.profileHeader}>
<Image
source={{ uri: user?.avatar || 'https://via.placeholder.com/150' }}
style={styles.avatar}
/>
<Text style={styles.username}>{user?.username}</Text>
<Text style={styles.email}>{user?.email}</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>用戶信息</Text>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>ID:</Text>
<Text style={styles.infoValue}>{user?.id}</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>創建日期:</Text>
<Text style={styles.infoValue}>{user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : 'N/A'}</Text>
</View>
</View>
<TouchableOpacity
style={styles.settingsButton}
onPress={() => navigation.navigate('Settings')}
>
<Text style={styles.settingsButtonText}>設置</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>登出</Text>
</TouchableOpacity>
</ScrollView>
);
};
修改路由名稱
// src\pages\login.page.tsx
type LoginPageProps = {
navigation: any;
};
const LoginPage: React.FC<LoginPageProps> = ({ navigation }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const setUser = useSetAtom(userAtom);
const setIsLoggedIn = useSetAtom(isLoggedInAtom);
const handleLogin = async () => {
try {
const user = await UserService.login(email, password);
setUser(user);
setIsLoggedIn(true);
navigation.navigate('UserProfile');
} catch (error) {
Alert.alert('登錄失敗', '請檢查您的郵箱和密碼');
}
};
return (
<View style={styles.container}>
<Text style={styles.title}>登錄</Text>
<TextInput
style={styles.input}
placeholder="郵箱"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
/>
<TextInput
style={styles.input}
placeholder="密碼"
value={password}
onChangeText={setPassword}
secureTextEntry
/>
<TouchableOpacity style={styles.loginButton} onPress={handleLogin}>
<Text style={styles.loginButtonText}>登錄</Text>
</TouchableOpacity>
</View>
);
};
添加新service
// src\services\task.service.ts
export const TaskService = {
saveTasks: (tasks: Task[]) => {
storage.set(TASKS_KEY, JSON.stringify(tasks));
},
getTasks: (): Task[] => {
const tasksJson = storage.getString(TASKS_KEY);
return tasksJson ? JSON.parse(tasksJson) : [];
},
getTaskById: (taskId: string): Task | undefined => {
const tasks = TaskService.getTasks();
return tasks.find(task => task.id === taskId);
},
如下:
// src\pages\task-detail.page.tsx
import React, { useCallback, useEffect, useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Switch, TextInput, ScrollView, useColorScheme } from 'react-native';
import { useAtom } from 'jotai';
import { tasksAtom } from '../stores/atoms';
import { Task } from '../types';
import { TaskService } from '../services/task.service';
import { RouteProp } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
type RootStackParamList = {
HomeList: undefined;
TaskDetails: { taskId: string };
};
type TaskDetailsPageProps = {
navigation: NativeStackScreenProps<RootStackParamList, 'TaskDetails'>;
route: RouteProp<RootStackParamList, 'TaskDetails'>;
};
const TaskDetailsPage: React.FC<TaskDetailsPageProps> = ({
navigation,
route,
}) => {
const isDarkMode = useColorScheme() === 'dark';
const { taskId } = route.params;
const [isEditing, setIsEditing] = useState(false);
const [editedTask, setEditedTask] = useState<Task | null>(null);
const [, setTasks] = useAtom(tasksAtom);
useEffect(() => {
const loadTask = async () => {
const task = await TaskService.getTaskById(taskId);
if (task) {
setEditedTask(task);
} else {
// Handle case when task is not found
navigation.goBack();
}
};
loadTask();
}, [taskId, navigation]);
const handleEdit = useCallback(() => {
setIsEditing(true);
}, []);
const handleSave = useCallback(async () => {
if (editedTask) {
await TaskService.updateTask(editedTask);
setTasks(await TaskService.getTasks());
setIsEditing(false);
}
}, [editedTask, setTasks]);
const handleDelete = useCallback(async () => {
if (editedTask) {
await TaskService.deleteTask(editedTask.id);
setTasks(await TaskService.getTasks());
navigation.goBack();
}
}, [editedTask, setTasks, navigation]);
const toggleCompletion = useCallback(async () => {
if (editedTask) {
const updatedTask = { ...editedTask, isDone: !editedTask.isDone };
setEditedTask(updatedTask);
await TaskService.updateTask(updatedTask);
setTasks(await TaskService.getTasks());
}
}, [editedTask, setTasks]);
const handleInputChange = useCallback((field: keyof Task, value: string) => {
setEditedTask(prev => prev ? { ...prev, [field]: value } : null);
}, []);
const addTag = useCallback(() => {
setEditedTask(prev => prev ? {
...prev,
tags: [...prev.tags, { title: '新標籤' }],
} : null);
}, []);
const removeTag = useCallback((index: number) => {
setEditedTask(prev => prev ? {
...prev,
tags: prev.tags.filter((_, i) => i !== index),
} : null);
}, []);
const updateTag = useCallback((index: number, newTitle: string) => {
setEditedTask(prev => prev ? {
...prev,
tags: prev.tags.map((tag, i) => i === index ? { ...tag, title: newTitle } : tag),
} : null);
}, []);
if (!editedTask) {
return null; // Or a loading indicator
}
return (
<ScrollView style={[styles.container, { backgroundColor: isDarkMode ? '#1a1a1a' : '#ffffff' }]}>
<TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
<Text style={[styles.backButtonText, { color: isDarkMode ? '#ffffff' : '#007AFF' }]}>返回</Text>
</TouchableOpacity>
{isEditing ? (
<>
<TextInput
style={[styles.input, { color: isDarkMode ? '#ffffff' : '#000000' }]}
value={editedTask.title}
onChangeText={(value) => handleInputChange('title', value)}
placeholder="任務標題"
placeholderTextColor={isDarkMode ? '#888888' : '#cccccc'}
/>
<TextInput
style={[styles.input, styles.descriptionInput, { color: isDarkMode ? '#ffffff' : '#000000' }]}
value={editedTask.description}
onChangeText={(value) => handleInputChange('description', value)}
placeholder="任務描述"
placeholderTextColor={isDarkMode ? '#888888' : '#cccccc'}
multiline
/>
<TextInput
style={[styles.input, { color: isDarkMode ? '#ffffff' : '#000000' }]}
value={editedTask?.startDate ?? ''}
onChangeText={(value) => handleInputChange('startDate', value)}
placeholder="開始日期 (YYYY-MM-DD)"
placeholderTextColor={isDarkMode ? '#888888' : '#cccccc'}
/>
<TextInput
style={[styles.input, { color: isDarkMode ? '#ffffff' : '#000000' }]}
value={editedTask?.endDate ?? ''}
onChangeText={(value) => handleInputChange('endDate', value)}
placeholder="結束日期 (YYYY-MM-DD)"
placeholderTextColor={isDarkMode ? '#888888' : '#cccccc'}
/>
</>
) : (
<>
<Text style={[styles.title, { color: isDarkMode ? '#ffffff' : '#000000' }]}>{editedTask.title}</Text>
<Text style={[styles.description, { color: isDarkMode ? '#dddddd' : '#333333' }]}>{editedTask.description}</Text>
<Text style={[styles.info, { color: isDarkMode ? '#bbbbbb' : '#666666' }]}>開始日期: {editedTask.startDate}</Text>
<Text style={[styles.info, { color: isDarkMode ? '#bbbbbb' : '#666666' }]}>結束日期: {editedTask.endDate}</Text>
</>
)}
<View style={styles.tagsContainer}>
{editedTask.tags.map((tag, index) => (
<View key={index} style={[styles.tag, { backgroundColor: isDarkMode ? '#333333' : '#E0E0E0' }]}>
{isEditing ? (
<>
<TextInput
style={[styles.tagInput, { color: isDarkMode ? '#ffffff' : '#000000' }]}
value={tag.title}
onChangeText={(value) => updateTag(index, value)}
/>
<TouchableOpacity onPress={() => removeTag(index)}>
<Text style={[styles.removeTagText, { color: isDarkMode ? '#ff6b6b' : '#FF3B30' }]}>X</Text>
</TouchableOpacity>
</>
) : (
<Text style={[styles.tagText, { color: isDarkMode ? '#ffffff' : '#000000' }]}>{tag.title}</Text>
)}
</View>
))}
{isEditing && (
<TouchableOpacity style={[styles.addTagButton, { backgroundColor: isDarkMode ? '#4a4a4a' : '#E0E0E0' }]} onPress={addTag}>
<Text style={[styles.addTagButtonText, { color: isDarkMode ? '#ffffff' : '#007AFF' }]}>+ 添加標籤</Text>
</TouchableOpacity>
)}
</View>
<View style={styles.actionContainer}>
{isEditing ? (
<TouchableOpacity style={[styles.actionButton, { backgroundColor: isDarkMode ? '#4a4a4a' : '#007AFF' }]} onPress={handleSave}>
<Text style={styles.actionButtonText}>保存</Text>
</TouchableOpacity>
) : (
<TouchableOpacity style={[styles.actionButton, { backgroundColor: isDarkMode ? '#4a4a4a' : '#007AFF' }]} onPress={handleEdit}>
<Text style={styles.actionButtonText}>編輯</Text>
</TouchableOpacity>
)}
<TouchableOpacity style={[styles.actionButton, styles.deleteButton, { backgroundColor: isDarkMode ? '#8b0000' : '#FF3B30' }]} onPress={handleDelete}>
<Text style={styles.actionButtonText}>刪除</Text>
</TouchableOpacity>
</View>
<View style={styles.completionContainer}>
<Text style={[styles.completionText, { color: isDarkMode ? '#ffffff' : '#000000' }]}>任務完成</Text>
<Switch
value={editedTask.isDone}
onValueChange={toggleCompletion}
trackColor={{ false: isDarkMode ? '#767577' : '#E0E0E0', true: isDarkMode ? '#81b0ff' : '#007AFF' }}
thumbColor={editedTask.isDone ? (isDarkMode ? '#f5dd4b' : '#f4f3f4') : '#f4f3f4'}
/>
</View>
</ScrollView>
);
};
如下
// src\pages\settings.page.tsx
import React from 'react';
import { View, Text, StyleSheet, Switch, TouchableOpacity, ScrollView } from 'react-native';
import { useAtom } from 'jotai';
import { userAtom } from '../stores/atoms';
type SettingsPageProps = {
navigation: any;
};
const SettingsPage: React.FC<SettingsPageProps> = ({ navigation }) => {
const [user] = useAtom(userAtom);
const [notifications, setNotifications] = React.useState(true);
const [autoSync, setAutoSync] = React.useState(true);
const handleLogout = () => {
console.log('User logged out');
// navigation.navigate('Login');
};
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>設置</Text>
<View style={styles.section}>
<Text style={styles.sectionTitle}>應用設置</Text>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>深色模式</Text>
{/* <Switch value={isDarkMode} onValueChange={toggleDarkMode} /> */}
</View>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>通知</Text>
<Switch value={notifications} onValueChange={setNotifications} />
</View>
<View style={styles.settingItem}>
<Text style={styles.settingLabel}>自動同步</Text>
<Switch value={autoSync} onValueChange={setAutoSync} />
</View>
</View>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>登出</Text>
</TouchableOpacity>
<View style={styles.aboutSection}>
<Text style={styles.sectionTitle}>關於應用</Text>
<Text style={styles.aboutText}>版本: 1.0.0</Text>
<Text style={styles.aboutText}>開發者: Your Company Name</Text>
</View>
</ScrollView>
);
};
修改如下
// src\pages\statistics-page.tsx
import React, { useRef, useEffect, useMemo } from 'react';
import { View, Text, StyleSheet, ScrollView, Dimensions, useColorScheme } from 'react-native';
import * as echarts from 'echarts/core';
import { SkiaChart, SVGRenderer } from '@wuba/react-native-echarts';
import { GaugeChart, PieChart, BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components';
import { useAtom } from 'jotai';
import { tasksAtom } from '../stores/atoms';
echarts.use([SVGRenderer, GaugeChart, PieChart, BarChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent]);
type StatisticsPageProps = {};
const { width } = Dimensions.get('window');
const chartWidth = width - 40; // Assuming 20px padding on each side
const StatisticsPage: React.FC<StatisticsPageProps> = () => {
const completionRateChartRef = useRef<any>(null);
const taskDistributionChartRef = useRef<any>(null);
const taskTrendChartRef = useRef<any>(null);
const isDarkMode = useColorScheme() === 'dark';
const [tasks] = useAtom(tasksAtom);
const taskStats = useMemo(() => {
const totalTasks = tasks.length;
const completedTasks = tasks.filter(task => task.isDone).length;
const completionRate = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
const tagCounts = tasks.reduce((acc, task) => {
task.tags.forEach(tag => {
acc[tag.title] = (acc[tag.title] || 0) + 1;
});
return acc;
}, {} as Record<string, number>);
const tasksByMonth = tasks.reduce((acc, task) => {
const month = task?.startDate ? new Date(task.startDate).getMonth() : new Date().getMonth();
acc[month] = (acc[month] || 0) + 1;
return acc;
}, {} as Record<number, number>);
return { totalTasks, completedTasks, completionRate, tagCounts, tasksByMonth };
}, [tasks]);
// ...
今天改成使用react navigator來進行路由導航。明天開始創接簡易的API。