美文网首页
flutter如何自定义一个controller

flutter如何自定义一个controller

作者: 那抹微笑丶 | 来源:发表于2021-11-02 16:13 被阅读0次

背景

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

image

controller是什么

在无状态组件当中,组件的ui由传入它的参数决定的,组件本身的不需要管理状态。而有状态组件会有多种状态,而它的状态是可以通过外部控制器来控制的。比如TextField,创建一个controller可以给TextField赋值初始值,也可以通过controller来获取到变化之后的value值,而这个控制器就是controller。可以用来控制一个有状态组件的行为以及状态的一个类

为什么要用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写起来也蛮舒服的,加油,打工人!

相关文章

网友评论

      本文标题:flutter如何自定义一个controller

      本文链接:https://www.haomeiwen.com/subject/cexfzltx.html