“This is the first day of my participation in the Gwen Challenge in November. Check out the details: The last Gwen Challenge in 2021”

background

Recently, I am writing a FLUTTER – UI library, which is similar to ANTD UI library. After a long time of Google, I have not found a UI library similar to ANTD that Chinese people like to use. Most of them are the material UI of foreign countries, because the company needs to use the flutter project several times each time. The first UI component to write is the calendar component. The UI and data of the calendar have been written. At present, I just need to write the controller for the calendar, so I have this article

What is the controller

In stateless components, the component’s UI is determined by the parameters passed to it, and the component’s own administrative state is not required. Stateful components have multiple states, and their states can be controlled by external controllers. For example, TextField, if you create a Controller, you can assign an initial value to TextField, or you can use a Controller to get a changed value, and that controller is a controller. A class that can be used to control the behavior and state of a stateful component

Why controller? What problem does it solve

Why controller? I didn’t understand why controller was used at first, because passing parameters can solve similar problems. Take TextField for example,

  1. The default value can be controlled by setting the TextField value
  2. Getting the latest value of the TextField can be done with its onChanged event

But later, I found that many internal behaviors of components cannot be controlled by wearing parameters, especially in the life cycle of special components. Controller can solve this problem well. In my own opinion, the use of controller is to provide the external ability to operate the current component. This includes the various states of the component, as well as the various behaviors of the component, for example 🌰

  1. For example, ScrollController, by creating an instance, can be used to control the scrolling behavior of scrollable components, such as scrolling to a certain pixel. In this case, there is no way to implement scrolling by passing parameters. Of course, you can also pass parameters, but there is no way to pass parameters. Officially, the behavior of the rolling component can be controlled by the controller, and the rolling distance of the current rolling component can be obtained in real time by the controller
  2. Another example is the Controller of TextField. Through its instance, it is very convenient for the parent component to obtain the information of the current TextField. It does not need the parent component to set onChanged to obtain the value, and it does not need to write an unelegant listening event to monitor the position of the cursor

To sum up, I understand that the function of controller is to expose the internal behavior of the component, attribute to the parent element, so that the parent element can easily use the parameters provided by the child element, without implementing the listener event to obtain

How to implement a custom controller

Back to the topic, how to achieve a controller of my own, as far as I am concerned, will not copy, copy who, of course, is super official! Read the official source code, see how it is implemented, and then we try to imitate, is not their own. Stealing a book is not stealing… Stealing books! … Can a scholar’s business be considered stealing?

Here is a reference to the source code of ScrollController, first analysis of the source code, the following is the source code of ScrollerController, I do not understand the English annotations deleted… This dish 🐔 do not understand delete

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 ! Description = 0.0). The 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'); }}}Copy the code

Look at it seems there is not much, the idea of the current class definition

class ScrollController extends ChangeNotifier
Copy the code

ChangeNotifier is inherited from the ChangeNotifier class, looking at this class suddenly feel familiar, by the way, is not we usually write provider with that east, refer to the official document, specifically explained in this way

A class that can be extended or mixed in that provides a change notification API using VoidCallback for notifications.

A class, it can be inherited, it can be mixed and it provides an Api for Notification using VoidCallback

The parent component can subscribe to changes to the controller. When the controller notifies other listeners, the listener’s callback function is executed. The attach in the above ScrollController happens to use notification method to notify the listener. I did not see the specific scrolling execution process, but I have a general understanding of the controller’s working principle

  1. The Observer provides properties and methods that are called notification when listener points need to be notified
  2. The listener receives notification from the Observer for subsequent event processing

All right, we got it. Let’s do it

So the first thing to think about is, what is this controller going to provide, the way I’m currently designing the calendar component, is it going to provide externally all the actions and events of the current calendar and the final values

  1. Last month, next month
  2. Values in Single mode, values in Multiple mode, and selected values in Range mode

Here is the mode I designed for the calendar component: 1. Single mode, which allows only one active date. 2.Multiple mode, allowing Multiple active dates. 3.Range mode, allowing multiple selections (start date and end date)

class CalendarController extends ChangeNotifier { DateTime currentDate = DateTime.now(); List<CalendarCellModel> active = []; List<List<CalendarCellModel>> range = []; goPreviousMonth() { currentDate = DateUtil.addMonthsToMonthDate(currentDate, -1); notifyListeners(); } goNextMonth() { currentDate = DateUtil.addMonthsToMonthDate(currentDate, 1); notifyListeners(); } @override void dispose() { range = []; active = []; }}Copy the code

At present, the controller I write is very simple. I only need to provide the external parent container with the methods of the previous month and the methods of the next month. Therefore, my controller is very simple, with only two methods, and after the method is executed, the message is notified to each subscriber. So the date component here, in the initState method of the date component, is listening on the Controller to change the UI

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

The outermost parent container looks like this, and the current demo uses setState to temporarily refresh the UI

Let’s see what happens

It looks good, but there are some UI interactions that need to be tweaked later

To be continued…

About me

Wechat id: CJS764901388

Public account: XSTxoo (Komatsu students oh)

Welcome everyone to add my friends or follow my wechat public number, there will be some articles produced from time to time, although it is not good, but it is also careful deliberation. Recently, I entered the pit of flutter, just want to do one line and love one line, but can not write their own title to death, just do front-end, only write pages. Flutter is also comfortable to write. Come on, workers!