Flutter主題切換之flutter redux
本文詳細講述怎樣在flutter中集成和使用redux,關于redux的概念揪罕、原理和實現(xiàn)淹朋,讀者可自行百度歧斟,本文不做累述。
個人博客
flutter redux
flutter redux組成
redux主要由Store
梗肝、Action
、Reducer
三部分組成
-
Store
用于存儲和管理State
-
Action
用于用戶觸發(fā)的一種行為 -
Reducer
用于根據(jù)Action產(chǎn)生新的State
flutter redux流程
-
Widget
通過StoreConnector
綁定Store
中的State
數(shù)據(jù) -
Widget
通過Action
觸發(fā)一種新的行為 -
Reducer
根據(jù)收到的Action
更新State
- 更新
Store
中的State
綁定的Widget
根據(jù)以上流程,我們實現(xiàn)項目中的主題切換功能齐唆。
項目集成
flutter redux庫
集成flutter redux
修改項目根目錄下pubspec.yaml
,并添加依賴
flutter_redux: ^0.5.3
初始化Store
首先看下Store
的構造函數(shù)冻河,如下面代碼所示
Store(
this.reducer, {
State initialState,
List<Middleware<State>> middleware = const [],
bool syncStream: false,
bool distinct: false,
})
: _changeController = new StreamController.broadcast(sync: syncStream) {
_state = initialState;
_dispatchers = _createDispatchers(
middleware,
_createReduceAndNotify(distinct),
);
}
根據(jù)上面的構造函數(shù)箍邮,我們首先需要創(chuàng)建State
茉帅,并且還需要完成State
初始化;然后需要創(chuàng)建Reducer
锭弊;最后需要創(chuàng)建Middleware
(暫不是本文需要講解的內(nèi)容)堪澎;
創(chuàng)建State
創(chuàng)建一個State
對象AppState
,用于儲存需要共享的主題數(shù)據(jù),并且完成AppState
初始化工作味滞,如下面代碼所示
class AppState {
ThemeData themeData;
AppState({this.themeData});
factory AppState.initial() => AppState(themeData: AppTheme.theme);
}
AppTheme
類中定義了一個默認主題theme
樱蛤,如下面代碼所示
class AppTheme {
static final ThemeData _themeData = new ThemeData.light();
static get theme {
return _themeData.copyWith(
primaryColor: Colors.black,
);
}
}
到此,完成了State的相關操作剑鞍。
創(chuàng)建Reducer
創(chuàng)建一個Reducer
方法appReducer
,為AppState
類里的每一個參數(shù)創(chuàng)建一個Reducer
昨凡,如下面代碼所示
AppState appReducer(AppState state, action) {
return AppState(
themeData: themeReducer(state.themeData, action),
);
}
而themeReducer
將ThemeData和所有跟切換主題的行為綁定在一起,如下面代碼所示
final themeReducer = combineReducers<ThemeData>([
TypedReducer<ThemeData, RefreshThemeDataAction>(_refresh),
]);
ThemeData _refresh(ThemeData themeData, action) {
themeData = action.themeData;
return themeData;
}
通過flutter redux
的combineReducers
與TypedReducer
將RefreshThemeDataAction
和_refresh
綁定在一起,當用戶每次發(fā)出RefreshThemeDataAction
時蚁署,都會觸發(fā)_refresh
,用來更新themeData
便脊。
創(chuàng)建Action
創(chuàng)建一個Action
對象RefreshThemeDataAction
,如下面代碼所示
class RefreshThemeDataAction{
final ThemeData themeData;
RefreshThemeDataAction(this.themeData);
}
RefreshThemeDataAction
的參數(shù)themeData是用來接收新切換的主題。
代碼集成
創(chuàng)建Store
所有的準備工作都已準備光戈,下面創(chuàng)建Store
哪痰,如下面代碼所示
final store = new Store<AppState>(
appReducer,
initialState: AppState.initial(),
);
然后用StoreProvider
加載store,MaterialApp
通過StoreConnector
與Store
保持連接。到此我們已經(jīng)完成了flutter redux
的初始化工作田度,如下面代碼所示
void main() {
final store = new Store<AppState>(
appReducer,
initialState: AppState.initial(),
);
runApp(OpenGitApp(store));
}
class OpenGitApp extends StatelessWidget {
final Store<AppState> store;
OpenGitApp(this.store);
@override
Widget build(BuildContext context) {
return new StoreProvider<AppState>(
store: store,
child: StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (context, vm) {
return new MaterialApp(
theme: vm.themeData,
routes: AppRoutes.getRoutes(),
);
},
),
);
}
}
StoreConnector
通過converter
在_ViewModel
中轉(zhuǎn)化store.state
的數(shù)據(jù)妒御,最后通過builder
返回實際需要更新主題的控件,這樣就完成了數(shù)據(jù)和控件的綁定镇饺。_ViewModel
的代碼如下面所示
class _ViewModel {
final ThemeData themeData;
_ViewModel({this.themeData});
static _ViewModel fromStore(Store<AppState> store) {
return _ViewModel(
themeData: store.state.themeData,
);
}
}
用戶行為
最后乎莉,只需要添加切換主題部分的代碼即可,這部分代碼是從官方gallery
demo里的Style/Colors copy出來的奸笤,不做過多分析惋啃,如下面代碼所示
const double kColorItemHeight = 48.0;
class Palette {
Palette({this.name, this.primary, this.accent, this.threshold = 900});
final String name;
final MaterialColor primary;
final MaterialAccentColor accent;
final int
threshold; // titles for indices > threshold are white, otherwise black
bool get isValid => name != null && primary != null && threshold != null;
}
final List<Palette> allPalettes = <Palette>[
new Palette(
name: 'RED',
primary: Colors.red,
accent: Colors.redAccent,
threshold: 300),
new Palette(
name: 'PINK',
primary: Colors.pink,
accent: Colors.pinkAccent,
threshold: 200),
new Palette(
name: 'PURPLE',
primary: Colors.purple,
accent: Colors.purpleAccent,
threshold: 200),
new Palette(
name: 'DEEP PURPLE',
primary: Colors.deepPurple,
accent: Colors.deepPurpleAccent,
threshold: 200),
new Palette(
name: 'INDIGO',
primary: Colors.indigo,
accent: Colors.indigoAccent,
threshold: 200),
new Palette(
name: 'BLUE',
primary: Colors.blue,
accent: Colors.blueAccent,
threshold: 400),
new Palette(
name: 'LIGHT BLUE',
primary: Colors.lightBlue,
accent: Colors.lightBlueAccent,
threshold: 500),
new Palette(
name: 'CYAN',
primary: Colors.cyan,
accent: Colors.cyanAccent,
threshold: 600),
new Palette(
name: 'TEAL',
primary: Colors.teal,
accent: Colors.tealAccent,
threshold: 400),
new Palette(
name: 'GREEN',
primary: Colors.green,
accent: Colors.greenAccent,
threshold: 500),
new Palette(
name: 'LIGHT GREEN',
primary: Colors.lightGreen,
accent: Colors.lightGreenAccent,
threshold: 600),
new Palette(
name: 'LIME',
primary: Colors.lime,
accent: Colors.limeAccent,
threshold: 800),
new Palette(
name: 'YELLOW', primary: Colors.yellow, accent: Colors.yellowAccent),
new Palette(name: 'AMBER', primary: Colors.amber, accent: Colors.amberAccent),
new Palette(
name: 'ORANGE',
primary: Colors.orange,
accent: Colors.orangeAccent,
threshold: 700),
new Palette(
name: 'DEEP ORANGE',
primary: Colors.deepOrange,
accent: Colors.deepOrangeAccent,
threshold: 400),
new Palette(name: 'BROWN', primary: Colors.brown, threshold: 200),
new Palette(name: 'GREY', primary: Colors.grey, threshold: 500),
new Palette(name: 'BLUE GREY', primary: Colors.blueGrey, threshold: 500),
];
class ColorItem extends StatelessWidget {
const ColorItem(
{Key key,
@required this.index,
@required this.color,
this.prefix = '',
this.onChangeTheme})
: assert(index != null),
assert(color != null),
assert(prefix != null),
super(key: key);
final int index;
final Color color;
final String prefix;
final Function(Color) onChangeTheme;
String colorString() =>
"#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
@override
Widget build(BuildContext context) {
return new Semantics(
container: true,
child: new Container(
height: kColorItemHeight,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
color: color,
child: new SafeArea(
top: false,
bottom: false,
child: FlatButton(
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new Text('$prefix$index'),
new Text(colorString()),
],
),
onPressed: () {
onChangeTheme(color);
},
),
),
),
);
}
}
class PaletteTabView extends StatelessWidget {
static const List<int> primaryKeys = const <int>[
50,
100,
200,
300,
400,
500,
600,
700,
800,
900
];
static const List<int> accentKeys = const <int>[100, 200, 400, 700];
PaletteTabView({Key key, @required this.colors, this.onChangeTheme})
: assert(colors != null && colors.isValid),
super(key: key);
final Palette colors;
final Function(Color) onChangeTheme;
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final TextStyle whiteTextStyle =
textTheme.body1.copyWith(color: Colors.white);
final TextStyle blackTextStyle =
textTheme.body1.copyWith(color: Colors.black);
final List<Widget> colorItems = primaryKeys.map((int index) {
return new DefaultTextStyle(
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
child: new ColorItem(
index: index,
color: colors.primary[index],
onChangeTheme: onChangeTheme),
);
}).toList();
if (colors.accent != null) {
colorItems.addAll(accentKeys.map((int index) {
return new DefaultTextStyle(
style: index > colors.threshold ? whiteTextStyle : blackTextStyle,
child: new ColorItem(
index: index,
color: colors.accent[index],
prefix: 'A',
onChangeTheme: onChangeTheme),
);
}).toList());
}
return new ListView(
itemExtent: kColorItemHeight,
children: colorItems,
);
}
}
class ThemeSelectPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (context, vm) {
return new DefaultTabController(
length: allPalettes.length,
child: new Scaffold(
appBar: new AppBar(
elevation: 0.0,
title: const Text("主題色"),
bottom: new TabBar(
isScrollable: true,
tabs: allPalettes
.map((Palette swatch) => new Tab(text: swatch.name))
.toList(),
),
),
body: new TabBarView(
children: allPalettes.map((Palette colors) {
return new PaletteTabView(
colors: colors,
onChangeTheme: vm.onChangeTheme,
);
}).toList(),
),
),
);
});
}
}
class _ViewModel {
final Function(Color) onChangeTheme;
_ViewModel({this.onChangeTheme});
static _ViewModel fromStore(Store<AppState> store) {
return _ViewModel(
onChangeTheme: (color) {
SharedPrfUtils.saveInt(SharedPrfKey.SP_KEY_THEME_COLOR, color.value);
store.dispatch(RefreshThemeDataAction(AppTheme.changeTheme(color)));
},
);
}
}
運行效果
執(zhí)行代碼,效果如下