(Flutter/Dartで)アプリのユーザー設定データを管理するクラス設計についてのベストプラクティスや、よく使用される設計を知りたいです。flutterで開発していますが、言語に依存しない設計思想でかまいません!!
ずっと個人でしかやってこず壁にぶち当たり、
保守性、拡張性、可読性を良くするため勉強し始めました
良ければみなさんの知見をご共有ください
以下は具体的に今私が直面している問題です。
実現したいこと
- 表示文字列と設定項目を外部ファイル化(テキストファイル、設定項目ファイルで設定を管理する)
- 1つのクラスでvalueと文字列データlabel,descriptionなどをすべて持つSettingItemクラスを、できれば定義したい
- Clean Architectureに準拠した設計
- 型安全性を保った設定管理
- 保守性・拡張性の確保
- ローカライズ対応(多言語表示、最優先ではない)
- 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層に記述される。そのため、
SettingMetadata
がlabel()
を保持していることが一見わかりにくい
SettingItemのまとめ方
- UIでは一部特殊な表示をし、他はリスト表示にしたい
検討した選択肢
List<SettingItem> settings
Map<SettingKey, SettingItem> settings
- 個別フィールド(
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}
よろしくお願いします
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。