Core Concepts
Core Concepts
Section titled â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 Convertor Pattern
Section titled âThe Convertor Patternâ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.
Convertor Types
Section titled âConvertor TypesâConvertors are categorized into three main types based on their output type. This differentiation primarily facilitates various extensions for chaining, mapping, interceptors, and decoration.
- Basic Convertor: The output is a simple type
T.
abstract class Convertor<To, From> { To execute(covariant From from);}- StreamConvertor: The output is a Dart
Stream<T>.
typedef StreamConvertor<To, From> = Convertor<Stream<To>, From>;- AsyncConvertor: The output is a Dart
Future<T>.
typedef AsyncConvertor<To, From> = Convertor<Future<To>, From>;Architectural Components
Section titled âArchitectural Componentsâ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.
Interceptor
Section titled âInterceptorâ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.
Decorator
Section titled âDecoratorâ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.
How the Convertor Pattern Works in Practice
Section titled âHow the Convertor Pattern Works in PracticeâModel Convertors
Section titled âModel ConvertorsâModel Convertors transform GQL raw models into clean domain models. They are applied in the repository layer:
// Model convertor: raw GQL type -> domain model@riverpodConvertor<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.
Filter and Sort Convertors
Section titled âFilter and Sort ConvertorsâThese transform domain filter/sort criteria into GQL-compatible filter/sort types:
@riverpodConvertor<GnotificationFilters, NotificationFilterCriteria>notificationFilterConvertor(Ref ref) { return Convertor((filter) { return GnotificationFilters((b) => b ..id = filter.id ..systemParentId = filter.systemParentId); });}Request Convertors
Section titled âRequest ConvertorsâThese build Ferry operation requests from typed parameters:
@riverpodConvertor<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; }); });}Datasource Pipelines
Section titled âDatasource PipelinesâConvertors chain together to form complete datasource pipelines:
@riverpodStreamConvertor<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 Management
Section titled âCache ManagementâCache operations use declarative CacheHandlerSpecs with UpdateCacheTypedLink:
// Merge mutation response into cached list queryCacheHandlerSpecs.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.
Convertor vs Strategy Pattern
Section titled âConvertor vs Strategy PatternâThe Convertor pattern replaces the legacy Strategy/DatasourceModule pattern:
| Concept | Strategy (Retired) | Convertor (Current) |
|---|---|---|
| Request building | RequestStrategy class | Convertor<Request, Params> function |
| Cache handling | CacheHandlerStrategy + manual registration | CacheHandlerSpecs + UpdateCacheTypedLink |
| Module setup | DatasourceModule.create() | @riverpod provider with direct composition |
| Composition | Limited (fixed slots) | Unlimited (.then(), .map(), .thenMap()) |
| Registration | Manual strategy registration with key enums | No registration needed |
| Testing | Mock strategy context | Test Convertors as pure functions |