Flutter Redux使用教程
网上的一些教程就是现从官方文档的最基础例子搬运过来的,无法给偏向基础的新手讲清楚Flutter_Redux,也无法让新人设置自己的项目结构。博主结合官网例子,和一些大神的项目,总结了一些使用经验,希望能帮助到你。
依赖配置
在项目的根路径,找到pubspec.yaml
文件。这个文件用来定义我们Flutter项目的依赖,下图是还没有配置redux
的原始文件。
dependencies
下配置生产环境(也就是项目上线的环境)所需的依赖。dev_dependencies
配置我们开发环境所需的依赖。显然,我们所要使用的redux
必须要装在生产环境下。
将pubspec.yaml
文件的dependencies
修改成如下:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.2
#下面两个就是我们所需要配的依赖
redux: ^3.0.0
flutter_redux: ^0.5.3
修改完成后,我们需要安装这些依赖。
在项目根目录执行flutter pub get
命令安装。
输出下面这段话,则安装成功。
Running "flutter pub get" in flutter_redux... 0.6s
这里说一个题外话,一般我们使用flutter是从国外的pub仓库下载依赖,这会导致有的是时候下载巨慢。但是根据官方说的,我们将下载源替换为
https://pub.flutter-io.cn
的时候也不见得多快,这就会导致我们在使用flutter upgrade
更新flutter,或者使用flutter channel stable
更换分支的时候速度贼慢。所以我们把这些地址通通替换为清华大学的镜像源。
Flutter国内镜像
windows 使用方式
右键我的电脑->属性->高级->环境变量
。在系统变量中新建两个项目。
变量 | 值 |
---|---|
FLUTTER_STORAGE_BASE_URL | https://mirrors.tuna.tsinghua.edu.cn/flutter |
PUB_HOSTED_URL | https://mirrors.tuna.tsinghua.edu.cn/dart-pub/ |
保存。重新开启cmd,即可飞速体验~
linux使用方式
下面两句命令分别执行一下,我这里写到了.bashrc
文件里面,如果你用的是zsh
的话,写到相应的~/.zshrc
文件中即可。
echo 'export FLUTTER_STORAGE_BASE_URL="https://mirrors.tuna.tsinghua.edu.cn/flutter"' >> ~/.bashrc
echo 'export PUB_HOSTED_URL="https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"' >> ~/.bashrc
Mac使用方式
我没有Mac所以这里不写了
思路都是一样的修改Mac的环境变量即可。
开始
惯例啰嗦一下。为了大家能更快上手,所以我只会完成两种功能实现:主题的更换、ListTitl的添加。虽然例子比较少,但是也都属于比较典型的问题。
Redux的一些概念
如果从来没接触过Redux的flutter初学者可能一上来就懵逼了,什么store
、reducer
、action
、State
、model
这些概念直接就把我们干懵逼了,所以这里我们最好了解一下这些概念,努力看,如果看不懂也只是我讲的不到位,一会看到代码我相信大家都能豁然开朗~
store
:全局状态管理者,一个程序应该只有一个。
state
:状态,状态是复杂的数据结构。比如下面这种。
[
{
"id":0,
"content":"1233213",
"title":"haha"
},
{
"id":1,
"content":"1233213",
"title":"xixi"
}
]
action
:从应用层发出的,通向stroe
的唯一来源。一般承载着要更新的数据。
model
:实际页面数据的抽象。
reducer
:指定了应用状态的变化如何响应action
。我们在这里定义更新操作。
我们这里可以不求甚解。往下看代码我们自然明白这些概念的实际意义。
创建ThemeModel
在项目的lib/
文件夹下面创建文件夹model
。这里面存放我们以后项目中所有的model
。在model
中创建theme_model.dart
文件,并写以下代码。
import 'package:flutter/material.dart';
class ThemeModel{
ThemeData themeData;
ThemeModel({
this.themeData,
});
}
创建全局State文件
在项目lib/
文件夹下,新建app_state.dart
文件,并写以下代码。
import 'package:flutter/material.dart';
import 'model/theme_model.dart';
class AppState {
ThemeModel themeState; //保存我们的主题状态
AppState({this.themeState});
/*
* 命名的构造方法
* 这里用来初始化
*/
AppState.initialState() {
themeState = ThemeModel(themeData: ThemeData.dark());
}
}
创建Action
在lib/
文件夹下新建actions
文件夹,进入actions
文件夹,并新建theme_action.dart
文件,输入以下代码。
import 'package:flutter/material.dart';
import '../model/theme_model.dart';
class SetThemeDataAction {
ThemeData themeData;
SetThemeDataAction({this.themeData}) : super();
/**
* 设置themedata主题
*/
static ThemeModel setTheme(ThemeModel theme, SetThemeDataAction action) {
theme?.themeData = action?.themeData;
return theme;
}
}
设置Reducer
这一步可能不好理解,我来解释一下,Action是由组件发出,并有store
接受的信号,但是我们必须规定store
接收到Action后应该干什么,这一层操作就是由Reducer完成的。
首先,修改我们的theme_action.dart
文件。
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import '../model/theme_model.dart';
class SetThemeDataAction {
ThemeData themeData;
SetThemeDataAction({this.themeData}) : super();
/**
* 设置themedata主题
*/
static ThemeModel setTheme(ThemeModel theme, SetThemeDataAction action) {
theme?.themeData = action?.themeData;
return theme;
}
}
//下面的代码加上
/*
* 绑定Action与动作
*/
final ThemeReducer = combineReducers<ThemeModel>([
TypedReducer<ThemeModel, SetThemeDataAction>(SetThemeDataAction.setTheme),
]);
然后修改app_state.dart
文件
import 'package:flutter/material.dart';
import 'actions/theme_action.dart';
import 'model/theme_model.dart';
class AppState {
ThemeModel themeState; //保存我们的主题状态
AppState({this.themeState});
/*
* 命名的构造方法
* 这里用来初始化
*/
AppState.initialState() {
themeState = ThemeModel(themeData: ThemeData.dark());
}
}
/**
* 定义Reducer
*/
AppState appReducer(AppState state, action) {
return AppState(themeState: ThemeReducer(state.themeState, action));
}
我们分了两个文件来定义Reducer
,这样做的好处是高内聚,低耦合
,ThemeReducer
全部交由theme_action.dart
文件管理,我们再增加Theme
的Action的时候,只需要去修改theme_action.dart
文件,并定义相应的Reducer即可。
ok,到这里我们的框架,项目结构已经是完成了。现在开始在flutter组件中使用Redux。
开始在组件中使用
redux在flutter中使用,一般使用下面这几个组件
Store
:创建store
必不可少的组件。
StoreProvider
:用来存放store
,供子孙元素获取store
来的。
StoreBuilder
:从StoreProvider
获取store
并将其传递给Widget builder
函数的后代Widget。一般用在StoreProvider
的正下方。
StoreConnector
:从最近的StoreProvider
祖先元素获取store
,并将store
转换为ViewModel
,来使用。这点我一会讲。
所以看了上面的介绍,我们的思路应该是这样的,直接将flutter的根元素替换为StoreProvider
,当我们需要发出Action的时候或者需要使用store
的时候,我们对这个组件进行StoreConnector
包裹。
拷贝下面的代码到你的main.dart
文件中,你会发现,已经可以获取到store
中的状态了。因为我这里启动的是flutter自带的例子,里面有很多初始代码,我也不删了,我们redux的核心代码全在MyApp
class的build
方法中了。
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:my_flutter_redux/app_state.dart';
import 'package:redux/redux.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final store =
Store<AppState>(appReducer, initialState: AppState.initialState());//创建 store
return StoreProvider(//使用StoreProvider 包裹根元素,使其提供store
store: store,//我们的store
child: StoreBuilder<AppState>(//为了能直接在child使用store,我们这里要继续包裹一层StoreBuilder
builder: (context, store) {
return MaterialApp(
title: 'Flutter Demo',
theme: store.state.themeState.themeData,//这里使用我们的stroe
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
},
),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
效果:
现在启动flutter程序,我们的例子已经换成dark
主题了,现在我们设计一个功能,在点击右下角的按钮时,不断切换主题。
切换主题
先说思路,我们肯定要在按钮的点击事件中获取store
,并且发出Action。好的,思路有了,开始干。
在组件中想要获取store
的话,我们要用StoreConnector
包裹需要获取stroe
的组件。我们这里要实现点击按钮切换主题,那肯定需要包裹FloatingActionButton
啦。
floatingActionButton: StoreConnector<AppState, _ViewModel>(
converter: (store) => _ViewModel.create(store),
builder: (context, viewModel) {
return FloatingActionButton(
onPressed: () {
_incrementCounter();
viewModel.onSetThemeData(viewModel.themeData == ThemeData.dark()
? ThemeData.light()
: ThemeData.dark());
},
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
)
//----其他代码
//在最后 写上
class _ViewModel {
ThemeData themeData;
Function(ThemeData) onSetThemeData;
_ViewModel({
this.themeData,
this.onSetThemeData,
});
factory _ViewModel.create(Store<AppState> store) {
_onSetThemeData(ThemeData themeData) {
store.dispatch(SetThemeDataAction(themeData: themeData));
}
return _ViewModel(
themeData: store.state.themeState.themeData,
onSetThemeData: _onSetThemeData,
);
}
}
拒绝弹射起步,我们一句一句讲解。
StoreConnector<AppState, _ViewModel>
这句话看到StoreConnector接收两个泛型,<AppState, _ViewModel>
第一个参数是state
的类型,一般我们这种使用全局State
那么填我们定义的AppState
就行。
第二个参数,_ViewModel
是builder
属性接收的类型,一般由我们自己定义。把它理解为中间视图层。
converter: (store) => _ViewModel.create(store),
这converter
需要返回StoreConnector<AppState, _ViewModel>
第二个泛型的类型,这里我们返回_ViewModel
的一个实例。
builder: (context, viewModel)
第一个返回上下文,这里一般就写context
就可以,关键是第二个viewModel
这里接受converter
返回的值,也就是一个_ViewModel
实例。
并且builder
需要返回Widget
,这个Widget
可以使用viewModel
,这个实参。
最后,在点击事件中加入我们的事件就成了。
viewModel.onSetThemeData(viewModel.themeData == ThemeData.dark()
? ThemeData.light()
: ThemeData.dark());
第二个例子:动态添加Titl
创建Model
每次引入新的、复杂的要管理的状态的时候,最好是创建一个Model类,这样与后端获取数据后,Json转对象,查看数据类型,都会非常方便。
当然,你如果只是接受一个bool
值,或者num
类型的值得话,就没有必要创建Model。
我们在lib/model
文件夹中创建article_model.dart
文件,用来定义我们的ArticleModel
类。
class ArticleModel {
String author;
num id;
String title;
ArticleModel({this.id, this.title, this.author});
}
这个类的数据结构,大家可以根据自己的需要随意改,而且一般在这个类,我们会放Json序列化方法。这里我只是简单规定了一下数据结构,id
用来表示从后端获取的主键,一般用这个跳转路由,获取数据。anthor
用来标识作者。title
表示文章名字。
不要问我为什么没有content
字段,编不下去。
创建Action与Reducer
定义完Model后我们要来定义这个状态的Action
和Reducer
。一般我是习惯这两个东西放到一个文件里面,比较方便管理。
在lib/actions
中创建article_action.dart
文件。并写以下代码。
import 'package:my_flutter_redux/model/article_model.dart';
import 'package:redux/redux.dart';
class AddArticleItemAction {
ArticleModel item;
AddArticleItemAction({this.item});
static List<ArticleModel> addArticleItem(
List<ArticleModel> list, AddArticleItemAction action) {
list?.add(action?.item);
return list;
}
}
class RemoveArticleItemAction {
ArticleModel item;
RemoveArticleItemAction({this.item});
static List<ArticleModel> removeArticleItem(
List<ArticleModel> list, RemoveArticleItemAction action) {
list.remove(action.item);
return list;
}
}
/*
* 绑定Action与动作
*/
final ArticlesReducer = combineReducers<List<ArticleModel>>([
TypedReducer<List<ArticleModel>, AddArticleItemAction>(
AddArticleItemAction.addArticleItem),
TypedReducer<List<ArticleModel>, RemoveArticleItemAction>(
RemoveArticleItemAction.removeArticleItem),
]);
我们定义了两个Action
类,一个是AddArticleItemAction
,它对应addArticleItem
方法,这个Action
主要是向我们的List<ArticleModel>
状态中添加成员。而RemoveArticleItemAction
中的removeArticleItem
则是删除成员。相信大家都可以看懂。
定义完Action
后,我们一定不要忘记用ArticlesReducer
,将Action
与所要做的动作联系到一起,不然的话,从组件发出Action
但却没有动作回应。
在全局State中设置状态
上面这些步骤都整完后,还差最后关键的一步。没错,就是我们从始至终都没有去全局State
中定义我们的文章状态
。
找到lib/app_state.dart
文件。
import 'package:flutter/material.dart';
import 'package:my_flutter_redux/actions/article_action.dart';
import 'package:my_flutter_redux/model/article_model.dart';
import 'actions/theme_action.dart';
import 'model/theme_model.dart';
class AppState {
ThemeModel themeState; //保存我们的主题状态
List<ArticleModel> articleListState; //保存文章list状态
AppState({this.themeState, this.articleListState});
/*
* 命名的构造方法
* 这里用来初始化
*/
AppState.initialState() {
themeState = ThemeModel(themeData: ThemeData.dark());
articleListState = List<ArticleModel>();
}
}
/**
* 定义Reducer
*/
AppState appReducer(AppState state, action) {
return AppState(
themeState: ThemeReducer(state.themeState, action),
articleListState: ArticlesReducer(state.articleListState, action),
);
}
大家来找茬。
创建页面
ok,到这里已经是万事俱备只欠东风了。我们只需要使用State
就行了。我这里是在主页添加了一个跳转页面按钮,通过设置好的路由跳转到新页面。
所以我们改一下main.dart
文件吧。
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:my_flutter_redux/app_state.dart';
import 'package:my_flutter_redux/model/theme_model.dart';
import 'package:redux/redux.dart';
import 'actions/theme_action.dart';
import 'pages/articles_page.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
final store =
Store<AppState>(appReducer, initialState: AppState.initialState());
return StoreProvider(
store: store,
child: StoreBuilder<AppState>(
builder: (context, store) {
return MaterialApp(
title: 'Flutter Demo',
theme: store.state.themeState.themeData,
routes:<String, WidgetBuilder>{//++++++++++++++++++++++新增了路由
"/article":(BuildContext context) => ArticlePage(),
} ,
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
},
),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
RaisedButton(child: Text("切换页面"),onPressed: (){//++++++++++++新增了跳转页面按钮
Navigator.of(context).pushNamed('/article');
},)
],
),
),
floatingActionButton: StoreConnector<AppState, _ViewModel>(
converter: (store) => _ViewModel.create(store),
builder: (context, viewModel) {
return FloatingActionButton(
onPressed: () {
_incrementCounter();
viewModel.onSetThemeData(viewModel.themeData == ThemeData.dark()
? ThemeData.light()
: ThemeData.dark());
},
tooltip: 'Increment',
child: Icon(Icons.add),
);
},
));
}
}
class _ViewModel {
ThemeData themeData;
Function(ThemeData) onSetThemeData;
_ViewModel({
this.themeData,
this.onSetThemeData,
});
factory _ViewModel.create(Store<AppState> store) {
_onSetThemeData(ThemeData themeData) {
store.dispatch(SetThemeDataAction(themeData: themeData));
}
return _ViewModel(
themeData: store.state.themeState.themeData,
onSetThemeData: _onSetThemeData,
);
}
}
然后,我们创建新的页面,在lib/pages
中新建articles_page.dart
文件。并写以下代码。
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:my_flutter_redux/actions/article_action.dart';
import 'package:my_flutter_redux/app_state.dart';
import 'package:my_flutter_redux/model/article_model.dart';
import 'package:redux/redux.dart';
class ArticlePage extends StatefulWidget {
State<StatefulWidget> createState() => _ArticlePageState();
}
class _ArticlePageState extends State<ArticlePage> {
@override
Widget build(BuildContext context) {
List<num> list = List(5);
return Scaffold(
appBar: AppBar(
title: Text("添加titl"),
actions: <Widget>[
StoreConnector<AppState, _ViewModel>(
converter: (store) => _ViewModel.create(store),
builder: (context, viewModel) {
return IconButton(
icon: Icon(
Icons.add,
size: 30.0,
),
onPressed: () {
viewModel.onAddItem(
ArticleModel(id: viewModel.articleList.length, title: "哈哈哈", author: "脑瘫码农"));
},
);
},
)
],
),
body: StoreConnector<AppState, _ViewModel>(
converter: (store) => _ViewModel.create(store),
builder: (context, viewModel) {
return ListView.builder(
itemBuilder: (contentx, index) {
return ListTile(
subtitle: Text((viewModel.articleList[index].id).toString()),
title: Text(viewModel.articleList[index].title),
leading: Text(viewModel.articleList[index].author),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: (){
viewModel.onRemoveItem(viewModel.articleList[index]);
},
));
},
itemCount: viewModel.articleList.length,
);
},
),
);
}
}
class _ViewModel {
List<ArticleModel> articleList;
Function(ArticleModel) onAddItem;
Function(ArticleModel) onRemoveItem;
_ViewModel({this.articleList, this.onAddItem, this.onRemoveItem});
factory _ViewModel.create(Store<AppState> store) {
_onAddItem(ArticleModel item) {
store.dispatch(AddArticleItemAction(item: item));
}
_onRemoveItem(ArticleModel item) {
store.dispatch(RemoveArticleItemAction(item: item));
}
return _ViewModel(
articleList: store.state.articleListState,
onAddItem: _onAddItem,
onRemoveItem: _onRemoveItem);
}
}
结尾
我虽然是刚开始学习Flutter的菜鸟,但是也走不少弯路,我希望把我的经验分享给大家。但是人的力量是有限的,文章难免不出现错误。如果看到这篇文章有错误,请务必给我评论,我会第一时间查证修改,谢谢大家了。
声明
脑瘫码农 纯属自学 如有错误 望请指正 共同学习 不胜感激