Flutter で Widget, Class, ファイルを分割するチュートリアル

概要

やりたいこと

flutter create 時に作成されるカウンターアプリを Class と Widget 分けていきます。

下記を参考にさせていただきました。追加で NullSafety などを対応していく。

Flutter 初期アプリ自体の解説はこのブログが神。

環境

Mac Big Surに構築。

$ flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.8.1, on macOS 11.4 20F71 darwin-x64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 13.2.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 4.2)
[✓] VS Code (version 1.63.2)
[✓] Connected device (2 available)

Widget, Class, ファイルを分割

初期

以下が flutter create 時の lib/main.dart。これを改造する。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

body を分ける

_MyHomePageState が大きい。body を別 Class にする。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: MyHomePageBody(counter: this._counter),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class MyHomePageBody extends StatelessWidget {
  MyHomePageBody({Key? key, this.counter}) : super(key: key);
  final int? counter;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Text('You have pushed the button this many times:'),
          Text('$counter', style: Theme.of(context).textTheme.headline4),
        ],
      ),
    );
  }
}

floatingActionButton を分ける

まだ _MyHomePageState 大きいです。 floatingActionButton も別 Class にしましょう。

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: MyHomePageBody(counter: this._counter),
      floatingActionButton: MyHomePageFab(incrementCounter: this._incrementCounter), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class MyHomePageFab extends StatelessWidget {
  MyHomePageFab({Key? key, this.incrementCounter}) : super(key: key);
  final VoidCallback? incrementCounter;

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: this.incrementCounter,
      tooltip: 'Increment',
      child: Icon(Icons.add),
    );
  }
}

ファイルを分ける

分け方(ディレクトリ構成、デザインパターン)は様々。今回は単純に class ごとに分ける。

分ける前。

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: MyHomePageBody(counter: this._counter),
      floatingActionButton: MyHomePageFab(incrementCounter: this._incrementCounter), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class MyHomePageBody extends StatelessWidget {
  MyHomePageBody({Key? key, this.counter}) : super(key: key);
  final int? counter;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          const Text('You have pushed the button this many times:'),
          Text('$counter', style: Theme.of(context).textTheme.headline4),
        ],
      ),
    );
  }
}

class MyHomePageFab extends StatelessWidget {
  MyHomePageFab({Key? key, this.incrementCounter}) : super(key: key);
  final VoidCallback? incrementCounter;

  @override
  Widget build(BuildContext context) {
    return FloatingActionButton(
      onPressed: this.incrementCounter,
      tooltip: 'Increment',
      child: Icon(Icons.add),
    );
  }
}

作成した2つの class ごとに、ファイルを分ける。

  • libs/MyHomePageBody.dart
  • libs/MyHomePageFab.dart

libs/MyHomePageBody.dart

import 'package:flutter/material.dart';

class MyHomePageBody extends StatelessWidget {
  // 略
}

libs/MyHomePageFab.dart

import 'package:flutter/material.dart';

class MyHomePageFab extends StatelessWidget {
  // 略
}

main.dart でファイルをインポートする。

import 'MyHomePageBody.dart';
import 'MyHomePageFab.dart';

main.dart と app.dart に分ける

main.dart。ここでは最低限のことしかしません。

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

void main() {
  runApp(const MyApp());
}

app.dart。アプリの基盤となる部分を定義。

  • MaterialApp の宣言
  • 状態の管理(いずれ別に)
import 'package:flutter/material.dart';
import 'MyHomePageBody.dart';
import 'MyHomePageFab.dart';

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: MyHomePageBody(counter: this._counter),
      floatingActionButton: MyHomePageFab(incrementCounter: this._incrementCounter), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

test/widget_test.dart で MyApp を呼び出しているため、import 先を変更。

//import 'package:hello/main.dart';
import 'package:hello/app.dart';

これでエラーなく起動されます。

まとめ

コードは Github にまとめてあります。

エラー

The parameter ‘counter’ can’t have a value of ‘null’ because of its type, but the implicit default value is ‘null’. (Documentation)

パラメータ「counter」は、そのタイプのために「null」の値を持つことはできませんが、暗黙のデフォルト値は「null」です。 (ドキュメンテーション)

元の記述。NullSafety な記述。

  MyHomePageBody({Key? key, this.counter}) : super(key: key);
  final int counter;

null を許容。

  MyHomePageBody({Key? key, this.counter}) : super(key: key);
  final int? counter;

The argument type ‘Function?’ can’t be assigned to the parameter type ‘void Function()?’.

引数の型「関数?」パラメータタイプ 'void Function()?'に割り当てることはできません。

こう書いてた。

  MyHomePageFab({Key? key, this.incrementCounter}) : super(key: key);
  final Function? incrementCounter;

VoidCallback に変更。

  MyHomePageFab({Key? key, this.incrementCounter}) : super(key: key);
  final VoidCallback? incrementCounter;

参考

Flutterに入門して疑問に思ったことと、そのとき調べたこと

The argument type ‘Function’ can’t be assigned to the parameter type ‘void Function()?’ after null safety

Flutter はじめの一歩

コメント

タイトルとURLをコピーしました