Я кодирую приложение во Flutter уже несколько недель и начал задумываться о том, какой может быть лучшая архитектура через несколько дней за go.
Сначала немного контекста:
- Это приложение для обмена сообщениями, использующее Firebase в качестве бэкэнда;
- В значительной степени оно опирается на замечательный пакет Provider для обработки состояния во всем приложении
- План состоит в том, чтобы иметь несколько функций, которые могут взаимодействовать друг с другом.
- Я довольно новичок в Flutter (в основном фон React / ReactNative), это может объяснить странный подход, который я ниже.
Я испытывал различные подходы к архитектуре и смог получить один рабочий, который, наконец, мне подходит.
Поскольку у меня будет несколько функций, которые можно использовать в разных местах приложения, я хочу разделить код по функциям (или модулям), которые затем могут использоваться независимо на разных экранах. Архитектура папок будет выглядеть следующим образом:
FlutterApp
|
|--> ios/
|--> android/
|--> lib/
|
|--> main.dart
|--> screens/
| |
| |--> logged/
| | |
| | |--> profile.dart
| | |--> settings.dart
| | |--> ...
| |
| |--> notLogged/
| | |
| | |--> home.dart
| | |--> loading.dart
| | |--> ...
|
|--> features/
|
|--> featureA/
| |
| |--> ui/
| | |--> simpleUI.dart
| | |--> complexUI.dart
| |--> provider/
| | |-->featureAProvider.dart
| |--> models/
| |--> featureAModel1.dart
| |--> featureAModel2.dart
| |--> ...
|
|
|--> featureB/
| |
| |--> ui/
| | |--> simpleUI.dart
| | |--> complexUI.dart
| |--> provider/
| | |--> featureBProvider.dart
| |--> models/
| |--> featureBModel1.dart
| |--> featureBModel2.dart
| |--> ...
|
...
В идеале каждая функция будет следовать следующим правилам:
- Каждая функция имеет часть logi c (часто с использованием пакета поставщика);
- Каждая часть logi c компонента может запрашивать переменные (члены класса ChangeNotifier) у другого Feature
- Каждая функция имеет (тупую) часть пользовательского интерфейса, которая может напрямую взаимодействовать с "logi c "part (таким образом, может быть, не так глупо);
- Каждая функция может иметь свою часть пользовательского интерфейса, заменяемую пользовательским интерфейсом, но затем пользовательский интерфейс должен реализовывать взаимодействие (я) с частью logi c на свой собственный;
- Каждая функция может иметь модели, представляющие ресурсы ресурсов, если мне потребуется сохранить их в Firebase позже
Я пробовал этот подход с одной функцией (или 2 зависит от того, как вы это видите) моего приложения, которое представляет собой возможность записи / прослушивания голосовых заметок. Мне это показалось интересным, потому что вы можете записывать в одном месте, но прослушивать запись во многих местах: например, сразу после записи или когда вам отправляют запись.
Вот что я придумал :
- структура папок теста В этом случае нет папки
models/
, потому что это просто файл, который я обрабатываю в другом месте - voiceRecorderNotifier обрабатывает файл (добавить / удалить) и запись (начало / конец)
- VoicePlayerNotifier требует, чтобы файл был создан (именованный конструктор), а затем обрабатывает воспроизведение аудиофайла (воспроизведение, пауза, стоп).
В коде это немного многословно, но работает, как и ожидалось, например, на экранах я могу запросить функцию voiceRecorder, например:
class Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => VoiceRecorderNotifier(),
child: Column(
children: [
AnUIWidget(),
AnotherUIWidget(),
...,
// The "dumb" feature UI widget from 'features/voiceRecorder/ui/simpleButton.dart' that can be overrided if you follow use the VoiceRecorderNotifier
VoiceRecorderButtonSimple(),
...,
]
)
);
}
}
У меня также могут быть две функции (voiceRecorder / voicePlayer), работающие вместе, например:
class Screen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => VoiceRecorderNotifier(),
child: Column(
children: [
AnUIWidget(),
AnotherUIWidget(),
...,
VoiceRecorderButtonSimple(),
...,
// adding the dependent voicePlayer feature (using the VoiceRecorderNotifier data);
Consumer<VoiceRecorderNotifier>(
builder: (_, _voiceRecorderNotifier, __) {
if (_voiceRecorderNotifier.audioFile != null) {
// We do have audio file, so we put the VoicePlayer feature
return ChangeNotifierProvider<VoicePlayerNotifier>(
create: (_) => VoicePlayerNotifier.withFile(_voiceRecorderNotifier.audioFile),
child: VoicePlayerButtonSimple(),
);
} else {
// We don't have audio file, so no voicePlayer needed
return AnotherUIWidget();
}
}
),
...,
AnotherUIWidget(),
]
)
);
}
}
Это тест fre sh, поэтому я предполагаю, что есть недостатки, которые я не вижу сейчас, но я чувствую, что есть несколько хороших моментов:
- Более чистая структура папок;
- Одно место для обработки "высокоуровневых" логи c, связанных с функцией, легко обновление;
- Легко добавлять, перемещать, удалять функцию везде, где есть приложение;
- Базовый c пользовательский интерфейс предоставляется для функции, как ожидается, такой как простой
Text('hi')
, но я все еще могу "переопределить" пользовательский интерфейс для конкретного c отображения функции;
- Я могу сосредоточиться на пользовательском интерфейсе и использовании функций, а не создание множества компонентов Stateful для репликации одного и того же лога c элемента в разных местах;
Недостатки, которые я вижу:
- Элемент Logi c - это «скрытый», мне нужно будет go через Уведомитель каждый раз, когда я хочу сделать что-то, определяющее c с функцией, чтобы запомнить, как работает функция;
- Реализация уведомителей в хорошем месте может стать беспорядком, если виджеты пользовательского интерфейса могут иметь несколько функций, т тогда мне потребуется несколько FeatureNotifier (даже если в этом случае полезен Multiprovider);
Наконец, вот вопросы:
- Считаете ли вы этот подход масштабируемым / рекомендуемым, если я смогу продолжать создавать функции таким образом, не испытывая проблем позже?
- Видите ли вы другие недостатки?