RN:Redux、React-Redux实践之更换主题色


为App根组件包裹Provider组件,以便整个项目能使用Redux和React-Redux

上一篇文章中,我们说到为了能让项目中所有的容器组件都能顺利拿到应用State,React-Redux规定我们要在应用根组件外面包一层Provider组件。

我们需要在项目中导入一个redux-helpers的三方库。

// AppContainer.js

import React, {Component} from 'react';

import {createAppContainer} from "react-navigation";
import SwitchNavigator from './SwitchNavigator';

import {createReactNavigationReduxMiddleware, createReduxContainer} from 'react-navigation-redux-helpers';
import {connect} from 'react-redux';

// 不使用Redux之前,我们是直接导出AppContainer,现在就不直接导出它了,而是给它包裹几层再到出去
const AppContainer = createAppContainer(SwitchNavigator);


{/* 第1步,使用react-navigation-redux-helpers,给AppContainer包裹一层ReduxContainer,下面都是固定写法,直接复制就行*/}

// 1.1 初始化react-navigation与redux的中间件
const reactNavigationReduxMiddleware = createReactNavigationReduxMiddleware(// 返回一个可用于store的middleware
    // 读取应用state的nav
    state => state.navigatorState,
    // 这个参数对于Redux Store来说必须是唯一的,并且与createReduxContainer下面的调用一致。
    'root',
);

// 1.2 把根组件先包装一层
const AppContainer_ReduxContainer = createReduxContainer(// 返回包装根导航器的高阶组件,用于代替根导航器的组件
    // 我们的根导航器
    AppContainer,
    // 和上面createReactNavigationReduxMiddleware设置的key一致
    'root',
);


{/* 第2步,使用React-Redux,创建AppContainer_ReduxContainer的容器组件,即我们最终要使用的根组件 */}

// 2.1 编写UI组件
// 即AppContainer_ReduxContainer

// 2.2 建立UI组件的props与外界的映射关系
// 你可能会问“这个UI组件是什么添加上这些props的呢”?没错,正是这个添加映射关系的过程为该UI组件添加上了这些props
// 输入逻辑类属性要和应用state里该组件state里的属性建立映射关系
function mapStateToProps(state) {
    return {
        state: state.navigatorState,
    }
}
// 输出逻辑类属性要和dispatch(addAction)建立映射关系
function mapDispatchToProps(dispatch) {
    return {

    }
}

// 2.3 使用connect方法生成UI组件对应的容器组件
// 将来我们就是使用UI组件的容器组件了,而不是使用UI组件
const AppContainer_ReduxContainer_Container = connect(
    mapStateToProps,
    // mapDispatchToProps,
)(AppContainer_ReduxContainer);

export {AppContainer_ReduxContainer_Container, reactNavigationReduxMiddleware};
// App.js

import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
import {AppContainer_ReduxContainer_Container} from './js/Navigator/AppContainer';
import store from './js/store/store';
import {Provider} from 'react-redux';

export default class App extends Component {
    render() {
        return (
            // 2.4 在根组件外面包一层Provider组件,记得一定要把store={store}传递进去,
            // 这个一定要在App.js里包裹,不要想着在这里包裹完直接供外界使用了,否则因为界面的加载有先后顺序,可能会导致意外的问题
            
                
            
        );
    }
}
// store.js

import {createStore, applyMiddleware} from 'redux';
import rootReducer from '../reducer/rootReducer';

import {reactNavigationReduxMiddleware} from '../Navigator/AppContainer';
import thunk from 'redux-thunk';
import {createLogger} from 'redux-logger';

// 中间件
const logger = createLogger();
const middlewares = [
    reactNavigationReduxMiddleware,
    thunk,
    logger,// logger一定要放在最后面
];

// 第1步:
// 创建项目唯一的store,发现需要一个reducer
// 所以接下来第2步,我们去创建一个reducer,回过头来填在这里,详见rootReducer.js文件
const store = createStore(rootReducer, applyMiddleware(...middlewares));

export default store;


更换主题色预期效果

RN:Redux、React-Redux实践之更换主题色_第1张图片


行动前的考虑

前面的文章有说过适合使用Redux和React-Redux的场景,现在我们再列一下。

  • 某个组件的state,需要共享
  • 一个组件需要改变另一个组件的state

  • 某个state,需要在任何地方都能拿到——即它应该是一个全局state
  • 一个组件需要改变某个全局state

现在我们要编写的更换主题色,就是这样的场景:更换主题色界面的某些操作会改变某个全局state——即App的主题色,而App的主题色这个全局state需要在App的各个组件内都能被拿到。因此我们考虑使用Redux和React-Redux来实现该功能。

当然要完整地实现该功能,需要考虑的东西确实比较多,所以一开始如果我们想不到那么多、那么远的话,就从最熟悉、最能上手的地方开始做吧,做一步思考一步,慢慢地也就做完了。现在我们先搭建更换主题色界面,并实现界面交互,这是最容易想到并实现的一步了。


搭建更换主题色界面,并实现界面交互

首先我们创建一个文件来专门存放App所有可更换的主题色。

-----------AllThemeColor.js-----------

const AllThemeColor = {
    Default: '#4caf50',
    Red: '#F44336',
    Pink: '#E91E63',
    Purple: '#9C27B0',
    DeepPurple: '#673AB7',
    Indigo: '#3F51B5',
    Blue: '#2196F3',
    LightBlue: '#03A9F4',
    Cyan: '#00BCD4',
    Teal: '#009688',
    Green: '#4CAF50',
    LightGreen: '#8BC34A',
    Lime: '#CDDC39',
    Yellow: '#FFEB3B',
    Amber: '#FFC107',
    Orange: '#FF9800',
    DeepOrange: '#FF5722',
    Brown: '#795548',
    Grey: '#9E9E9E',
    BlueGrey: '#607D8B',
    Black: '#000000'
};

export default AllThemeColor;

接下来我们搭建更换主题色界面,项目里要求的是更换主题色界面是模态出来的。但是RN有个毛病,如果使用它的navigate方法来做模态,就得在路由配置的一开始就设置mode: 'modal',但是这会导致该navigator下所有的路由都是模态效果,而不是某个单独的路由是模态效果,因此我们得使用Modal组件来实现单个界面的模态效果。

-----------ChangeThemeColorPage.js-----------

import React, {Component} from 'react';
import {Platform, StyleSheet, View, Modal, FlatList} from 'react-native';
import AllThemeColor from '../../../Const/AllThemeColor';
import ProjectNavigationBar from '../../../View/Other/ProjectNavigationBar';
import ChangeThemeColorCell from '../../../View/My/ChangeThemeColor/ChangeThemeColorCell';

// 获取所有主题色的key,组成颜色数组
const allThemeColorArray = Object.keys(AllThemeColor);

export default class ChangeThemeColorPage extends Component {
    // 这里你就感受到了,如果一个组件的state只是它自己使用,不为别人所使用的,就不用放在应用State里,那样反而使得编码复杂
    constructor(props) {
        super(props);

        this.state = {
            visible: false,
        }
    }

    render() {
        return (
             {
                    this.dismiss();
                }}
            >
                
                
                    
                         this._renderItem({item, index})}
                            keyExtractor={(item) => item.key}
                            showsVerticalScrollIndicator={false}
                            numColumns={3}// 显示三列
                        />
                    
                
            
        );
    }

    // 显示该界面
    show() {
        this.setState({
            visible: true,
        })
    }

    // 消失该界面
    dismiss() {
        this.setState({
            visible: false,
        })
    }

    _renderItem({item, index}) {
        return (
             {
                    // 该界面消失
                    this.dismiss();
                }}
            />
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: 'white',
        margin: 10,
        marginTop: Platform.OS === 'ios' ? -44 : -50,
        padding: 3,

        borderRadius: 4,
        shadowColor: 'gray',
        shadowOffset: {width: 2, height: 2},
        shadowOpacity: 0.5,
        shadowRadius: 2,
    },
    flatList: {
        flex: 1,
        backgroundColor: 'white',
    },
});

界面中用到了ChangeThemeColorCell,如下。

-----------ChangeThemeColorCell.js-----------

import React, {Component} from 'react';
import {StyleSheet, Text, TouchableOpacity} from 'react-native';

export default class ChangeThemeColorCell extends Component {
    render() {
        // 这个读取数据要写在这里,写在constructor里是不行的,因为数据改变后重新渲染时只走render方法,写在上面就无法读取到最新的数据
        this.dataDict = this.props.dataDict;
        if (!this.dataDict) return null;

        return (
             this.props.didSelectThemeColor()}
            >
                {this.dataDict.colorKey}
            
        )
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        height: 120,

        justifyContent: 'center',
        alignItems: 'center',

        margin: 3,
    },
    text: {
        color: 'white',
        fontWeight: '400',
    },
});

然后在MyPagerender()方法里使用该界面。

 this.changeThemeColorPage = changeThemeColorPage}
/>

MyPage里点击相应的cell,模态出该界面就可以了。

this.changeThemeColorPage.show();

这样更换主题色界面就搭好了,而且界面也可以顺利的模态出来和消失掉。接下来我们还是先不考虑更换主题色这个效果,而是先考虑该功能里别的需要做的,因为那些比较简单一些。于是自然的就考虑到,点击更换主题色界面的每个cell后,是需要把主题色持久化的,这样下次打开App时才能显示相应的主题色。因此接下来,我们做主题色持久化的部分。


持久化主题色

这里我们写了一个专门针对持久化主题色写入与读取的工具类,Dao的意思是数据访问对象,而更换主题色也仅仅是App更换主题的一种(比如我们还可以更换主题图标),为了方便以后可能有的扩展,我们给这个工具类起名字为ThemeDao.js,而不是ThemeColorDao.js

-----------ThemeDao.js-----------

/**
 * AsyncStorage和NSUserDefaults是一样的,它们的写入和读取操作都是异步的
 */

import {AsyncStorage} from "react-native";
import AllThemeColor from "../../Const/AllThemeColor";

// 持久化时的key
const THEME_COLOR = 'themeColor';

export default class ThemeDao {
    /**
     * 存储主题色
     * @param themeColor 本身就是个颜色字符串,所以可以直接存储
     */
    static saveThemeColor(themeColor) {
        AsyncStorage.setItem(THEME_COLOR, themeColor, error => {
            if (error) {
                console.log('更换主题色,写入数据出错:', error);
            }
        });
    }

    /**
     * 读取主题色:因为读取主题色是异步操作,所以我们得用Promise把读取结果给它传出去
     * @returns {Promise | Promise}
     */
    static getThemeColor() {
        return new Promise((resolve, reject) => {
            AsyncStorage.getItem(THEME_COLOR, (error, value) => {
                if (error) {
                    reject(error);
                } else {
                    if (!value) {// 数据库中还没有存主题色
                        // 那就搞个默认的主题色
                        value = AllThemeColor.Default;

                        // 存起来
                        this.saveThemeColor(value);
                    }

                    // 传出去
                    resolve(value);
                }
            });
        });
    }
}

然后我们返回到ChangeThemeColorPage界面为点击cell更换主题色那里添加一下持久化主题色的操作。

_renderItem({item, index}) {
    return (
         {
                // 持久化该主题色
                ThemeDao.saveThemeColor(AllThemeColor[item]);

                // 该界面消失
                this.dismiss();
            }}
        />
    );
}

这样持久化主题色就做完了,接下来就该考虑大部头的内容了——即点击cell的时候真正的改变一下App的主题色,于是就该Redux和React-Redux部分了。


Redux部分

第一步:确定该功能块该有哪些action

我们考虑,更换主题色只需要在更换主题色成功后,发出一个“更换主题色成功”的action,让App中所有订阅了该功能块State的组件都重新渲染一下就可以了。

所以,我们只需要为这个功能块提供一个action

-----------type.js-----------

/**
 * 多数情况下,action的type会被定义成字符串常量,放在单独的文件里,方便管理
 */

export default  {
    // 更换主题色
    THEME_COLOR_DID_CHANGE: 'THEME_COLOR_DID_CHANGE',
}

编写具体的action生成器和action,即点击cell更换主题色时,dispatch出用这个生成器生成的action就可以了。

-----------ThemeAction.js-----------

import Type from './type';

export function changeThemeColor(themeColor) {
    return {type: Type.THEME_COLOR_DID_CHANGE, themeColor: themeColor};
}

然后在rootAction里导入并导出一下这个action

-----------rootAction.js-----------

/**
 * 项目的根aciton
 *
 * 因为项目中可能有很多action,所以我们统一到这个地方,外界导入使用的时候就方便了
 */


// 导入所有的Action Creator
import {changeThemeColor} from './ThemeColorAction';


// 再导出
export default {
     // 更换主题色
    changeThemeColor,
}

第二步:编写该功能块的reducer,并设计该功能块State的数据结构

考虑到该功能块State只需要向外提供一个主题色,所以我们设置它的数据结构如下,并且先不给它默认值,看后面什么情况再说。

themeState = {
    themeColor: 'red',
};

reducer收到改变主题色的action之后,把该功能块State的主题色属性改变为最新的主题色就可以了,该功能块State一变化,那么但凡订阅了该功能块State的组件就都能收到回调触发重新渲染。

-----------themeReducer.js-----------

const defaultState = {};
const themeColorReducer = (state = defaultState, action) => {
    switch (action.type) {
        case Type.THEME_COLOR_DID_CHANGE:
            return {
                ...state,
                themeColor: action.themeColor,
            };
        default:
            return state;
    }
};

export default themeColorReducer;

在根reducer里合并一下这个功能块reducer

-----------rootReducer.js-----------

/**
 * 项目的根reducer
 *
 * 1、因为创建store的时候只能填写一个reducer,而项目中通常会有很多reducer,
 * 所以我们就专门创建了这个reducer专门用来合并其它所有的子reducer,它不负责应用state具体的变化,只负责合并操作
 * 我们把它称为根reducer了,供创建store时使用
 *
 * 2、请注意:
 * 这个根reducer是个极其重要的东西,因为正是它合并子reducer的过程,决定了应用state里到底存放着什么东西,即什么组件的state要被放在它里面,
 * 什么组件的state想要交由应用的state来统一管理,我们就为该组件编写一个对应的子reducer来描述它state具体变化的过程并返回一个state,然后把这个reducer作为value存放在应用state里(即合并子reducer的时候)
 * 刚创建根reducer时,我们可能不知道将来会有那些组件的state会被放在应用state里来统一管理,所以可以先空着,什么时候需要什么时候往这里添加就可以。
 */
import {combineReducers} from 'redux';
import themeReducer from "./themeReducer.js";

// 创建根reducer,合并所有子reducer(为了方便管理,我们会把子reduce分别写在单独的文件里)
const rootReducer = combineReducers({// 这个对象就是应用的state
    // 应用state里的属性名当然可以随便起,但是为了好理解,我们就起做xxxState,为什么这么起呢?
    // 因为应用state的定义就是,它里面存放着项目中所有想被统一管理state的组件的state,所以我们起做xxxState,将来使用时很方便理解,比如state.counterState,就代表从应用state里取出counterState
    // 而且它的值就是对应的该组件的那个子reducer嘛,而reducer函数又总是返回一个state,这样xxxState = 某个state值,也很好理解
    themeState: themeReducer,
});

export default rootReducer;

这样更换主题色这个功能的Redux部分我们就编写完成了,接下来编写React-Redux部分。


React-Redux部分

因为是在ChangeThemeColorPage里面改变App的主题色,所以它肯定是要用Redux的,那我们就以ChangeThemeColorPage为例,看下该功能的React-Redux部分怎么编写。

第一步:在原先ChangeThemeColorPage(即UI组件)的基础上,用connect包裹UI组件,搞好容器组件和应用State、dispatch(action)的映射关系。

function mapStateToProps(state) {
    return {
        
    }
}

function mapDispatchToProps(dispatch) {
    return {
        changeThemeColor: (themeColor) => dispatch(Action.changeThemeColor(themeColor)),
    }
}

const ChangeThemeColorPageContainer = connect(
    mapStateToProps,
    mapDispatchToProps,
    null,
    // 注意:这里千万要写上这句话,否则用了Redux后的组件是无法使用ref获取该组件的
    {forwardRef: true}
)(ChangeThemeColorPage);

export default ChangeThemeColorPageContainer;

第二步:在需要发出action的地方,通过调用一下propschangeThemeColor方法(本质就是发出一个action,因为调用方法和发出action建立了映射关系)就可以了,这里我们再次修改下点击cell修改主题色的方法。

_renderItem({item, index}) {
    return (
         {
                // 修改主题色
                this.props.changeThemeColor(AllThemeColor[item]);

                // 持久化该主题色
                ThemeDao.saveThemeColor(AllThemeColor[item]);

                // 该界面消失
                this.dismiss();
            }}
        />
    );
}

经过以上操作,ChangeThemeColorPage的React-Redux部分就编写完毕了,很简单吧!这是我们点击cell,其实已经可以完成主题色的更换了,但是因为没有别的组件订阅更换主题色功能块State,所以我们看不出变化,那接下来我们就以ProjectNavigationBar为例,看看App中其它的组件怎么来相应主题色的更换的。

其实和ChangeThemeColorPage一样的,我们也只需要对ProjectNavigationBar做React-Redux的部分。在ProjectNavigationBar(即UI组件)的基础上,用connect包裹UI组件,搞好容器组件和应用State、dispatch(action)的映射关系。

function mapStateToProps(state) {
    return {
        themeState: state.themeState,
    }
}

function mapDispatchToProps(dispatch) {
    return {

    }
}

const ProjectNavigationBarContainer = connect(
    mapStateToProps,
    mapDispatchToProps,
)(ProjectNavigationBar);

export default ProjectNavigationBarContainer;

上面代码中,因为ProjectNavigationBar只需要读取ThemeState的主题色,而不需要修改ThemeState的主题色,所以它只需要实现mapStateToProps函数就可以了,mapDispatchToProps空着就行,这正好和ChangeThemeColorPage相反。

接下来我们只需要在render()方法里,设置导航栏的背景色从映射的ThemeState里读取就可以了,很简单吧。

return (
    // 注意:this.props.style是外界传进来的style,一定要放在styles.container我们内部定义的style后面,否则外面设置的覆盖不了前面的,用户设置的就没效果了
    
        {/* 状态栏 */}
        {statusBar}
        {/* 导航栏 */}
        {navigationBar}
    
);

只要做到这一步,你就可以实现切换主题色,导航栏实时地跟着变化了,可以去试一试。


补充初始化主题色的功能

做完了上面的步骤,更换主题色时没什么的问题了,但是更换后,每次重新打开App,我们还没有去读取持久化的主题色呢,不读导航栏也是一片白。

那我们第一眼的考虑是,是不是可以把持久化的主题色读取出来赋值给themeReducer.js里的defaultState,想法很好,但是没法实现,因为AsyncStorage的读取操作是异步的,根本没法给defaultState赋值,themeReducer.js界面里的代码就执行完了。因此,我们只能为初始化主题色功能新增一个action,文件变化依次如下。

-----------type.js-----------

/**
 * 多数情况下,action的type会被定义成字符串常量,放在单独的文件里,方便管理
 */

export default  {
    // 初始化主题色
    INIT_THEME_COLOR: 'INIT_THEME_COLOR',
    // 更换主题色
    THEME_COLOR_DID_CHANGE: 'THEME_COLOR_DID_CHANGE',
}
-----------ThemeAction.js-----------

import Type from './type';
import ThemeDao from "../expand/dao/ThemeDao";

export function changeThemeColor(themeColor) {
    return {type: Type.THEME_COLOR_DID_CHANGE, themeColor: themeColor};
}

export function initThemeColor() {
    return dispatch => {
        ThemeDao.getThemeColor()
            .then(themeColor => {
                dispatch({type: Type.INIT_THEME_COLOR, themeColor: themeColor});
            })
    }
}
-----------rootAction.js-----------

/**
 * 项目的根aciton
 *
 * 因为项目中可能有很多action,所以我们统一到这个地方,外界导入使用的时候就方便了
 */


// 导入所有的Action Creator
import {initThemeColor, changeThemeColor} from './ThemeAction';


// 再导出
export default {
    // 初始化主题
    initThemeColor,
    // 更换主题色
    changeThemeColor,
}
-----------themeReducer.js-----------

import Type from '../action/type';

const defaultState = {};

const themeReducer = (state = defaultState, action) => {
    switch (action.type) {
        case Type.THEME_COLOR_DID_CHANGE:
            return {
                ...state,
                themeColor: action.themeColor,
            };
        case Type.INIT_THEME_COLOR:
            return {
                ...state,
                themeColor: action.themeColor,
            };
        default:
            return state;
    }
};

export default themeReducer;

这就补充完毕了。接下来,我们考虑应该在哪里发出初始化主题色这个action呢?越早越好吧!在我们项目中,组件的加载顺序为AppContainer.jsSwitchNavigator.jsStackNavigator.jsDynamicBottomNavigator.js,因为前三个都没有做Redux,因为主题色变了它们也不需要跟着变,而DynamicBottomNavigator.js(即TabBar)它正好需要订阅ThemeState,所以我们索性就在它这儿初始化主题色吧,也犯不着专门去前三个组件里初始化了。

class TabBarComponent extends Component {
    render() {
        return (
            
        );
    }

    componentDidMount() {
        // 初始化主题色
        this.props.initThemeColor();
    }
}

function mapStateToProps(state) {
    return {
        themeState: state.themeState,
    }
}
function mapDispatchToProps(dispatch) {
    return {
        initThemeColor: () => dispatch(Action.initThemeColor()),
    }
}

const TabBarComponentContainer = connect(
    mapStateToProps,
    mapDispatchToProps,
)(TabBarComponent);

这样,主题色就初始化好了,TabBar也能响应主题色的变化了。


额外

至此使用Redux和React-Redux实现更换主题色的功能就基本实现了,接下来就是让App中该订阅ThemeState的组件使用React-Redux订阅订阅就行了,这里额外提醒一点App中有那么多组件都需要相应主题色的变化,那我们是不是都让它们订阅呢?这样可以,但是没必要,因为每个组件都订阅的话,项目确实显得有点臃肿。我们推荐的做法是:

  • 导航栏和TabBar自己订阅,因为它们要及时地刷新成最新颜色。
  • App第一级界面自己订阅,因为它们要及时地刷新成最新颜色,而二级、三级界面则通过第一级界面给他们传递过去。
  • 父组件自己订阅,因为它们要及时地刷新成最新颜色,而子组件则通过父组件给他们传递过去。
  • 当然,针对二级、三级界面、子组件,我们也可以不通过传递的方式做,搞个单例就可以了嘛!

此外,项目中的自定义标签、标签排序、自定义语言、语言排序功能也都是通过Redux和React-Redux实现的,原理同上。

你可能感兴趣的:(RN:Redux、React-Redux实践之更换主题色)