Только вчера задумался поискать хорошую статью про 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();
}
}
Что-то в этом духе мы в проекте и используем.
Почему бы не использовать для di — GetIt ?
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 вам не нужно знать, как устроена авторизация в том или ином сервисе, вам просто нужен метод для работы с этим сервисом. Поэтому детали реализации работы с конкретным бэком, на мой взгляд, должны оставаться на уровне сервиса.
Flutter + чистая архитектура: разбираем на примере