Комментарии 20
Спасибо большое!
Только вчера задумался поискать хорошую статью про Flutter и чистую архитектуру!

Хорошая статья! Несколько замечаний / рац. предложений из опыта нашей команды, если позволите.


Во-первых, я бы не стал, пожалуй, выделять ApiUtil как отдельный класс. На мой взгляд, эти детали реализации вполне могут быть ответственностью DayDataRepository


Во-вторых, функциональность мапперов хорошо ложится на методы-расширения.


В-третьих, стоит упомянуть о таких мега-полезных библиотеках, как retrofit и json_annotation. Они убирают тонну бойлерплейта.


С учетом сказанного, слой API я бы реализовал примерно так (конечно, это надо раскидать по разным файлам, но для простоты я выложу одной портянкой):


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)
1) С этим согласен, но не хотелось усложнять код — здесь же больше не про UI шла речь, поэтому опустил эти нюансы.
2) Мысль интересная, надо подумать.
3) Почему так? Мне просто больше нравится плоская структура, а не вложенная. Но если есть какие-то нюансы, буду рад узнать.
4) У нас же все репозитории, по сути, являются синглтонами. А это нужно чтобы не создавать их все сразу, а только в момент, когда он впервые понадобился.
2) Padding добавляет пустое пространство к краям child'а, не растягивая его. А SizedBox создает коробку фиксированного размера, и его child будет растягиваться на указанную ширину и высоту. Вы также можете использовать оба виджета без child, но Padding дает больше возможностей для дальнейшего изменения ui. А также Padding лучше чем SizedBox по производительности (хоть разница в наносекундах, но чем больше таких элементов тем больше таких наносекунд)

4) Это уже холиварная тема из разряда загрузить все разом или загружать по необходимости. У обеих подходов есть свои плюсы и минусы. Помимо di есть также Service locator и Factory pattern
Как ведут себя в данном случае Padding и SizedBox я знаю, а вот про разницу в производительности не знал. А есть какие-нибудь бенчмарки? Откуда информация, что Padding быстрее?
Бенчмарков нет, данную информацию вы можете проверить сами, достаточно посмотреть из чего состоят эти виджеты и оценить их. RenderPadding (RenderShiftedBox) vs RenderConstrainedBox (RenderProxyBox)
Мне нужен был пример работы с сетью, поэтому и воспользовался тем сервисом.
Можно отлавливать такие ошибки на уровне SunriseService и на их основе генерировать более высокоуровневые ошибки для слоя ApiUtil.
Это полезно еще вот с какой стороны: в общем случае у вас может быть несколько сервисов, которые возвращают разные ошибки (разный код, разное тело ответа), но имеющие один смысл (например, что вы не авторизованы). Все такие ошибки будут иметь один тип на уровне ApiUtil.
Может кто написать про связку shelf + angulardart + postgres + gRPC(protobuf), ммм?))
может быть бложек там какой, ну чисто CRUD показать)) ну можно еще Oauth2 туда же)))

Спасибо за такой подробный разбор построения архитектуры. После прочтения, загорелся применить в такой подход в одном из своих приложений. По началу все отлично переносилось и прекрасно ложилось на новую архитектуру. Но внезапно уперся в довольно очевидную проблему — авторизация. На каком слое она должна быть?


Пытаться все затянуть на слой api, но у меня вся авторизация — генерация хэша по логину-паролю и методы для сохранения и чтения этого хэша из SecureStorage, и дальнейшее внедрение в headers всех запросов к серверу. Не очень то похоже на API.


Если выносить в отдельный модуль, то получается его нужно внедрять в сервис, чтобы подмешивать в headers в dio. Проблема усугубляется тем, что чтение из SecureStorage асинхронное, а это значит нужно как-то синхронизировать момент инициализации модуля авторизации и модуля API.


Как вы решаете эту проблему в своих проектах?


П.С.: В dart и flutter я новичок и еще не вник в то как принято строить архитектуру приложений, поэтому мой вопрос может выглядеть глупо! =)

Внедрение заголовков я бы сделал на уровне service.
В общем случае у вас может быть несколько сервисов, у которых может отличаться механизм авторизации. На уровне ApiUtils вам не нужно знать, как устроена авторизация в том или ином сервисе, вам просто нужен метод для работы с этим сервисом. Поэтому детали реализации работы с конкретным бэком, на мой взгляд, должны оставаться на уровне сервиса.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.