「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
背景
最近在写一个flutter-ui库,类似于antd一样的ui库,google了很久,都没有发现一个类似antd这种国人喜欢用的ui库,大部分都是国外的那种material ui,因为公司多个flutter项目都需要用,每次都是写好几遍,而且还很难维护所以才有了这个打算,第一个要写的ui组件就是日历组件,日历的ui以及数据,都已经写完了,目前正好需要给日历写控制器,所以才有了这篇文章
controller是什么
在无状态组件当中,组件的ui由传入它的参数决定的,组件本身的不需要管理状态。而有状态组件会有多种状态,而它的状态是可以通过外部控制器来控制的。比如TextField,创建一个controller可以给TextField赋值初始值,也可以通过controller来获取到变化之后的value值,而这个控制器就是controller。可以用来控制一个有状态组件的行为以及状态的一个类
为什么要用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上的交互需要后续去调整
未完待续...
关于我
微信号:cjs764901388
公众号:xstxoo(小松同学哦)
欢迎大家加我好友或者关注我的微信公众号,不定时的会有一些文章产出,虽然算不上精品,但也是细细推敲,最近入了flutter的坑,就想着做一行爱一行,也不能把自己的头衔写死了就只做前端,只写页面。flutter写起来也蛮舒服的,加油,打工人!