【Flutter】ボタンの配置・表示内容・文字サイズ・機能をファイルから割当てる

2024/04/08に公開

概要

ボタンの配置・表示内容・文字サイズ・機能(下画像のようなもの)をファイルから割当ててみました。

動機

Android向けの業務アプリを作成時に、各お客さん向けに機能は一緒だけどボタンの表示内容を変えて欲しい(業務運用に必要のため)と言われまして、設定ファイルを作成し対応したことがありました。お客さん毎にプログラムを作成するなんてことは管理上難しいですし、アプリ内に設定値を設けた場合製造部で出荷時に端末1台ずつ設定を変更してもらうことになると、(設定項目が増えて)製造部に文句を言われることになるので…
そのため、設定ファイルを読み込むことで画面構成を任意に変更できるようにし、お客さん毎に製造部の方で設定ファイルを作成し、それを各端末にアプリインストール時に一緒に設定ファイルを置くことで対応したという経緯がありました。Flutterでどのようしたらできるのかの確認をしてみたいということが動機になります。

パッケージ

https://pub.dev/packages/path_provider

実装

  1. ターミナルで下記を実行
 flutter pub add path_provider
 flutter pub get
  1. 設定ファイル
    下記のような設定ファイルを用意し、実行時に内部ストレージ内のandroid/data/プロジェクト名/files/setting_ini.txtに置きます。
setting_ini.txt

// タブ・ボタン設定サンプル

[TAB]
// 機能番号、項目名称、文字サイズ
// タブ機能番号 0:なし, 1:画面0へ, 2:画面1へ, 3:,
tab00=01,1ページ,16
tab01=02,2ページ,16
tab02=00
tab03=00

[PAGE0]
title=1ページ
// 機能番号、項目名称、文字サイズ
// 機能番号 0:なし, 1:プリント, 2:プリント, 3:プリント,
button00=01,ボタン1,24
button01=02,ボタン2,20
button02=03,ボタン3,16
button03=01,ボタン4,12
button04=01,abc\n123,20
button05=01,あいう\n一二三,20
button06=01,#*+,20
button07=00

[PAGE1]
title=2ページ
// 機能番号、項目名称、文字サイズ
// 機能番号 0:なし, 1:, 2:, 3:,
button00=01,ボタン1,20
button01=01,ボタン2,20
button02=01,ボタン3,20
button03=01,ボタン4,20
button04=01,ボタン5,20
button05=01,ボタン6,20
button06=01,ボタン7,20
button07=01,ボタン8,20

  1. 実装
main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:layout_sample3/first_page.dart';
import 'package:layout_sample3/setting.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([
    DeviceOrientation.landscapeLeft, // 横画面固定
    DeviceOrientation.landscapeRight
  ]);
  var setting = new Setting();
  setting.init();
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: FirstPage(),
    );
  }
}
first_page.dart
import 'package:flutter/material.dart';
import 'package:layout_sample3/second_page.dart';
import 'package:layout_sample3/setting.dart';

class FirstPage extends StatefulWidget {
  FirstPage({Key? key}) : super(key: key);

  @override
  State<FirstPage> createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
  static late TabInfo tabInfo;
  static late PageInfo pageInfo;
  static var setup = false;

  @override
  void initState() {
    super.initState();
    wait();
  }

  // wait
  void wait() async {
    while (!Setting.getInstance().getSetup()) {
      await new Future.delayed(new Duration(milliseconds: 100));
      // print("settingFile() setup=$setup.");
    }
    tabInfo = Setting.getInstance().getTabInfo();
    pageInfo = Setting.getInstance().getPageInfo_0();
    setup = true;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return setup
        ? Scaffold(
            appBar: AppBar(
                backgroundColor: const Color.fromARGB(255, 217, 217, 217),
                title: Text(
                  pageInfo.title,
                  style: TextStyle(color: const Color.fromARGB(255, 0, 37, 81)),
                )),
            backgroundColor: const Color.fromARGB(255, 0, 37, 81),
            body: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Container(
                  alignment: Alignment.center,
                  width: 150,
                  //color: const Color.fromARGB(255, 0, 37, 81),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: [
                      SizedBox(height: 20),
                      _myContainer(0),
                      _myContainer(1),
                      _myContainer(2),
                      _myContainer(3),
                    ],
                  ),
                ),
                Container(
                  child: SingleChildScrollView(
                    child: Column(
                      children: [
                        Row(
                          children: [
                            const SizedBox(width: 20),
                            _button(0),
                            _button(1),
                            _button(2),
                            _button(3),
                          ],
                        ),
                        Row(
                          children: [
                            const SizedBox(width: 20),
                            _button(4),
                            _button(5),
                            _button(6),
                            _button(7),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          )
        : Scaffold(
            body: Container(
              alignment: Alignment.center,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.start,
                children: [
                  CircularProgressIndicator(),
                  Text("ローディング中"),
                ],
              ),
            ),
          );
  }

  Container _myContainer(int no) {
    return (tabInfo.tab[no].action != 0)
        ? Container(
            margin: const EdgeInsets.all(8),
            width: 120,
            height: 45,
            //color: Colors.black,
            child: ElevatedButton(
              onPressed: () {
                _tabAction(no);
              },
              child: Text(
                tabInfo.tab[no].name,
                style: TextStyle(
                    fontSize: tabInfo.tab[no].size,
                    color: const Color.fromARGB(255, 0, 37, 81)),
              ),
              style: ElevatedButton.styleFrom(
                  primary: Color.fromARGB(255, 217, 217, 217)),
            ),
          )
        : Container(
            margin: const EdgeInsets.all(8),
            width: 120,
            height: 45,
          );
  }

  Container _button(int no) {
    return (pageInfo.button[no].action != 0)
        ? Container(
            margin: const EdgeInsets.all(10),
            width: 110,
            height: 110,
            child: ElevatedButton(
              onPressed: () {
                _buttonAction(no);
              },
              child: Text(pageInfo.button[no].name,
                  style: TextStyle(
                      fontSize: pageInfo.button[no].size,
                      color: const Color.fromARGB(255, 0, 37, 81))),
              style: ElevatedButton.styleFrom(
                  primary: Color.fromARGB(255, 217, 217, 217)),
            ),
          )
        : Container(
            margin: const EdgeInsets.all(10),
            width: 110,
            height: 110,
          );
  }

  // タブ押下時の各アクション
  void _tabAction(int no) {
    if (tabInfo.tab.length <= no) return;
    var action = tabInfo.tab[no].action;
    switch (action) {
      case 1:
        _tabAction1();
        break;
      case 2:
        _tabAction2();
        break;
      case 3:
        _tabAction3();
        break;
      case 0:
      default:
    }
  }

  void _tabAction1() {
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) {
        return FirstPage();
      }),
    );
  }

  void _tabAction2() {
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) {
        return SecondPage();
      }),
    );
  }

  void _tabAction3() {}

  // ボタン押下時の各アクション
  void _buttonAction(int no) {
    if (pageInfo.button.length <= no) return;
    var action = pageInfo.button[no].action;
    switch (action) {
      case 1:
        _buttonAction1(no);
        break;
      case 2:
        _buttonAction2(no);
        break;
      case 3:
        _buttonAction3(no);
        break;
      case 0:
      default:
    }
  }

  void _buttonAction1(int no) {
    print(pageInfo.button[no].name + "が押されました。");
  }

  void _buttonAction2(int no) {
    print(pageInfo.button[no].name + "が押されました。");
  }

  void _buttonAction3(int no) {
    print(pageInfo.button[no].name + "が押されました。");
  }
}
second_page.dart
import 'package:flutter/material.dart';
import 'package:layout_sample3/first_page.dart';
import 'package:layout_sample3/setting.dart';

class SecondPage extends StatefulWidget {
  SecondPage({Key? key}) : super(key: key);

  @override
  State<SecondPage> createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  static late TabInfo tabInfo;
  static late PageInfo pageInfo;
  static var setup = false;

  @override
  void initState() {
    super.initState();
    wait();
  }

  // wait
  void wait() async {
    while (!Setting.getInstance().getSetup()) {
      await new Future.delayed(new Duration(milliseconds: 100));
      // print("settingFile() setup=$setup.");
    }
    tabInfo = Setting.getInstance().getTabInfo();
    pageInfo = Setting.getInstance().getPageInfo_1();
    setup = true;
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return setup
        ? Scaffold(
            appBar: AppBar(
                backgroundColor: const Color.fromARGB(255, 217, 217, 217),
                title: Text(
                  pageInfo.title,
                  style: TextStyle(color: const Color.fromARGB(255, 0, 37, 81)),
                )),
            backgroundColor: const Color.fromARGB(255, 0, 37, 81),
            body: Row(
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Container(
                  alignment: Alignment.center,
                  width: 150,
                  //color: const Color.fromARGB(255, 0, 37, 81),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: [
                      SizedBox(height: 20),
                      _myContainer(0),
                      _myContainer(1),
                      _myContainer(2),
                      _myContainer(3),
                    ],
                  ),
                ),
                Container(
                  child: SingleChildScrollView(
                    child: Column(
                      children: [
                        Row(
                          children: [
                            const SizedBox(width: 20),
                            _button(0),
                            _button(1),
                            _button(2),
                            _button(3),
                          ],
                        ),
                        Row(
                          children: [
                            const SizedBox(width: 20),
                            _button(4),
                            _button(5),
                            _button(6),
                            _button(7),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          )
        : Scaffold(
            body: Container(
              alignment: Alignment.center,
              child: Column(
                mainAxisAlignment: MainAxisAlignment.start,
                children: [
                  CircularProgressIndicator(),
                  Text("ローディング中"),
                ],
              ),
            ),
          );
  }

  Container _myContainer(int no) {
    return (tabInfo.tab[no].action != 0)
        ? Container(
            margin: const EdgeInsets.all(8),
            width: 120,
            height: 45,
            //color: Colors.black,
            child: ElevatedButton(
              onPressed: () {
                _tabAction(no);
              },
              child: Text(
                tabInfo.tab[no].name,
                style: TextStyle(
                    fontSize: tabInfo.tab[no].size,
                    color: const Color.fromARGB(255, 0, 37, 81)),
              ),
              style: ElevatedButton.styleFrom(
                  primary: Color.fromARGB(255, 217, 217, 217)),
            ),
          )
        : Container(
            margin: const EdgeInsets.all(8),
            width: 120,
            height: 45,
          );
  }

  Container _button(int no) {
    return (pageInfo.button[no].action != 0)
        ? Container(
            margin: const EdgeInsets.all(10),
            width: 110,
            height: 110,
            child: ElevatedButton(
              onPressed: () {
                _buttonAction(no);
              },
              child: Text(pageInfo.button[no].name,
                  style: TextStyle(
                      fontSize: pageInfo.button[no].size,
                      color: const Color.fromARGB(255, 0, 37, 81))),
              style: ElevatedButton.styleFrom(
                  primary: Color.fromARGB(255, 217, 217, 217)),
            ),
          )
        : Container(
            margin: const EdgeInsets.all(10),
            width: 110,
            height: 110,
          );
  }

  // タブ押下時の各アクション
  void _tabAction(int no) {
    if (tabInfo.tab.length <= no) return;
    var action = tabInfo.tab[no].action;
    switch (action) {
      case 1:
        _tabAction1();
        break;
      case 2:
        _tabAction2();
        break;
      case 3:
        _tabAction3();
        break;
      case 0:
      default:
    }
  }

  void _tabAction1() {
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) {
        return FirstPage();
      }),
    );
  }

  void _tabAction2() {
    Navigator.pushReplacement(
      context,
      MaterialPageRoute(builder: (context) {
        return SecondPage();
      }),
    );
  }

  void _tabAction3() {}

  // ボタン押下時の各アクション
  void _buttonAction(int no) {
    if (pageInfo.button.length <= no) return;
    var action = pageInfo.button[no].action;
    switch (action) {
      case 1:
        _buttonAction1(no);
        break;
      case 2:
        _buttonAction2(no);
        break;
      case 3:
        _buttonAction3(no);
        break;
      case 0:
      default:
    }
  }

  void _buttonAction1(int no) {
    print(pageInfo.button[no].name + "が押されました。");
  }

  void _buttonAction2(int no) {
    print(pageInfo.button[no].name + "が押されました。");
  }

  void _buttonAction3(int no) {
    print(pageInfo.button[no].name + "が押されました。");
  }
}

setting.dart
import 'dart:io';

import 'package:path_provider/path_provider.dart';

class Setting {
  static Setting _instance = new Setting();
  static var _tabInfo = new TabInfo();
  static var _pageInfo_0 = new PageInfo();
  static var _pageInfo_1 = new PageInfo();
  static bool _setup = false;

  static Setting getInstance() {
    return _instance;
  }

  // 初期
  void init() {
    settingFile();
  }

  TabInfo getTabInfo() {
    return _tabInfo;
  }

  PageInfo getPageInfo_0() {
    return _pageInfo_0;
  }

  PageInfo getPageInfo_1() {
    return _pageInfo_1;
  }

  bool getSetup() {
    return _setup;
  }

  // 機能(アクション)番号の取得
  int _getAction(String data) {
    int ret = 0;
    try {
      ret = int.parse(data.substring(0, 2));
      // print("_getAction() ret=${ret}");
    } catch (e) {
      print("_getAction() error=${e.toString()}");
    }
    return ret;
  }

  // 項目名称の取得
  String _getName(String data) {
    String ret = "";
    int comma1 = 0, comma2 = 0;
    int fromIndex = 0;
    try {
      comma1 = data.indexOf(",");
      comma2 = data.indexOf(",", comma1 + 1);
      ret = data.substring(comma1 + 1, comma2);

      while ((fromIndex = _getLF(ret, fromIndex)) != -1) {
        // 改行コード
        ret = ret.substring(0, fromIndex) + "\n" + ret.substring(fromIndex + 2);
      }
      // print("_getName() ret=${ret}");
    } catch (e) {
      print("_getName() error=${e.toString()}");
    }
    return ret;
  }

  // 文字サイズの取得
  double _getSize(String data) {
    double ret = 0;
    int comma;
    try {
      comma = data.lastIndexOf(",");
      ret = double.parse(data.substring(comma + 1));
      // print("_getSize() ret=${ret}");
    } catch (e) {
      print("_getSize() error=${e.toString()}");
    }
    return ret;
  }

  // 改行用の文字が入っているか
  int _getLF(String data, int fromIndex) {
    int ret = -1;
    try {
      ret = data.indexOf("\\n", fromIndex);
    } catch (e) {
      // 改行なし
    }
    return ret;
  }

  // 情報の取り込み
  Future<bool> _capture(File file) async {
    int equal = 0; // 「=」の位置
    int no = 0; // 「tab**」or「button**」の**の番号
    String data; // 「tab**」or「button**」の=以降のデータ
    int section = 0; // 0:なし、1:タブ、2:ページ0、3:ページ1
    bool ret = true;
    // print("_capture() start.");

    if (await file.exists()) {
      try {
        var lines = await file.readAsLines();
        for (int i = 0; i < lines.length; i++) {
          // print("_capture() data:${lines[i]}");
          // 文字が入っていない時は読み飛ばし
          if ((lines[i].length < 1) || (lines[i].isEmpty)) {
            //print("_capture() 読み飛ばし.");
            continue;
          }
          // 5文字以下は読み飛ばし
          if (lines[i].length < 5) {
            //print("_capture() 5文字以下で読み飛ばし.");
            continue;
          }
          // コメントは読み飛ばし
          if (lines[i].substring(0, 2) == "//") {
            //print("_capture() コメント読み飛ばし.");
            continue;
          }
          // セクション確認
          //print("_capture() sectionWord=${sectionWord}");
          if (lines[i] == "[TAB]") {
            section = 1;
            // print("_capture() section=$section");
            continue;
          } else if (lines[i] == "[PAGE0]") {
            section = 2;
            // print("_capture() section=$section");
            continue;
          } else if (lines[i] == "[PAGE1]") {
            section = 3;
            // print("_capture() section=$section");
            continue;
          }

          // [TAB]
          if (section == 1) {
            String word = lines[i].substring(0, 3);
            //print("_capture() 項目設定 ${word}");
            if (word == "tab") {
              equal = lines[i].indexOf('=');
              no = int.parse(lines[i].substring(3, equal));
              data = lines[i].substring(equal + 1);
              // print("_capture() equal=$equal, no=$no, data=$data");
              int action = _getAction(data);
              String name = "";
              double size = 0;
              if (action != 0) {
                name = _getName(data);
                size = _getSize(data);
              }
              var tab = new ItemInfo(action, name, size);
              _tabInfo.tab.insert(no, tab);
              // print(
              //     "_capture() new tab[$no]: no=${_tabInfo.tab[no].action}, name=${_tabInfo.tab[no].name}, size=${_tabInfo.tab[no].size}");
            }
          }
          // [PAGE0]
          else if (section == 2) {
            String word = lines[i].substring(0, 6);
            // print("_capture() 項目設定 ${word}");
            if (word == "title=") {
              equal = lines[i].indexOf("=");
              data = lines[i].substring(equal + 1);
              _pageInfo_0.title = data;
              // print("_capture() title=${_pageInfo_0.title}");
            } else if (word == "button") {
              equal = lines[i].indexOf("=");
              no = int.parse(lines[i].substring(6, equal));
              data = lines[i].substring(equal + 1);
              // print("_capture() equal=$equal, no=$no, data=$data");
              int action = _getAction(data);
              String name = "";
              double size = 0;
              if (action != 0) {
                name = _getName(data);
                size = _getSize(data);
              }
              var button = new ItemInfo(action, name, size);
              _pageInfo_0.button.insert(no, button);
              // print(
              //     "_capture() new button[$no]: no=${_pageInfo_0.button[no].action}, name=${_pageInfo_0.button[no].name}, size=${_pageInfo_0.button[no].size}");
            }
          }
          // [PAGE1]
          else if (section == 3) {
            String word = lines[i].substring(0, 6);
            //print("_capture() 項目設定 ${word}");
            if (word == "title=") {
              equal = lines[i].indexOf("=");
              data = lines[i].substring(equal + 1);
              _pageInfo_1.title = data;
            } else if (word == "button") {
              equal = lines[i].indexOf("=");
              no = int.parse(lines[i].substring(6, equal));
              data = lines[i].substring(equal + 1);
              // print("_capture() equal=$equal, no=$no, data=$data");
              int action = _getAction(data);
              String name = "";
              double size = 0;
              if (action != 0) {
                name = _getName(data);
                size = _getSize(data);
              }
              var button = new ItemInfo(action, name, size);
              _pageInfo_1.button.insert(no, button);
              // print(
              //     "_capture() new button[$no]: no=${_pageInfo_1.button[no].action}, name=${_pageInfo_1.button[no].name}, size=${_pageInfo_1.button[no].size}");
            }
          }
        }
      } catch (e) {
        ret = false;
        // print("_capture() error=${e.toString()}");
      }
      // print("_capture() end.");
    } else {
      ret = false;
    }
    return ret;
  }

  // 設定ファイル読み込み
  Future<void> settingFile() async {
    // print("settingFile() start.");
    final directory = await getExternalStorageDirectory();
    File file = File('${directory?.path}/setting_ini.txt');

    if (await file.exists()) {
      if (await _capture(file)) {
        _setup = true;
      }
    }
    // print("settingFile() end.");
  }
}

class TabInfo {
  var tab = <ItemInfo>[];
}

class PageInfo {
  var title; // タイトル
  var button = <ItemInfo>[];
}

class ItemInfo {
  var action; // 機能番号
  var name; // 項目名称
  var size; // 文字サイズ

  ItemInfo(this.action, this.name, this.size);
}
  1. 実行
    実行するとボタンの配置・表示内容・文字サイズが反映された画面が表示されます。

setting_ini.txt

// タブ・ボタン設定サンプル

[TAB]
// 機能番号、項目名称、文字サイズ
// タブ機能番号 0:なし, 1:画面0へ, 2:画面1へ, 3:,
tab00=01,1ページ,16
tab01=02,2ページ,16
tab02=00
tab03=00

[PAGE0]
title=1ページ
// 機能番号、項目名称、文字サイズ
// 機能番号 0:なし, 1:プリント, 2:プリント, 3:プリント,
button00=01,ボタン1,24
button01=02,ボタン2,20
button02=03,ボタン3,16
button03=01,ボタン4,12
button04=01,abc\n123,20
button05=01,あいう\n一二三,20
button06=01,#*+,20
button07=00

[PAGE1]
title=2ページ
// 機能番号、項目名称、文字サイズ
// 機能番号 0:なし, 1:, 2:, 3:,
button00=01,ボタン1,20
button01=01,ボタン2,20
button02=01,ボタン3,20
button03=01,ボタン4,20
button04=01,ボタン5,20
button05=01,ボタン6,20
button06=01,ボタン7,20
button07=01,ボタン8,20

ボタンを押したときの機能については、first_page,second_page内のボタンアクション関数に記述することで、それぞれ役割に振り分けることが出来ました。

  // ボタン押下時の各アクション
  void _buttonAction(int no) {
    if (pageInfo.button.length <= no) return;
    var action = pageInfo.button[no].action;
    switch (action) {
      case 1:
        _buttonAction1(no);
        break;
      case 2:
        _buttonAction2(no);
        break;
      case 3:
        _buttonAction3(no);
        break;
      case 0:
      default:
    }
  }

  void _buttonAction1(int no) {
    print(pageInfo.button[no].name + "が押されました。");
  }

  void _buttonAction2(int no) {
    print(pageInfo.button[no].name + "が押されました。");
  }

  void _buttonAction3(int no) {
    print(pageInfo.button[no].name + "が押されました。");
  }

おわりに

以上、ボタンの配置・表示内容・文字サイズ・機能をファイルから割当てるという記事でした。Flutterを勉強し始めて拙いところが多々あり、記述に無駄や読みにくいところもあるとは思いますが、最後まで読んで頂きましてありがとうございました。
この記事では、外部の設定ファイルとしてテキストファイルで読込み実行しました。ですが本番環境で、製造部の方が設定することになるなら、エクセルファイル等で作成し、変更履歴の項目やエクセルからテキストに変換するマクロを作成したり、項目ごとに説明を入れたりする工夫を入れ、管理しやすいようにしないとですね。

参考

https://pub.dev/packages/path_provider

Discussion