Dart

23. FlutterでのCustomWidgetの作り方

今回はFlutterでCustomWidgetを作成する方法について学習します。
例えば、Text('文字列')というウィジェットを継承してカスタムなTextを作成したい場合などに使う用法ですね。
このやり方を知る前はFlutterでもTextを継承するだけだと思ったのですが、これは違うみたいです。

具体例

サンプルとして、AppBarとbodyにはColumnウィジェットが乗った簡単なレイアウトを作成しました。
Columnの中にはTextとRaisedButtonが乗っかっています。
さらに今回はStatelessWidgetを使っています。

main.dart

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

  int index = 0;

  void increment() {
    index = index + 1;
    print(index);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Customなウィジェットの作り方'),
        ),
        body: Center(
          child: Column(
            children: [
              Text('カスタムにしたいウィジェットです。$index'),
              RaisedButton(
                  child: Text('普通のボタン'),
                  onPressed: increment
              ),
            ],
          ),
        ),
      ),
    );
  }
}

こちらをビルドすると次のような画面が表示されます。

f:id:qed805:20200223142931p:plain

ビルドしたときの画面

これをもとにしてカスタムにしたいウィジェットですと書かれたTextウィジェットをカスタムにします。

カスタムなウィジェットを作成する

まず、libに新しいファイルとしてcustom_text.dartというファイル名で新規作成します。
新しいTextウィジェットを作成しますが、今回はStatelessWidgetを使うことにします。

作成したcustom_text.dartに次のコードを書きます。

custom_text.dart

import 'package:flutter/material.dart';

class CustomText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

しばらくはStatelessWidgetのテンプレートをこれにして学習します。

これでmain.dartにimportするとCustomTextが使えるようになります。
次にmain.dartのColumnに乗っているTextウィジェットをCustomTextに移動させます。

custom_text.dart

import 'package:flutter/material.dart';

class CustomText extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Text('カスタムにしたいウィジェットです。$index');
  }
}

これでTextウィジェットを移動させることができました。
ただし、この状態ではCustomTextにはindexの変数が存在しないのでビルドがエラーになって失敗してしまいます。

カスタムウィジェットのコンストラクタを作成

コンストラクタとは呼び出し元でインスタンスを生成するときに使うメソッドをイメージして頂くとだいたい意味が同じになります。
前項でindexの変数が存在しないことによるエラーを解消させるためにindexをプロパティとして持たせる必要があります。

custom_text.dart

import 'package:flutter/material.dart';

class CustomText extends StatelessWidget {
  /// 1. indexのプロパティを宣言する
  final int index;
  @override
  Widget build(BuildContext context) {
    return Text('カスタムにしたいウィジェットです。$index');
  }
}

ですが、まだコンストラクタがないのでエラーのままです。

コンストラクタがないことによるエラー

ここからはDartの文法の話しになりますのでDartを学習する必要がありますが、
簡単に解決法を述べると次のようなコンストラクタを書けば解消できます。

CustomText(this.index);

またDartには何種類か?の書き方が存在して

/// 1. クラス()
CustomText(this.index);

/// 2. クラス({})
CustomText({this.index});

/// 3. クラス({必須プロパティ})
CustomText({@required this.index});

のような書き方ができるそうです。

今回は特にこだわりはありませんので1を採用します。

custom_text.dart

import 'package:flutter/material.dart';

class CustomText extends StatelessWidget {
  /// 1. indexのプロパティを宣言する
  final int index;

  /// 2. CustomTextのコンストラクタを宣言する
  CustomText(this.index);

  @override
  Widget build(BuildContext context) {
    return Text('カスタムにしたいウィジェットです。$index');
  }
}

これでintのindexを渡せるカスタムなTextを生成できるようになりました。

それでは、main.dartに戻ります。
Textウィジェットを消してCustomTextに書き直します。

main.dart

import 'package:flutter/material.dart';
import 'package:quiz_app/custom_text.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

  int index = 0;

  void increment() {
    index = index + 1;
    print(index);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Customなウィジェットの作り方'),
        ),
        body: Center(
          child: Column(
            children: [
              CustomText(index),
              RaisedButton(
                  child: Text('普通のボタン'),
                  onPressed: increment
              ),
            ],
          ),
        ),
      ),
    );
  }
}

これでアプリをビルドしてみましょう。
画面表示が前回と同じであれば成功です。

f:id:qed805:20200223143108p:plain

これでStatelessWidgetを用いたカスタムなウィジェットの作り方が分かりました。

必須プロパティにするとどうなるか

最後にコンストラクタのときに@requiredのアノテーションがありましたね。
試しにcustom_text.dartを次のように変更してみましょう。

custom_text.dart

import 'package:flutter/material.dart';

class CustomText extends StatelessWidget {
  /// 1. indexのプロパティを宣言する
  final int index;

  /// 2. indexを必須プロパティに変更する
  CustomText({@required this.index});

  @override
  Widget build(BuildContext context) {
    return Text('カスタムにしたいウィジェットです。$index');
  }
}

するとmain.dartがエラーになってしまいます。

main.dartでCustomTextを生成するところを次のように変更します。

CustomText(index: index)

main.dart全体ではこのようになります。

import 'package:flutter/material.dart';
import 'package:quiz_app/custom_text.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {

  int index = 0;

  void increment() {
    index = index + 1;
    print(index);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Customなウィジェットの作り方'),
        ),
        body: Center(
          child: Column(
            children: [
              CustomText(index: index),
              RaisedButton(
                  child: Text('普通のボタン'),
                  onPressed: increment
              ),
            ],
          ),
        ),
      ),
    );
  }
}

ということで、@requiredをつけることでインスタンス生成時にそのプロパティがSwiftでいう引数ラベルのように
扱われることになります。
これが@requiredの効果でした。
で、私はSwiftに慣れているので引数ラベルがあったほうが読みやすいのですが、
それは頑張ることにします。

ということでカスタムウィジェットの作成方法については以上になります。
Flutterではできる限りウィジェットは分割して扱うのが流儀らしいのでFatなウィジェットを作らないように心がけないといけませんね。

ABOUT ME
tamappe
都内で働くiOSアプリエンジニアのTamappeです。 当ブログではモバイルアプリの開発手法について紹介しています。メインはiOS、サブでFlutter, Android も対応できます。 執筆・講演のご相談は tamapppe@gmail.com までお問い合わせください。