这篇文章基于 Google I/O talk。Build reactive mobile apps with Flutter
- widget状态传递
- InheritedWidget & ScopedModel
- RxDart & Widget
- redux and more
widget状态传递
先看一个简单的例子,这是一个加法器。当我们创建一个新的flutter app工程的时候,模版就提供给你了。我们通常的做法是把组件提取出来,比如例子中的FAB。
...
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Flutter Demo Home Page')),
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: () {
setState(() {
_counter += 1;
});
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
...
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _increment() {
setState(() {
_counter += 1;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new Incrementer(_increment),
);
}
}
class Incrementer extends StatefulWidget {
final Function increment;
Incrementer(this.increment);
@override
IncrementerState createState() {
return new IncrementerState();
}
}
class IncrementerState extends State<Incrementer> {
@override
Widget build(BuildContext context) {
return new FloatingActionButton(
onPressed: widget.increment,
tooltip: 'Increment',
child: new Icon(Icons.add),
);
}
}
复制代码
虽然这种传递是可以使用的, 但是有一个问题,当我们构建大型应用的时候,整个view tree都需要传递状态,这显然是不合理的。
InheritedWidget & ScopedModel
Flutter给我们提供了一个组件,InheritedWidget
。他可以解决状态传递的问题。
但是有一个问题,当我们只使用InheritedWidget的时候,所有的状态是final的。不可改变,这在大多数复杂场景下并不是适用。因此官方提供了另一个组件来解决数据数据的问题ScopedModel
。
ScopedModel有以下几个特性:
- 外部扩展包
- 基于InheritedWidget
- 使用,升级&改变
下面是一个购物车的例子
其中包括几个功能:
- 点击产品添加到购物车中
- 购物车按钮实时更新篮子中的数量
- 购物车页面可以查看已添加的列表
我们需要准备一个CartModel
class CartModel extends Model {
final _cart = Cart();
List<CartItem> get items => _cart.items;
int get itemCount => _cart.itemCount;
void add(Product product) {
_cart.add(product);
notifyListeners(); //notify all data update
}
}
复制代码
在之前页面的基础上,我们需要使用ScopedModel
包装在view tree最上层,使用ScopedModelDescendant<CartModel>
替换需要使用Model的Widget。
...
class CatalogHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scoped Model'),
actions: <Widget>[
// SCOPED MODEL: Wraps the cart button in a ScopdeModelDescendent to access
// the nr of items in the cart
ScopedModelDescendant<CartModel>(
builder: (context, child, model) => CartButton(
itemCount: model.itemCount,
onPressed: ...,
),
)
],
),
body: ProductGrid(),
);
}
}
class ProductGrid extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
children: catalog.products.map((product) {
// SCOPED MODEL: Wraps items in the grid in a ScopedModelDecendent to access
// the add() function in the cart model
return ScopedModelDescendant<CartModel>(
rebuildOnChange: false, //hart
builder: (context, child, model) => ProductSquare(
product: product,
onTap: () => model.add(product),
),
);
}).toList(),
);
}
}
复制代码
需要注意的是:rebuildOnChange: false
这个属性,默认是true,即Model改变时会rebuild widget。如果我们不想每次都rebuild,那需要设置为false。
ScopedModel虽然解决了数据的传递的问题,但是在我们使用时需要关心哪些widget需要rebuild,那些不需要,这也是一个繁琐的工作。
RxDart & Widget
写过Java的小伙伴一定非常熟悉了,ReactiveX就是为了解决类似的问题。还有一个好消息是dart core lib支持stream。Stream&ReactiveX = RxDart。
下面是官方推荐的架构模式(architectural pattern)。
business logic最为结合input&output的桥梁,在整个模式中至关重要。
- Sink 包装输入
- Stream 对应输出
class CartAddition {
final Product product;
final int count;
CartAddition(this.product, [this.count = 1]);
}
class CartBloc {
final Cart _cart = Cart();
final BehaviorSubject<List<CartItem>> _items =
BehaviorSubject<List<CartItem>>(seedValue: []);
final BehaviorSubject<int> _itemCount =
BehaviorSubject<int>(seedValue: 0);
final StreamController<CartAddition> _cartAdditionController =
StreamController<CartAddition>();
Sink<CartAddition> get cartAddition => _cartAdditionController.sink;
Stream<int> get itemCount => _itemCount.stream;
Stream<List<CartItem>> get items => _items.stream;
CartBloc() {
_cartAdditionController.stream.listen((addition) {
int currentCount = _cart.itemCount;
_cart.add(addition.product, addition.count);
_items.add(_cart.items);
int updatedCount = _cart.itemCount;
if (updatedCount != currentCount) {
_itemCount.add(updatedCount);
}
});
}
}
复制代码
另一个例子是,当我们要做本地化的时候,我们可以很方便的根据Sink提供的locale对totalCost进行修改。
class CartBloc {
Sink<Product> addition;
Sink<Locale> locale;
Stream<int> iteamCount; //if locale: China => "¥ 600.00"; locale: US => "$100.00"
Stream<String> totalCost;
}
复制代码
这个模式是官方大力推荐的,尤其是复杂APP,推荐使用此种模式。 详见:Github: bloc_complex
reudx and more
非官方提供了Redux的实现,简单讲就是dart的实现版本详见:Github。对应sample。
其他方式:详见来自官方小哥哥的repo: Github。
其他资源: