はじめてのProvider - Flutter初心者がカウンターアプリを読み解いてみた
2024-05-24
azblob://2024/05/27/eyecatch/2024-05-27-flutter-provider-leading000.jpg

はじめに

こんにちは、2024年入社の林です。

今回、Flutterのキャッチアップのためにコードリーディングを行いました。


コードリーディングしたもの


今回のコードは以下で公開されています。
https://github.com/flutter/samples/tree/main/provider_counter


このアプリはProviderで状態管理をし、右下のボタンを押すことで画面中央の数値が1ずつ足されていくカウントアップアプリです。

Providerとは



Providerとは、Flutterで状態管理をする際に使用するパッケージです。


Providerの使い方


flutterプロジェクトでパッケージをインストール

flutter pub add provider

実際にコードリーディング


コードの全体はこちらになります。

import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:window_size/window_size.dart';
void main() {
setupWindow();
runApp(
   ChangeNotifierProvider(
     child: const MyApp(),
 ),
);
}
const double windowWidth = 360;
const double windowHeight = 640;
void setupWindow() {
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
 WidgetsFlutterBinding.ensureInitialized();
 setWindowTitle('Provider Counter');
 setWindowMinSize(const Size(windowWidth, windowHeight));
 setWindowMaxSize(const Size(windowWidth, windowHeight));
 getCurrentScreen().then((screen) {
  setWindowFrame(Rect.fromCenter(
   center: screen!.frame.center,
   width: windowWidth,
   height: windowHeight,
  ));
 });
}
}
class Counter with ChangeNotifier {
int value = 0;
void increment() {
 value += 1;
 notifyListeners();
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
 return MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
   primarySwatch: Colors.blue,
  ),
  home: const MyHomePage(),
 );
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
 return Scaffold(
  appBar: AppBar(
   title: const Text('Flutter Demo Home Page'),
  ),
  body: Center(
   child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
     const Text('You have pushed the button this many times:'),
     Consumer<Counter>(
      builder: (context, counter, child) => Text(
       '${counter.value}',
       style: Theme.of(context).textTheme.headlineMedium,
      ),
     ),
    ],
   ),
  ),
  floatingActionButton: FloatingActionButton(
   onPressed: () {
    var counter = context.read<Counter>();
    counter.increment();
   },
   tooltip: 'Increment',
   child: const Icon(Icons.add),
  ),
 );
}
}
 

import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:window_size/window_size.dart';
void main() {
setupWindow();
runApp(
  ChangeNotifierProvider(
  child: const MyApp(),
 ),
);
}

まず、main関数内ではsetupWindow関数を呼び出しています。
runApp()の中では、ChangeNotifierProviderを呼び出しています。
詳しくはChangeNotifierProviderの定義の部分で説明します。

続いてのコード

const double windowWidth = 360;
const double windowHeight = 640;
void setupWindow() {
 if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
   WidgetsFlutterBinding.ensureInitialized();
   setWindowTitle('Provider Counter');
   setWindowMinSize(const Size(windowWidth, windowHeight));
   setWindowMaxSize(const Size(windowWidth, windowHeight));
   getCurrentScreen().then((screen) {
     setWindowFrame(Rect.fromCenter(
       center: screen!.frame.center,
       width: windowWidth,
       height: windowHeight,
     ));
   });
 }
}

const double windowWidth = 360;
const double windowHeight = 640;

ここではsetupWindow関数を定義しています。

setupWindow関数はデスクトップアプリ環境に対して処理しています。

WidgetsFlutterBinding クラスはWidgetsBinding インスタンスを生成するためのものです。
WidgetsBinding とはSystemChannels を利用してFlutterEngine と通信の設定やロケールのシステムの設定変更などを行っています。


FlutterEngine とFlutterFramework を通信できるようにしているということで、アプリ実行前に行うことにより、FlutterEngine と通信できるようになるみたいです。
WidgetsFlutterBinding.ensureInitialized()の行はFlutterEngine の機能を利用したい場合にコールします。
FlutterEngine の機能とは、プラットフォームの画面の向きやロケールです。利用するプラグインによっては、runApp()の前に動作しているとこの設定が事前に必要になります。


ensureInitialized() は、WidgetsFlutterBinding がまだ初期化されていない場合にのみ、初期化処理を実行します。すでに初期化されている場合は、何も行われません。
setWindowTitle()メソッドでウィンドウのタイトルをProvider Counter に設定しています。
setWindowMinSize()メソッドではウィンドウの最小サイズを指定しています。
setWindowMaxSize()メソッドではウィンドウの最大サイズを指定しています。


つまり、この部分ではデスクトップアプリ用にFlutterEngine とFlutterFrameWork 初期化し、ウィンドウのタイトル、サイズ、位置を設定するための処理が実行されています。

続いてのコード

class Counter with ChangeNotifier {
 int value = 0;
 void increment() {
   value += 1;
   notifyListeners();
 }
}

ここではCounter クラスを定義しています。


このクラスはアプリケーション内で状態を管理するためのクラスです。
Counter クラスはChangeNotifier をMixin しています。
ChangeNotifier はオブジェクトの変更を監視しCounter クラスの変更を監視し、リスナーに通知します。
class 内の値が変更された場合に、notifyListeners()はChangeNotifier を使用しているclass に使用できる関数で値が持っていることを知らせて更新させることができ
ます。


つまり、notifyListeners()メソッドを呼び出すことで、変更を監視しているウィジェットに通知し、再描画できます。

続いてのコード

class MyApp extends StatelessWidget {
 const MyApp({super.key});
 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     title: 'Flutter Demo',
     theme: ThemeData(
       primarySwatch: Colors.blue,
     ),
     home: const MyHomePage(),
   );
 }
}
class MyHomePage extends StatelessWidget {
 const MyHomePage({super.key});
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
       title: const Text('Flutter Demo Home Page'),
     ),
     body: Center(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: [
           const Text('You have pushed the button this many times:'),
           Consumer<Counter>(
             builder: (context, counter, child) => Text(
               '${counter.value}',
               style: Theme.of(context).textTheme.headlineMedium,
             ),
           ),
         ],
       ),
     ),
     floatingActionButton: FloatingActionButton(
       onPressed: () {
         var counter = context.read<Counter>();
         counter.increment();
       },
       tooltip: 'Increment',
       child: const Icon(Icons.add),
     ),
   );
 }
}

MyHomePageクラスはStatelessWidgetになっており、appbarやbodyプロパティやfloatingActionButtonが定義されています。


このウィジェットはアプリのメインの画面みたいなもので、Counterと連携してカウンターの値を表示してボタンタップによってカウンターの数値が足されていく機能を持っています。

特にProviderと関係性があるのはConsumerウィジェットです。
ConsumerはProviderからCounterを取得し更新されるとウィジェットが再描画されるようになっています。
今回では'${counter.value}'の部分を監視しています。
このConsumerウィジェットが'${counter.value}'を監視し、値が更新されるたびにbuilder関数が呼び出され、更新されたcounter.valueの値を使用してTextウィジェットを再構築し画面上に表示しています。

StatefulWidget だとsetState()が呼ばれたときに、そのStatefulWidget と、そのWidget の配下にあるWidget が再描画の対象になります。不要な再描画が発生することがあり、大規模なアプリではパフォーマンスが低下する可能性があります。
一方、Provider を使用すると今回のコードのようにConsumer で囲い、notifyListeners()で再描画されることで状態が変更された際に再描画が必要なWidget のみを再描画することができるので、不要な再描画を抑えることができます。

まとめ


以上がProviderを用いたCounterアプリのコードリーディングです。

FlutterEngineやFlutterFrameworkなど初めて聞く単語がありましたが、自分なりに理解できたのでこれからも頑張ります。