Flutter：Flavorごとに切り替えるFlavorBannerの帯をつけてみる

# 前置き
`今まで書いたFlutter関連の記事`をみたうえで、本稿の記事を書きます。

https://qiita.com/daidara_bo/items/dba67f8cadeb3582aa0e

# FlavorBannerとは？
https://pub.dev/packages/flavorbanner

こんな感じのことが実現できます。
右側には、`DEBUG`というFlutterお馴染みのバナーがついています。
左側には、新たに`DEV`というバナーがつけられていることがわかります。
![FlavorBanner](https://github.com/p02diada/flutter-flavor-banner/raw/master/resources/screenshot-1.png "FlavorBanner")

本稿でやりたいことは、
**alpha or productがわかるように、FlavorBannerをつけて、視覚的にすぐわかるようにしたい。**
ということです。

# カスタムクラスを作って諸々制御

こちらの記事が参考になりました。

https://medium.com/flutter-community/flutter-ready-to-go-e59873f9d7de

dart2.x系向けのソースコードになっているので多少の手直しが必要です。
まず、必要そうなフォルダ構成から〜。必要な箇所以外は割愛。

```
Fllutter_Project_Root
|
...
├── flavor
│   ├── alpha.json
│   └── product.json
├── flutter_launcher_icons-alpha.yaml
├── flutter_launcher_icons-product.yaml
├── lib
│   ├── main_window.dart // main window
│   ....
│   ├── custom_flavor_config.dart // 今回使用するファイル
│   ├── main.dart
│   ├── util
│   │   │
│   │   ....
│   │   └── enum_util.dart // 今回使用するファイル
│   └── views
│       └── custom_flavor_banner.dart // 今回使用するファイル
├── macos
│   ├── Flutter
│   │
│   ....
│   ├── Runner
│   │   ├── AppDelegate.swift
│   │   ├── Assets.xcassets
│   │   ...
│   ├── Runner.xcodeproj
│   │   │   
│   │   .... 
├── pubspec.lock
├── pubspec.yaml
└── windows
    ├── CMakeLists.txt
    ├── flutter
    │   ├── CMakeLists.txt
    │   ├── ephemeral
    │   ├── generated_plugin_registrant.cc
    │   ├── generated_plugin_registrant.h
    │   └── generated_plugins.cmake
    └── runner
        ├── CMakeLists.txt
        ├── Runner.rc
        ├── flutter_window.cpp
        ├── flutter_window.h
        ├── main.cpp
        ├── resource.h
        ├── resources
        │   ├── app_icon.ico
        │   └── app_icon_alpha.ico
        ├── utils.cpp
        ├── utils.h
        ├── win32_window.cpp
        └── win32_window.h
```
実は、Flutterは、MainWindow.dartとかいうファイル名はお作法として間違っていて、全部小文字で書くルールになっています。
ので、ビルド時にワーニングは出ます。一応無視するということも選べるは選べますがお作法にならって、小文字でファイル名を管理することにします。

https://dart.dev/tools/linter-rules/file_names

今回追加するファイルは、ここらへんです。
```
...
├── lib
│   ├── main_window.dart // main window
│   ....
│   ├── custom_flavor_config.dart // 今回使用するファイル
│   ├── main.dart
│   ├── util
│   │   │
│   │   ....
│   │   └── enum_util.dart // 今回使用するファイル
│   └── views
│       └── custom_flavor_banner.dart // 今回使用するファイル
...
```

- lib/custom_flavor_config.dart
    - flavorの設定値管理
- lib/util/enum_util.dart
    - enumの便利メソッド少ないので拡張
- lib/views/custom_flavor_banner.dart
    - bannerをカスタマイズ

こんな感じです。
ソースコードですが、基本的には参考にしたサイトを引用すればOKです。
dart3.x系には対応してないので、nullを許さなくなってしまうv3.x系でも動くように少しだけ直します。
また、参考サイトでは、左上にバナーが表示されていましたが、今回作成するアプリの都合上、右下にFlavorのタイプを表示するようにします。

```lib/custom_flavor_config.dart
import 'package:flutter/material.dart';

// flavor種類
enum Flavor { alpha, product }

extension ParseToString on Flavor {
  // enumの要素文字列だけ返す
  String toShortString() {
    return toString().split('.').last;
  }
}

// フレーバーごとに保持する値
class FlavorValues {
  FlavorValues({required this.baseUrl});
  final String baseUrl;
}

// フレーバーの設定値を管理するクラス
class CustomFlavorConfig {
  final Flavor flavor;
  final String name;
  final Color color;
  final FlavorValues values;
  static late CustomFlavorConfig _instance;

  CustomFlavorConfig._internal(this.flavor, this.name, this.color, this.values);
  static CustomFlavorConfig get instance {
    return _instance;
  }

  static bool isProduct() => _instance.flavor == Flavor.product;
  static bool isAlpha() => _instance.flavor == Flavor.alpha;

  factory CustomFlavorConfig(
      {required Flavor flavor,
      Color color = Colors.green,
      required FlavorValues values}) {
    _instance = CustomFlavorConfig._internal(
        flavor, flavor.toShortString(), color, values);
    return _instance;
  }
}
```
補足：enumは、Flavor.Alphaではなくて、Flavor.alphaとかにしないと怒られるみたいですね。
isAlpha()やisProduct()で、フレーバータイプを取得します。

**enumのextension**
Flutterは、まだまだenumの処理が少し不便です。そこに拡張を入れられるようになっているので、extensionとして入れ込んでいます。
やっているのは、Flavorのalpha or productを取るだけ、といえば取るだけです。

https://tech.excite.co.jp/entry/2022/07/06/160819


```lib/util/enum_util.dart
// enumの便利機能が少ないのでutil作って便利にする
class EnumUtil {
  // inputがlistの中に存在するかチェック 
  // @param values:enum.values
  // @param input:チェックしたい文字列
  // @return 合致してたらtrue
  static bool any(Iterable values, String input) {
    return values.any((element) => element.toString().split('.').last == input);
  }
}
```
補足：これは、enum.listを取得時に、Flavor.alpha, Flavor.productみたいな形で取得されるのですが、
ほしいのが、alpha or productだったので、文字列をごにょごにょして取り出しています。

```lib/views/custom_flavor_banner.dart
import 'package:flutter/cupertino.dart';
import '../custom_flavor_config.dart';

// FlavorBannerをカスタマイズ
// ignore: must_be_immutable
class CustomFlavorBanner extends StatelessWidget {
  final double defaultSize = 50;
  final Widget child;
  late BannerConfig bannerConfig;
  CustomFlavorBanner({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    // product版の時はバナー表示せずchildだけ返す
    if (CustomFlavorConfig.isProduct()) {
      return child;
    }

    bannerConfig = _getDefaultBanner();
    return Stack(
      children: <Widget>[child, _buildBanner(context)],
    );
  }

  BannerConfig _getDefaultBanner() {
    return BannerConfig(
        bannerName: CustomFlavorConfig.instance.name.toUpperCase(),
        bannerColor: CustomFlavorConfig.instance.color);
  }

  Widget _buildBanner(BuildContext context) {
    // flavorごとに描画するバナーを描く。デフォルト右下。(メニューと位置がかぶるから)
    return Align(
        alignment: Alignment.bottomRight,
        child: SizedBox(
          width: defaultSize,
          height: defaultSize,
          child: CustomPaint(
            painter: BannerPainter(
                message: bannerConfig.bannerName,
                textDirection: Directionality.of(context),
                layoutDirection: Directionality.of(context),
                location: BannerLocation.bottomEnd,
                color: bannerConfig.bannerColor),
          ),
        ));
  }
}

class BannerConfig {
  final String bannerName;
  final Color bannerColor;
  BannerConfig({required this.bannerName, required this.bannerColor});
}
```
補足：
flavorbannerを描画する時に、右下か左上かみたいなのを決めています。固定です。
ここらへんはカスタマイズしてもいいかな〜　と思います、好みに合わせて。
FlutterのmainWindowにメニューをいれた関係で左上だと邪魔だったので右下にしています。


```lib/main.dart
...
import 'util/enum_util.dart';

...
void main(List<String> args) async {
  //runAppの前なのでfluttereEngineの初期
  WidgetsFlutterBinding.ensureInitialized();
  runMainWindow();
}

//main画面実行
void runMainWindow() async {
  //アプリ情報の取得
  final String version = packageInfo.version;
  final String appName = packageInfo.appName;
  final String packageName = packageInfo.packageName;

  debugPrint('appName : $appName packageName = $packageName');

  // 環境変数取得しているだけ。今回の件とは関係ない
  const flavorType = String.fromEnvironment('flavor');
  debugPrint('flavor : $flavorType');

  // ここでflavorの値を取得
  var definedFlavor = EnumUtil.any(Flavor.values, flavorType);

  // 初期化時にFlavorConfigの設定をする
  // 何も設定してなければ、alpha挙動
  // URLはとりあえず適当に設定しちゃってます
  CustomFlavorConfig(
      flavor: definedFlavor ? Flavor.values.byName(flavorType) : Flavor.alpha,
      color: Colors.pink.shade100,
      values: FlavorValues(baseUrl: "https://local-testapp.com/api"));

  runApp(MainWindow(version: version));
}

```
少し割愛しました。　

```lib/main_window.dart
...
import 'views/custom_flavor_banner.dart';

...
class MainWindow extends StatelessWidget {
  const MainWindow({super.key, required this.version});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    ....

    return MaterialApp(
          title: getTitleFromVersion(version),
          theme: ThemeData(
            // This is the theme of your application.
            //
            // Try running your application with "flutter run". You'll see the
            // application has a blue toolbar. Then, without quitting the app, try
            // changing the primarySwatch below to Colors.green and then invoke
            // "hot reload" (press "r" in the console where you ran "flutter run",
            // or simply save your changes to "hot reload" in a Flutter IDE).
            // Notice that the counter didn't reset back to zero; the application
            // is not restarted.
            primarySwatch: ThemeDefine.themeColor,
          ),
          home: MainWindow(title: getTitleFromVersion(version)),
          // アプリのローカリゼーション設定
          localizationsDelegates: const [
            AppLocalizations.delegate,
            GlobalMaterialLocalizations.delegate,
            GlobalWidgetsLocalizations.delegate,
            GlobalCupertinoLocalizations.delegate,
          ],
          // アプリのサポートするロケール種別設定
          supportedLocales: const [
            Locale('ja', ''), //日本語
          ],
        );
  }

class MainWindowState extends State<MainWindow> {
....
  @override
  Widget build(BuildContext context) {
...
    var appLocalizations = AppLocalizations.of(context)!;

    return CustomFlavorBanner(
        child: Scaffold(
        body: Center(
          child: Text('Hello, Flutter!'),
        ),
    ));
  }
}
```
サンプルなので、色々割愛しています。必要に応じて引用してください。
たらないimportもあると思います。

で、こちらを実行すると

**alpha**
![スクリーンショット 2023-08-08 13.25.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988173/e56c7988-98eb-f486-5daa-8c7f26a89e6b.png)


**product**
![スクリーンショット 2023-08-08 13.26.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988173/91ca1344-696b-6520-4864-74e148f57de3.png)

alphaの方には、右下側にbannerが表示されていることがわかります。
状況に応じて、isAlpha()や、内部の設定を拡張していけば、alphaの時だけ行うデフォルト挙動などを管理することも可能となるでしょう。

# まとめ
alpha or productなど、ビルドの種類に応じて処理や設定を切り替える方法について紹介しました。Flutterは、ここらへんのビルド関連の自動化がまだまだ甘く、xcodeだったらschemeで切り替えるだけなのだが・・・。windowsだったら・・・・　という悩みが色々存在します。

英語記事だと出てくることもあったり、なかったり。でもdartのバージョンが上がるのも早いから、そのままだとビルドが通らないこともよくある。
windowsは対応してない、なんてこともよくある。
臨機応変に調べながら、有効か、無効か、検証していくのは良いと思います。

個人的には、GUIを構成する時に、()でめちゃくちゃ括るのがすごいだるいなあ、という気はします。xmlやxaml、yamlみたいな感じで、viewを構成できたら楽かな、というのはありますね。
pyqtみたいに、qtdesignerみたいなツールがあれば、GUIなどを作りやすくなるかな、と思います。
(もしかしてあったらごめんなさい）

以上、感想でした。
もう少し、ここら辺のビルド周り、自動化周りが楽になるといいですね〜。