Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion example/test/home_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
loadProviders();

group('HomePage tests | ', () {
testWidgets(
'correct UI',
Expand Down Expand Up @@ -77,7 +79,10 @@ void main() {
expect(graphQLTileFinder, findsOneWidget);

await tester.tap(graphQLTileFinder);
await tester.pumpAndSettle();
// pumpAndSettle times out here; as the page has non-deterministic loading indicator
// so pumping each frame individually
await tester.pump();
await tester.pump();

expect(find.byType(CountryUI), findsOneWidget);
},
Expand Down
18 changes: 5 additions & 13 deletions packages/clean_framework/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,8 @@ import 'package:clean_framework_example/providers.dart';
import 'package:clean_framework_example/routing/routes.dart';
import 'package:clean_framework_router/clean_framework_router.dart';
import 'package:flutter/material.dart';
import 'package:stack_trace/stack_trace.dart' as stack_trace;

final container = ProviderContainer();

void main() {
FlutterError.demangleStackTrace = (StackTrace stack) {
if (stack is stack_trace.Trace) return stack.vmTrace;
if (stack is stack_trace.Chain) return stack.toTrace().vmTrace;
return stack;
};
initializeExternalInterfaces(container);

runApp(const MyApp());
}

Expand All @@ -23,13 +13,16 @@ class MyApp extends StatelessWidget {

@override
Widget build(BuildContext context) {
return ProviderScope(
parent: container,
return AppProviderScope(
externalInterfaceProviders: [
pokemonExternalInterfaceProvider,
],
child: AppRouterScope(
create: PokeRouter.new,
builder: (context) {
return MaterialApp.router(
title: 'Clean Framework Example',
routerConfig: context.router.config,
theme: ThemeData.from(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
useMaterial3: true,
Expand All @@ -42,7 +35,6 @@ class MyApp extends StatelessWidget {
useMaterial3: true,
),
themeMode: ThemeMode.dark,
routerConfig: context.router.config,
);
},
),
Expand Down
4 changes: 0 additions & 4 deletions packages/clean_framework/example/lib/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,3 @@ final pokemonExternalInterfaceProvider = ExternalInterfaceProvider(
pokemonSpeciesGateway,
],
);

void initializeExternalInterfaces(ProviderContainer container) {
pokemonExternalInterfaceProvider.initializeFor(container);
}
81 changes: 81 additions & 0 deletions packages/clean_framework/lib/src/core/app_provider_scope.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:clean_framework/clean_framework_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stack_trace/stack_trace.dart';

class AppProviderScope extends StatelessWidget {
const AppProviderScope({
super.key,
required this.child,
this.externalInterfaceProviders = const [],
this.overrides = const [],
this.observers,
this.parent,
});

final Widget child;

final ProviderContainer? parent;

/// The listeners that subscribes to changes on providers
/// stored on this [ProviderScope].
final List<ProviderObserver>? observers;

/// Information on how to override a provider/family.
final List<Override> overrides;

final List<ExternalInterfaceProvider> externalInterfaceProviders;

@override
Widget build(BuildContext context) {
return ProviderScope(
parent: parent,
observers: observers,
overrides: overrides,
child: Builder(
builder: (context) {
return _ProviderInitializer(
container: ProviderScope.containerOf(context),
externalInterfaceProviders: externalInterfaceProviders,
child: child,
);
},
),
);
}
}

class _ProviderInitializer extends StatefulWidget {
const _ProviderInitializer({
required this.container,
required this.child,
required this.externalInterfaceProviders,
});

final ProviderContainer container;
final Widget child;
final List<ExternalInterfaceProvider> externalInterfaceProviders;

@override
State<_ProviderInitializer> createState() => _ProviderInitializerState();
}

class _ProviderInitializerState extends State<_ProviderInitializer> {
@override
void initState() {
super.initState();

FlutterError.demangleStackTrace = (StackTrace stack) {
if (stack is Trace) return stack.vmTrace;
if (stack is Chain) return stack.toTrace().vmTrace;
return stack;
};

for (final provider in widget.externalInterfaceProviders) {
provider.initializeFor(widget.container);
}
}

@override
Widget build(BuildContext context) => widget.child;
}
1 change: 1 addition & 0 deletions packages/clean_framework/lib/src/core/core.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'app_provider_scope.dart';
export 'external_interface/external_interface.dart';
export 'external_interface/external_interface_provider.dart';
export 'external_interface/request.dart';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ extension InputFilterMapExtension<E extends Entity> on InputFilterMap<E> {
final processor = this[I];

if (processor == null) {
throw StateError('Input processor not defined for $I');
throw StateError(
'\n\nInput filter not defined for "$I".\n'
'Filters available for: ${keys.isEmpty ? 'none' : keys.join(', ')}\n'
'Dependency: $E\n\n',
);
}

return processor(input, entity);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ extension OutputFilterMapExtension<E extends Entity> on OutputFilterMap<E> {

if (builder == null) {
throw StateError(
'Output filter not defined for "$O".\n'
'Filters available for: ${keys.join(', ')}',
'\n\nOutput filter not defined for "$O".\n'
'Filters available for: ${keys.isEmpty ? 'none' : keys.join(', ')}\n'
'Dependency: $E\n\n',
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension RequestSubscriptionMapExtension<I extends Input>
on RequestSubscriptionMap<I> {
void add<O extends Output>(RequestSubscription<I> subscription) {
if (this[O] != null) {
throw StateError('A subscription for $O already exists');
throw StateError('A subscription for $O already exists.');
}

this[O] = subscription;
Expand All @@ -27,8 +27,28 @@ extension RequestSubscriptionMapExtension<I extends Input>
final subscription = this[O];

if (subscription == null) {
return Either<NoSubscriptionFailureInput, S>.left(
NoSubscriptionFailureInput<O>(),
throw StateError(
'\n\nNo subscription for "$O" exists.\n\n'
'Please follow the steps below in order to fix this issue:\n'
'1. Ensure that the use case that requests "$O" is attached '
'the appropriate gateway.\n'
' AppropriateGatewayProvider(\n'
' ...,\n'
' useCases: [<<useCaseProvider>>],\n'
' )\n'
'2. Ensure that the gateway that belongs to "$O" is attached '
'the appropriate external interface.\n'
' AppropriateExternalInterfaceProvider(\n'
' ...,\n'
' gateways: [<<gatewayProvider>>],\n'
' )\n'
'3. Ensure that the associated external interface is initialized.\n'
' AppProviderScope(\n'
' ...,\n'
' externalInterfaceProviders: [\n'
' <<associatedExternalInterfaceProvider>>,\n'
' ],\n'
' )\n',
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/clean_framework/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ dependencies:
flutter_riverpod: ^2.1.3
meta: '>=1.8.0 <1.9.0'
riverpod: ^2.1.3
stack_trace: ">=1.10.0 <1.12.0"

dev_dependencies:
clean_framework_test: ^0.1.0
Expand Down
15 changes: 8 additions & 7 deletions packages/clean_framework/test/core/use_case/use_case_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,16 @@ void main() {
);

test(
'request fails if there is no appropriate subscription present',
'throws if there is no appropriate subscription present',
() async {
await useCase.request<TestGatewayOutput, TestSuccessInput>(
TestGatewayOutput(name: 'World'),
onSuccess: (success) => TestEntity(foo: success.message),
onFailure: (failure) => TestEntity(foo: 'Hello Anonymous!'),
expect(
() => useCase.request<TestGatewayOutput, TestSuccessInput>(
TestGatewayOutput(name: 'World'),
onSuccess: (success) => TestEntity(),
onFailure: (failure) => TestEntity(),
),
throwsStateError,
);

expect(useCase.entity, TestEntity(foo: 'Hello Anonymous!'));
},
);

Expand Down