Skip to content

Core Concepts

This section explains the foundational components that define the MOFA architecture’s datasource layer. The Convertor pattern is implemented as a separate shared Dart package, reusable across all projects.

The central concept is the Convertor, a component that accepts a single input and computes a single output.

Components are chainable, meaning the output of one component becomes the input for the next. This allows the creation of modular pipelines similar to the functional programming paradigm found in rxdart.

The Convertor pattern is the sole datasource pattern in MOFA architecture, replacing the legacy Strategy/DatasourceModule pattern. See ADR-005 for the migration rationale.

Convertors are categorized into three main types based on their output type. This differentiation primarily facilitates various extensions for chaining, mapping, interceptors, and decoration.

  1. Basic Convertor: The output is a simple type T.
abstract class Convertor<To, From> {
To execute(covariant From from);
}
  1. StreamConvertor: The output is a Dart Stream<T>.
typedef StreamConvertor<To, From> = Convertor<Stream<To>, From>;
  1. AsyncConvertor: The output is a Dart Future<T>.
typedef AsyncConvertor<To, From> = Convertor<Future<To>, From>;

A Chain is a sequence of multiple Convertors queued together. The Chain itself acts as a single Convertor:

  • Its input type is the input type of the first Convertor.
  • Its output type is the output type of the last Convertor.

An Interceptor is a component that wraps a Convertor. Its purpose is to allow side effects without modifying the data flow.

  • It intercepts the input before the Convertor is executed.
  • It intercepts the output after the Convertor is executed.
  • Crucially, an Interceptor should not modify the input or the output.
  • The result of the wrapping is a new Convertor with the same input and output types as the wrapped one.

A Decorator wraps a Convertor and is used to allow modification of the input and/or output of that Convertor.

It offers the flexibility to:

  • Modify the input before it reaches the wrapped Convertor.
  • Modify the output after it’s returned.
  • Bypass the wrapped Convertor entirely.
  • Return a completely different output (of the same output type) without executing the original logic.

Model Convertors transform GQL raw models into clean domain models. They are applied in the repository layer:

// Model convertor: raw GQL type -> domain model
@riverpod
Convertor<NotificationModel,
GQueryNotificationData_notification_notificationItems>
notificationModelConvertor(Ref ref) {
return Convertor((raw) {
return NotificationModel(
id: raw.id ?? '',
title: raw.title ?? 'Untitled',
date: raw.date ?? DateTime.now(),
attachments: raw.images
?.map((i) => AttachmentModel(id: i?.id ?? '', mediaUrl: i?.mediaUrl ?? ''))
.toList() ?? [],
);
});
}

Note: GQL raw models are generated by Ferry via build.yaml custom type mapping, which maps GQL scalar types directly to Dart classes. This eliminates the need for manual conversion extensions that were prone to forgotten fields.

These transform domain filter/sort criteria into GQL-compatible filter/sort types:

@riverpod
Convertor<GnotificationFilters, NotificationFilterCriteria>
notificationFilterConvertor(Ref ref) {
return Convertor((filter) {
return GnotificationFilters((b) => b
..id = filter.id
..systemParentId = filter.systemParentId);
});
}

These build Ferry operation requests from typed parameters:

@riverpod
Convertor<GQueryNotificationReq,
ListRequestParams<GnotificationFilters, GnotificationOrder>>
notificationListQueryConvertor(Ref ref) {
return Convertor((params) {
return GQueryNotificationReq((builder) {
builder
..requestId = 'notification_list_${params.toString()}'
..vars.G_filter = params.filter?.toBuilder()
..vars.G_sort = params.sort?.toBuilder()
..vars.G_skip = params.skip
..vars.G_take = params.take;
});
});
}

Convertors chain together to form complete datasource pipelines:

@riverpod
StreamConvertor<List<GQueryNotificationData_notification_notificationItems>,
ListRequestParams<GnotificationFilters, GnotificationOrder>>
notificationListDatasource(Ref ref) {
return GraphQLRequestExecutor<GQueryNotificationData,
ListRequestParams<GnotificationFilters, GnotificationOrder>,
GQueryNotificationVars>(
gqlClient: ref.watch(gqlClientProvider),
convertor: ref.watch(notificationListQueryConvertorProvider),
)
.then(GraphQLStreamConvertor())
.map((data) => data.notification!.notificationItems!.nonNulls.toList());
}

Cache operations use declarative CacheHandlerSpecs with UpdateCacheTypedLink:

// Merge mutation response into cached list query
CacheHandlerSpecs.merge(
mapToCachedRequest: Convertor((request) => /* map to cached query request */),
mapResponse: Convertor((mutationData) => /* extract data to merge */),
mergeCachedData: Convertor(((oldData, newData)) => /* merge logic */),
);

This replaces the imperative CacheHandlerStrategy registration from the legacy Strategy pattern.

The Convertor pattern replaces the legacy Strategy/DatasourceModule pattern:

ConceptStrategy (Retired)Convertor (Current)
Request buildingRequestStrategy classConvertor<Request, Params> function
Cache handlingCacheHandlerStrategy + manual registrationCacheHandlerSpecs + UpdateCacheTypedLink
Module setupDatasourceModule.create()@riverpod provider with direct composition
CompositionLimited (fixed slots)Unlimited (.then(), .map(), .thenMap())
RegistrationManual strategy registration with key enumsNo registration needed
TestingMock strategy contextTest Convertors as pure functions