flutter如何自定義一個controller

背景

最近在寫一個flutter-ui庫,類似于antd一樣的ui庫,google了很久,都沒有發(fā)現(xiàn)一個類似antd這種國人喜歡用的ui庫,大部分都是國外的那種material ui,因為公司多個flutter項目都需要用,每次都是寫好幾遍,而且還很難維護(hù)所以才有了這個打算,第一個要寫的ui組件就是日歷組件,日歷的ui以及數(shù)據(jù),都已經(jīng)寫完了,目前正好需要給日歷寫控制器,所以才有了這篇文章

image

controller是什么

在無狀態(tài)組件當(dāng)中,組件的ui由傳入它的參數(shù)決定的,組件本身的不需要管理狀態(tài)。而有狀態(tài)組件會有多種狀態(tài),而它的狀態(tài)是可以通過外部控制器來控制的。比如TextField,創(chuàng)建一個controller可以給TextField賦值初始值,也可以通過controller來獲取到變化之后的value值,而這個控制器就是controller??梢杂脕砜刂埔粋€有狀態(tài)組件的行為以及狀態(tài)的一個類

為什么要用controller,它解決了什么問題

為什么要用controller呢,起初我也沒想明白為什么要用,因為傳參數(shù)也可以解決類似的問題啊,就拿TextField來說,

  1. 默認(rèn)值可以通過設(shè)置TextField的value值來控制

  2. 獲取TextField的最新的值可以通過其onChanged事件來獲取最新的

但后來我發(fā)現(xiàn),很多組件內(nèi)部的行為是沒辦法通過傳參數(shù)來控制的,尤其是在特殊的組件生命周期中,沒辦法實現(xiàn),而通過controller,可以很好的解決這個問題,我自己感覺,controller的用處就是提供給外部操作當(dāng)前組件的能力,包括組件的各種狀態(tài),以及組件的各種行為,這里舉個栗子??

  1. 比如ScrollController,通過創(chuàng)建一個實例,可以通過該controller來控制可滾動組件的滾動行為,比如滾動到某個像素,這個時候就沒有辦法通過傳參數(shù)來實現(xiàn)滾動來,當(dāng)然也可以通傳參數(shù)來實現(xiàn),只不過官方?jīng)]有提供傳參數(shù)的途徑而已,官方提供的是通過controller來控制滾動組件的行為,也可以通過controller去實時拿到當(dāng)前滾動組件滾動的距離

  2. 再比如TextField的controller,通過它的實例,可以很方便的讓父組件獲取到當(dāng)前TextField的信息,而不需要父組件去通過設(shè)置onChanged來獲取value,不需要寫不太優(yōu)雅的監(jiān)聽事件來監(jiān)聽光標(biāo)所在的位置

綜上,個人理解controller的作用就是暴露組件內(nèi)部的行為,屬性給父元素,使父元素可以很方便使用子元素提供的參數(shù),而不需要去實現(xiàn)監(jiān)聽事件來獲取

如何實現(xiàn)一個自定義的controller

回到正題,那么如何實現(xiàn)一個自己的controller呢,對我而言,不會就抄,抄誰的呢,當(dāng)然是超官方的!讀官方的源碼,看它如何實現(xiàn),然后我們加以模仿,不就是自己的了。竊書不能算偷……竊書!……讀書人的事,能算偷么?

這里借鑒了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');    }  }}

看了看好像也沒多少東西,注意當(dāng)前類的定義

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進(jìn)行通知的 notification Api

盲猜和provider用法差不多,都是觀察者模式模式,父組件可以訂閱該controller的更改,當(dāng)該controller通知其他監(jiān)聽器的時候,監(jiān)聽器的回調(diào)函數(shù)將被執(zhí)行,上面ScrollController中的attach中正好也使用了notification方法來通知監(jiān)聽者,具體滾動執(zhí)行的過程沒有看到,但是大致了解了controller的工作原理

  1. observer 提供屬性以及方法,當(dāng)需要通知監(jiān)聽者點時候,調(diào)用notification去通知

  2. 監(jiān)聽者收到observer 的通知,進(jìn)行后續(xù)的事件處理

好了,知道原理了,開搞

首先得思考,這個controller會提供什么,按照我當(dāng)前給日歷組件的設(shè)計,目前會給外部提供當(dāng)前日歷所有的行為事件以及最終的值

  1. 上個月,下個月

  2. Single模式下的value以及Multiple模式下的values值,還有Range模式下的選區(qū)的值

這里是我設(shè)計的日歷組件設(shè)計的mode:1. Single模式,只允許有一個處于active的日期。2.Multiple模式,允許多個處于active的日期。3.Range模式,允許有多個選區(qū)(起始日期和結(jié)束日期)

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很簡單,只需要給外部父容器提供上一個月,下一個月的方法可以使用就可以,所以我的控制器很簡單,只有兩個方法,并且方法執(zhí)行完成之后進(jìn)行消息通知,通知到各個訂閱者,也就是這里的日期組件 在日期組件的 initState方法中,對controller進(jìn)行監(jiān)聽,從而改變ui

widget.controller.addListener(() {  setState(() {    calendarDataSource = CalendarCore.getMonthDetailInfo(        widget.controller.currentDate.year,        widget.controller.currentDate.month);  });});

最外層父容器是這樣的,當(dāng)前demo用setState臨時刷新ui

image

看看效果如何

image
image

看起來還不錯,還有一些ui上的交互需要后續(xù)去調(diào)整

未完待續(xù)...

關(guān)于我

最近入了flutter的坑,就想著做一行愛一行,也不能把自己的頭銜寫死了就只做前端,只寫頁面。flutter寫起來也蠻舒服的,加油,打工人!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容