
Stateful Widgetを再描画したいけど、自分以外の別のクラスから再描画を発動する方法(setStateを実行する方法)が分からない。どうしたらいいの?
という方向けの記事です。
例えば、AppBarとbody(Scaffoldのbodyプロパティ)のWidgetを別クラスにしたとき、
AppBar上のボタンを押したら、bodyのWidgetを再描画して更新したい
みたいな場合です。
このようなケースでは、プログラムの構造を
- View層(UI・見た目)
- ViewModel層(View層とModel層のつなぎ・View層への更新通知)
- Model層(ロジック・計算)
の3層に分割した「MVVM(Model-View-ViewModel)」モデルにし、ViewModel層経由でView層に更新を通知する方法が、1つの対処法かと思います。
しかし、ViewModel層からView層に更新を通知して、Widgetを再描画するには、「Provider」や「Riverpod」などの状態管理パッケージを使う必要があり、使い方が初学者には難しい印象です。。。
自分自身、リリースしたアプリでは、一応、全て「Provider」か「Riverpod」を使っていますが、今だに難しく感じています。。
そこで、以前から、
簡単なアプリだったら、Stateful WidgetとsetStateだけで、何とかならないだろうか?
と考えていました。
ややマニアック、またはアンチパターン(やってはいけないこと)かもしれないのですが、、、ネットを調べると、Flutterの状態管理に関する情報は多くあり、近い情報は見つかったものの、明確に今回の疑問に合致する情報は見つけられなかったので、自分で考察してみました。
なお、MVVMを使わず、View層に全てのロジックも持たせ、setStateで全体を再描画する手もありますが、前述の例では、AppBarとbodyのWidgetを同一クラスにする必要があり、簡単なアプリとは言え、コードが肥大化して、メンテナンスしづらくなってしまいます。
結論として、以下のような方法で検証したら実現できたので、その例(サンプルコード)を共有したいと思います。
- View層は、再描画したい単位でStateful Widgetに分ける(分け方は粗目でも可とする)
- 各Stateful WidgetのsetStateを、Function型のプロパティとしてViewModel層に渡す
- ViewModel層で、渡されたsetStateを、再描画したいタイミングで実行する
理解が未熟ゆえ、おかしい点が多々あるかと思いますので、お気づきの点があれば、ご指摘くださいm(_ _)m。
40代からプログラミング(Flutter)を始めて、GooglePlayとAppStoreにアプリを公開しているhalzo appdevです。
作成したアプリはこちら↓ 全てFlutterで開発したアプリです。
サンプルコードの前提となるWidgetの構造
以下のような、単純な例を考えました。

実行時の画面は、「Aを更新」ボタンを押すと、WidgetAのカウンターだけが増え、「Bを更新」ボタンを押すと、WidgetBのカウンターだけが増え、「両方を更新」ボタンを押すと、両方のカウンターが増えるというものです。

一般に、これをStateful WidgetとsetStateだけで実現(状態管理:変数の変更に伴うWidgetの再描画)をしようとすると、以下のようなデメリットがあるかと思います。
- AppBarWidgetOからBodyWidgetAOだけを再描画しようとすると、両クラスは親子関係にないため(Widgetツリーが異なるため)、BodyWidgetAOのsetStateを発動できない。
- 下位のBodyWidgetAOから上位のWidget(例えばSampleScreenO)を再描画しようとすると、上位のWidgetのsetStateを引数で下位のWidgetに渡していく必要があり、コーディングが煩雑になる。
- SampleScreenO以下を全て一つのクラスにし、再描画を1つのsetStateで処理する方法もあるが、
- Widgetの階層構造が深くなり、表示(UI)と論理(ロジック)が一体化したコードになるので、メンテナンスしにくくなる。
- 無駄な再描画が増えるので、処理が重くなる。
そのため、下記のとおり、MVVMモデルを導入し、変数の更新(カウントアップする計算)はModel層で行い、ViewModel層では、更新したいWidgetに対してのみ、再描画のための更新通知を行う構造としました。

ここで、一般的には「Provider」や「Riverpod」などの状態管理パッケージを導入し、ViewModel層から、「notifyListeners()」や、「state」プロパティの更新を通じて、更新通知を行うと思います。
しかし、今回はこれらのパッケージを使わずに、Stateful WidgetとsetStateだけで実現する方法を考えました。
Stateful WidgetとsetStateだけで状態管理する方法
全体像は下図のとおりです。

大まかには、
- View層では、再描画する可能性のあるWidgetは、全てStateful Widgetとし、
- 各WidgetのsetStateメソッドを、ViewModel層に渡す(initStateメソッドの中で実行)。
- ViewModel層では、更新したいWidgetから渡されたsetStateメソッドを実行することで、Model層の計算結果を、当該Widgetに反映し、再描画する
という感じです。
一般に非推奨とされるグローバル変数は、使わないようにしました。
この方法で、ProviderやRiverpodを使わずに、実現できました。
サンプルコードの全体像
今回作成したサンプルコードの全体像を掲載します。
「main.dart」にそのままコピペいただけば、挙動を確認いただけます。
本来は、ViewModel層のクラス(SampleViewModelO)やModel層のクラス(SampleModelO)は、別のdartファイルにすべきですが、1回のコピペで済むように、あえて1つのdartファイルに集約しました。
// クラス名、メソッド名、プロパティ名(変数名)について、筆者が作成したもの(名前変更可のもの)
// の名前の末尾には、大文字のオー「O」をつけています
// ※ライブラリ(パッケージ)で予め決められているもの(名前の変更不可のもの)と、
// 自分で作成したもの(名前の変更可のもの)の区別をしやすくするため
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "setState and MVVM Demo",
theme: ThemeData.light(),
home: SampleScreenO(),
);
}
}
/// View層 ///////////////////////////////////////////////////
class SampleScreenO extends StatefulWidget {
@override
_SampleScreenOState createState() => _SampleScreenOState();
}
class _SampleScreenOState extends State<SampleScreenO> {
@override
Widget build(BuildContext context) {
debugPrint("全体を描画");
return Scaffold(
appBar: AppBar(title: AppBarWidgetO()),
body: BodyWidgetO(),
);
}
}
// AppBar部分のクラス
// AppBarを再描画しない場合は、StatelessWidgetでも良い
class AppBarWidgetO extends StatefulWidget {
const AppBarWidgetO({Key? key}) : super(key: key);
@override
_AppBarWidgetOState createState() => _AppBarWidgetOState();
}
class _AppBarWidgetOState extends State<AppBarWidgetO> {
@override
Widget build(BuildContext context) {
debugPrint("AppBarを描画");
// テキストボタンのスタイルを設定
ButtonStyle buttonStyleO =
ButtonStyle(backgroundColor: MaterialStateProperty.all<Color>(Theme.of(context).primaryColorLight));
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// WidgetAを更新するボタン
TextButton(
child: Text("Aを更新", style: Theme.of(context).textTheme.button),
style: buttonStyleO,
onPressed: () {
// ボタンを押したら、ViewModel内のWidgetAのカウンターを加算表示するメソッドを実行
// ※ViewModelはシングルトン化しており、staticのインスタンス経由でアクセスするため、
// クラス名.インスタンス名.メソッド名で書く
SampleViewModelO.instanceO.countUpForWidgetAO();
},
),
// WidgetBを更新するボタン
TextButton(
child: Text("Bを更新", style: Theme.of(context).textTheme.button),
style: buttonStyleO,
onPressed: () {
// ボタンを押したら、ViewModel内のWidgetBのカウンターを加算表示するメソッドを実行
SampleViewModelO.instanceO.countUpForWidgetBO();
},
),
// bodyのWidget全体(=WidgetAとB両方)を更新するボタン
TextButton(
child: Text("両方更新", style: Theme.of(context).textTheme.button),
style: buttonStyleO,
onPressed: () {
// ボタンを押したら、ViewModel内のWidgetAとB両方のカウンターを加算表示するメソッドを実行
SampleViewModelO.instanceO.countUpForBothO();
},
),
],
);
}
}
// body部分のウィジェット
class BodyWidgetO extends StatefulWidget {
const BodyWidgetO({Key? key}) : super(key: key);
@override
State<BodyWidgetO> createState() => _BodyWidgetOState();
}
class _BodyWidgetOState extends State<BodyWidgetO> {
// bodyのWidget全体(=WidgetAとBの両方)を再描画するメソッドを定義
void rebuildBothWidgetsO(){
setState(() {});
}
@override
void initState() {
// ViewModel内に、上記で定義した再描画メソッドを渡す
SampleViewModelO.instanceO.setRebuildMethodBothO(rebuildBothWidgetsO);
super.initState();
}
@override
Widget build(BuildContext context) {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
BodyWidgetAO(),
BodyWidgetBO(),
],
),
);
}
}
// WidgetAのクラス
class BodyWidgetAO extends StatefulWidget {
const BodyWidgetAO({Key? key}) : super(key: key);
@override
_BodyWidgetAOState createState() => _BodyWidgetAOState();
}
class _BodyWidgetAOState extends State<BodyWidgetAO> {
// WidgetAを再描画するメソッドを定義
void rebuildWidgetAO(){
setState(() {});
}
@override
void initState() {
// ViewModel内に、上記で定義した再描画メソッドを渡す
SampleViewModelO.instanceO.setRebuildMethodAO(rebuildWidgetAO);
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("WidgetAを描画");
// ViewModel内のカウンター変数を参照して表示
int displayNumberO = SampleViewModelO.instanceO.countNumberO;
return Text("WidgetA\nCount\n$displayNumberO", textAlign: TextAlign.center);
}
}
// WidgetBのクラス
class BodyWidgetBO extends StatefulWidget {
const BodyWidgetBO({Key? key}) : super(key: key);
@override
_BodyWidgetBOState createState() => _BodyWidgetBOState();
}
class _BodyWidgetBOState extends State<BodyWidgetBO> {
// WidgetBを再描画するメソッドを定義
void rebuildWidgetBO(){
setState(() {});
}
@override
void initState() {
// ViewModel内に、上記で定義した再描画メソッドを渡す
SampleViewModelO.instanceO.setRebuildMethodBO(rebuildWidgetBO);
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("WidgetBを描画");
// ViewModel内のカウンター変数を参照して表示
int displayNumberO = SampleViewModelO.instanceO.countNumberO;
return Text("WidgetB\nCount\n$displayNumberO", textAlign: TextAlign.center);
}
}
/// ViewModel層のクラス ///////////////////////////////////////////////////
class SampleViewModelO {
// インスタンスを1つしか生成しないようにするため
// シングルトン化する
SampleViewModelO._();
static final instanceO = SampleViewModelO._();
// Model層のインスタンスを定義
SampleModelO modelO = SampleModelO();
// カウンター変数を定義(初期値は0)
int countNumberO = 0;
// View層から受け取った再描画メソッドを代入するFunction型プロパティ
late Function rebuildWidgetAFromVMO;
late Function rebuildWidgetBFromVMO;
late Function rebuildBothFromVMO;
// WidgetAを再描画するメソッドをViewModel内のプロパティに代入
void setRebuildMethodAO(Function methodO) {
rebuildWidgetAFromVMO = methodO;
}
// WidgetBを再描画するメソッドをViewModel内のプロパティに代入
void setRebuildMethodBO(Function methodO) {
rebuildWidgetBFromVMO = methodO;
}
// bodyのWidget全体(=WidgetAとB両方)を再描画するメソッドをViewModel内のプロパティに代入
void setRebuildMethodBothO(Function methodO) {
rebuildBothFromVMO = methodO;
}
void countUpForWidgetAO() {
// カウンター変数をModel層の計算メソッドに渡し、計算結果を返り値で取得
int calculatedNumberO = modelO.countUpO(countNumberO);
countNumberO = calculatedNumberO;
// WidgetAの再描画メソッドを実行
rebuildWidgetAFromVMO();
}
void countUpForWidgetBO() {
// カウンター変数をModel層の計算メソッドに渡し、計算結果を返り値で取得
int calculatedNumberO = modelO.countUpO(countNumberO);
countNumberO = calculatedNumberO;
// WidgetBの再描画メソッドを実行
rebuildWidgetBFromVMO();
}
void countUpForBothO() {
// カウンター変数をModel層の計算メソッドに渡し、計算結果を返り値で取得
int calculatedNumberO = modelO.countUpO(countNumberO);
countNumberO = calculatedNumberO;
// bodyのWidget全体(=WidgetAとBの両方)の再描画メソッドを実行
rebuildBothFromVMO();
}
}
/// Model層のクラス ///////////////////////////////////////////////////
class SampleModelO {
// カウントアップ計算を実行し、結果をViewModel層に返す
int countUpO(int countNumberO) {
countNumberO = countNumberO + 1;
return countNumberO;
}
}
実行したときのイメージを再掲します。

特定のWidgetのみ更新されることが確認できるよう、WidgetAとWidgetBには、あえて同じカウンター変数を参照させています。
そのため、WidgetAだけを更新したときは、WidgetBの更新は止まり、その後、WidgetBも更新すると、WidgetAの値まで一気に追いつく挙動になります(AとBが逆の場合も同様です)。
また、両方を更新するボタンを押すと、親WidgetであるBodyWidgetOが再描画されるので、WidgetAとBの両方が更新されます。
詳細な説明はコード中にも記載していますが、主な要点は以下のとおりです。
- 各Stateful Widget内で、setStateを実行するメソッドを定義(setStateを実行するだけのFunction型のメソッドを定義)する(104〜106行目、139〜141行目、169〜171行目)
- View層の各Widgetは、ViewModel層のクラス(SampleViewModelO)内の同じメソッド・プロパティにアクセス(取得・更新)できる必要があるため、SampleViewModelOは、インスタンスを1つしか生成できないよう、シングルトン化しておく(194〜195行目)
- 各Stateful WidgetのinitState内で、上記で定義したメソッドを、ViewModel内のメソッドに、引数で渡す(111、146、176行目)
- ViewModel内のメソッドでは、引数で渡されたFunction型のプロパティを、ViewModel内で定義したFunction型のプロパティに代入し、ViewModel内から、View層の各Widgetに紐付いたsetStateを利用できるようにする(209〜221行目)
シングルトンを導入する部分は、こちらの記事が大変参考になりました。ありがとうございます!
なお、本来、Model層は、データベースやストレージへのアクセス処理を書き、単純な計算レベルは、ViewModel層で処理することも多いと思われますが、今回はケースを単純化するため、データベースやストレージへのアクセスは想定せず、単純な計算のみをModel層で実行する形としました。
また、今回はViewModel層のクラスとModel層のクラスが1つずつのため、Model層はシングルトン化していませんが、複数のViewModel層から、同一のModel層を参照する場合は、Model層もシングルトン化する必要があります。
Provider、Riverpodと比較した感想
Providerでは、notifyListenersメソッドで再描画を通知しますが、この通知がView層のどの部分にヒットするのか、分かりづらいと感じていました。
特に、複数のViewから同じViewModelを参照していると、notifyListenersで想定外のViewが更新されることもあり、影響範囲の確認が大変でした(自分の使い方が下手なだけなのでしょうけどw)。
また、Riverpodは、「state」というプロパティを更新したときに、再描画が通知されるので、ピンポイントで更新できるメリットは大きいのですが、stateという名称で統一されているので、どの変数を更新しているのか、分かりづらいと感じていました(もっと上手い方法を知らないだけかもしれませんがw)。
モデルクラスを導入してcopyWithで更新すれば、どの変数を更新したか分かりやすくはなりますが、ピンポイントで更新される分、該当の変数がコード内に散在していると、更新箇所を検索するのに手間がかかります。
一方、今回検証した方法では、ピンポイントでの再描画はできないものの、再描画対象のWidget(クラス)が明確なので、コードを書いていて分かりやすい(再描画の影響範囲が分かりやすい)、というメリットを感じました。
ただ、留意点として、上位のWidgetを再描画した場合は、クラスを分けていたとしても、下位のWidgetは全て再描画されます。
そのため、再描画の範囲を限定したい場合は、再描画対象のWidgetを、できるだけWidgetツリーの下位のクラスに配置する必要があります。

追加サンプルコード(AppBarも更新する場合)
追加のサンプルコードとして、カウンターの値が10に達すると、AppBarのボタン色が変わる(暗くなる)、というケースも作成したので、ご参考に掲載します。
これも「main.dart」にコピペするだけで確認いただけます。
前回のサンプルから追加・修正した部分だけ、「↓追加・修正部分」というコメントを付しています。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "setState and MVVM Demo",
theme: ThemeData.light(),
home: SampleScreenO(),
);
}
}
/// View層 ///////////////////////////////////////////////////
class SampleScreenO extends StatefulWidget {
@override
_SampleScreenOState createState() => _SampleScreenOState();
}
class _SampleScreenOState extends State<SampleScreenO> {
@override
Widget build(BuildContext context) {
debugPrint("全体を描画");
return Scaffold(
appBar: AppBar(title: AppBarWidgetO()),
body: BodyWidgetO(),
);
}
}
class AppBarWidgetO extends StatefulWidget {
const AppBarWidgetO({Key? key}) : super(key: key);
@override
_AppBarWidgetOState createState() => _AppBarWidgetOState();
}
class _AppBarWidgetOState extends State<AppBarWidgetO> {
// ↓追加・修正部分
// AppBarのウィジェットを再描画するメソッドを定義
void rebuildAppBarO(){
setState(() {});
}
// ↓追加・修正部分
@override
void initState() {
// ViewModel内に、上記で定義した再描画メソッドを渡す
SampleViewModelO.instanceO.setRebuildAppBarO(rebuildAppBarO);
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("AppBarを描画");
// ↓追加・修正部分
// カウンターが10以上になったらテキストボタンのスタイルを変更
ButtonStyle buttonStyleO =
(SampleViewModelO.instanceO.countNumberO >= 10)
? ButtonStyle(backgroundColor: MaterialStateProperty.all<Color>(Theme.of(context).primaryColorDark))
: ButtonStyle(backgroundColor: MaterialStateProperty.all<Color>(Theme.of(context).primaryColorLight));
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
child: Text("Aを更新", style: Theme.of(context).textTheme.button),
style: buttonStyleO,
onPressed: () {
SampleViewModelO.instanceO.countUpForWidgetAO();
},
),
TextButton(
child: Text("Bを更新", style: Theme.of(context).textTheme.button),
style: buttonStyleO,
onPressed: () {
SampleViewModelO.instanceO.countUpForWidgetBO();
},
),
TextButton(
child: Text("両方更新", style: Theme.of(context).textTheme.button),
style: buttonStyleO,
onPressed: () {
SampleViewModelO.instanceO.countUpForBothO();
},
),
],
);
}
}
class BodyWidgetO extends StatefulWidget {
const BodyWidgetO({Key? key}) : super(key: key);
@override
State<BodyWidgetO> createState() => _BodyWidgetOState();
}
class _BodyWidgetOState extends State<BodyWidgetO> {
void rebuildBothWidgetsO(){
setState(() {});
}
@override
void initState() {
SampleViewModelO.instanceO.setRebuildMethodBothO(rebuildBothWidgetsO);
super.initState();
}
@override
Widget build(BuildContext context) {
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
BodyWidgetAO(),
BodyWidgetBO(),
],
),
);
}
}
class BodyWidgetAO extends StatefulWidget {
const BodyWidgetAO({Key? key}) : super(key: key);
@override
_BodyWidgetAOState createState() => _BodyWidgetAOState();
}
class _BodyWidgetAOState extends State<BodyWidgetAO> {
void rebuildWidgetAO(){
setState(() {});
}
@override
void initState() {
SampleViewModelO.instanceO.setRebuildMethodAO(rebuildWidgetAO);
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("WidgetAを描画");
int displayNumberO = SampleViewModelO.instanceO.countNumberO;
return Text("WidgetA\nCount\n$displayNumberO", textAlign: TextAlign.center);
}
}
class BodyWidgetBO extends StatefulWidget {
const BodyWidgetBO({Key? key}) : super(key: key);
@override
_BodyWidgetBOState createState() => _BodyWidgetBOState();
}
class _BodyWidgetBOState extends State<BodyWidgetBO> {
void rebuildWidgetBO(){
setState(() {});
}
@override
void initState() {
SampleViewModelO.instanceO.setRebuildMethodBO(rebuildWidgetBO);
super.initState();
}
@override
Widget build(BuildContext context) {
debugPrint("WidgetBを描画");
int displayNumberO = SampleViewModelO.instanceO.countNumberO;
return Text("WidgetB\nCount\n$displayNumberO", textAlign: TextAlign.center);
}
}
/// ViewModel層のクラス ///////////////////////////////////////////////////
class SampleViewModelO {
SampleViewModelO._();
static final instanceO = SampleViewModelO._();
SampleModelO modelO = SampleModelO();
int countNumberO = 0;
late Function rebuildWidgetAFromVMO;
late Function rebuildWidgetBFromVMO;
late Function rebuildBothFromVMO;
// ↓追加・修正部分
late Function rebuildAppBarFromVMO;
void setRebuildMethodAO(Function methodO) {
rebuildWidgetAFromVMO = methodO;
}
void setRebuildMethodBO(Function methodO) {
rebuildWidgetBFromVMO = methodO;
}
void setRebuildMethodBothO(Function methodO) {
rebuildBothFromVMO = methodO;
}
// ↓追加・修正部分
// AppBarを再描画するメソッドをViewModel内のプロパティに代入
void setRebuildAppBarO(Function() methodO) {
rebuildAppBarFromVMO = methodO;
}
void countUpForWidgetAO() {
int calculatedNumberO = modelO.countUpO(countNumberO);
countNumberO = calculatedNumberO;
rebuildWidgetAFromVMO();
// ↓追加・修正部分
// カウンターが10になったらAppBarを再描画するメソッドを実行
if (countNumberO == 10) rebuildAppBarFromVMO();
}
void countUpForWidgetBO() {
int calculatedNumberO = modelO.countUpO(countNumberO);
countNumberO = calculatedNumberO;
rebuildWidgetBFromVMO();
// ↓追加・修正部分
if (countNumberO == 10) rebuildAppBarFromVMO();
}
void countUpForBothO() {
int calculatedNumberO = modelO.countUpO(countNumberO);
countNumberO = calculatedNumberO;
rebuildBothFromVMO();
// ↓追加・修正部分
if (countNumberO == 10) rebuildAppBarFromVMO();
}
}
/// Model層のクラス ///////////////////////////////////////////////////
class SampleModelO {
int countUpO(int countNumberO) {
countNumberO = countNumberO + 1;
return countNumberO;
}
}
その他補足(Widgetのクラスをどの程度分けるか)
今回、検証した方法では、再描画の範囲を限定したい場合、Widgetを別クラスに分ける必要があり、Stateful Widgetの数が多くなります。
しかし、大規模なアプリでなければ、Widgetのクラス分けは大まかで良く、多少、クラス内に再描画不要なWidgetが含まれていても問題ないと思われます。
実際、クラス分けを細かくした場合と、大まかにした場合とで再描画速度の違いを比べましたが、Flutterの処理速度は早いので、速度差は体感できませんでした。
こちらの記事でも、大変分かりやすく解説くださっており、再描画によるコスト(負荷)はさほど重要ではなく、むしろシッカリconstをつけておくことの方が重要とされています(大変勉強になりました!)。
最後に
今回はシンプルなコードだったので、MVVMを用いることで、他クラスからのsetStateによる再描画を実現できましたが、複雑化すると不具合も出そうなので、引き続き検証したいと思います。
どなたかのご参考になれば幸いです。
最後までお読みいただき、ありがとうございました。
個人アプリ開発で役立ったもの
おすすめの学習教材
\超初心者向けでオススメな元Udemyの講座/
\キャンペーン時を狙えば安価で網羅的な内容が学べる(日本語訳あり)/
\Gitの基礎について無料で学べる/
おすすめの学習書籍
\実用的。image_pickerに関してかなり助けられた/
\Dartの基礎文法を素早くインプットできる/









コメント