附錄二、用 React Native + Firebase 開發跨平台行動應用程式

用 React Native + Firebase 開發跨平台行動應用程式

前言

跨平台(Wirte once, Run Everywhere)一直以來是軟體工程的聖杯。過去一段時間市場上有許多嘗試跨平台開發原生行動裝置(Native Mobile App)的解決方案,嘗試運用 HTML、CSS 和 JavaScript 等網頁前端技術達到跨平台的效果,例如:運用 jQuery MobileIonicFramework7 等 Mobile UI 框架(Framework)結合 JavaScript 框架並搭配 Cordova/PhoneGap 進行跨平台行動裝置開發。然而,因為這些解決方案通常都是運行在 WebView 之上,導致效能和體驗要真正趨近於原生應用程式(Native App)還有一段路要走。

不過,隨著 Facebook 工程團隊開發的 React Native 橫空出世,想嘗試跨平台解決方案的開發者又有了新的選擇。

React Native 特色

在正式開始開發 React Native App 之前我們先來介紹一下 React Native 的主要特色:

  1. 使用 JavaScript(ES6+)和 React 打造跨平台原生應用程式(Learn once, write anywhere)
  2. 使用 Native Components,更貼近原生使用者體驗
  3. 在 JavaScript 和 Native 之間的操作為非同步(Asynchronous)執行,並可用 Chrome 開發者工具除錯,支援 Hot Reloading
  4. 使用 Flexbox 進行排版和布局
  5. 良好的可擴展性(Extensibility),容易整合 Web 生態系標準(XMLHttpRequest、 navigator.geolocation 等)或是原生的元件或函式庫(Objective-C、Java 或 Swift)
  6. Facebook 已使用 React Native 於自家 Production App 且將持續維護,另外也有持續蓬勃發展的技術社群
  7. 讓 Web 開發者可以使用熟悉的技術切入 Native App 開發
  8. 2015/3 釋出 iOS 版本,2015/9 釋出 Android 版本
  9. 目前更新速度快,平均每兩週發佈新的版本。社群也還持續在尋找最佳實踐,關於版本進展可以參考這個文件
  10. 支援的作業系統為 >= Android 4.1 (API 16) 和 >= iOS 7.0

React Native 初體驗

在了解了 React Native 特色後,我們準備開始開發我們的 React Native 應用程式!由於我們的範例可以讓程式跨平台共用,所以你可以使用 iOS 和 Android 平台運行。不過若是想在 iOS 平台開發需要先準備 Mac OS 和安裝 Xcode 開發工具,若是你準備使用 Android 平台的話建議先行安裝 Android StudioGenymotion 模擬器。在我們範例我們使用筆者使用的 MacO OS 作業系統並使用 Android 平台為主要範例,若有其他作業系統需求的讀者可以參考 官方安裝說明

一開始請先安裝 NodeWatchman 和 React Native command line 工具:

  1. // 若你使用 Mac OS 你可以使用官網安裝方式或是使用 homebrew 安裝
  2. $ brew install node
  3. // watchman 可以監看檔案是否有修改
  4. $ brew install watchman
  1. // 安裝 React Native command line 工具
  2. $ npm install -g react-native-cli

由於我們是要開發 Android 平台,所以必須安裝:

  1. 安裝 JDK
  2. 安裝 Android SDK
  3. 設定一些環境變數

以上可以透過 Install Android Studio 官網和 官方安裝說明 步驟完成。

現在,我們先透過一個簡單的 HelloWorldApp,讓大家感受一下 React Native 專案如何開發。

首先,我們先初始化一個 React Native Project:

  1. $ react-native init HelloWorldApp

初始的資料夾結構長相:

用 React Native + Firebase 開發跨平台行動應用程式

接下來請先安裝註冊 Genymotion,Genymotion 是一個透過電腦模擬 Android 系統的好用開發模擬器環境。安裝完後可以打開並選擇欲使用的螢幕大小和 API 版本的 Android 系統。建立裝置後就可以啟動我們的裝置:

用 React Native + Firebase 開發跨平台行動應用程式

若你是使用 Mac OS 作業系統的話可以執行 run-ios,若是使用 Android 平台則使用 run-android 啟動你的 App。在這邊我們先使用 Android 平台進行開發(若你希望實機測試,請將電腦接上你的 Android 手機,記得確保 menu 中的 ip 位置要和電腦網路 相同。若是遇到連不到程式 server 且手機為 Android 5.0+ 系統,可以執行 adb reverse tcp:8081 tcp:8081,詳細情形可以參考官網說明):

  1. $ react-native run-android

如果一切順利的話就可以在模擬器中看到初始畫面:

用 React Native + Firebase 開發跨平台行動應用程式

接著打開 index.android.js 就可以看到以下程式碼:

  1. import React, { Component } from 'react';
  2. import {
  3. AppRegistry,
  4. StyleSheet,
  5. Text,
  6. View
  7. } from 'react-native';
  8. // 元件式的開發方式和 React 如出一轍,但要注意的是在 React Native 中我們不使用 HTML 元素而是使用 React Native 元件進行開發,這也符合 Learn once, write anywhere 的原則。
  9. class HelloWorldApp extends Component {
  10. render() {
  11. return (
  12. <View style={styles.container}>
  13. <Text style={styles.welcome}>
  14. Welcome to React Native!
  15. </Text>
  16. <Text style={styles.instructions}>
  17. To get started, edit index.android.js
  18. </Text>
  19. <Text style={styles.instructions}>
  20. Double tap R on your keyboard to reload,{'\n'}
  21. Shake or press menu button for dev menu
  22. </Text>
  23. </View>
  24. );
  25. }
  26. }
  27. // 在 React Native 中 styles 是使用 JavaScript 形式來撰寫,與一般 CSS 比較不同的是他使用駝峰式的屬性命名:
  28. const styles = StyleSheet.create({
  29. container: {
  30. flex: 1,
  31. justifyContent: 'center',
  32. alignItems: 'center',
  33. backgroundColor: '#F5FCFF',
  34. },
  35. welcome: {
  36. fontSize: 20,
  37. textAlign: 'center',
  38. margin: 10,
  39. },
  40. instructions: {
  41. textAlign: 'center',
  42. color: '#333333',
  43. marginBottom: 5,
  44. },
  45. });
  46. // 告訴 React Native App 你的進入點:
  47. AppRegistry.registerComponent('HelloWorldApp', () => HelloWorldApp);

由於 React Native 有支援 Hot Reloading,若我們更改了檔案內容,我們可以使用打開模擬器 Menu 重新刷新頁面,此時就可以在看到原本的 Welcome to React Native! 文字已經改成 Welcome to React Native Rock!!!!

用 React Native + Firebase 開發跨平台行動應用程式

用 React Native + Firebase 開發跨平台行動應用程式

嗯,有沒有感覺在開發網頁的感覺?

動手實作

相信看到這裡讀者們一定等不及想大展身手,使用 React Native 開發你第一個 App。俗話說學習一項新技術最好的方式就是做一個 TodoApp。所以,接下來的文章,筆者將帶大家使用 React Native 結合 Redux/ImmutableJS 和 Firebase 開發一個記錄和刪除名言佳句(Mottos)的 Mobile App!

專案成果截圖

用 React Native + Firebase 開發跨平台行動應用程式

用 React Native + Firebase 開發跨平台行動應用程式

環境安裝與設定

相關套件安裝:

  1. $ npm install --save redux react-redux immutable redux-immutable redux-actions uuid firebase
  1. $ npm install --save-dev babel-core babel-eslint babel-loader babel-preset-es2015 babel-preset-react babel-preset-react-native eslint-plugin-react-native eslint eslint-config-airbnb eslint-loader eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react redux-logger

安裝完相關工具後我們可以初始化我們專案:

  1. // 注意專案不能使用 - 或 _ 命名
  2. $ react-native init ReactNativeFirebaseMotto
  3. $ cd ReactNativeFirebaseMotto

我們先準備一下我們資料夾架構,將它設計成:

用 React Native + Firebase 開發跨平台行動應用程式

Firebase 簡介與設定

在這個專案中我們會使用到 Firebase 這個 Back-End as Service的服務,也就是說我們不用自己建立後端程式資料庫,只要使用 Firebase 所提供的 API 就好像有了一個 NoSQL 資料庫一樣,當然 Firebase 不單只有提供資料儲存的功能,但限於篇幅我們這邊將只介紹資料儲存的功能。

  1. 首先我們進到 Firebase 首頁
    用 React Native + Firebase 開發跨平台行動應用程式

  2. 登入後點選建立專案,依照自己想取的專案名稱命名

    用 React Native + Firebase 開發跨平台行動應用程式

  3. 選擇將 Firebase 加入你的網路應用程式的按鈕可以取得 App ID 的 config 資料,待會我們將會使用到

    用 React Native + Firebase 開發跨平台行動應用程式

  4. 點選左邊選單中的 Database 並點選 Realtime Database Tab 中的規則

    用 React Native + Firebase 開發跨平台行動應用程式

    設定改為,在範例中為求簡單,我們先不用驗證方式即可操作:

    1. {
    2. "rules": {
    3. ".read": true,
    4. ".write": true
    5. }
    6. }

Firebase 在使用上有許多優點,其中一個使用 Back-End As Service 的好處是你可以專注在應用程式的開發便免花過多時間處理後端基礎建設的部份,更可以讓 Back-End 共用在不同的 client side 中。此外 Firebase 在和 React 整合上也十分容易,你可以想成 Firebase 負責資料的儲存,透過 API 和 React 元件互動,Redux 負責接收管理 client state,若是監聽到 Firebase 後端資料更新後同步更新 state 並重新 render 頁面。

使用 Flexbox 進行 UI 布局設計

在 React Native 中是使用 Flexbox 進行排版,若讀者對於 Flexbox 尚不熟悉,建議可以參考這篇文章,若有需要遊戲化的學習工具,也非常推薦這兩個教學小遊戲:FlexDefenseFLEXBOX FROGGY

事實上我們可以將 Flexbox 視為一個箱子,最外層是 flex containers、內層包的是 flex items,在屬性上也有分是針對flex containers 還是針對是 flex items 設計的。在方向性上由左而右是 main axis,而上到下是 cross axis

用 React Native + Firebase 開發跨平台行動應用程式

在 Flexbox 有許多屬性值,其中最重要的當數 justifyContentalignItems 以及 flexDirection(注意 React Native Style 都是駝峰式寫法),所以我們這邊主要介紹這三個屬性:

Flex Direction 負責決定整個 flex containers 的方向,預設為 row 也可以改為 columnrow-reversecolumn-reverse

用 React Native + Firebase 開發跨平台行動應用程式

Justify Content 負責決定整個 flex containers 內的 items 的水平擺設,主要屬性值有:flex-startflex-endcenterspace-betweenspace-around

用 React Native + Firebase 開發跨平台行動應用程式

Align Items 負責決定整個 flex containers 內的 items 的垂直擺設,主要屬性值有:flex-startflex-endcenterstretchbaseline

用 React Native + Firebase 開發跨平台行動應用程式

動手實作

有了前面的準備,現在我們終於要開始進入核心的應用程式開發了!

首先我們先設定好整個 App 的進入檔 index.android.js,在這個檔案中我們設定了初始化的設定和主要元件 <Main />

  1. /**
  2. * Sample React Native App
  3. * https://github.com/facebook/react-native
  4. * @flow
  5. */
  6. import React, { Component } from 'react';
  7. import {
  8. AppRegistry,
  9. Text,
  10. View
  11. } from 'react-native';
  12. import Main from './src/components/Main';
  13. class ReactNativeFirebaseMotto extends Component {
  14. render() {
  15. return (
  16. <Main />
  17. );
  18. }
  19. }
  20. AppRegistry.registerComponent('ReactNativeFirebaseMotto', () => ReactNativeFirebaseMotto);

src/components/Main/Main.js 中我們設定好整個 Component 的布局和並將 Firebase 引入並初始化,將操作 Firebase 資料庫的參考往下傳,根節點我們命名為 items,所以之後所有新增的 motto 都會在這個根節點之下並擁有特定的 key 值。在 Main 我們同樣規劃了整個布局,包括:<ToolBar /><MottoListContainer /><ActionButtonContainer /><InputModalContainer />

  1. import React from 'react';
  2. import ReactNative from 'react-native';
  3. import { Provider } from 'react-redux';
  4. import ToolBar from '../ToolBar';
  5. import MottoListContainer from '../../containers/MottoListContainer';
  6. import ActionButtonContainer from '../../containers/ActionButtonContainer';
  7. import InputModalContainer from '../../containers/InputModalContainer';
  8. import ListItem from '../ListItem';
  9. import * as firebase from 'firebase';
  10. // 將 Firebase 的 config 值引入
  11. import { firebaseConfig } from '../../constants/config';
  12. // 引用 Redux store
  13. import store from '../../store';
  14. const { View, Text } = ReactNative;
  15. // Initialize Firebase
  16. const firebaseApp = firebase.initializeApp(firebaseConfig);
  17. // Create a reference with .ref() instead of new Firebase(url)
  18. const rootRef = firebaseApp.database().ref();
  19. const itemsRef = rootRef.child('items');
  20. // 將 Redux 的 store 透過 Provider 往下傳
  21. const Main = () => (
  22. <Provider store={store}>
  23. <View>
  24. <ToolBar style={styles.toolBar} />
  25. <MottoListContainer itemsRef={itemsRef} />
  26. <ActionButtonContainer />
  27. <InputModalContainer itemsRef={itemsRef} />
  28. </View>
  29. </Provider>
  30. );
  31. export default Main;

設定完了基本的布局方式後我們來設定 Actions 和其使用的常數,src/actions/mottoActions.js

  1. export const GET_MOTTOS = 'GET_MOTTOS';
  2. export const CREATE_MOTTO = 'CREATE_MOTTO';
  3. export const SET_IN_MOTTO = 'SET_IN_MOTTO';
  4. export const TOGGLE_MODAL = 'TOGGLE_MODAL';

我們在 constants 資料夾中也設定了我們整個 data 的資料結構,以下是 src/constants/models.js

  1. import Immutable from 'immutable';
  2. export const MottoState = Immutable.fromJS({
  3. mottos: [],
  4. motto: {
  5. id : '',
  6. text: '',
  7. updatedAt: '',
  8. }
  9. });
  10. export const UiState = Immutable.fromJS({
  11. isModalVisible: false,
  12. });

還記得我們提到的 Firebase config 嗎?這邊我們把相關的設定檔放在src/configs/config.js中:

  1. export const firebaseConfig = {
  2. apiKey: "apiKey",
  3. authDomain: "authDomain",
  4. databaseURL: "databaseURL",
  5. storageBucket: "storageBucket",
  6. };

在我們應用程式中同樣使用了 reduxredux-actions。在這個範例中我們設計了:GET_MOTTOS、CREATE_MOTTO、SET_IN_MOTTO 三個操作 motto 的 action,分別代表從 Firebase 取出資料、新增資料和 set 資料。以下是 src/actions/mottoActions.js

  1. import { createAction } from 'redux-actions';
  2. import {
  3. GET_MOTTOS,
  4. CREATE_MOTTO,
  5. SET_IN_MOTTO,
  6. } from '../constants/actionTypes';
  7. export const getMottos = createAction('GET_MOTTOS');
  8. export const createMotto = createAction('CREATE_MOTTO');
  9. export const setInMotto = createAction('SET_IN_MOTTO');

同樣地,由於我們設計了當使用者想新增 motto 時會跳出 modal,所以我們可以設定一個 TOGGLE_MODAL 負責開關 modal 的 state。以下是 src/actions/uiActions.js

  1. import { createAction } from 'redux-actions';
  2. import {
  3. TOGGLE_MODAL,
  4. } from '../constants/actionTypes';
  5. export const toggleModal = createAction('TOGGLE_MODAL');

以下是 src/actions/index.js,用來匯出我們的 actions:

  1. export * from './uiActions';
  2. export * from './mottoActions';

設定完我們的 actions 後我們來設定 reducers,在這邊我們同樣使用 redux-actions 整合 ImmutableJS

  1. import { handleActions } from 'redux-actions';
  2. // 引入 initialState
  3. import {
  4. MottoState
  5. } from '../../constants/models';
  6. import {
  7. GET_MOTTOS,
  8. CREATE_MOTTO,
  9. SET_IN_MOTTO,
  10. } from '../../constants/actionTypes';
  11. // 透過 set 和 seIn 可以產生 newState
  12. const mottoReducers = handleActions({
  13. GET_MOTTOS: (state, { payload }) => (
  14. state.set(
  15. 'mottos',
  16. payload.mottos
  17. )
  18. ),
  19. CREATE_MOTTO: (state) => (
  20. state.set(
  21. 'mottos',
  22. state.get('mottos').push(state.get('motto'))
  23. )
  24. ),
  25. SET_IN_MOTTO: (state, { payload }) => (
  26. state.setIn(
  27. payload.path,
  28. payload.value
  29. )
  30. )
  31. }, MottoState);
  32. export default mottoReducers;

以下是 src/reducers/uiState.js

  1. import { handleActions } from 'redux-actions';
  2. import {
  3. UiState,
  4. } from '../../constants/models';
  5. import {
  6. TOGGLE_MODAL,
  7. } from '../../constants/actionTypes';
  8. // modal 的顯示與否
  9. const uiReducers = handleActions({
  10. TOGGLE_MODAL: (state) => (
  11. state.set(
  12. 'isModalVisible',
  13. !state.get('isModalVisible')
  14. )
  15. ),
  16. }, UiState);
  17. export default uiReducers;

以下是 src/reducers/index.js,將所有 reducers combine 在一起:

  1. import { combineReducers } from 'redux-immutable';
  2. import ui from './ui/uiReducers';
  3. import motto from './data/mottoReducers';
  4. const rootReducer = combineReducers({
  5. ui,
  6. motto,
  7. });
  8. export default rootReducer;

透過 src/store/configureStore.js將 reducers 和 initialState 以及要使用的 middleware 整合成 store:

  1. import { createStore, applyMiddleware } from 'redux';
  2. import createLogger from 'redux-logger';
  3. import Immutable from 'immutable';
  4. import rootReducer from '../reducers';
  5. const initialState = Immutable.Map();
  6. export default createStore(
  7. rootReducer,
  8. initialState,
  9. applyMiddleware(createLogger({ stateTransformer: state => state.toJS() }))
  10. );

設定完資料層的架構後,我們又重新回到 View 的部份,我們開始依序設定我們的 Component 和 Container。首先,我們先設計我們的標題列 ToolBar,以下是 src/components/ToolBar/ToolBar.js

  1. import React from 'react';
  2. import ReactNative from 'react-native';
  3. import styles from './toolBarStyles';
  4. const { View, Text } = ReactNative;
  5. const ToolBar = () => (
  6. <View style={styles.toolBarContainer}>
  7. <Text style={styles.toolBarText}>Startup Mottos</Text>
  8. </View>
  9. );
  10. export default ToolBar;

以下是 src/components/ToolBar/toolBarStyles.js,將底色設定為黃色,文字置中:

  1. import { StyleSheet } from 'react-native';
  2. export default StyleSheet.create({
  3. toolBarContainer: {
  4. height: 40,
  5. justifyContent: 'center',
  6. alignItems: 'center',
  7. flexDirection: 'column',
  8. backgroundColor: '#ffeb3b',
  9. },
  10. toolBarText: {
  11. fontSize: 20,
  12. color: '#212121'
  13. }
  14. });

以下是 src/components/MottoList/MottoList.js,這個 Component 中稍微複雜一些,主要是使用到了 React Native 中的 ListView Component 將資料陣列傳進 dataSource,透過 renderRow 把一個個 row 給 render 出來,過程中我們透過 !Immutable.is(r1.get('id'), r2.get('id')) 去判斷整個 ListView 畫面是否需要 loading 新的 item 進來,這樣就可以提高整個 ListView 的效能。

  1. import React, { Component } from 'react';
  2. import ReactNative from 'react-native';
  3. import Immutable from 'immutable';
  4. import ListItem from '../ListItem';
  5. import styles from './mottoStyles';
  6. const { View, Text, ListView } = ReactNative;
  7. class MottoList extends Component {
  8. constructor(props) {
  9. super(props);
  10. this.renderListItem = this.renderListItem.bind(this);
  11. this.listenForItems = this.listenForItems.bind(this);
  12. this.ds = new ListView.DataSource({
  13. rowHasChanged: (r1, r2) => !Immutable.is(r1.get('id'), r2.get('id')),
  14. })
  15. }
  16. renderListItem(item) {
  17. return (
  18. <ListItem item={item} onDeleteMotto={this.props.onDeleteMotto} itemsRef={this.props.itemsRef} />
  19. );
  20. }
  21. listenForItems(itemsRef) {
  22. itemsRef.on('value', (snap) => {
  23. if(snap.val() === null) {
  24. this.props.onGetMottos(Immutable.fromJS([]));
  25. } else {
  26. this.props.onGetMottos(Immutable.fromJS(snap.val()));
  27. }
  28. });
  29. }
  30. componentDidMount() {
  31. this.listenForItems(this.props.itemsRef);
  32. }
  33. render() {
  34. return (
  35. <View>
  36. <ListView
  37. style={styles.listView}
  38. dataSource={this.ds.cloneWithRows(this.props.mottos.toArray())}
  39. renderRow={this.renderListItem}
  40. enableEmptySections={true}
  41. />
  42. </View>
  43. );
  44. }
  45. }
  46. export default MottoList;

以下是 src/components/MottoList/mottoListStyles.js,我們使用到了 Dimensions,可以根據螢幕的高度來設定整個 ListView 高度:

  1. import { StyleSheet, Dimensions } from 'react-native';
  2. const { height } = Dimensions.get('window');
  3. export default StyleSheet.create({
  4. listView: {
  5. flex: 1,
  6. flexDirection: 'column',
  7. height: height - 105,
  8. },
  9. });

以下是 src/components/ListItem/ListItem.js,我們從 props 收到了上層傳進來的 motto item,顯示出 motto 文字內容。當我們點擊 <TouchableHighlight> 時就會刪除該 motto。

  1. import React from 'react';
  2. import ReactNative from 'react-native';
  3. import styles from './listItemStyles';
  4. const { View, Text, TouchableHighlight } = ReactNative;
  5. const ListItem = (props) => {
  6. return (
  7. <View style={styles.listItemContainer}>
  8. <Text style={styles.listItemText}>{props.item.get('text')}</Text>
  9. <TouchableHighlight onPress={props.onDeleteMotto(props.item.get('id'), props.itemsRef)}>
  10. <Text>Delete</Text>
  11. </TouchableHighlight>
  12. </View>
  13. )
  14. };
  15. export default ListItem;

以下是 src/components/ListItem/listItemStyles.js

  1. import { StyleSheet } from 'react-native';
  2. export default StyleSheet.create({
  3. listItemContainer: {
  4. flex: 1,
  5. flexDirection: 'row',
  6. padding: 10,
  7. margin: 5,
  8. },
  9. listItemText: {
  10. flex: 10,
  11. fontSize: 18,
  12. color: '#212121',
  13. }
  14. });

以下是 src/components/ActionButton/ActionButton.js,當點擊了按鈕則會觸發 onToggleModal 方法,出現新增 motto 的 modal:

  1. import React from 'react';
  2. import ReactNative from 'react-native';
  3. import styles from './actionButtonStyles';
  4. const { View, Text, Modal, TextInput, TouchableHighlight } = ReactNative;
  5. const ActionButton = (props) => (
  6. <TouchableHighlight onPress={props.onToggleModal}>
  7. <View style={styles.buttonContainer}>
  8. <Text style={styles.buttonText}>Add Motto</Text>
  9. </View>
  10. </TouchableHighlight>
  11. );
  12. export default ActionButton;

以下是 src/components/ActionButton/actionButtonStyles.js

  1. import { StyleSheet } from 'react-native';
  2. export default StyleSheet.create({
  3. buttonContainer: {
  4. height: 40,
  5. justifyContent: 'center',
  6. alignItems: 'center',
  7. flexDirection: 'column',
  8. backgroundColor: '#66bb6a',
  9. },
  10. buttonText: {
  11. fontSize: 20,
  12. color: '#e8f5e9'
  13. }
  14. });

以下是 src/components/InputModal/InputModal.js,其主要負責 Modal Component 的設計,當輸入內容會觸發 onChangeMottoText 發出 action,注意的是當按下送出鍵,同時會把 Firebase 的參考 itemsRef 送入 onCreateMotto 中,方便透過 API 去即時新增到 Firebase Database,並更新 client state 和重新渲染了 View:

  1. import React from 'react';
  2. import ReactNative from 'react-native';
  3. import styles from './inputModelStyles';
  4. const { View, Text, Modal, TextInput, TouchableHighlight } = ReactNative;
  5. const InputModal = (props) => (
  6. <View>
  7. <Modal
  8. animationType={"slide"}
  9. transparent={false}
  10. visible={props.isModalVisible}
  11. onRequestClose={props.onToggleModal}
  12. >
  13. <View>
  14. <View>
  15. <Text style={styles.modalHeader}>Please Keyin your Motto!</Text>
  16. <TextInput
  17. onChangeText={props.onChangeMottoText}
  18. />
  19. <View style={styles.buttonContainer}>
  20. <TouchableHighlight
  21. onPress={props.onToggleModal}
  22. style={[styles.cancelButton]}
  23. >
  24. <Text
  25. style={styles.buttonText}
  26. >
  27. Cancel
  28. </Text>
  29. </TouchableHighlight>
  30. <TouchableHighlight
  31. onPress={props.onCreateMotto(props.itemsRef)}
  32. style={[styles.submitButton]}
  33. >
  34. <Text
  35. style={styles.buttonText}
  36. >
  37. Submit
  38. </Text>
  39. </TouchableHighlight>
  40. </View>
  41. </View>
  42. </View>
  43. </Modal>
  44. </View>
  45. );
  46. export default InputModal;

以下是 src/components/InputModal/inputModalStyles.js

  1. import { StyleSheet } from 'react-native';
  2. export default StyleSheet.create({
  3. modalHeader: {
  4. flex: 1,
  5. height: 30,
  6. padding: 10,
  7. flexDirection: 'row',
  8. backgroundColor: '#ffc107',
  9. fontSize: 20,
  10. },
  11. buttonContainer: {
  12. flex: 1,
  13. flexDirection: 'row',
  14. },
  15. button: {
  16. borderRadius: 5,
  17. },
  18. cancelButton: {
  19. flex: 1,
  20. height: 40,
  21. alignItems: 'center',
  22. justifyContent: 'center',
  23. backgroundColor: '#eceff1',
  24. margin: 5,
  25. },
  26. submitButton: {
  27. flex: 1,
  28. height: 40,
  29. alignItems: 'center',
  30. justifyContent: 'center',
  31. backgroundColor: '#4fc3f7',
  32. margin: 5,
  33. },
  34. buttonText: {
  35. fontSize: 20,
  36. }
  37. });

設定完了 Component,我們來探討一下 Container 的部份。以下是 src/containers/ActionButtonContainer/ActionButtonContainer.js

  1. import { connect } from 'react-redux';
  2. import ActionButton from '../../components/ActionButton';
  3. import {
  4. toggleModal,
  5. } from '../../actions';
  6. export default connect(
  7. (state) => ({}),
  8. (dispatch) => ({
  9. onToggleModal: () => (
  10. dispatch(toggleModal())
  11. )
  12. })
  13. )(ActionButton);

以下是 src/containers/InputModalContainer/InputModalContainer.js

  1. import { connect } from 'react-redux';
  2. import InputModal from '../../components/InputModal';
  3. import Immutable from 'immutable';
  4. import {
  5. toggleModal,
  6. setInMotto,
  7. createMotto,
  8. } from '../../actions';
  9. import uuid from 'uuid';
  10. export default connect(
  11. (state) => ({
  12. isModalVisible: state.getIn(['ui', 'isModalVisible']),
  13. motto: state.getIn(['motto', 'motto']),
  14. }),
  15. (dispatch) => ({
  16. onToggleModal: () => (
  17. dispatch(toggleModal())
  18. ),
  19. onChangeMottoText: (text) => (
  20. dispatch(setInMotto({ path: ['motto', 'text'], value: text }))
  21. ),
  22. // 新增 motto 是透過 itemsRef 將新增的 motto push 進去,新增後要把本地端的 motto 清空,並關閉 modal:
  23. onCreateMotto: (motto) => (itemsRef) => () => {
  24. itemsRef.push({ id: uuid.v4(), text: motto.get('text'), updatedAt: Date.now() });
  25. dispatch(setInMotto({ path: ['motto'], value: Immutable.fromJS({ id: '', text: '', updatedAt: '' })}));
  26. dispatch(toggleModal());
  27. }
  28. }),
  29. (stateToProps, dispatchToProps, ownProps) => {
  30. const { motto } = stateToProps;
  31. const { onCreateMotto } = dispatchToProps;
  32. return Object.assign({}, stateToProps, dispatchToProps, ownProps, {
  33. onCreateMotto: onCreateMotto(motto),
  34. });
  35. },
  36. )(InputModal);

以下是 src/containers/MottoListContainer/MottoListContainer.js

  1. import { connect } from 'react-redux';
  2. import MottoList from '../../components/MottoList';
  3. import Immutable from 'immutable';
  4. import uuid from 'uuid';
  5. import {
  6. createMotto,
  7. getMottos,
  8. changeMottoTitle,
  9. } from '../../actions';
  10. export default connect(
  11. (state) => ({
  12. mottos: state.getIn(['motto', 'mottos']),
  13. }),
  14. (dispatch) => ({
  15. onCreateMotto: () => (
  16. dispatch(createMotto())
  17. ),
  18. onGetMottos: (mottos) => (
  19. dispatch(getMottos({ mottos }))
  20. ),
  21. onChangeMottoTitle: (title) => (
  22. dispatch(changeMottoTitle({ value: title }))
  23. ),
  24. // 判斷點擊的是哪一個 item 取出其 key,透過 itemsRef 將其移除
  25. onDeleteMotto: (mottos) => (id, itemsRef) => () => {
  26. mottos.forEach((value, key) => {
  27. if(value.get('id') === id) {
  28. itemsRef.child(key).remove();
  29. }
  30. });
  31. }
  32. }),
  33. (stateToProps, dispatchToProps, ownProps) => {
  34. const { mottos } = stateToProps;
  35. const { onDeleteMotto } = dispatchToProps;
  36. return Object.assign({}, stateToProps, dispatchToProps, ownProps, {
  37. onDeleteMotto: onDeleteMotto(mottos),
  38. });
  39. }
  40. )(MottoList);

最後我們可以透過啟動模擬器後使用以下指令開啟我們 App!

  1. $ react-native run-android

最後的成果:

用 React Native + Firebase 開發跨平台行動應用程式

同時你可以在 Firebase 後台進行觀察,當呼叫 Firebase API 進行資料更動時,Firebase Realtime Database 就會即時更新:

用 React Native + Firebase 開發跨平台行動應用程式

總結

恭喜你!你已經完成了你的第一個 React Native App,若你希望將你開發的應用程式簽章後上架,請參考官方的說明文件,當你完成簽章打包等流程後,我們可以獲得 .apk 檔,這時就可以上架到市集讓隔壁班心儀的女生,啊不是,是廣大的 Android 使用者使用你的 App 啦!當然,由於我們的程式碼可以 100% 共用於 iOS 和 Android 端,所以你也可以同步上架到 Apple Store!

延伸閱讀

  1. React Native 官方網站
  2. React 官方網站
  3. Redux 官方文件
  4. Ionic Framework vs React Native
  5. How to Build a Todo App Using React, Redux, and Immutable.js
  6. Your First Immutable React & Redux App
  7. React, Redux and Immutable.js: Ingredients for Efficient Web Applications
  8. Full-Stack Redux Tutorial
  9. redux与immutable实例
  10. gajus/redux-immutable
  11. acdlite/redux-actions
  12. Flux Standard Action
  13. React Native ImmutableJS ListView Example
  14. React Native 0.23.1 warning: ‘In next release empty section headers will be rendered’
  15. js.coach
  16. React Native Package Manager
  17. React Native 学习笔记
  18. The beginners guide to React Native and Firebase
  19. Authentication in React Native with Firebase
  20. bruz/react-native-redux-groceries
  21. Building a Simple ToDo App With React Native and Firebase
  22. Firebase Permission Denied
  23. Best Practices: Arrays in Firebase
  24. Avoiding plaintext passwords in gradle
  25. Generating Signed APK

(image via moduscreatecss-tricksteamtreehouseteamtreehousecss-trickscss-tricks)

| 回首頁 | 上一章:附錄一、React ES5、ES6+ 常見用法對照表 | 下一章:附錄三、React 測試入門教學 |

| 勘誤、提問或許願 |