🏋️

【Flutter】コードが長い、、短くしたい、、じゃあ共通化!!!

2023/09/21に公開

概要

flutterのコードをコンポーネントで切り分け、
共通化するといった基本的な手順について説明する記事になります!
分かりづらい点や、誤り等ございましたら、コメントやX(Twitter)のDMでご指摘いただけるとありがたいです。
この記事がどなたかの参考になれば幸いです!

どんな人向け?

flutter初心者、flutterを勉強し始めたけど、「コードが冗長でモチベが削がれる」、「もっと綺麗に書きたい」と思っている方向けです!


環境

  • Flutter version 3.3.5

本題

例えばこんな画面を作りました!


そしたらこんなコードになりますよね〜〜 (共通化前)

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

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    final emailTextController = TextEditingController();
    final passwordTextController = TextEditingController();

    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: const Text(
          'Sign In',
          style: TextStyle(
              color: Colors.blueGrey,
              fontSize: 24,
              fontWeight: FontWeight.bold),
        ),
        elevation: 0,
        backgroundColor: Colors.white,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: TextFormField(
                controller: emailTextController,
                decoration: InputDecoration(
                  labelText: 'Email',
                  floatingLabelBehavior: FloatingLabelBehavior.always,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
              child: TextFormField(
                controller: passwordTextController,
                decoration: InputDecoration(
                  labelText: 'Password',
                  floatingLabelBehavior: FloatingLabelBehavior.always,
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(12),
                  ),
                ),
              ),
            ),
            Container(
              padding: const EdgeInsets.all(16),
              width: double.infinity,
              child: ElevatedButton(
                style: ElevatedButton.styleFrom(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(16),
                  ),
                ),
                onPressed: () {},
                child: const Text('Sign In'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

うーん画面の描画の割にコードちょっと長いし、同じようなコードある、、

ということで

似たようなコードを共通化しよう!!!

基本的には共通化はWidgetに対してすることが多いです!
ただ関数やスタイル、文字列に対してもすることが可能です!

今回はWidgetを例に共通化していきます!

先ほどのコードに戻るとColumn内にPaddingで囲まれたTextFormFieldが二つあるのがわかりますね〜
この部分を共通化していきます!!!!

sign_in_page.dart
Padding(
  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  child: TextFormField(
    controller: emailTextController,
    decoration: InputDecoration(
      labelText: 'Email',
      floatingLabelBehavior: FloatingLabelBehavior.always,
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
      ),
    ),
  ),
),
Padding(
  padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
  child: TextFormField(
    controller: passwordTextController,
    decoration: InputDecoration(
      labelText: 'Password',
      floatingLabelBehavior: FloatingLabelBehavior.always,
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(12),
      ),
    ),
  ),
),

ここで別ファイルを作成し、記述する

どこにファイルを作るか(自己流)

自分はsign_inフォルダの中に、componentsフォルダとsign_in_page.dartファイルを作成します。
componentsフォルダ内にはsign_in_page.dartファイル内で共通化したWidgetのファイルなどを入れます!

まずはtext_field_widget.dartという名前でファイルを作成し、stlsショートカットでStatelessWidgetのスニペットを記述していきます!

作成したファイル内に先ほどのPaddingで囲まれた二つTextFormFieldのうち一つをreturnに渡します!

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

class TextFieldWidget extends StatelessWidget {
  const TextFieldWidget({super.key});

  
  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+      child: TextFormField(
+        controller: passwordTextController,
+        decoration: InputDecoration(
+          labelText: 'Password',
+          floatingLabelBehavior: FloatingLabelBehavior.always,
+          border: OutlineInputBorder(
+            borderRadius: BorderRadius.circular(12),
+          ),
+        ),
+      ),
+    );
  }
}

ここまで終わった人はエラーが1箇所出ているはずです!

なんのエラーかというと、
指定されているオブジェクト(emailTextControllerかpasswordTextController)がファイル内に存在しないよーと言われています

じゃあ定義してあげればいいじゃん

ではなくて”引数を持たせる”ということをしていきます!

引数

下記のようにコンストラクタに引数を渡していきます
エラー部分も今定義した引数を使うように書き換えましょう!

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

class TextFieldWidget extends StatelessWidget {
  const TextFieldWidget({
    super.key,
+    required this.textEditingController,
  });
+  final TextEditingController textEditingController;

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: TextFormField(
+        controller: textEditingController,
        decoration: InputDecoration(
          labelText: 'Password',
          floatingLabelBehavior: FloatingLabelBehavior.always,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
    );
  }
}

勘が良い方はもう気づいているかもしれませんが、今作ったWidgetをsign_in_pageで使っていきます!

使用方法

ということでsign_in_page.dartファイルでTextFieldWidgetを使っていきます!

下記のように元々あったPaddingで囲まれた二つTextFormFieldTextFieldWidgetに置き換えましょう!

そしたら最後にTextEditingControllerを渡してリロード!!

sign_in_page.dart
import 'package:flutter/material.dart';
import 'package:example_app/text_field_widget.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    final emailTextController = TextEditingController();
    final passwordTextController = TextEditingController();

    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: const Text(
          'Sign In',
          style: TextStyle(
              color: Colors.blueGrey,
              fontSize: 24,
              fontWeight: FontWeight.bold),
        ),
        elevation: 0,
        backgroundColor: Colors.white,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
+            TextFieldWidget(textEditingController: emailTextController),
-            Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-              child: TextFormField(
-                controller: emailTextController,
-                decoration: InputDecoration(
-                  labelText: 'Email',
-                  floatingLabelBehavior: FloatingLabelBehavior.always,
-                  border: OutlineInputBorder(
-                    borderRadius: BorderRadius.circular(12),
-                  ),
-                ),
-              ),
-            ),
+            TextFieldWidget(textEditingController: passwordTextController),
-            Padding(
-              padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
-              child: TextFormField(
-                controller: passwordTextController,
-                decoration: InputDecoration(
-                  labelText: 'Password',
-                  floatingLabelBehavior: FloatingLabelBehavior.always,
-                  border: OutlineInputBorder(
-                    borderRadius: BorderRadius.circular(12),
-                  ),
-                ),
-              ),
-            ),
            Container(
              padding: const EdgeInsets.all(16),
              width: double.infinity,
              child: ElevatedButton(
                style: ElevatedButton.styleFrom(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(16),
                  ),
                ),
                onPressed: () {},
                child: const Text('Sign In'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

あれ?なんか二つパスワード(メール)のWidgetあるじゃん、、

ということで最後はご自分で直してみてください!

問題

Q. 二つEmail(Password)と書いてある部分を修正するにはlabelTextというプロパティを修正する必要があります。(大ヒント)
text_field_widget.dartファイルを修正してEmailとPasswordという文字列をそれぞれ表示してください。

答え
text_field_widget.dart
import 'package:flutter/material.dart';

class TextFieldWidget extends StatelessWidget {
  const TextFieldWidget({
    super.key,
    required this.textEditingController,
+    required this.labelText,
  });
  final TextEditingController textEditingController;
+  final String labelText;

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: TextFormField(
        controller: textEditingController,
        decoration: InputDecoration(
+          labelText: labelText,
-	  labelText: 'Password',
          floatingLabelBehavior: FloatingLabelBehavior.always,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
    );
  }
}

sign_in_page.dart
import 'package:flutter/material.dart';
import 'package:example_app/text_field_widget.dart';

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context) {
    final emailTextController = TextEditingController();
    final passwordTextController = TextEditingController();

    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: const Text(
          'Sign In',
          style: TextStyle(
              color: Colors.blueGrey,
              fontSize: 24,
              fontWeight: FontWeight.bold),
        ),
        elevation: 0,
        backgroundColor: Colors.white,
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextFieldWidget(
              textEditingController: emailTextController,
+              labelText: 'Email',
            ),
            TextFieldWidget(
              textEditingController: passwordTextController,
+              labelText: 'Password',
            ),
            Container(
              padding: const EdgeInsets.all(16),
              width: double.infinity,
              child: ElevatedButton(
                style: ElevatedButton.styleFrom(
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(16),
                  ),
                ),
                onPressed: () {},
                child: const Text('Sign In'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

応用(?)編

①引数にデフォルト値を設定する

例えばobscureTextという入力された値を隠すか隠さないかのboolean型のプロパティがあります。
EmailのWidgetではfalseに、PasswordのWidgetではtrueにしたいです。
下記のようにコンストラクタからrequiredを外し、値に初期値を入れることで、呼び出し先でtrueにしたい場合のみ入力するだけで良いようになります!

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

class TextFieldWidget extends StatelessWidget {
  const TextFieldWidget({
    super.key,
    required this.textEditingController,
    required this.labelText,
+    this.obscureText = false,
  });
  final TextEditingController textEditingController;
  final String labelText;
+  final bool obscureText;

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: TextFormField(
+        obscureText: obscureText,
        controller: textEditingController,
        decoration: InputDecoration(
          labelText: labelText,
          floatingLabelBehavior: FloatingLabelBehavior.always,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
          ),
        ),
      ),
    );
  }
}

sign_in_page.dart
TextFieldWidget(
textEditingController: emailTextController,
labelText: 'Email',
// 何も指定しないと初期値のfalseが入る
),
TextFieldWidget(
textEditingController: passwordTextController,
labelText: 'Password',
+obscureText: true,
),

②引数をオプショナルにする

例えばsuffixIconというTextFormFieldの右側にアイコンを表示させるプロパティがあります。
今回はPasswordのWidgetに目のアイコンを追加して、よくあるパスワードの表示非表示を切り替えられそうなアイコンを表示していきます!
下記のようにsuffixIconの型に?をつけることで Nullable型(Null許容型)にすることができます!

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

class TextFieldWidget extends StatelessWidget {
  const TextFieldWidget({
    super.key,
    required this.textEditingController,
    required this.labelText,
    this.obscureText = false,
+    this.suffixIcon,
  });
  final TextEditingController textEditingController;
  final String labelText;
  final bool obscureText;
+  final Widget? suffixIcon;

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      child: TextFormField(
        obscureText: obscureText,
        controller: textEditingController,
        decoration: InputDecoration(
          labelText: labelText,
          floatingLabelBehavior: FloatingLabelBehavior.always,
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
          ),
+          suffixIcon: suffixIcon,
        ),
      ),
    );
  }
}


sign_in_page.dart
TextFieldWidget(
textEditingController: emailTextController,
labelText: 'Email',
),
TextFieldWidget(
textEditingController: passwordTextController,
labelText: 'Password',
obscureText: true,
+ suffixIcon: const Icon(Icons.visibility_off),
),

以上

いかがだったでしょうか??
とても簡単な内容にはなっていますが、知らないと冗長なコードが多くなってしまうというデメリットがあると思いますので、知らなかった方や復習したい方に届けば幸いです^^
記事が良かったと思ってくださった方はいいねを送っていただけると励みになります!!

ソースコード(短いですが)

https://github.com/Haru-Kobayashi073/basis_components_zenn



たまにX(旧twitter)でflutterのことをぼやいているので、ぼやき仲間になってくださる方はフォローしてくださると嬉しいです!!!

Discussion