質問するログイン新規登録
Flutter

Flutterは、iOSとAndroidのアプリを同じコードで開発するためのフレームワークです。オープンソースで開発言語はDart。双方のプラットフォームにおける高度な実行パフォーマンスと開発効率を提供することを目的としています。

設計

設計は、ソフトウェアやシステムを作る上での設計方針、仕様策定、アーキテクチャ選定などに関する投稿です。

クリーンアーキテクチャ

クリーンアーキテクチャは、レイヤードアキテクチャの一種でRobert C. Martin(Uncle Bob)が2012年に提唱したアーキテクチャーです。

意見交換

7回答

585閲覧

ユーザ設定管理クラスの設計はどのようにすべきですか。値とテキストデータ両方を保持したモデルを定義したい

dedede914

総合スコア63

Flutter

Flutterは、iOSとAndroidのアプリを同じコードで開発するためのフレームワークです。オープンソースで開発言語はDart。双方のプラットフォームにおける高度な実行パフォーマンスと開発効率を提供することを目的としています。

設計

設計は、ソフトウェアやシステムを作る上での設計方針、仕様策定、アーキテクチャ選定などに関する投稿です。

クリーンアーキテクチャ

クリーンアーキテクチャは、レイヤードアキテクチャの一種でRobert C. Martin(Uncle Bob)が2012年に提唱したアーキテクチャーです。

0グッド

3クリップ

投稿2025/07/23 22:35

0

3

(Flutter/Dartで)アプリのユーザー設定データを管理するクラス設計についてのベストプラクティスや、よく使用される設計を知りたいです。flutterで開発していますが、言語に依存しない設計思想でかまいません!!
ずっと個人でしかやってこず壁にぶち当たり、
保守性、拡張性、可読性を良くするため勉強し始めました
良ければみなさんの知見をご共有ください


以下は具体的に今私が直面している問題です。

実現したいこと

  1. 表示文字列と設定項目を外部ファイル化(テキストファイル、設定項目ファイルで設定を管理する)
  2. 1つのクラスでvalueと文字列データlabel,descriptionなどをすべて持つSettingItemクラスを、できれば定義したい
  3. Clean Architectureに準拠した設計
  4. 型安全性を保った設定管理
  5. 保守性・拡張性の確保
  6. ローカライズ対応(多言語表示、最優先ではない)
  7. UI表示の柔軟性(特殊配置とリスト配置の組み合わせ)

前半はsettingItemのクラス設計、後半はsettingItemをどうまとめて扱うかについてです。


採用した設計 value + enum key

  • SettingItemクラスではvalueと文字列を参照するkeyだけを保持
  • 実際の文字列はwidget内でキーを参照して取得
    色々考えた結果、今この方針で進めていますが完全には納得はしていません...

dart

1enum SettingKey with KeyEnumMixin { 2 darkMode, 3 textSize, 4 // ... 5} 6 7mixin KeyEnumMixin on Enum { 8 String get label => '${name}.label'; 9 String get description => '${name}.description'; 10} 11 12class SettingItem<T> { 13 final Enum settingKey; 14 final T value; 15 final T defaultValue; 16 17 String get labelKey => (settingKey as KeyEnumMixin).label; 18 String get descriptionKey => (settingKey as KeyEnumMixin).description; 19} 20 21// UI内 22final label = AppLocalizations.of(context)!.translate(settingItem.labelKey);

arb

1// テキストファイル 2// lib/l10n/intl_en.arb 3{ 4 "SettingKey.darkMode.label": "Dark Mode", 5 "SettingKey.darkMode.description": "Enable dark theme" 6 ... 7}

json

1// 設定項目ファイル 2// assets/settings_definitions.json 3[ 4 { 5 "key": "SettingKey.darkMode", 6 "labelKey": "SettingKey.darkMode.label", 7 "descriptionKey": "SettingKey.darkMode.description" 8 }, 9 { 10 "key": "SettingKey.language", 11 "labelKey": "SettingKey.language.label", 12 "descriptionKey": "SettingKey.language.description", 13 "options": ["en", "ja", "es"] 14 } 15 ... 16]

考慮した設計

1. metadata class + value class + acess key

dart

1class UserSetting { 2 final bool darkMode; 3 final int textSize; 4 5 dynamic getValue(String key) { 6 switch (key) { 7 case 'darkMode': 8 return darkMode; 9 case 'textSize': 10 return textSize; 11 } 12} 13 14class SettingMetadata { 15 final String key; 16 final SettingType type; 17 final String labelKey; 18 final String descriptionKey; 19 final List<String>? options; 20} 21 22class SettingsViewModel extends ChangeNotifier { 23 late List<SettingMetadata> metadata; 24 late UserSettings userSettings; 25 26 dynamic getValue(String key) => userSettings.getValue(key); 27} 28 29// UI内 30final meta = viewModel.metadata[index]; 31final value = viewModel.getValue(meta.key); 32final label = AppLocalizations.of(context)!.translate(meta.labelKey);
  • valueとmetadataで分割されているのがあまり納得いかない

2. メソッドでcontextを受け取る、関数型プロパティ

dart

1class SettingItem<T> { 2 final Enum settingKey; 3 final T value; 4 5 String label(BuildContext context) => AppLocalizations.of(context)!.translate(settingKey.label); 6 String description(BuildContext context) => AppLocalizations.of(context)!.translate(settingKey.description); 7}

dart

1class SettingItem<T> { 2 final T value; 3 final String Function(BuildContext)? showLabel; 4 final String Function(BuildContext)? showdescription; 5}

一番これが楽に扱えそうで好みだったが、依存関係がcleanarchitectureに違反してしまう

  • Model層がPresentation層(BuildContext)に依存してしまう

3. Extension + 純粋なModel

dart

1// Model層 2class SettingItem<T> { 3 final Enum settingKey; 4 final T value; 5} 6 7// Presentation層 8extension SettingItemLocalizations on SettingItem { 9 String label(BuildContext context) => AppLocalizations.of(context)!.translate(settingKey.name); 10}
  • extensionはBuildContextを持つためpresentation層に記述される。そのため、SettingMetadatalabel()を保持していることが一見わかりにくい

SettingItemのまとめ方

  • UIでは一部特殊な表示をし、他はリスト表示にしたい
    検討した選択肢
  1. List<SettingItem> settings
  2. Map<SettingKey, SettingItem> settings
  3. 個別フィールド(SettingItem darkMode; SettingItem language;

Map + 個別のハイブリッド + interable

dart

1class UserSetting { 2 final Map<MangaToolSettingKey, SettingItem> _settings; 3 4 SettingItem<bool> get darkMode => _getSetting<bool>(SettingKey.darkMode); 5 6 Iterable<SettingItem> get allSettings => _settings.values; 7}

よろしくお願いします

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

回答7

#1

u2025

総合スコア55

投稿2025/07/24 05:52

回答を思いついたので回答しました。
考慮した設計パターンが複雑でどういったパターンか、どういった文脈かは考えられていません。
またユーザー設定クラスといっていますが、ユーザー設定自体をどう扱うかということかなと思って回答します。間違っていれば読み飛ばして大丈夫です。
また、回答者はFlutterについて詳しくありません。
(言いたいことがうまくまとめられなかったのでAIに校正してもらっています)

どのようにデータを扱うかについて

設計パターンにおけるデータ管理の方針についてですが、まず重要なのは「データがどのように見られるか」を統一することです。
通常、Repository クラスでデータベースの SELECT 文をカプセル化し、上位層にデータを返しますが、この返すデータの形式を共通化しておくことで、取得元がAPIやファイルであっても、あるいはDBの種類が変更されたとしても、他の層はその違いを意識せずに済みます。

つまり、上位層は常に同じ形式・インタフェースでデータを扱えるため、処理を抽象化しやすく、結果として柔軟で保守性の高い設計になります。
なお、もし特殊なデータ構造を扱う必要がある場合、それは通常のデータと異なる性質を持つため、専用のパターンや仕組みを別途設けるべきです。

Clean Architectureに準拠した設計、保守性・拡張性の確保というのは要は一貫性が欲しいということですよね。

(この考え方はDBからデータを持ってくる処理のあと、ModelClassにマッピングするということを一貫してやればいいという話です。)

データをどのように保存するのかについて

上記のことから、他のデータがDBで管理されているのであればDBでの管理が一番一貫性があります。ただし、それがオーバースペックだったり列の変更が煩雑というのであれば、ファイルに保存して問題ないでしょう。
DataClassやHashMapをファイルやjsonにエクスポート、インポートする機能がなければ自作する必要があります。
この場合Class自体がスキーマになるため、変更もある程度容易であるといえます。


茶々入れ1

質問自体はユーザー設定データをどのように扱うのかという所なのですが具体的な要件を照らし合わせてユーザー設定データが実際には何を指すのかが質問からは正確に読み取ることが難しく回答にずれがあるかもしれません。

茶々入れ2

同じく個人開発で設計パターンに足を取られた経験がありますが、設計パターンとは何か進行中のプロジェクトに問題があって次に生かすためにこうしようというものであって、先に設計パターンを決めるというのは順序が逆です。
また、過度に込み入った設計は保守性が逆に低くなる上に、開発効率も悪くなり二軸で足を引っ張る可能性さえあります。

茶々入れ3

この質問はある種設計の投げやりで他人への依頼ではあるのですが、これ自体はとてもいいことだと私は考えています。パブリックな場所で意見を出し合うことはよりいいノウハウを残すことに貢献します。

茶々入れ4

案2でAppLocalizations.of(context)!.translate(settingItem.labelKey);
ってのをmodelに持たせたい動機がよくわからないのですが、そもそもとしてこれらはどういう構造を想定しているんだろう?私の想像しているものと根本的に違うんだろうか。

全体的にクラスの責務が不明確でUserSettingは何をするクラスなんですか?
Clean Architectureという話が出ていますが、何らかの設計パターンに従っていて責務がよくわからないクラスなどできるのだろうか。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#2

dedede914

総合スコア63

投稿2025/07/24 07:43

#1
回答ありがとうございます!頭の中でぐるぐるしていて正確な質問ができておらずすみません!
ザック理由と、ボイラーコードを減らすのと、テキストデータとvlueを持ったsettingItemクラスを作りたいという感じです
今一度整理しました↓↓

想定仕様

  • userSetting:
  1. 一般アプリの設定を想像してもらって大丈夫です。スマホアプリの設定、VScodeのsettings等。リストで一定パターンのUIの繰り返し。一部繰り返しではない特殊なUIも入れたい。
    例)
    • ダークモードのオンオフ
    • リモートdbとの同期間隔
  2. 特定機能の設定
    例)画像出力機能の圧縮率、拡張子

やりたかったこと

  • 設定データクラスを定義し、そのvalueと項目名や説明文を持たせたかった。管理しやすそう
    例)UserSettingクラスが以下の様なsettingItemクラスを複数所持みたいな

    dart

    1 darkMode=SettingItem(True,"ダークモード","UIを黒ベースで表示します") 2 darkMode.value=True 3 darkMode.label="ダークモード" 4 darkMode.description="UIを黒ベースで表示します"
  • 設定データに関する文字列(項目名や説明文)を別ファイルで管理。文字列を一括管理したい
    例) [説明文]: UIを黒ベースで表示します
    [項目名]:ダークモード 

これらをふまえて、

  • json形式で保持 → 文字列ファイル、(設定値が保存してある)ユーザー設定ファイル
  • jsonのkeyを指定するのに補間が働いてほしかったのでenum使用

問題

SettingItemにvalueと文字列を持たせ、ボイラーコードを減らすために(key)キーのみを渡すようにすると、Model内部にrepositoryが入ってしまう
...ということは、案Aのように文字列を渡せばこの場合は解決!?でも項目が増えるごとにどえらい量の記述が、、、
(簡単のためkeyにenumでなく文字列を渡しています)

dart

1// 案A 2final value=userSettingRepository.getValue("darkMode");//True 3final label=stringRepository.getValue("darkMode.label");//"ダークモード" 4final description=stringRepository.getValue("darkMode.description");//"UIを黒ベースで表示します" 5 6final darkMode=SettingItem(value,label,description);

dart

1//案B 2final value=userSettingRepository.getValue("darkMode");//True 3final darkMode=SettingItem(value,"darkMode",stringRepository); 4 5class SettingItem<T> { 6  final T value; 7  final String settingKey; 8  final StringRepository stringRepository; 9 10  SettingItem({required this.settingKey, required this.value, required this.stringRepository }); 11  12  String get label => stringRepository.getValue("${settingKey}.label"); 13  String get description => stringRepository.getValue("${settingKey}.description"); 14}

darkModeならdarkMode.label, darkMode.description
textSizeならtextSize.label, textSize.description
のようにどの項目かによって必要なkeyは決定している。そもそも、文字列データはユーザーが変更するものではなく最初から決まっている定数なので、valueのようにちゃんと取得して渡す必要を感じなかった

AppLocalizations.of(context)!.translate(settingItem.labelKey); について

これはFlutterの多言語対応できるライブラリを使用しています。端末の言語設定によってtext_jaoanese.arb, text_english.arbなど言語ごとに参照するファイルを切り替えてくれるやつです。
ここで使われているcontextというやつがデータバインディングなどに必要なUI層でしか使えないflutter特有のやつで、これがあるから直接model内にかけないなというやつでした。

書いてて思いましたが、案Aのように一回外で値を全部取得してからモデルに渡せば一応解決はしますね。でもめちゃくちゃボイラーコードが増えていやだなという感じです

長々すみません!

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#3

dedede914

総合スコア63

投稿2025/07/24 07:57

#1
よくよく考えた結果、私が聞きたかったのは、
labelテキスト、descriptionテキストと共にvalueを持つクラスをどのように管理するか?
かもしれないです
あとプラスで、以下のようにUI表示用文字列を別で管理する場合など、keyと実際の値とどちらをクラスに持たせるか
ですかね

json

1// 表示用テキストファイル 2[ 3{ 4 "SettingKey.darkMode.label": "ダークモード", 5 "SettingKey.darkMode.description": "UIを黒ベースで表示します" 6}, 7 ... 8]

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#4

u2025

総合スコア55

投稿2025/07/24 08:54

うーんなるほど。返信ありがとうございます。一度消したのですがこっちの観点も生きているのか。文章だと煩雑だから通話したい笑
一度考えた内容をもう一度書き起こしているので詳細な部分で発言に抜けがあるかもしれませんが、それについてはそちらの反応をみて補足等させていただきたいです。

まず、データ構造から考えたいのですが、

darkModeがyesかNoかという設定がまずあるということですよね。
flutterの設計上こうなっていないかもしれないですが整理のためにここでは「Thema」とします。

まず、シンプルな話でユーザーがdarkModeを選択したらそれを保存したいので
Themaというキーに"darkMode"と書き込みたいです(実装上はカプセル化して、文字列は型かなんかにマッピングしてください)

でこのThemaの選択としてフレーズがあるわけですよね。
これは上記で格納するファイルとはまた別で作成すべきです。
例えばja-localeやen-localeでしょうか。ここがjaかenか、その他のロケールかは抽象化できますよね。
で例えばja-localeの
Themaというキーでアクセスした先に、
Label, Descriptionというキーを設定して文字列を取得する。
ということをすれば構造としては何ら問題はなくなるんじゃないかと思います。

仮にdarkModeがYesかNoか単純なものではなく選択肢の一つならdarkModeというキーの中にするか、
選択肢すべてを配列としてModeというキーに"darkMode"を設定してください。

これでThemaというキーを軸にデータの保存と参照もできますし、言語設定の抽象化もできます。
(つまりThema以外の設定も同じ構造として持てばよいという話。)

ちなみにAppLocalizations.of(context)!.translate(settingItem.labelKey);は先の回答と全く関係はないのですが、データフローが破綻していないのであればAppLocalizations.of(context)!.translateという関数(参照)だけを引数に取って引数をクラス内で与えることも考えられます。(そうすれば抽象化になりますんでcontext事態にも依存せず、インスタンスの方で好きなソースを選択することもできます。)
オブジェクト指向の視点からはぶれますけどね。

簡単そうな話題から先に触れましたので、複雑な話が続きとしてあれば追って返信いたします。(余裕あれば)

で、上記で整理したのですが
そうではなくユーザー入力(設定)と言語ごとのcaptionを同時に保持したいという需要自体があるなら、その理由が謎ではあります。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#5

u2025

総合スコア55

投稿2025/07/24 09:03

あとプラスで、以下のようにUI表示用文字列を別で管理する場合など、keyと実際の値とどちらをクラスに持たせるか
ですかね

質問者さんの中でも色々と整理してもらいたいのですが、これらの文字列はコンテキストが必要でしょうし、それでなくてもView専用です。
Viewで取得するのが自然であればそうするのがいいと思います。

抽象化したいという話で煩雑になっているかもしれませんが、普通にUIを作るのと同じでいいでしょう。その際、keyを使用するならそうする以外の理由はないと思います。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#6

dedede914

総合スコア63

投稿2025/07/24 19:19

#5

わかりやすい文書いてくださっていますが、本当に電話で説明いただきたいです笑

私が採用したのが、valueテキスト取得のためのkeyを保持するItemクラスを定義し、テキストは毎回そのkeyを参照するというものです。なので、かねがね回答者さんが提示してくれたものと似ているかなという感じです

テキストを取得するのに毎回keyを参照して取得というのが、keyをもつItemクラステキストデータを持ったMapの2つを扱うのでめんどくさいなと思っていました。
が、回答者さんの話を聞き、確かに(外部ファイルから取得する)テキストはUI層で管理すべきだなと思いました。定数だからもっと内側で持たせてもいいと思い込んでいました。

あと、例で出したdarkModeは良くなかったです。確かにThema.darkMode, Thema.lightMode のほうがいいですね。


以下具体的な設計

  • label,descriptionを持つEnumを拡張した(Flutter特有の?)mixinを作り、それを継承してenumのSettingKeyを作ります

dart

1mixin KeyEnumMixin on Enum { 2  String get label => '$name.label'; 3  String get description => '$name.description'; 4  T getDefaultValue<T>(); 5} 6 7 8enum SettingKey with KeyEnumMixin { 9  hideItemA(False), // 初期値False 10  textSize(10); // 初期値10 11 12  final dynamic _defaultValue; 13  const SettingKey(this._defaultValue); 14  15  16  T getDefaultValue<T>() => _defaultValue as T; 17}
  • SettingItemにvalueとそのkeyを持たせる

dart

1class SettingItem<T> { 2 final SettingKey settingKey; 3 final T value; 4 SettingItem({ required SettingKey settingKey,required T super.value}); 5 6 7 String get labelKey => settingKey.label; 8 9 T get defaultValue => settingKey.getDefaultValue<T>(); 10}
  • UI内で実際にlabel等を扱う

dart

1final textData=textRepository.getSettingItems(); 2// textData = {"SettingKey.hideItemA.label": "ItemAの表示状態",...} 3final label=textData[settingItem.settingKey.label];

AppLocalizations.of(context)!.translate についてですが、Flutterで関数を引数として渡すときその関数の引数も書きます(書いた方がいい)
その時にAppLocalizations.of(context)!.translateに必ず渡さなければならない引数contextが、完全にUIに依存するBuildContextクラスなのでModel内には書けません

dart

1final String Function(BuildContext)? showLabel; 2//返り値String, 引数BuildContext である Function

もやもやも少し晴れましたし、頭の中もすっきりさせることができました!
ありがとうございます!

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

#7

u2025

総合スコア55

投稿2025/07/25 01:02

うーん。解決したんですかね?
前提として、

txt

1文字列リソースは文字列リソースとして管理する。 2言語翻訳の煩雑さを回避するために特定の場所に管理し、どの言語でも同一のkeyでアクセスできるようにする。 3また、この文字列リソースは静的であるため、他の層にデータフローとして伝える必要はない。 4必要のない層に伝えることで、ここが変更になった際にそのほかのすべてに影響があるため保守性が下がる。 5意図が不明確になる。 6(それ自体の際する抽象化自体はまた別の話題)

という所を整理できれば十分だと思います。


AppLocalizations.of(context)!.translate についてですが、Flutterで関数を引数として渡すときその関数の引数も書きます(書いた方がいい)

そんな稀有な仕様はないと思います。

イメージととしてはこんな感じで、BuildContextではなく変換関数に依存しています。
(これがいいという話ではなくテクニックとしてできるという話)
(ラムダ関数を使用しているが、関数を直接でもいいというのが前回の話)

dart

1class SettingItem<T> { 2 final Enum settingKey; 3 final T value; 4 5 String label(String Function(String) source) { 6 source(settingKey.label) 7 } 8 9// String label(BuildContext context) => AppLocalizations.of(context)!.translate(settingKey.label); 10// String description(BuildContext context) => AppLocalizations.of(context)!.translate(settingKey.description); 11 12} 13 14// UI側 15// contextは定義済みとする 16 17final item = SettingItem( 18 settingKey: MySettings.darkMode, 19 value: true, 20); 21 22 23// 関数を明示的にラップして渡す(匿名関数) 24final labelText = item.label( 25 (key) => AppLocalizations.of(context)!.translate(key), 26);

ちなみにこういうテクニックもあります。

dart

1 2class SettingItem<T> { 3 final Enum settingKey; 4 final T value; 5 6 String access( 7 String Function(String) source, 8 String Function(Enum) keySelector, 9 ) { 10 final k = keySelector(settingKey); 11 return source(k); 12 } 13 14// String label(BuildContext context) => AppLocalizations.of(context)!.translate(settingKey.label); 15// String description(BuildContext context) => AppLocalizations.of(context)!.translate(settingKey.description); 16 17} 18 19 20final item = SettingItem( 21 settingKey: MySettings.darkMode, 22 value: true, 23); 24 25final labelText = item.access( 26 source: (key) => AppLocalizations.of(context)!.translate(key), // 翻訳方法 27 keySelector: (enum) => enum.label, // ラベルの取り方 28); 29 30 31final descriptionText = item.access( 32 source: (key) => AppLocalizations.of(context)!.translate(key), 33 keySelector: (enum) => enum.descri, 34);

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

この意見交換はまだ受付中です。

会員登録して回答してみよう

アカウントをお持ちの方は

関連した質問