Skip to content

Latest commit

 

History

History
1018 lines (752 loc) · 19.9 KB

INSTRUCTIONS.md

File metadata and controls

1018 lines (752 loc) · 19.9 KB
Change #1

Add new dependencies in pubspec.yaml file:

dependencies:
  ...
  json_annotation: 4.9.0

dev_dependencies:
  ...
  json_serializable: 6.8.0

Perform pub get.

Change #2

In the event_summary.dart file, add a new part file below imports list:

part 'event_summary.g.dart';

Add a new .fromJson named constructor in the end of the EventSummary class:

@freezed
class EventSummary with _$EventSummary {
  ...
  factory EventSummary.fromJson(Map<String, dynamic> json) => 
      _$EventSummaryFromJson(json);
}

Run code generation with:

dart run build_runner build --delete-conflicting-outputs
Change #3

Add analyzer:exclude section to analysis_options.yaml:

analyzer:
  exclude:
    - '**/*.g.dart'
    - '**/*.freezed.dart'
Change #4

In the event_summary.dart file, replace image field declaration with:

@JsonKey(name: 'imageUrl') required Uri? image,

Add analyzer:errors section to analysis_options.yaml:

analyzer:
  ...

  errors:
    invalid_annotation_target: ignore

Run code generation in watch mode with:

dart run build_runner watch --delete-conflicting-outputs
Change #5

Create a new folder api/core/converter with a new file date_time_to_dashed_string_converter.dart:

import 'package:json_annotation/json_annotation.dart';

class DateTimeToDashedStringConverter extends JsonConverter<DateTime, String> {
  const DateTimeToDashedStringConverter();

  @override
  DateTime fromJson(String json) {
    final dates = json.split('-');
    return DateTime.utc(
      int.parse(dates[0]),
      int.parse(dates[1]),
      int.parse(dates[2]),
    );
  }

  @override
  String toJson(DateTime object) => '${object.year}'
      '-${_twoDigits(object.month)}'
      '-${_twoDigits(object.day)}';

  static String _twoDigits(int n) => n >= 10 ? '$n' : '0$n';
}

In the event_summary.dart file, replace startDate field declaration with:

@DateTimeToDashedStringConverter() required DateTime? startDate,

Fix missing import with:

import 'package:ftcon24usa_workshop/api/core/converter/date_time_to_dashed_string_converter.dart';

In the date_time_to_dashed_string_converter.dart file, add dateTimeToDashedStringConverter global constant outside of DateTimeToDashedStringConverter class declaration:

const dateTimeToDashedStringConverter = DateTimeToDashedStringConverter();

In the event_summary.dart file, replace endDate field declaration with:

@dateTimeToDashedStringConverter required DateTime? endDate,

Create a new file date_time_to_separated_string_converter.dart next to the existing converter:

import 'package:json_annotation/json_annotation.dart';

class DateTimeToSeparatedStringConverter extends JsonConverter<DateTime, String> {
  const DateTimeToSeparatedStringConverter({
    required this.separator,
  });

  final String separator;

  @override
  DateTime fromJson(String json) {
    final dates = json.split(separator);
    return DateTime.utc(
      int.parse(dates[0]),
      int.parse(dates[1]),
      int.parse(dates[2]),
    );
  }

  @override
  String toJson(DateTime object) => '${object.year}'
      '$separator'
      '${_twoDigits(object.month)}'
      '$separator'
      '${_twoDigits(object.day)}';

  static String _twoDigits(int n) => n >= 10 ? '$n' : '0$n';
}

In the new date_time_to_separated_string_converter.dart file, add global constants outside of DateTimeToSeparatedStringConverter class declaration:

const dateTimeToDashedStringConverter = 
    DateTimeToSeparatedStringConverter(separator: '-');

const dateTimeToSlashedStringConverter = 
    DateTimeToSeparatedStringConverter(separator: '/');
Change #6

Update event_type.dart file to:

import 'package:json_annotation/json_annotation.dart';

enum EventType {
  unknown,
  @JsonValue('CONF')
  conference,
  @JsonValue('MEETUP')
  meetup,
  @JsonValue('DEV_FEST')
  devfest,
}

In the event_summary.dart file, replace type field declaration with:

@JsonKey(unknownEnumValue: EventType.unknown) required EventType type,
Change #7

In the event_summary.dart file, replace name field declaration with:

@Default('') String name,

and replace type field declaration with:

@JsonKey(unknownEnumValue: EventType.unknown) 
@Default(EventType.unknown) 
EventType type,
Change #8

In the event_summary.dart file, replace all fields declaration with:

@JsonKey(name: 'id') required String id,
@JsonKey(name: 'name') @Default('') String name,
@JsonKey(name: 'type', unknownEnumValue: EventType.unknown)
@Default(EventType.unknown)
EventType type,
@JsonKey(name: 'imageUrl') required Uri? image,
@JsonKey(name: 'startDate')
@DateTimeToDashedStringConverter()
required DateTime? startDate,
@JsonKey(name: 'endDate')
@dateTimeToDashedStringConverter
required DateTime? endDate,
Change #9

In the events_api.dart file, replace all usages of ApiEventSummary with EventSummary:

Future<ApiResponse<List<EventSummary>>> getEvents(String sort) async {
  ...
  final value = ApiResponse<List<EventSummary>>.fromJson(
      response.data!,
      (json) => (json as List)
          .map((e) => EventSummary.fromJson(e as Map<String, dynamic>))
          .toList(),
  );
  ...
}

Fix import by adding:

import 'package:ftcon24usa_workshop/domain/model/event_summary.dart';

and removing:

import 'package:ftcon24usa_workshop/api/events/model/api_event_summary.dart';

In the events_repository.dart file, update the last line of the getEvents method to:

Future<List<EventSummary>> getEvents(EventSorting sort) async {
  ...
  return response.data;
}

And fix imports by removing:

import 'package:ftcon24usa_workshop/api/events/mapper/event_summary_mapper.dart';

Now, api_event_summary.dart and event_summary_mapper.dart files can be deleted.

Change #10

In the event_location.dart file, add a new part file below imports list:

part 'event_location.g.dart';

Add a new .fromJson named constructor in the end of the EventLocation class:

@freezed
class EventLocation with _$EventLocation {
  ...
  factory EventLocation.fromJson(Map<String, dynamic> json) => 
      _$EventLocationFromJson(json);
}
Change #11

In the event_location.dart file, replace @freezed with:

@Freezed(unionKey: 'type')
class EventLocation with _$EventLocation {
  ...
}

And add @FreezedUnionValue above each named constructor:

@FreezedUnionValue('online')
const factory EventLocation.online({
  ...
}) = OnlineEventLocation;
@FreezedUnionValue('in-person')
const factory EventLocation.inPerson({
  ...
}) = InPersonEventLocation;
@FreezedUnionValue('hybrid')
const factory EventLocation.hybrid({
  ...
}) = HybridEventLocation;

Replace @Freezed(unionKey: 'type') with:

@Freezed(unionKey: 'type', fallbackUnion: 'unknown')
class EventLocation with _$EventLocation {
  ...
}
Change #12

In the api_event_details.dart file, replace ApiEventLocation usages with EventLocation:

class AnnouncedApiEventDetails implements ApiEventDetails {
  ...

  final EventLocation location;

  factory AnnouncedApiEventDetails.fromJson(Map<String, dynamic> json) =>
      AnnouncedApiEventDetails(
        ...
        location: EventLocation.fromJson(json['location'] as Map<String, dynamic>),
      );
}

Replace import with:

import 'package:ftcon24usa_workshop/domain/model/event_location.dart';

In the event_details_mapper.dart file, remove:

import 'package:ftcon24usa_workshop/api/events/mapper/event_location_mapper.dart';

And update AnnouncedApiEventDetailsX.toModel() method:

extension AnnouncedApiEventDetailsX on AnnouncedApiEventDetails {
  AnnouncedEventDetails toModel() => AnnouncedEventDetails(
        ...
        location: location,
      );
}

Remove api_event_location.dart and event_location_mapper.dart files.

Change #13

In the event_details.dart file, add a new part file below imports list:

part 'event_details.g.dart';

Add a new .fromJson named constructor in the end of the EventDetails class:

@freezed
class EventDetails with _$EventDetails {
  ...
  factory EventDetails.fromJson(Map<String, dynamic> json) =>
      _$EventDetailsFromJson(json);
}

Replace @freezed with:

@Freezed(fallbackUnion: 'unknown')
class EventDetails with _$EventDetails {
  ...
}
Change #14

In the event.dart file, add a new part file below imports list:

part 'event.g.dart';

Add a new .fromJson named constructor in the end of the Event class:

@freezed
class Event with _$Event {
  ...
  factory Event.fromJson(Map<String, dynamic> json) => 
      _$EventFromJson(json);
}
Change #15

In the event.dart file, replace details field declaration with:

@JsonKey(readValue: _readEventDetails) required EventDetails details,

In the end of the file, add global function:

Object? _readEventDetails(Map json, String key) {
  final announced = json['announced'] as bool?;
  final detailsRuntimeType = switch (announced) {
    true => 'announced',
    false => 'notAnnounced',
    null => 'unknown',
  };
  final detailsJson = json[key];
  return detailsJson?..['runtimeType'] = detailsRuntimeType;
}

Replace details field declaration with:

@JsonKey(readValue: _readEventDetails)
@Default(EventDetails.unknown())
EventDetails details,
Change #16

In the events_api.dart file, replace usages of ApiEvent with Event:

Future<ApiResponse<Event>> getEvent(String id) async {
  ...
  final value = ApiResponse<Event>.fromJson(
    response.data!,
    (json) => Event.fromJson(json as Map<String, dynamic>),
  );
  ...
}

Fix import by adding:

import 'package:ftcon24usa_workshop/domain/model/event.dart';

and removing:

import 'package:ftcon24usa_workshop/api/events/model/api_event.dart';

In the events_repository.dart file, update the last line of the getEvent method to:

Future<Event> getEvent(String id) async {
  ...
  return response.data;
}

And fix imports by removing:

import 'package:ftcon24usa_workshop/api/events/mapper/event_mapper.dart';

Now, api_event.dart, api_event_details.dart, api_event_type.dart files and api/events/mapper and api/events/model folders can be deleted.

Change #17

In the event.dart file, update image field declaration with:

@JsonKey(name: 'imageUrl') required Uri? image,

Update type field declaration with:

@JsonKey(name: 'type', unknownEnumValue: EventType.unknown)
@Default(EventType.unknown)
EventType type,
Change #18

Create build.yaml file next to the pubspec.yaml file:

targets:
  $default:
    builders:
      json_serializable:
        options:
          include_if_null: false
          explicit_to_json: true
Change #19

Replace api_response.dart file with

import 'package:freezed_annotation/freezed_annotation.dart';

part 'api_response.freezed.dart';
part 'api_response.g.dart';

@Freezed(genericArgumentFactories: true)
class ApiResponse<T> with _$ApiResponse<T> {
  const factory ApiResponse({
    required T data,
  }) = _ApiResponse;

  factory ApiResponse.fromJson(
    Map<String, dynamic> json,
    T Function(Object?) fromJsonT,
  ) =>
      _$ApiResponseFromJson(json, fromJsonT);
}
Change #20

Remove ApiKeyManager class usage from EventsApi class in events_api.dart file:

class EventsApi {
  const EventsApi(
    this._dio,
    this._authDataManager,
  );

  final Dio _dio;
  final AuthDataManager _authDataManager;
  
  Future<ApiResponse<List<EventSummary>>> getEvents(String sort) async {
    final response = await _dio.get<Map<String, dynamic>>(
      '/events',
      queryParameters: {'sort': sort},
    );
    ...
  }

  Future<ApiResponse<Event>> getEvent(String id) async {
    ...
    final response = await _dio.get<Map<String, dynamic>>(
      '/events/$id',
      options: Options(
        headers: {
          'auth': authData,
        },
      ),
    );
    ...
  }

Remove unused import:

import 'package:ftcon24usa_workshop/api/core/manager/api_key_manager.dart';

In the service_locator.dart file, update repository getter:

EventsRepository get eventsRepository => EventsRepository(
  EventsApi(
    _dio,
    const AuthDataManager(),
  ),
);
Change #21

Under api/core/client/ folder, create an interceptor folder with append_api_key_interceptor.dart file:

import 'package:dartx/dartx.dart';
import 'package:dio/dio.dart';
import 'package:ftcon24usa_workshop/api/core/manager/api_key_manager.dart';

class AppendApiKeyInterceptor extends Interceptor {
  const AppendApiKeyInterceptor(this._apiKeyManager);

  final ApiKeyManager _apiKeyManager;

  static const _headerName = 'apikey';

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    final apiKey = _apiKeyManager.apiKey;

    if (apiKey.isNotNullOrEmpty) {
      return handler.next(options..headers[_headerName] = apiKey);
    } else {
      return handler.reject(
        DioException(
          requestOptions: options,
          error: Exception('$_headerName is empty'),
        ),
      );
    }
  }
}

In the service_locator.dart file, update createDio method call:

final _dio = createDio([
  const AppendApiKeyInterceptor(ApiKeyManager()),
]);

Fix import by adding:

import 'package:ftcon24usa_workshop/api/core/client/interceptor/append_api_key_interceptor.dart';
Change #22

Remove AuthDataManager class usage from EventsApi class in events_api.dart file:

class EventsApi {
  const EventsApi(
    this._dio,
  );

  final Dio _dio;

  Future<ApiResponse<Event>> getEvent(String id) async {
    final response = await _dio.get<Map<String, dynamic>>(
      '/events/$id',
    );
    ...
  }

Remove unused import:

import 'package:ftcon24usa_workshop/api/core/manager/auth_data_manager.dart';

In the service_locator.dart file, update repository getter:

EventsRepository get eventsRepository => EventsRepository(
  EventsApi(
    _dio,
  ),
);
Change #23

Under api/core folder create a marker folder with authenticated_request_marker.dart file:

const authenticatedRequestExtraKey = 'append_auth_header';

Under api/core/interceptor folder create a append_auth_interceptor.dart file:

import 'package:dartx/dartx.dart';
import 'package:dio/dio.dart';
import 'package:ftcon24usa_workshop/api/core/manager/auth_data_manager.dart';
import 'package:ftcon24usa_workshop/api/core/marker/authenticated_request_marker.dart';

class AppendAuthInterceptor extends Interceptor {
  const AppendAuthInterceptor(this._authDataManager);

  final AuthDataManager _authDataManager;

  static const _headerName = 'auth';

  @override
  Future<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    final appendAuthHeader =
        (options.extra[authenticatedRequestExtraKey] as bool?) ?? false;

    if (!appendAuthHeader) {
      return super.onRequest(options, handler);
    }

    final authData = await _authDataManager.authData;
    if (authData.isNotNullOrEmpty) {
      return handler.next(options..headers[_headerName] = authData);
    } else {
      return handler.reject(
        DioException(
          requestOptions: options,
          error: Exception('$_headerName is empty'),
        ),
      );
    }
  }
}

In the service_locator.dart file, update createDio method call:

final _dio = createDio([
  const AppendApiKeyInterceptor(ApiKeyManager()),
  const AppendAuthInterceptor(AuthDataManager()),
]);

and add new imports:

import 'package:ftcon24usa_workshop/api/core/client/interceptor/append_auth_interceptor.dart';
import 'package:ftcon24usa_workshop/api/core/manager/auth_data_manager.dart';

In the events_api.dart file, instead of providing options.headers:

options: Options(
  headers: {
    'auth': authData,
  },
),

provide:

options: Options(
    extra: {
      authenticatedRequestExtraKey: true,
    },
),

Add a new import:

import 'package:ftcon24usa_workshop/api/core/marker/authenticated_request_marker.dart';
Change #24

Add new dependencies in pubspec.yaml file:

dependencies:
  ...
  retrofit: 4.4.1

dev_dependencies:
  ...
  retrofit_generator: 9.1.2

Perform pub get and re-run code generation with:

dart run build_runner watch --delete-conflicting-outputs
Change #25

Replace events_api.dart file with:

import 'package:dio/dio.dart';
import 'package:ftcon24usa_workshop/api/core/model/api_response.dart';
import 'package:ftcon24usa_workshop/domain/model/event.dart';
import 'package:ftcon24usa_workshop/domain/model/event_summary.dart';
import 'package:retrofit/retrofit.dart';

part 'events_api.g.dart';

@RestApi()
abstract class EventsApi {
  factory EventsApi(Dio dio) = _EventsApi;

  @GET('/events')
  Future<ApiResponse<List<EventSummary>>> getEvents(@Query('sort') String sort);

  @GET('/events/{id}')
  Future<ApiResponse<Event>> getEvent(@Path('id') String id);
}
Change #26

Add an @Extra on top of the getEvent method:

@Extra({authenticatedRequestExtraKey: true})
@GET('/events/{id}')
...

Add a new import:

import 'package:ftcon24usa_workshop/api/core/marker/authenticated_request_marker.dart';

In the authenticated_request_marker.dart file, add:

import 'package:retrofit/retrofit.dart';

...

const authenticatedRequest = Extra({authenticatedRequestExtraKey: true});

In events_api.dart file, replace @Extra({authenticatedRequestExtraKey: true}) with:

@authenticatedRequest
@GET('/events/{id}')
...
Change #27

Add global_options section in the end of build.yaml file:

...

global_options:
  freezed:
    runs_before:
      - json_serializable
  json_serializable:
    runs_before:
      - retrofit_generator
Change #28

In the events_api.dart file, update getEvents method:

@GET('/events')
Future<ApiResponse<List<EventSummary>>> getEvents(
  @Query('sort') EventSorting sort,
);

In the events_repository.dart file, update getEvents method:

Future<List<EventSummary>> getEvents(EventSorting sort) async {
  final response = await _api.getEvents(sort);
  return response.data;
}

Replace event_sorting.dart file with:

import 'package:json_annotation/json_annotation.dart';

part 'event_sorting.g.dart';

@JsonEnum(alwaysCreate: true)
enum EventSorting {
  byName,
  byDate;
}

Add a .toJson method:

enum EventSorting {
  ...

  String? toJson() => _$EventSortingEnumMap[this];
}

Update an annotation above EventSorting:

@JsonEnum(alwaysCreate: true, fieldRename: FieldRename.kebab)
enum EventSorting {
  ...
}