WidgetからControllerを分離する

ntaoo
13 min readMay 14, 2020

--

FlutterのWidgetは、MVCアーキテクチャーにおけるViewとControllerが合体したものとみなせます。WidgetのViewまたはController部分が肥大化してくると見通しが悪くなるため、WidgetをViewとControllerに分離して、両者の見通しをそれぞれ良くしたいことがあります。その場合、Widgetのがなんらかの(複数の)resourceを使用する場合は、その開放が必要な処理 ( dispose ) のControllerへの移譲もしたいです。

この手法は、Modelと通信するStatefulWidgetに適用すると良いでしょう。どのような場合でもControllerを分離すれば良いわけではなく、単純な機能のWidgetに対してそうする必要はありません。

基本 : StatefulWidget + Controller

StatefulWidgetのライフサイクルメソッドでControllerの生成と破棄を行います。( TextEditingControllerなどのリソースは、Controllerで管理します。)

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Model {
Stream _stream;
}

class WidgetAController {
final Model _model;
final TextEditingController textEditingController = TextEditingController();
StreamSubscription _streamSubscription;

WidgetAController(this._model) {
_streamSubscription = _model._stream.listen((event) {
// handle something.
});
}

void handleSubmitted(String value) {
// process with model.
}

void dispose() {
_streamSubscription?.cancel();
textEditingController.dispose();
}
}

class WidgetA extends StatefulWidget {
@override
_WidgetAState createState() => _WidgetAState();
}

class _WidgetAState extends State<WidgetA> {
WidgetAController _controller;
@override
void initState() {
super.initState();
// Create the controller for this widget. Additional resource
// is passed via "Provider.of(context, listen: false)".
// If it is already created, skip this process by using "??=".
_controller ??=
WidgetAController(Provider.of<Model>(context, listen: false));
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
// Or, here. It depends on a use case.
// _controller ??= WidgetAController(Provider.of<Model>(context, listen: true));
}

@override
void dispose() {
// Don't forget to dispose it.
_controller?.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Container(
child: TextField(
maxLines: null,
decoration: InputDecoration.collapsed(hintText: 'Please Input'),
controller: _controller.textEditingController,
onSubmitted: (value) => _controller.handleSubmitted(value),
),
);
}
}

利点

リソース管理手法が明示的でドキュメントやサンプルも豊富なため、学びやすく理解しやすいです。

欠点

リソースの開放を忘れがち、との声があります。

なお、ControllerがStatefulWidgetのライフサイクルに沿っているかぎり、そのStatefulWidgetのBuildContextをControllerのconstructorに渡してControllerのfieldに保持しても、おそらく問題ないでしょう。ただし、思わぬ不具合を避けるため、自分がなにをしているかを正確に理解している自信が持てないならば、BuildContextは常にControllerのmethodのparameterとして渡されるべきだと思います。

StatelessWidget / StatefulWidget, in Controller Provider

Controllerに移譲したResourceの開放処理を、Providerに任せます。

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class Model {
Stream _stream;
}

class WidgetAController {
final Model _model;
final TextEditingController textEditingController = TextEditingController();
StreamSubscription _streamSubscription;

WidgetAController(this._model) {
_streamSubscription = _model._stream.listen((event) {
// handle something.
});
}

void handleSubmitted(String value) {
// process with model.
}

void dispose() {
_streamSubscription?.cancel();
textEditingController.dispose();
}
}

class WidgetA extends StatelessWidget {
static Widget inControllerProvider({Key key}) {
return Provider<WidgetAController>(
key: key,
create: (context) => WidgetAController(Provider.of<Model>(context)),
dispose: (_, self) => self.dispose(),
child: WidgetA._(),
);
}

// Just to prevent from creating implicit default constructor which is public.
WidgetA._();

@override
Widget build(BuildContext context) {
final controller = Provider.of<WidgetAController>(context);

return Container(
child: TextField(
maxLines: null,
decoration: InputDecoration.collapsed(hintText: 'Please Input'),
controller: controller.textEditingController,
onSubmitted: (value) => controller.handleSubmitted(value),
),
);
}
}

利点

Resourceの生成と開放のライフサイクルがProviderに任せられるほどに単純な場合は、それをProviderに任せ、StatelessWidgetで済ませられます。

また、ChangeNotifierProviderをMixinしたり、または後述のResourceの開放を暗黙に実行するProviderを作るならば、Resourceの開放忘れを防止できます。

欠点

Resourceを操作するためにStatefulWidgetのライフサイクルメソッド(didChangeDependencies, deactivateなど) が必要になれば、または、TickerProviderStateMixinのようにStatefulWidgetが求められるならば、結局はStatelessWidgetをStatefulWidgetに書き換えることになります。考慮事項が増えます。

また、欠点というほどでもないですが、そのProviderの分、Widget階層がひとつ増えます。

なお、BuildContextがProviderと子のWidgetで異なるので、ControllerにはそのconstructorでWidgetのBuildContextを渡すことはできず、かわりに必ずInstance Methodのparameterとして渡します。

Resourceの開放を担当するProviderを作る

ChangeNotifierProviderのように、Providerの破棄時に自動的にdispose methodを呼ぶProviderを作ると便利です。

以下のコードは、任意のInterfaceを定義し、それをimplementしたObjectを操作してResourceの開放をする、ResourceProviderを定義したものです。

import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';

abstract class Resource {
void dispose();
}

class ResourceProvider<T extends Resource> extends Provider<T> {
static void _dispose(BuildContext context, Resource resource) {
resource?.dispose();
}

ResourceProvider({
Key key,
@required Create<T> create,
bool lazy,
TransitionBuilder builder,
Widget child,
}) : super(
key: key,
create: create,
dispose: _dispose,
lazy: lazy,
builder: builder,
child: child,
);
}

実装は、単純にProviderを拡張しただけです。必要ならばResourceProxyProviderを作っていきます。

そして、ControllerにResource interfaceを実装します。

class WidgetAController implements Resource {}

これで、ChangeNotifierProviderと同じように、disposeの記述が不要になり、resourceの開放忘れがなくなるようにできました。

static Widget inControllerProvider({Key key}) {
return ResourceProvider<WidgetAController>(
key: key,
create: (context) => WidgetAController(Provider.of<Model>(context)),
child: WidgetA._(),
);
}

結論

基本のStatefulWidget + Controllerで見通しがよく、慣用的かつ汎用的なので、それで問題ないのではと思います。しかし、もしも実際にResourceの開放忘れが問題化して頭を悩ませているならば、Resourceの開放を担当するProviderを作る手法を導入する価値はあるのではないでしょうか。

異論反論はコメント欄かTwitterでお知らせいただけるとありがたいです。

蛇足

「Widgetは、MVCアーキテクチャーにおけるViewとControllerが合体したものとみなすことができます」と書きましたが、細かいことを言うと、オリジナルのSmalltalkのMVCでは、ControllerはViewを更新せず、ViewがModelをobserveして自身を更新します。

この記事におけるControllerは、MVVMアーキテクチャーではViewModelと解釈するかもしれません。また、iOS開発の文脈ではControllerと呼んでいるものになるのでしょう。文脈によって名前の解釈は揺れます。

MVCという用語の発明者は、

MVCは人間とそのメンタルモデルに関するものであり、オブザーバパターンに関するものではない。| DCIアーキテクチャ — Trygve Reenskaug and James O. Coplien

と語っています。

私はここで、わかり易さのためにControllerという普及した用語を採用しました。個人的には、Subjectという用語を採用すればしっくりくるのではと思います。”Subject = Controller + Presenter” です。アプリケーションの主体 ( Subject )として、Controller相当のMethodでModelを操作し、PresenterでModelのdataをView用に加工します。Presenterは、実装によってはただのMethodに過ぎないかもしれませんし、複雑なデータ変換が必要ならばPresenter classが出現してSubject内部で使用するでしょう。SubjectのInterfaceの制約は基本的にありません。MVCアーキテクチャーのような単方向データフローになるように気をつけたら良いでしょう。

--

--