用RN( ListView + Navigator ) + Redux來(lái)開(kāi)發(fā)一個(gè)ToDoList

教程說(shuō)明
如何初始化一個(gè)redux Store
如何使用action+reducer來(lái)管理state
如何在react-native里更新ui
這個(gè)例子可能不是很具體,但是對(duì)于理解用法比較好(目前看到的例子都是counter)

SCREENSHOT


PACKAGE.JSON
一些版本的東西(因?yàn)閞eact-redux暫時(shí)用3.x,原因看 this React Native issue)

PACKAGE.JSON
一些版本的東西(因?yàn)閞eact-redux暫時(shí)用3.x,原因看 [this React Native issue](https://github.com/facebook/react-native/issues/2985))
{
    "name": "TodoList", 
    "version": "0.0.1", 
    "private": true,
     "scripts": { 
         "start": "node_modules/react-native/packager/packager.sh"
   },
 "dependencies": { 
     "normalizr": "^1.4.0", 
    "react-native": "^0.14.2",
     "react-redux": "^3.0.1",
     "redux": "^3.0.4",
     "redux-thunk": "^1.0.0" 
}}

目錄結(jié)構(gòu)

.
├── index.ios.js
├── index.android.js
└── src
├── actions //存放Actions
├── containers //UI && Component
└── reducers //存放Reducers
首先index.io.js改下入口:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */
'use strict';

import React from 'react-native';
import App from './src/containers/App';
var {
  AppRegistry,
} = React;

var TodoList = React.createClass({
  render: function() {
    return (
      <App />
    );
  }
});

AppRegistry.registerComponent('TodoList', () => TodoList);

把我們的入口設(shè)置成 App.js.

現(xiàn)在來(lái)創(chuàng)建App.js文件,位于./src/containers/App.js :

import React, { Component, View, Text } from 'react-native';
import { Provider } from 'react-redux/native';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import * as reducers from '../reducers';
import BaseApp from './BaseApp';

//apply thunk
const createStoreWithThunk = applyMiddleware(thunk)(createStore);
const reducer = combineReducers(reducers);
const store = createStoreWithThunk(reducer);

export default class App extends Component {
    render() {
        return (
            <Provider store={store}>
                { () => <BaseApp /> }
            </Provider>
        );
    }
}

先來(lái)解釋這個(gè)地方具體做什么事芭挽,這里把reducers收集起來(lái)了,然后打包成一個(gè)叫做Store的東西膛薛,這個(gè)Store就是我們后面用到的所有state合集刽射,具體不清楚军拟,可以選擇直接console.log(Store),發(fā)現(xiàn)Store有這些method:

dispatch: (action)
getState: getState()
replaceReducer: replaceReducer(nextReducer)
subscribe: subscribe(listener)
具體是做什么用誓禁,后面說(shuō)明懈息。

關(guān)于reducers,其實(shí)就是定義了我們整個(gè)state的數(shù)據(jù)結(jié)構(gòu)的一個(gè)東西摹恰。就像我們要做一個(gè)todoList辫继,它最基本的數(shù)據(jù)結(jié)構(gòu)就是:

{
    todos: [ 
        { text: "吃飯" , selected: false },
        { text: "上班" , selected: false },
        { text: "寫(xiě)代碼" , selected: true },
        ...
    ]
}

那么我們的App里,reducer就需要返回這個(gè)俗慈,所以簡(jiǎn)單的說(shuō)姑宽,你就理解成:

var reducer = (condition) => {
    //根據(jù)條件做了一些羞羞的事情
    a();
    b();
    c();
    
    return {
        todos: [ 
            { text: "吃飯" , selected: false },
            { text: "上班" , selected: false },
            { text: "寫(xiě)代碼" , selected: true },
            ...
        ]
    }
}

就好了,具體如何做姜盈,看我們后面解釋低千。
所以這里 App.js里面,我們就拿到了后面定義的所有state的讀取的權(quán)力馏颂。這里有行代碼:

const createStoreWithThunk = applyMiddleware(thunk)(createStore);

關(guān)于thunk是什么 說(shuō)簡(jiǎn)單點(diǎn)示血,就是給我們的代碼提供了異步的功能,也就是在promise里還可以同時(shí)做很多操作救拉,比如更新列表难审,彈出提醒等等,詳見(jiàn)后面亿絮。

<Provider store={store}> { () => <BaseApp /> }</Provider>

我們現(xiàn)在通過(guò)Provider把Store遞交給了真正的App入口告喊,也就是開(kāi)始渲染界面的東西: BaseApp.js。
現(xiàn)在來(lái)創(chuàng)建BaseApp.js文件派昧,文件位于 ./src/containers/BaseApp.js:

import React, {
    Component,
    View,
    Text,
    Navigator,
    TabBarIOS,
} from 'react-native';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux/native';
import * as actions from '../actions';
import List from './List';

@connect(state => ({
    state: state
}))
export default class BaseApp extends Component {
    constructor(props) {
        super(props);
        this.initialRoute = {
            name: 'List',
            component: List,
        }
    }

    configureScene() {
        return Navigator.SceneConfigs.VerticalDownSwipeJump;
    }

    renderScene(route, navigator) {

        let Component = route.component;
    const { state, dispatch } = this.props;
    const action = bindActionCreators(actions, dispatch);

      return (
        <Component 
            state={state}
            actions={action}
            {...route.params} 
            navigator={navigator} />
      );
    }

    render() {
        var _this = this;
        return (
            <Navigator
                initialRoute={_this.initialRoute}
                configureScene={_this.configureScene.bind(_this)}
                renderScene={_this.renderScene.bind(_this)} />
        );
    }
}

這是一個(gè)簡(jiǎn)單的 Navigator黔姜,關(guān)于Navigator的用法,請(qǐng)看我上一個(gè)帖子蒂萎。

但是唯一區(qū)別的地方在于:

@connect(state => ({
    state: state
}))

這是一個(gè)es6的語(yǔ)法秆吵,叫es7.decorators,具體操作是把上一個(gè)App入口傳入的Store里的state取到五慈,然后作為props在BaseApp里面使用纳寂。

既然說(shuō)到了state,那就先去創(chuàng)建一個(gè)reducer吧泻拦,先定義一下初始的state結(jié)構(gòu):

創(chuàng)建todo.js 文件位于 ./src/reducers/todo.js:


const defaultTodos = [
                    {text: '寫(xiě)代碼'},
                    {text: '哄妹紙'},
                    {text: '做飯洗碗家務(wù)事'},
                    {text: '等等...'}
                ];

module.exports = function(state, action) {
    state = state || {
        type: 'INITIAL_TODOS',
        todos: []
    }

    return {
        ...state
    }
}

這里定義了默認(rèn)的todoList結(jié)構(gòu)毙芜,然后返回這個(gè)函數(shù)給了exports。

為了方便import,我們?cè)谶@個(gè)目錄下再創(chuàng)建一個(gè)index.js

創(chuàng)建index.js 文件位于 ./src/reducers/index.js:

module.exports.todo = require('./todo');

導(dǎo)出為todo就好了争拐,這個(gè)todo就是一個(gè)整個(gè)state數(shù)據(jù)結(jié)構(gòu)里的一部分了腋粥。

這里看到了 type: 'INITIAL_TODOS' ,也就是這個(gè)操作就是初始化todos,那么加載出來(lái)defaultTodos怎么寫(xiě)呢:

修改todo.js 文件位于 ./src/reducers/todo.js:

import React, {
    ListView
} from 'react-native';

const defaultTodos = [
                    {text: '寫(xiě)代碼'},
                    {text: '哄妹紙'},
                    {text: '做飯洗碗家務(wù)事'},
                    {text: '等等...'}
                ];

module.exports = function(state, action) {
    state = state || {
        type: 'INITIAL_TODOS',
        todos: []
    }
    
    switch(action.type) {
        case 'LOAD_TODOS': {
            var dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
            dataSource = dataSource.cloneWithRows(defaultTodos);
            return {
                ...state,
                ...action,
                todos: defaultTodos,
                dataSource,
            }
        }

    return {
        ...state
    }
}

這里給action的type開(kāi)始做判斷了隘冲,action是我們的一些具體操作金赦,比如 loadTodos就是加載todoList數(shù)據(jù),這里L(fēng)OAD_TODOS先創(chuàng)建一個(gè)ListView用于后面渲染todoList的內(nèi)容对嚼,
然后把初始數(shù)據(jù)給拷貝給了ListView夹抗,然后用 ...展開(kāi)方法,把state纵竖,action漠烧,todos,dataSource都給返回了靡砌。

現(xiàn)在要?jiǎng)?chuàng)建我們的action了忿磅,這里記住reducer只是單純的負(fù)責(zé)返回?cái)?shù)據(jù)結(jié)構(gòu)塘雳,并不能做抓取數(shù)據(jù)/更新/修改/刪除數(shù)據(jù)的操作,CRUD這些操作都是在action中進(jìn)行。

創(chuàng)建TodoActions.js文件 文件位于 ./src/actions/TodoActions.js:

const LOAD_TODOS = 'LOAD_TODOS';
const SELECT_TODO = 'SELECT_TODO';
const APPEND_TODO = 'APPEND_TODO';

var loadTodos = () => {
    return (dispatch) => {
        setTimeout(() => {
            dispatch({ type: LOAD_TODOS });
        }, 1000);

        // fetch().then() => dispatch in promise 
    }
}

var appendTodo = (text, cleanUIState) => {
    if(text) {
        if(cleanUIState) 
            cleanUIState();
        return {
            type: APPEND_TODO,
            todo: { text },
        }
    }

    return ;
}

var selectTodo = (selected) => {
    return {
        type: SELECT_TODO,
        selected
    }
}

module.exports = {
    loadTodos,
    appendTodo,
    selectTodo,
}

同理為了方便 import ,我們創(chuàng)建index.js文件 文件位于 ./src/actions/index.js:

//exports很多對(duì)象時(shí)候的另一種寫(xiě)法而已

var todo = require('./TodoActions');
var actions = {};
Object.assign(actions, todo);
module.exports = actions;

這里我定義了三個(gè)常亮跟三個(gè)方法彤路,三個(gè)方法分別用于加載todo任務(wù)徒恋,追加todo任務(wù)兵拢,以及完成/撤銷todo任務(wù)介褥,然后再提交給exports,這里千萬(wàn)別忘了module.exports曲聂。
var loadTodos = () => {
return (dispatch) => {
setTimeout(() => {
dispatch({ type: LOAD_TODOS });
}, 1000);

    // fetch().then() => dispatch in promise 
}

}
這里是做了一個(gè)獲取數(shù)據(jù)的操作霹购,我給它延時(shí)1s操作,就是為了模擬從本地讀取或者從服務(wù)器抓取數(shù)據(jù)朋腋,這些因?yàn)槭钱惒讲僮髌敫恚倩氐角懊婺莻€(gè)thunk的middleware,就是在這里起作用了旭咽。

我們?cè)趓eact-native中就可以直接用 loadTodos() 來(lái)觸發(fā)初始化todoList的操作了贞奋。

這里看最后的 dispatch({ type: LOAD_TODOS });
這個(gè)dispatch就會(huì)把我們的數(shù)據(jù)傳遞到reducer里的 module.exports = function(state, action) 在./src/reducers/todo.js打印那個(gè)console.log(action)就會(huì)顯示我們這里dispatch的

{ type: LOAD_TODOS }

如果我們換成

{ type: LOAD_TODOS, defaultTodos: [{text: '我在dispatch數(shù)據(jù)給reducer'}] }

試試看,你會(huì)看到什么穷绵。轿塔。。

再回到我們的BaseApp里的

renderScene(route, navigator) {

    let Component = route.component;
    const { state, dispatch } = this.props;
    const action = bindActionCreators(actions, dispatch);

    return (
        <Component 
            state={state}
            actions={action}
            {...route.params} 
            navigator={navigator} />
  );
}

state,dispath 都來(lái)自于我們的Store请垛,
然后bindActionCreators把我們定義的所有的actions通過(guò)dispatch的參數(shù)來(lái)關(guān)聯(lián)到reducer的返回,也就是state催训。于是我們action中寫(xiě)的所有method洽议,都可以通過(guò)action.type來(lái)reducer中找到對(duì)應(yīng)的返回的state宗收!

最后都傳遞給我們的組件的Props。所以這里發(fā)生了一個(gè)事情亚兄,就是在所有通過(guò)Navigator導(dǎo)航的Component里混稽,我們都可以操作全局的state。

現(xiàn)在來(lái)完善我們的 reducers.js:
todo 文件位于 ./scr/reducers/todo.js:

import React, {
    ListView
} from 'react-native';

const defaultTodos = [
                    {text: '寫(xiě)代碼'},
                    {text: '哄妹紙'},
                    {text: '做飯洗碗家務(wù)事'},
                    {text: '等等...'}
                ];

module.exports = function(state, action) {
    state = state || {
        type: 'INITIAL_TODOS',
        todos: []
    }

    switch(action.type) {
        
        case 'LOAD_TODOS': {
            var dataSource = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
            dataSource = dataSource.cloneWithRows(defaultTodos);

            return {
                ...state,
                ...action,
                todos: defaultTodos,
                dataSource,
            }
        }

        case 'APPEND_TODO': {
            var todos = [ ...state.todos ];
            todos.unshift(action.todo);
            dataSource = state.dataSource.cloneWithRows(todos);
            return {
                ...state,
                ...action,
                todos,
                dataSource
            }
        }

        case 'SELECT_TODO': {
            var selected = action.selected;
            var todos = [ ...state.todos ];
            var index = todos.indexOf(selected);
            
            if(todos[index].selected) {
                todos[index] = { text: todos[index].text }
            }else {
                todos[index] = { text: todos[index].text, selected: true }
            }

            dataSource = state.dataSource.cloneWithRows(todos);
            return {
                ...state,
                ...action,
                todos,
                dataSource
            }
        }
    }   

    return {
        ...state
    }
}

會(huì)看到這里都在用 ...展開(kāi),這樣可以創(chuàng)建一個(gè)新的對(duì)象匈勋,而舊的對(duì)象不會(huì)發(fā)生改變礼旅,這個(gè)的目的是為了另一個(gè)功能,具體這里先不解釋了洽洁。

現(xiàn)在已經(jīng)有了Navigator痘系,有個(gè)操作state的三個(gè)action,有了可以返回完整數(shù)據(jù)結(jié)構(gòu)的reducer饿自,現(xiàn)在只需要寫(xiě)一個(gè)List的頁(yè)面來(lái)載入汰翠,添加,完成/撤銷todo任務(wù)就可以了昭雌。

創(chuàng)建文件 List.js 文件位于 ./src/containers/List.js:

import React, {
    Component,
    View,
    ListView,
    TextInput,
    Text,
    Image,
    Dimensions,
    TouchableOpacity,
    ActivityIndicatorIOS,
    StyleSheet,
} from 'react-native';

const fullWidth = Dimensions.get('window').width;

const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 20,
    },
    todoRow: {
        paddingLeft: 10,
        paddingRight: 10,
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-between',
        width: fullWidth,
        height: 40,
        borderBottomColor: '#EEEEEE',
        borderBottomWidth: 1,
    },
    todoText: {
        fontSize: 16,
        color: '#666666',
    },
    todoTextDone: {
        fontSize: 16,
        color: '999999',
        textDecorationColor: '#999999',
        textDecorationLine: 'line-through', 
        textDecorationStyle: 'solid'
    },
    success: {
        color: 'green',
    },
    pendding: {
        color: 'blue',
    },
    inputText: {
        height: 40,
        width: (fullWidth-20)*0.8,
        borderBottomColor: '#EEEEEE',
        borderBottomWidth: 1,
    },
    button: {
        alignItems: 'center',
        justifyContent: 'center',
        width: (fullWidth - 20)*0.2,
        backgroundColor: '#EEEEEE',
        padding: 10,
    }
});

export default class List extends Component {
    constructor(props) {
        super(props);

        this.state = {
            text: null,
            placeholder: '寫(xiě)下你將來(lái)要做的事情'
        }
    }

    componentDidMount() {
        const { loadTodos } = this.props.actions;
        loadTodos();
    }

    appendTodoList() {
        const text = this.state.text;
        const { appendTodo } = this.props.actions;
        appendTodo(text);
        this.setState({ text: null });
    }

    renderHeader() {
        return (
            <View style={styles.todoRow}>
                <TextInput
                    value={this.state.text}
                    placeholder={this.state.placeholder}
                    onChangeText={(text) => this.setState({ text })}
                    style={styles.inputText} />

                <TouchableOpacity onPress={this.appendTodoList.bind(this)} style={styles.button}>
                    <Text style={styles.buttonText}>添加</Text>
                </TouchableOpacity>

            </View>
        );
    }

    renderRow(dataRow) {
        const { selectTodo } = this.props.actions;
        return (
        
            <View style={styles.todoRow}>
                <Text style={ dataRow.selected ? styles.todoTextDone : styles.todoText}>{dataRow.text}</Text>
                <TouchableOpacity onPress={() => selectTodo(dataRow)}>
                    { dataRow.selected ? <Text style={styles.success}>完成</Text> : <Text style={styles.pendding}>待辦</Text> }
                </TouchableOpacity>

            </View>
        
        )
    } 

    renderList() {
        const { todo } = this.props.state;
        return (
            <ListView 
                style={styles.container}
                dataSource={todo.dataSource}
                renderHeader={this.renderHeader.bind(this)}
                renderRow={this.renderRow.bind(this)} />
        );
    }

    renderIndicator() {
        return (
            <ActivityIndicatorIOS animating={true} color={'#808080'} size={'small'} />
        );
    }

    render() {
        const { todo } = this.props.state;
        return (
            <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
                { todo.type != 'INITIAL_TODOS' ? this.renderList() : this.renderIndicator() } 
            </View>
        );
    }
}

目前就寫(xiě)這么多了复唤,可能會(huì)有一些錯(cuò)誤的地方,后面可以跟帖補(bǔ)上烛卧。
黑色斜體高亮部分是完整的代碼佛纫,拷貝或者改動(dòng)都可以用的。代碼地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末总放,一起剝皮案震驚了整個(gè)濱河市呈宇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌局雄,老刑警劉巖攒盈,帶你破解...
    沈念sama閱讀 206,602評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異哎榴,居然都是意外死亡型豁,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門尚蝌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)迎变,“玉大人,你說(shuō)我怎么就攤上這事飘言∫滦危” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,878評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵姿鸿,是天一觀的道長(zhǎng)谆吴。 經(jīng)常有香客問(wèn)我,道長(zhǎng)苛预,這世上最難降的妖魔是什么句狼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,306評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮热某,結(jié)果婚禮上腻菇,老公的妹妹穿的比我還像新娘胳螟。我一直安慰自己,他們只是感情好筹吐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布糖耸。 她就那樣靜靜地躺著,像睡著了一般丘薛。 火紅的嫁衣襯著肌膚如雪嘉竟。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,071評(píng)論 1 285
  • 那天洋侨,我揣著相機(jī)與錄音周拐,去河邊找鬼。 笑死凰兑,一個(gè)胖子當(dāng)著我的面吹牛妥粟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播吏够,決...
    沈念sama閱讀 38,382評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼勾给,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了锅知?” 一聲冷哼從身側(cè)響起播急,我...
    開(kāi)封第一講書(shū)人閱讀 37,006評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎售睹,沒(méi)想到半個(gè)月后桩警,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昌妹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評(píng)論 2 325
  • 正文 我和宋清朗相戀三年捶枢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片飞崖。...
    茶點(diǎn)故事閱讀 38,094評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡烂叔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出固歪,到底是詐尸還是另有隱情蒜鸡,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評(píng)論 4 323
  • 正文 年R本政府宣布牢裳,位于F島的核電站逢防,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蒲讯。R本人自食惡果不足惜忘朝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望伶椿。 院中可真熱鬧辜伟,春花似錦、人聲如沸脊另。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,286評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)偎痛。三九已至旱捧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間踩麦,已是汗流浹背枚赡。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,512評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谓谦,地道東北人贫橙。 一個(gè)月前我還...
    沈念sama閱讀 45,536評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像反粥,于是被迫代替她去往敵國(guó)和親卢肃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容