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 import
s:
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 {
...
}