変更(イベント)通知(ProviderとChangeNotifier/Consumer)を用いる方法
画面更新をするプログラムにおいて、変更通知を用いる実装方法は他にも多々あり、非常に分かりやすいのが特徴です。Flutterにおいて広く使われている変更通知を管理する方法の1つがProviderパッケージを使う方法です。
インストール方法
Providerは標準パッケージではないため、リスト2のように追加インストールが必要です。
$flutter pub add provider -- pubspec.yamlへ記述する場合 -- dependencies: provider: ^6.0.5
全体の構造
まず、実際のコードを説明する前に全体の構造について示したのが図3です。ここでの大きな特徴は、画面処理とビジネスロジックが完全に分離していることです。また、細かい名称部分はこの後紹介するコード例と照らし合わせてください。
コード例
構造に対応した実装コード例がリスト3になります。
// (1) イベント通知を管理するオブジェクト
class WeekModel with ChangeNotifier {
bool _loading = false;
List<DateItem> _items = [];
// (2) 外部から参照できる変数
List<DateItem> get items => _items;
bool get loading => _loading;
// (3) ロード処理
void load() async{
_loading = true;
// (4) ローディング開始
Future.delayed(Duration.zero,(){
notifyListeners();
});
var res = await http.get(Uri.parse("http://192.168.1.1/list.json"),headers: {
'Accept' : 'application/json'
});
var json = jsonDecode(utf8.decode(res.bodyBytes));
_items = json['items'].map<DateItem>((e) => DateItem.fromJson(e)).toList();
_loading = false;
// (5) 処理完了通知
notifyListeners();
}
}
class ProviderScreen extends StatelessWidget{
const ProviderScreen({super.key});
@override
Widget build(BuildContext context) {
// (6) イベント管理する親ウィジェット
return ChangeNotifierProvider(
create: (_) => WeekModel(),
child: Scaffold(
appBar: AppBar(
title: const Text('サンプル(2)'),
actions: [
Builder(
builder: (context){
// (7) リロード処理
final week = Provider.of<WeekModel>(context, listen: true);
return IconButton(
onPressed: week.loading ? null : (){
week.load();
},
icon: const Icon(Icons.refresh));
})
],
),
body: const DateListScreen(),
),
);
}
}
class _DateListScreen extends State<DateListScreen>{
@override
void initState() {
// (8) ロード処理
final week = Provider.of<WeekModel>(context, listen: false);
week.load();
}
@override
Widget build(BuildContext context) {
// (9) イベントを受け取るウィジェット
return Consumer<WeekModel>(
builder: (context,week,child) {
return Stack(
children: [
Column(
children: _createListChildren(context, week),
),
if(week.loading)
_loadingWidget(context)
],
);
}
);
}
}
まず、最初にビジネスロジックをクラスを定義したのが(1)です。ProviderではChangeNotifierをmixinsしたクラスを作成する必要があります。そして、変数はState管理した同様の変数を(2)のように用意します。
そして実際にJsonデータを取得する処理が(3)です。このメソッドを他のプログラムからコールします。
他のプログラムに対してこのインスタンスが変更されたことを通知する処理が(4)(5)であり、その場合にはnotifyListeners()をコールすると該当する画面更新が実行されます。
ただし、(4)ではFuture.delayed()を使って通知を遅らせています。これは、initState()処理内で実行された場合のエラー回避方法の一例です。
この処理を入れないと、画面の表示処理が終わっていない間にnotifyListeners()が実行され、その結果、再描画しようとしてエラーが生じます。そのため、このように画面描写の次のFrameに処理をまわすようにしています。
この問題は、次に紹介するBLoCパターンでは考慮する必要がないような構造になっているので、実装の違いによる制限を知る上でも参考になるはずです。
ここまでが、データの変更通知をするためのクラスです。今回は同一コード内に記述しましたが、実際にはコードを分割することで画面処理とロジックを分離して管理することができます。
変更通知を受け取るウィジェットが、(6)のChangeNotifierProviderです。このウィジェットのcreateで先ほど作成したWeekModelのインスタンスを指定します。
このインスタンスにアクセスできるのは、このウィジェット配下のみになります。画面初期化やリロード処理などでインスタンスにアクセスしているのが(7)(8)です。listenプロパティがありますが、画面の変更通知が必要な場合にはtrueを指定し、必要ない場合にはfalseを指定します。(7)では処理中はボタンがdisabled状態になるようにしているのでtrueを指定する必要があり、initState()処理内では画面表示は行えないので、falseを指定することになります。
また、(7)のようにしてWeekModelインスタンスにアクセスする方法もありますが、(9)のようにConsumerウィジェットを使うことで変更通知が来た場合には自動的に画面を再描写することもできます。
そして、今回はinitState()で初期データをロードするためにStatefulWidgetとして定義が必要でしたが、Providerを使えば値の変更管理をStateで行う必要がないためStatelessWidgetとして定義することも可能になります。
