flutter如何自定义一个controller

背景

最近在写一个flutter-ui库,类似于antd一样的ui库,google了很久,都没有发现一个类似antd这种国人喜欢用的ui库,大部分都是国外的那种material ui,因为公司多个flutter项目都需要用,每次都是写好几遍,而且还很难维护所以才有了这个打算,第一个要写的ui组件就是日历组件,日历的ui以及数据,都已经写完了,目前正好需要给日历写控制器,所以才有了这篇文章

image

controller是什么

在无状态组件当中,组件的ui由传入它的参数决定的,组件本身的不需要管理状态。而有状态组件会有多种状态,而它的状态是可以通过外部控制器来控制的。比如TextField,创建一个controller可以给TextField赋值初始值,也可以通过controller来获取到变化之后的value值,而这个控制器就是controller??梢杂美纯刂埔桓鲇凶刺榧男形约白刺囊桓隼?/p>

为什么要用controller,它解决了什么问题

为什么要用controller呢,起初我也没想明白为什么要用,因为传参数也可以解决类似的问题啊,就拿TextField来说,

  1. 默认值可以通过设置TextField的value值来控制

  2. 获取TextField的最新的值可以通过其onChanged事件来获取最新的

但后来我发现,很多组件内部的行为是没办法通过传参数来控制的,尤其是在特殊的组件生命周期中,没办法实现,而通过controller,可以很好的解决这个问题,我自己感觉,controller的用处就是提供给外部操作当前组件的能力,包括组件的各种状态,以及组件的各种行为,这里举个栗子??

  1. 比如ScrollController,通过创建一个实例,可以通过该controller来控制可滚动组件的滚动行为,比如滚动到某个像素,这个时候就没有办法通过传参数来实现滚动来,当然也可以通传参数来实现,只不过官方没有提供传参数的途径而已,官方提供的是通过controller来控制滚动组件的行为,也可以通过controller去实时拿到当前滚动组件滚动的距离

  2. 再比如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的工作原理

  1. observer 提供属性以及方法,当需要通知监听者点时候,调用notification去通知

  2. 监听者收到observer 的通知,进行后续的事件处理

好了,知道原理了,开搞

首先得思考,这个controller会提供什么,按照我当前给日历组件的设计,目前会给外部提供当前日历所有的行为事件以及最终的值

  1. 上个月,下个月

  2. 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

image

看看效果如何

image
image

看起来还不错,还有一些ui上的交互需要后续去调整

未完待续...

关于我

最近入了flutter的坑,就想着做一行爱一行,也不能把自己的头衔写死了就只做前端,只写页面。flutter写起来也蛮舒服的,加油,打工人!

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351

推荐阅读更多精彩内容