背景
最近在写一个flutter-ui库,类似于antd一样的ui库,google了很久,都没有发现一个类似antd这种国人喜欢用的ui库,大部分都是国外的那种material ui,因为公司多个flutter项目都需要用,每次都是写好几遍,而且还很难维护所以才有了这个打算,第一个要写的ui组件就是日历组件,日历的ui以及数据,都已经写完了,目前正好需要给日历写控制器,所以才有了这篇文章
controller是什么
在无状态组件当中,组件的ui由传入它的参数决定的,组件本身的不需要管理状态。而有状态组件会有多种状态,而它的状态是可以通过外部控制器来控制的。比如TextField,创建一个controller可以给TextField赋值初始值,也可以通过controller来获取到变化之后的value值,而这个控制器就是controller??梢杂美纯刂埔桓鲇凶刺榧男形约白刺囊桓隼?/p>
为什么要用controller,它解决了什么问题
为什么要用controller呢,起初我也没想明白为什么要用,因为传参数也可以解决类似的问题啊,就拿TextField来说,
默认值可以通过设置TextField的value值来控制
获取TextField的最新的值可以通过其onChanged事件来获取最新的
但后来我发现,很多组件内部的行为是没办法通过传参数来控制的,尤其是在特殊的组件生命周期中,没办法实现,而通过controller,可以很好的解决这个问题,我自己感觉,controller的用处就是提供给外部操作当前组件的能力,包括组件的各种状态,以及组件的各种行为,这里举个栗子??
比如ScrollController,通过创建一个实例,可以通过该controller来控制可滚动组件的滚动行为,比如滚动到某个像素,这个时候就没有办法通过传参数来实现滚动来,当然也可以通传参数来实现,只不过官方没有提供传参数的途径而已,官方提供的是通过controller来控制滚动组件的行为,也可以通过controller去实时拿到当前滚动组件滚动的距离
再比如TextField的controller,通过它的实例,可以很方便的让父组件获取到当前TextField的信息,而不需要父组件去通过设置onChanged来获取value,不需要写不太优雅的监听事件来监听光标所在的位置
综上,个人理解controller的作用就是暴露组件内部的行为,属性给父元素,使父元素可以很方便使用子元素提供的参数,而不需要去实现监听事件来获取
如何实现一个自定义的controller
回到正题,那么如何实现一个自己的controller呢,对我而言,不会就抄,抄谁的呢,当然是超官方的!读官方的源码,看它如何实现,然后我们加以模仿,不就是自己的了。窃书不能算偷……窃书!……读书人的事,能算偷么?
这里借鉴了ScrollController的源码,首先分析下源码,以下是ScrollerController的源码,我把看不懂的英文注释删掉了...本菜??看不懂就删
import 'dart:async';import 'package:flutter/animation.dart';import 'package:flutter/foundation.dart';import 'scroll_context.dart';import 'scroll_physics.dart';import 'scroll_position.dart';import 'scroll_position_with_single_context.dart';class ScrollController extends ChangeNotifier { ScrollController({ double initialScrollOffset = 0.0, this.keepScrollOffset = true, this.debugLabel, }) : assert(initialScrollOffset != null), assert(keepScrollOffset != null), _initialScrollOffset = initialScrollOffset; double get initialScrollOffset => _initialScrollOffset; final double _initialScrollOffset; final bool keepScrollOffset; final String debugLabel; @protected Iterable<ScrollPosition> get positions => _positions; final List<ScrollPosition> _positions = <ScrollPosition>[]; bool get hasClients => _positions.isNotEmpty; ScrollPosition get position { assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); assert(_positions.length == 1, 'ScrollController attached to multiple scroll views.'); return _positions.single; } double get offset => position.pixels; Future<void> animateTo( double offset, { @required Duration duration, @required Curve curve, }) { assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); final List<Future<void>> animations = List<Future<void>>(_positions.length); for (int i = 0; i < _positions.length; i += 1) animations[i] = _positions[i].animateTo(offset, duration: duration, curve: curve); return Future.wait<void>(animations).then<void>((List<void> _) => null); } void jumpTo(double value) { assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.'); for (final ScrollPosition position in List<ScrollPosition>.from(_positions)) position.jumpTo(value); } void attach(ScrollPosition position) { assert(!_positions.contains(position)); _positions.add(position); position.addListener(notifyListeners); } void detach(ScrollPosition position) { assert(_positions.contains(position)); position.removeListener(notifyListeners); _positions.remove(position); } @override void dispose() { for (final ScrollPosition position in _positions) position.removeListener(notifyListeners); super.dispose(); } ScrollPosition createScrollPosition( ScrollPhysics physics, ScrollContext context, ScrollPosition oldPosition, ) { return ScrollPositionWithSingleContext( physics: physics, context: context, initialPixels: initialScrollOffset, keepScrollOffset: keepScrollOffset, oldPosition: oldPosition, debugLabel: debugLabel, ); } @override String toString() { final List<String> description = <String>[]; debugFillDescription(description); return '${describeIdentity(this)}(${description.join(", ")})'; } @mustCallSuper void debugFillDescription(List<String> description) { if (debugLabel != null) description.add(debugLabel); if (initialScrollOffset != 0.0) description.add('initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, '); if (_positions.isEmpty) { description.add('no clients'); } else if (_positions.length == 1) { // Don't actually list the client itself, since its toString may refer to us. description.add('one client, offset ${offset?.toStringAsFixed(1)}'); } else { description.add('${_positions.length} clients'); } }}
看了看好像也没多少东西,注意当前类的定义
class ScrollController extends ChangeNotifier
是继承了ChangeNotifier类,看着这个类顿时觉得好眼熟有没有,对了,不就是我们平时写provider用的那个东东嘛,查阅了官方文档,具体是这么解释的
A class that can be extended or mixed in that provides a change notification API using VoidCallback for notifications.
用我这渣渣英语翻译大概的意思就是,一个类,它可以被继承,它可以被混合并且它提供了使用VoidCallback进行通知的 notification Api
盲猜和provider用法差不多,都是观察者模式模式,父组件可以订阅该controller的更改,当该controller通知其他监听器的时候,监听器的回调函数将被执行,上面ScrollController中的attach中正好也使用了notification方法来通知监听者,具体滚动执行的过程没有看到,但是大致了解了controller的工作原理
observer 提供属性以及方法,当需要通知监听者点时候,调用notification去通知
监听者收到observer 的通知,进行后续的事件处理
好了,知道原理了,开搞
首先得思考,这个controller会提供什么,按照我当前给日历组件的设计,目前会给外部提供当前日历所有的行为事件以及最终的值
上个月,下个月
Single模式下的value以及Multiple模式下的values值,还有Range模式下的选区的值
这里是我设计的日历组件设计的mode:1. Single模式,只允许有一个处于active的日期。2.Multiple模式,允许多个处于active的日期。3.Range模式,允许有多个选区(起始日期和结束日期)
class CalendarController extends ChangeNotifier { DateTime currentDate = DateTime.now(); /// 所有激活日期的集合 List<CalendarCellModel> active = []; /// range模式下选中的集合 List<List<CalendarCellModel>> range = []; goPreviousMonth() { currentDate = DateUtil.addMonthsToMonthDate(currentDate, -1); notifyListeners(); } goNextMonth() { currentDate = DateUtil.addMonthsToMonthDate(currentDate, 1); notifyListeners(); } @override void dispose() { range = []; active = []; }}
目前我写的controller很简单,只需要给外部父容器提供上一个月,下一个月的方法可以使用就可以,所以我的控制器很简单,只有两个方法,并且方法执行完成之后进行消息通知,通知到各个订阅者,也就是这里的日期组件 在日期组件的 initState方法中,对controller进行监听,从而改变ui
widget.controller.addListener(() { setState(() { calendarDataSource = CalendarCore.getMonthDetailInfo( widget.controller.currentDate.year, widget.controller.currentDate.month); });});
最外层父容器是这样的,当前demo用setState临时刷新ui
看看效果如何
看起来还不错,还有一些ui上的交互需要后续去调整
未完待续...
关于我
最近入了flutter的坑,就想着做一行爱一行,也不能把自己的头衔写死了就只做前端,只写页面。flutter写起来也蛮舒服的,加油,打工人!