Flutter的设计灵感部分来自于React,主要是数据与视图分离,由数据来驱动视图的渲染。而对于我们在实际工程中的应用,就目前状态来讲,只是用来做UI,并没有用Flutter来做多少业务逻辑,涉及到的逻辑也不过是界面之间的数据、状态传递等。但并不排除将来会将重心稍微往Flutter侧偏移。
目前使用StatefulWidget完全可以适应目前的需求。但是需要考虑到后续扩展,需要找一种能够解决状态同步问题的方案。在了解了几种方案后确定使用BLoC。
https://juejin.im/post/5bac54c45188255c681589d3
https://www.jianshu.com/p/e7e1bced6890
https://www.jianshu.com/p/7573dee97dbb
Stream
Stream看起来和rx家族东西东西很像,我们可以通过StreamController的sink传输一些数据,然后监听StreamSubscription流来感知数据,甚至可以通过StreamTransformer对数据流进行操作。当然也可以通过Flutter提供的StreamBuilder来构建Widget,
1 2 3 4 5 6 7 8 9 10 11
| Center( child: StreamBuilder<int>( stream: bloc.stream, initialData: bloc.value, builder: (BuildContext context, AsyncSnapshot<int> snapshot) { return Text( 'You hit me: ${snapshot.data} times', style: Theme.of(context).textTheme.display1, ); }), )
|
这里的snapshot则是通过sink传输过来的数据,然后显示在Text控件中。
一个简单的Stream演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import 'dart:async';
void main() { StreamController streamController = StreamController();
StreamSubscription subscription = streamController.stream.listen((event){ print("$event"); }); subscription.onData((data) { print("-->$data"); }); subscription.onDone(() => {print("on done")});
streamController.sink.add("abc"); streamController.sink.add("123"); streamController.sink.add("def");
streamController.close(); }
|
我们会得到这样一个输出:
1 2 3 4
| -->abc -->123 -->def on done
|
当然如果我们没有再次重写subscription.onData
方法,则会执行print("$event");
方法。
Stream有两种类型:单订阅Stream和广播Stream。单订阅Stream只允许在该Stream的整个生命周期内使用单个监听器,即使第一个subscription被取消了,你也没法在这个流上监听到第二次事件;而广播Stream允许任意个数的subscription,你可以随时随地给它添加subscription,只要新的监听开始工作流,它就能收到新的事件。
简化版的demo
官方的计数器demo的简化版。
1 2 3 4 5 6 7 8 9
| class CountBLoC { int _count = 0; final _controller = StreamController<int>(); Stream<int> get stream => _controller.stream; int get value => _count; increment() { _controller.sink.add(++_count); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class CounterWidget extends StatelessWidget { @override Widget build(BuildContext context) { final bloc = CountBLoC(); return Scaffold( appBar: AppBar( title: Text('Top Page'), ), body: Center( child: StreamBuilder<int>( stream: bloc.stream, initialData: bloc.value, builder: (BuildContext context, AsyncSnapshot<int> snapshot) { return Text( 'You hit me: ${snapshot.data} times', style: Theme.of(context).textTheme.display1, ); }), ), floatingActionButton: FloatingActionButton( onPressed: () => bloc.increment(), child: Icon(Icons.add), ), ); } }
|
- 首先我们定义了一个BLoC类,在里面声明了一个StreamController和一个
increment()
方法,当我们调用increment()方法的时候,则会将计数加1,然后丢到sink里面,这样我们在任何监听streamController.stream
的地方都将会收到这个数据,然后进行之后的工作,在上面的代码中,监听者是StreamBuilder
,当有数据流入的时候,它会进行UI重绘,将数据显示到控件上。
- 我们在点击按钮的时候,调用了
bloc.increment()
方法,产生了数据流。
这很像mvp模式中的P层的作用,业务逻辑处理都在这里面进行,然后通知UI重绘,当然这只是一个很粗糙的样例。
关于Bloc的可访问性
以上的功能都是基于BLoC进行的,所以BLoC的可访问性需要得到保证
全局单例(global Singleton):这种方式很简单,但是不推荐,因为Dart中对类没有析构函数(destructor)的概念,因此资源永远无法释放。
局部变量(local instance):你可以创建一个Bloc局部实例,在某些情况下可以完美解决问题。但是美中不足的是,你需要在StatefulWidget中初始化,并记得在dispose()
中释放它。
由祖先(ancestor)来提供:这也是最常见的一种方法,通过一个实现了StatefulWidget的父控件来获取访问权。
在大佬写的项目中有这种实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| abstract class BlocBase { void dispose(); }
class BlocProvider<T extends BlocBase> extends StatefulWidget { BlocProvider({ Key key, @required this.child, @required this.bloc, }): super(key: key);
final T bloc; final Widget child;
@override _BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){ final type = _typeOf<BlocProvider<T>>(); BlocProvider<T> provider = context.ancestorWidgetOfExactType(type); return provider.bloc; }
static Type _typeOf<T>() => T; }
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{ @override void dispose(){ widget.bloc.dispose(); super.dispose(); }
@override Widget build(BuildContext context){ return widget.child; } }
|
当我们使用的时候,
自己定义处理逻辑的BLoC继承BLoCBase(这是为了能释放掉Stream资源)
之前我们定义了一个界面叫Joke,使用的时候直接用new Joke()
就好了,现在我们需要
```dart
BLoCProvider(
bloc: JokeBLoC(),
child: Joke(),
)
1 2 3 4 5 6 7 8
| 当然为了能够使用BLoC,需要对Joke进行改造,改造之后是`JokeWithBLoC`,于是就成了这样
``` dart BLoCProvider<JokeBLoC>( bloc: JokeBLoC(), child: JokeWithBLoC(), )
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| class JokeBLoC extends BLoCBase{
StreamController<List<JokeBean>> _resultController = StreamController.broadcast(); Stream<List<JokeBean>> get outResultList => _resultController.stream; StreamController<bool> _indexController = StreamController<bool>.broadcast(); Sink<bool> get inJokesIndex => _indexController.sink;
List<JokeBean> datas = []; var pageNumber = 1;
NewsBLoC() { _indexController.stream.listen(_handleIndex); _resultController.addError( (error) => print("_jokeResultController error->${error.toString()}")); _indexController.addError( (error) => print("_jokeIndexController error->${error.toString()}")); }
void _handleIndex(bool isLoadMore) async { if (isLoadMore) { pageNumber++; } else { pageNumber = 1; }
String dataUrl = "https://i.jandan.net/?oxwlxojflwblxbsapi=jandan.get_duan_comments&page=$pageNumber"; try { Response<Map<String, dynamic>> response = await Dio().get(dataUrl); if (response.statusCode == 200) { var jokeModel = JokeModel.fromJson(response.data); if(isLoadMore){ datas.addAll(jokeModel.comments); }else{ datas = jokeModel.comments; } _resultController.add(datas);
} else { showError(); } } catch (e) { print(e.toString()); } } @override void dispose() { }
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| class JokeWithBLoC extends StatelessWidget { JokeBLoC jokeBLoC; @override Widget build(BuildContext context) { jokeBLoC = BLoCProvider.of<JokeBLoC>(context); ScrollController _scrollController = new ScrollController(); _scrollController.addListener(() { if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) { jokeBLoC.inJokesIndex.add(true); } }); jokeBLoC.inJokesIndex.add(false);
return RefreshIndicator( onRefresh: () => refresh(), child: StreamBuilder<List<JokeBean>>( stream: jokeBLoC.outResultList, builder: (BuildContext context, AsyncSnapshot<List<JokeBean>> snapshot) { if (snapshot.data == null || snapshot.data.isEmpty) { return SpinKitWave( color: Colors.redAccent, type: SpinKitWaveType.start); } return ListView.builder( controller: _scrollController, itemCount: snapshot.data == null ? 1 : snapshot.data.length + 1, itemBuilder: (BuildContext context, int position) { return _getRow(context, snapshot.data, position); }); }), ); }
Future<void> refresh() async { jokeBLoC.inJokesIndex.add(false); } }
|
这是一个大佬写的https://github.com/boeledi/Streams-Block-Reactive-Programming-in-Flutter
想要跑起来需要自己去申请一个key,具体看tmdb_api.dart
这是我写的 https://github.com/huangyuanlove/JanDan_flutter
不要过度设计你的代码
以上