Комментарии 36
Только вчера задумался поискать хорошую статью про Flutter и чистую архитектуру!
Хорошая статья! Несколько замечаний / рац. предложений из опыта нашей команды, если позволите.
Во-первых, я бы не стал, пожалуй, выделять ApiUtil
как отдельный класс. На мой взгляд, эти детали реализации вполне могут быть ответственностью DayDataRepository
Во-вторых, функциональность мапперов хорошо ложится на методы-расширения.
В-третьих, стоит упомянуть о таких мега-полезных библиотеках, как retrofit
и json_annotation
. Они убирают тонну бойлерплейта.
С учетом сказанного, слой API я бы реализовал примерно так (конечно, это надо раскидать по разным файлам, но для простоты я выложу одной портянкой):
// DTOs
@JsonSerializable()
class DayDto {
DayDto({this.sunrise, this.sunset, this.solar_noon, this.day_length});
final String sunrise;
final String sunset;
final String solar_noon;
final num day_length;
factory DayDto.fromJson(Map<String, dynamic> json) => _$DayDtoFromJson(json);
}
@JsonSerializable()
class DayResponseDto {
DayResponseDto({this.results});
final DayDto results;
factory DayResponseDto.fromJson(Map<String, dynamic> json) =>
_$DayResponseDtoFromJson(json);
}
@JsonSerializable()
class DayRequestDto {
DayRequestDto({this.lat, this.lng, this.formatted});
final double lat;
final double lng;
final int formatted;
Map<String, dynamic> toJson() => _$DayRequestDtoToJson(this);
}
// Backend API interface
@RestApi(baseUrl: 'https://api.sunrise-sunset.org/')
abstract class SunriseApi {
factory SunriseApi(Dio dio, {String baseUrl}) = _SunriseApi;
@GET('/json')
Future<DayResponseDto> getDay(@Queries() DayRequestDto request);
}
// Mappers
extension DayMapper on DayDto {
Day toModel() => Day(
sunrise: DateTime.tryParse(sunrise),
sunset: DateTime.tryParse(sunset),
solarNoon: DateTime.tryParse(solar_noon),
dayLength: day_length.toInt(),
);
}
// Repository implementation
class DayDataRepository extends DayRepository {
final SunriseApi _api;
DayDataRepository(this._api);
@override
Future<Day> getDay({double latitude, double longitude}) async {
final request = DayRequestDto(
lat: latitude,
lng: longitude,
formatted: 0,
);
final response = await _api.getDay(request);
return response.results.toModel();
}
}
Что-то в этом духе мы в проекте и используем.
1. Такие методы лучше выделять в отдельные виджеты (это связано с оптимизацией):
Widget _getBody() {...}
2. Как по мне, данный метод должен существовать в отдельном файле (а еще лучше, полностью в слое бизнес-логики):
void _getDay() {...}
3. Вместо SizedBox нужно использовать Padding
4. Не уверен в том, di будет что-либо кэшировать (максимум в рамках где он используется):
if (_dayRepository == null)
2) Мысль интересная, надо подумать.
3) Почему так? Мне просто больше нравится плоская структура, а не вложенная. Но если есть какие-то нюансы, буду рад узнать.
4) У нас же все репозитории, по сути, являются синглтонами. А это нужно чтобы не создавать их все сразу, а только в момент, когда он впервые понадобился.
4) Это уже холиварная тема из разряда загрузить все разом или загружать по необходимости. У обеих подходов есть свои плюсы и минусы. Помимо di есть также Service locator и Factory pattern
Это полезно еще вот с какой стороны: в общем случае у вас может быть несколько сервисов, которые возвращают разные ошибки (разный код, разное тело ответа), но имеющие один смысл (например, что вы не авторизованы). Все такие ошибки будут иметь один тип на уровне ApiUtil.
может быть бложек там какой, ну чисто CRUD показать)) ну можно еще Oauth2 туда же)))
Спасибо за такой подробный разбор построения архитектуры. После прочтения, загорелся применить в такой подход в одном из своих приложений. По началу все отлично переносилось и прекрасно ложилось на новую архитектуру. Но внезапно уперся в довольно очевидную проблему — авторизация. На каком слое она должна быть?
Пытаться все затянуть на слой api
, но у меня вся авторизация — генерация хэша по логину-паролю и методы для сохранения и чтения этого хэша из SecureStorage
, и дальнейшее внедрение в headers
всех запросов к серверу. Не очень то похоже на API.
Если выносить в отдельный модуль, то получается его нужно внедрять в сервис, чтобы подмешивать в headers
в dio
. Проблема усугубляется тем, что чтение из SecureStorage
асинхронное, а это значит нужно как-то синхронизировать момент инициализации модуля авторизации и модуля API.
Как вы решаете эту проблему в своих проектах?
П.С.: В dart и flutter я новичок и еще не вник в то как принято строить архитектуру приложений, поэтому мой вопрос может выглядеть глупо! =)
В общем случае у вас может быть несколько сервисов, у которых может отличаться механизм авторизации. На уровне ApiUtils вам не нужно знать, как устроена авторизация в том или ином сервисе, вам просто нужен метод для работы с этим сервисом. Поэтому детали реализации работы с конкретным бэком, на мой взгляд, должны оставаться на уровне сервиса.
И это не из-за криворукости бэкендера, просто на каждое предстваление одних и тех же данных глупо создавать десяток однотипных api.
*простите за некропостинг
DTO оправданы только если бэкендер криворук, и с сервера приходит что-то ну совсем не совпадающее с моделью
К сожалению, на моей практике такое происходило чаще, чем хотелось бы.
Использование нафиг ненужных DTO-шек — только нагромождение лишнего бойлерплейта
Всё-таки я предпочитаю разделять модели бизнес-логики и данных. Например, это может быть удобно, если нужно хранить в базе какие-то «хитрые» данные — с помощью мапперов мы нивелируем эту «хитрость», в результате в базе будут примитивные типы, в бизнес-логике — сколь угодно сложные.
Но да, ценой гибкости и прозрачности является некоторая избыточность кода.
если тебе понадобились DTO-шки, и без них никак — статик маппер не нужен
На мой взгляд, это детали реализации. Выше в комментах предлагали использовать экстеншены для мапперов и это действительно может быть удобно. Там же указали, что с использованием пакет jsonable можно сократить количество бойлерплейта за счёт кодогенерации. Так что это вопрос вкуса — выбирайте, какой по душе!:)
рекомендую обратить внимание на Retrofit
Благодарю за наводку, раньше действительно с ним не сталкивался.
Хотя не могу не заметить, что не всем доступна такая роскошь как сваггер:) Но это так, поворчать.
Отличная статья! С радостью бы посмотрел на разбор ещё парочки интересных архитектурных решений.
Спасибо большое, все ещё актуально. Третий день работаю с flutter, архитектура - наше все!
Удивительно... 2.5 года, и уже ни черта не работает)) И ладно бы синтаксис стал лучше, так ведь нет! Просто он немного изменился и сиди гадай в чем дело (у меня конкретно проблема - Classes can only mix in mixins and classes.). Код, который я написал на котлине три года назад все так же работает. Мдаа, ну флаттер и флаттер
Благодарю за подробный материал - крайне интересно пожонглировать в воображении архитектурой! Некоторые дополнения/замечания (с учётом времени рождения статьи):
Текстовые контроллеры и прочие контроллеры рекомендуется инициализировать в
initState
, потому что это может быть удобным по ряду причин; одна из таких - вы можете присвоить какое-либо начальное значение на основе других данных. Затем, любые контроллеры необходимо утилизировать в методеdispose
виджета (контроллеры могут быть сложней/тяжелей - привет утечка памяти).
class _HomeState extends State<Home> {
late final TextEditingController _latController;
late final TextEditingController _lngController;
HomeState _homeState;
@override
void initState() {
super.initState();
_latController = TextEditingController();
_lngController = TextEditingController();
_homeState = HomeModule.homeState();
}
@override
void dispose() {
super.dispose();
_latController.dispose();
_lngController.dispose();
}
Нераскрыта история про обработку ошибок/исключений. Сделали бы вы разделение по слоям, когда на каждом уровне был бы свой тип ошибки? Сделали бы это в алгебраическом стиле или ином?
Вероятно, реализация папки
data
чрезмерно сложная для простых проектов. 3 уровня вложенности можно смело заменить одним-двумя максимум + как и было указано, воспользоваться кодогенерацией и расширениями классов. И опять же, мы в мире dart - никто не отменял factory, чтобы жонглировать ModelDTO, Model, ModelUI. Тут это by designРеализация вместо наследования:
import 'package:habr_flutter_clean_arch/data/api/api_util.dart'; import 'package:habr_flutter_clean_arch/domain/model/day.dart'; import 'package:habr_flutter_clean_arch/domain/repository/day_repository.dart'; class DayDataRepository extends DayRepository {...} // может всё-таки реализуем контракт? class DayDataRepository implements DayRepository {...}
Конечно, сейчас для внедрения зависимостей стоит воспользоваться
provider,
getIt,
injectable
или что-то в этом духе. Если научитесь делать чистыйInherited
тоже будет удобно. Опять же,of(context)
- это сок дерева flutterЧасто приходится использовать что-то в духе mvvс и иметь view-model для управления некоторым (сложным и не очень) состоянием view. В данном случае так же встаёт вопрос о роутинге, который может быть сильно подвязан к ? чему угодно в некоторых проектах, что я видывал. Где он будет размещён, какую роль играет?
Вопросы тестирования данных сервисов/репозиториев/бизнес-логики. В долгоживущих проектах это критически важно. Как это можно реализовать и в том числе с учетом такого кода?:
final Dio _dio = Dio(
BaseOptions(baseUrl: _BASE_URL),
);
Flutter + чистая архитектура: разбираем на примере