背景
最近在寫一個flutter-ui庫,類似于antd一樣的ui庫,google了很久,都沒有發(fā)現(xiàn)一個類似antd這種國人喜歡用的ui庫,大部分都是國外的那種material ui,因為公司多個flutter項目都需要用,每次都是寫好幾遍,而且還很難維護(hù)所以才有了這個打算,第一個要寫的ui組件就是日歷組件,日歷的ui以及數(shù)據(jù),都已經(jīng)寫完了,目前正好需要給日歷寫控制器,所以才有了這篇文章
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來說,
默認(rèn)值可以通過設(shè)置TextField的value值來控制
獲取TextField的最新的值可以通過其onChanged事件來獲取最新的
但后來我發(fā)現(xiàn),很多組件內(nèi)部的行為是沒辦法通過傳參數(shù)來控制的,尤其是在特殊的組件生命周期中,沒辦法實現(xiàn),而通過controller,可以很好的解決這個問題,我自己感覺,controller的用處就是提供給外部操作當(dāng)前組件的能力,包括組件的各種狀態(tài),以及組件的各種行為,這里舉個栗子??
比如ScrollController,通過創(chuàng)建一個實例,可以通過該controller來控制可滾動組件的滾動行為,比如滾動到某個像素,這個時候就沒有辦法通過傳參數(shù)來實現(xiàn)滾動來,當(dāng)然也可以通傳參數(shù)來實現(xiàn),只不過官方?jīng)]有提供傳參數(shù)的途徑而已,官方提供的是通過controller來控制滾動組件的行為,也可以通過controller去實時拿到當(dāng)前滾動組件滾動的距離
再比如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的工作原理
observer 提供屬性以及方法,當(dāng)需要通知監(jiān)聽者點時候,調(diào)用notification去通知
監(jiān)聽者收到observer 的通知,進(jìn)行后續(xù)的事件處理
好了,知道原理了,開搞
首先得思考,這個controller會提供什么,按照我當(dāng)前給日歷組件的設(shè)計,目前會給外部提供當(dāng)前日歷所有的行為事件以及最終的值
上個月,下個月
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
看看效果如何
看起來還不錯,還有一些ui上的交互需要后續(xù)去調(diào)整
未完待續(xù)...
關(guān)于我
最近入了flutter的坑,就想著做一行愛一行,也不能把自己的頭銜寫死了就只做前端,只寫頁面。flutter寫起來也蠻舒服的,加油,打工人!