From 24d13de2c79603cc5a219503d54a6b86fe06ac0f Mon Sep 17 00:00:00 2001 From: Joaquin Neschisi Date: Tue, 23 Jul 2024 15:34:50 -0300 Subject: [PATCH 1/2] refactor: app flavors --- lib/demo/view/demo_page.dart | 65 +++++++++-------- lib/demo/widgets/device_frame.dart | 5 +- lib/flavor_button/cubit/flavor_cubit.dart | 9 +++ lib/flavor_button/flavor_button.dart | 2 + lib/flavor_button/view/flavor_button.dart | 40 +++++++++++ .../view/theme_potential_thumbnail.dart | 4 +- test/demo/demo_page_test.dart | 50 +++++++++++++- test/helpers/pump_experience.dart | 11 ++- .../cubit/flavor_cubit_test.dart | 20 ++++++ .../view/flavor_button_test.dart | 69 +++++++++++++++++++ .../theme_potential_thumbnail_test.dart | 7 ++ 11 files changed, 240 insertions(+), 42 deletions(-) create mode 100644 lib/flavor_button/cubit/flavor_cubit.dart create mode 100644 lib/flavor_button/flavor_button.dart create mode 100644 lib/flavor_button/view/flavor_button.dart create mode 100644 test/src/flavor_button/cubit/flavor_cubit_test.dart create mode 100644 test/src/flavor_button/view/flavor_button_test.dart diff --git a/lib/demo/view/demo_page.dart b/lib/demo/view/demo_page.dart index 232cbe7..f7e05b2 100644 --- a/lib/demo/view/demo_page.dart +++ b/lib/demo/view/demo_page.dart @@ -1,4 +1,5 @@ import 'package:financial_dashboard/demo/demo.dart'; +import 'package:financial_dashboard/flavor_button/flavor_button.dart'; import 'package:financial_dashboard/l10n/l10n.dart'; import 'package:financial_dashboard/theme_button/theme_button.dart'; import 'package:financial_dashboard/ui/ui.dart'; @@ -10,8 +11,11 @@ class DemoPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (_) => ThemeModeCubit(), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => ThemeModeCubit()), + BlocProvider(create: (_) => FlavorCubit()), + ], child: const DemoView(), ); } @@ -27,39 +31,34 @@ class DemoView extends StatelessWidget { return Scaffold( appBar: AppBar( title: Text(l10n.appBarTitleText), - actions: const [ThemeButton()], + actions: const [ + ThemeButton(), + SizedBox(width: AppSpacing.sm), + FlavorButton(flavor: AppFlavor.one), + FlavorButton(flavor: AppFlavor.two), + FlavorButton(flavor: AppFlavor.three), + ], ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(AppSpacing.xlg), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Wrap( - spacing: AppSpacing.xlg, - runSpacing: AppSpacing.xlg, - alignment: WrapAlignment.center, - children: [ - DeviceFrame( - lightTheme: const FlavorOneTheme().themeData, - darkTheme: const FlavorOneDarkTheme().themeData, - child: const AppOne(), - ), - DeviceFrame( - lightTheme: const FlavorTwoTheme().themeData, - darkTheme: const FlavorTwoDarkTheme().themeData, - child: const AppTwo(), - ), - DeviceFrame( - lightTheme: const FlavorThreeTheme().themeData, - darkTheme: const FlavorThreeDarkTheme().themeData, - child: const AppThree(), - ), - ], + body: BlocBuilder( + builder: (context, state) { + return switch (state) { + AppFlavor.one => DeviceFrame( + lightTheme: const FlavorOneTheme().themeData, + darkTheme: const FlavorOneDarkTheme().themeData, + child: const AppOne(), ), - ], - ), - ), + AppFlavor.two => DeviceFrame( + lightTheme: const FlavorTwoTheme().themeData, + darkTheme: const FlavorTwoDarkTheme().themeData, + child: const AppTwo(), + ), + AppFlavor.three => DeviceFrame( + lightTheme: const FlavorThreeTheme().themeData, + darkTheme: const FlavorThreeDarkTheme().themeData, + child: const AppThree(), + ), + }; + }, ), ); } diff --git a/lib/demo/widgets/device_frame.dart b/lib/demo/widgets/device_frame.dart index 19b9feb..73946a4 100644 --- a/lib/demo/widgets/device_frame.dart +++ b/lib/demo/widgets/device_frame.dart @@ -1,5 +1,4 @@ import 'package:financial_dashboard/theme_button/theme_button.dart'; -import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,8 +8,8 @@ class DeviceFrame extends StatelessWidget { required this.darkTheme, required this.child, super.key, - this.borderRadius = AppSpacing.xlg, - this.elevation = AppSpacing.xxs, + this.borderRadius = 0, + this.elevation = 0, }); final ThemeData lightTheme; diff --git a/lib/flavor_button/cubit/flavor_cubit.dart b/lib/flavor_button/cubit/flavor_cubit.dart new file mode 100644 index 0000000..5941c2f --- /dev/null +++ b/lib/flavor_button/cubit/flavor_cubit.dart @@ -0,0 +1,9 @@ +import 'package:bloc/bloc.dart'; + +enum AppFlavor { one, two, three } + +class FlavorCubit extends Cubit { + FlavorCubit() : super(AppFlavor.one); + + void select(AppFlavor flavor) => emit(flavor); +} diff --git a/lib/flavor_button/flavor_button.dart b/lib/flavor_button/flavor_button.dart new file mode 100644 index 0000000..ec2e1e9 --- /dev/null +++ b/lib/flavor_button/flavor_button.dart @@ -0,0 +1,2 @@ +export 'cubit/flavor_cubit.dart'; +export 'view/flavor_button.dart'; diff --git a/lib/flavor_button/view/flavor_button.dart b/lib/flavor_button/view/flavor_button.dart new file mode 100644 index 0000000..a21c2bc --- /dev/null +++ b/lib/flavor_button/view/flavor_button.dart @@ -0,0 +1,40 @@ +import 'package:financial_dashboard/flavor_button/flavor_button.dart'; +import 'package:financial_dashboard/ui/ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FlavorButton extends StatelessWidget { + const FlavorButton({required this.flavor, super.key}); + + final AppFlavor flavor; + + @override + Widget build(BuildContext context) { + final currentFlavor = context.watch().state; + + final outlinedIcon = switch (flavor) { + AppFlavor.one => Icons.looks_one_outlined, + AppFlavor.two => Icons.looks_two_outlined, + AppFlavor.three => Icons.looks_3_outlined, + }; + + final solidIcon = switch (flavor) { + AppFlavor.one => Icons.looks_one, + AppFlavor.two => Icons.looks_two, + AppFlavor.three => Icons.looks_3, + }; + + final icon = currentFlavor == flavor ? solidIcon : outlinedIcon; + + return Align( + child: InkWell( + onTap: () => context.read().select(flavor), + customBorder: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(AppSpacing.sm), + child: Icon(icon), + ), + ), + ); + } +} diff --git a/lib/thumbnail/view/theme_potential_thumbnail.dart b/lib/thumbnail/view/theme_potential_thumbnail.dart index 773d013..c666d21 100644 --- a/lib/thumbnail/view/theme_potential_thumbnail.dart +++ b/lib/thumbnail/view/theme_potential_thumbnail.dart @@ -136,8 +136,8 @@ class _AppView extends StatelessWidget { return DeviceFrame( lightTheme: lightTheme, darkTheme: darkTheme, - borderRadius: 0, - elevation: 0, + borderRadius: AppSpacing.xlg, + elevation: AppSpacing.xxs, child: child, ); } diff --git a/test/demo/demo_page_test.dart b/test/demo/demo_page_test.dart index 0d931da..67ccecb 100644 --- a/test/demo/demo_page_test.dart +++ b/test/demo/demo_page_test.dart @@ -1,13 +1,59 @@ import 'package:financial_dashboard/demo/demo.dart'; +import 'package:financial_dashboard/flavor_button/flavor_button.dart'; +import 'package:financial_dashboard/theme_button/theme_button.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import '../helpers/helpers.dart'; void main() { group('DemoPage', () { - testWidgets('renders three DeviceFrames', (tester) async { + testWidgets('renders DemoView', (tester) async { await tester.pumpExperience(const DemoPage()); - expect(find.byType(DeviceFrame), findsNWidgets(3)); + expect(find.byType(DemoView), findsOneWidget); + }); + }); + + group('DemoView', () { + late FlavorCubit flavorCubit; + late ThemeModeCubit themeModeCubit; + + setUp(() { + flavorCubit = MockFlavorCubit(); + themeModeCubit = MockThemeModeCubit(); + + when(() => themeModeCubit.state).thenReturn(ThemeMode.light); + }); + + testWidgets('renders AppOne when AppFlavor is one', (tester) async { + when(() => flavorCubit.state).thenReturn(AppFlavor.one); + await tester.pumpExperience( + const DemoView(), + flavorCubit: flavorCubit, + themeModeCubit: themeModeCubit, + ); + expect(find.byType(AppOne), findsOneWidget); + }); + + testWidgets('renders AppTwo when AppFlavor is two', (tester) async { + when(() => flavorCubit.state).thenReturn(AppFlavor.two); + await tester.pumpExperience( + const DemoView(), + flavorCubit: flavorCubit, + themeModeCubit: themeModeCubit, + ); + expect(find.byType(AppTwo), findsOneWidget); + }); + + testWidgets('renders AppThree when AppFlavor is three', (tester) async { + when(() => flavorCubit.state).thenReturn(AppFlavor.three); + await tester.pumpExperience( + const DemoView(), + flavorCubit: flavorCubit, + themeModeCubit: themeModeCubit, + ); + expect(find.byType(AppThree), findsOneWidget); }); }); } diff --git a/test/helpers/pump_experience.dart b/test/helpers/pump_experience.dart index 5557f8e..0a9acfc 100644 --- a/test/helpers/pump_experience.dart +++ b/test/helpers/pump_experience.dart @@ -1,4 +1,5 @@ import 'package:bloc_test/bloc_test.dart'; +import 'package:financial_dashboard/flavor_button/cubit/flavor_cubit.dart'; import 'package:financial_dashboard/l10n/l10n.dart'; import 'package:financial_dashboard/theme_button/theme_button.dart'; import 'package:flutter/material.dart'; @@ -8,14 +9,20 @@ import 'package:flutter_test/flutter_test.dart'; class MockThemeModeCubit extends MockCubit implements ThemeModeCubit {} +class MockFlavorCubit extends MockCubit implements FlavorCubit {} + extension PumpExperience on WidgetTester { Future pumpExperience( Widget widget, { ThemeModeCubit? themeModeCubit, + FlavorCubit? flavorCubit, }) { return pumpWidget( - BlocProvider( - create: (_) => themeModeCubit ?? MockThemeModeCubit(), + MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => themeModeCubit ?? MockThemeModeCubit()), + BlocProvider(create: (_) => flavorCubit ?? MockFlavorCubit()), + ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, diff --git a/test/src/flavor_button/cubit/flavor_cubit_test.dart b/test/src/flavor_button/cubit/flavor_cubit_test.dart new file mode 100644 index 0000000..471bc73 --- /dev/null +++ b/test/src/flavor_button/cubit/flavor_cubit_test.dart @@ -0,0 +1,20 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:financial_dashboard/flavor_button/cubit/flavor_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FlavorCubit', () { + blocTest( + 'initial state is AppFlavor.one', + build: FlavorCubit.new, + verify: (cubit) => expect(cubit.state, AppFlavor.one), + ); + + blocTest( + 'select method changes AppFlavor', + build: FlavorCubit.new, + act: (cubit) => cubit.select(AppFlavor.two), + expect: () => [AppFlavor.two], + ); + }); +} diff --git a/test/src/flavor_button/view/flavor_button_test.dart b/test/src/flavor_button/view/flavor_button_test.dart new file mode 100644 index 0000000..e74279c --- /dev/null +++ b/test/src/flavor_button/view/flavor_button_test.dart @@ -0,0 +1,69 @@ +import 'package:financial_dashboard/flavor_button/flavor_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../../helpers/helpers.dart'; + +void main() { + group('FlavorButton', () { + Widget buildSubject(AppFlavor flavor) => Scaffold( + body: FlavorButton( + flavor: flavor, + ), + ); + + late FlavorCubit flavorCubit; + + setUp(() { + flavorCubit = MockFlavorCubit(); + }); + + testWidgets( + 'renders solid icon when it is current flavor', + (tester) async { + when(() => flavorCubit.state).thenReturn(AppFlavor.one); + await tester.pumpExperience( + BlocProvider.value( + value: flavorCubit, + child: buildSubject(AppFlavor.one), + ), + ); + + expect(find.byIcon(Icons.looks_one), findsOneWidget); + }, + ); + + testWidgets( + 'renders outlined icon when it is not current flavor', + (tester) async { + when(() => flavorCubit.state).thenReturn(AppFlavor.two); + await tester.pumpExperience( + BlocProvider.value( + value: flavorCubit, + child: buildSubject(AppFlavor.one), + ), + ); + + expect(find.byIcon(Icons.looks_one_outlined), findsOneWidget); + }, + ); + + testWidgets( + 'sets the new AppFlavor on tap', + (tester) async { + when(() => flavorCubit.state).thenReturn(AppFlavor.two); + await tester.pumpExperience( + BlocProvider.value( + value: flavorCubit, + child: buildSubject(AppFlavor.one), + ), + ); + + await tester.tap(find.byType(InkWell)); + verify(() => flavorCubit.select(AppFlavor.one)).called(1); + }, + ); + }); +} diff --git a/test/thumbnail/theme_potential_thumbnail_test.dart b/test/thumbnail/theme_potential_thumbnail_test.dart index dbfe922..d120d8c 100644 --- a/test/thumbnail/theme_potential_thumbnail_test.dart +++ b/test/thumbnail/theme_potential_thumbnail_test.dart @@ -1,5 +1,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:financial_dashboard/demo/demo.dart'; +import 'package:financial_dashboard/flavor_button/cubit/flavor_cubit.dart'; import 'package:financial_dashboard/theme_button/theme_button.dart'; import 'package:financial_dashboard/thumbnail/thumbnail.dart'; import 'package:flutter/material.dart'; @@ -11,6 +12,8 @@ import '../helpers/helpers.dart'; class MockThemeModeCubit extends MockCubit implements ThemeModeCubit {} +class MockFlavorCubit extends MockCubit implements FlavorCubit {} + void main() { group('ThemePotentialThumbnail', () { testWidgets('renders AppFlavorView', (tester) async { @@ -21,15 +24,18 @@ void main() { group('AppFlavorView', () { late ThemeModeCubit themeModeCubit; + late FlavorCubit flavorCubit; late AnimationController controller; setUp(() { themeModeCubit = MockThemeModeCubit(); + flavorCubit = MockFlavorCubit(); controller = AnimationController( vsync: const TestVSync(), duration: const Duration(milliseconds: 3500), ); when(() => themeModeCubit.state).thenReturn(ThemeMode.light); + when(() => flavorCubit.state).thenReturn(AppFlavor.one); }); testWidgets('renders correct widgets on animation', (tester) async { @@ -38,6 +44,7 @@ void main() { animationController: controller, ), themeModeCubit: themeModeCubit, + flavorCubit: flavorCubit, ); controller From 84bdce7954be3202ac3328e9c58bd272a2da78e5 Mon Sep 17 00:00:00 2001 From: Joaquin Neschisi Date: Wed, 24 Jul 2024 09:54:32 -0300 Subject: [PATCH 2/2] fix: analyzer infos --- lib/demo/widgets/app_one.dart | 4 ++-- lib/demo/widgets/app_three.dart | 4 ++-- lib/demo/widgets/app_two.dart | 6 +++--- .../widgets/charts/retirement_prediction_chart.dart | 11 +++++------ .../charts/retirement_prediction_chart_test.dart | 2 +- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/demo/widgets/app_one.dart b/lib/demo/widgets/app_one.dart index fe62e40..49aa46c 100644 --- a/lib/demo/widgets/app_one.dart +++ b/lib/demo/widgets/app_one.dart @@ -25,7 +25,7 @@ class AppOne extends StatelessWidget { children: [ CurrentSavings(savings: _currentSavings), const SizedBox(height: AppSpacing.md), - RetirementPredicitionChart( + RetirementPredictionChart( onCurrentSavings: (value) => _currentSavings.value = value, showAnnotations: true, ), @@ -47,7 +47,7 @@ class AppOne extends StatelessWidget { const SizedBox(width: AppSpacing.xlg), const Expanded( child: MonthlyGoal(amount: r'$3,125.00'), - ) + ), ], ), ), diff --git a/lib/demo/widgets/app_three.dart b/lib/demo/widgets/app_three.dart index 7403f80..7c9aae7 100644 --- a/lib/demo/widgets/app_three.dart +++ b/lib/demo/widgets/app_three.dart @@ -26,7 +26,7 @@ class AppThree extends StatelessWidget { children: [ CurrentSavings(savings: _currentSavings), const SizedBox(height: AppSpacing.md), - RetirementPredicitionChart( + RetirementPredictionChart( showAreaElement: true, selectedPointRadius: AppSpacing.xs, onCurrentSavings: (value) => _currentSavings.value = value, @@ -49,7 +49,7 @@ class AppThree extends StatelessWidget { const SizedBox(width: AppSpacing.xlg), const Expanded( child: MonthlyGoal(amount: r'$1,125.00'), - ) + ), ], ), ), diff --git a/lib/demo/widgets/app_two.dart b/lib/demo/widgets/app_two.dart index 9d341c6..1874c28 100644 --- a/lib/demo/widgets/app_two.dart +++ b/lib/demo/widgets/app_two.dart @@ -30,7 +30,7 @@ class AppTwo extends StatelessWidget { padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xlg, ), - child: RetirementPredicitionChart( + child: RetirementPredictionChart( onCurrentSavings: (value) => _currentSavings.value = value, ), ), @@ -55,7 +55,7 @@ class AppTwo extends StatelessWidget { left: AppSpacing.xlg, right: AppSpacing.xlg, child: Material( - color: colorScheme.surfaceVariant, + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(AppSpacing.xlg), child: Container( padding: const EdgeInsets.symmetric( @@ -65,7 +65,7 @@ class AppTwo extends StatelessWidget { child: CurrentSavings(savings: _currentSavings), ), ), - ) + ), ], ), ), diff --git a/lib/ui/widgets/charts/retirement_prediction_chart.dart b/lib/ui/widgets/charts/retirement_prediction_chart.dart index 04d1020..bc9fc8b 100644 --- a/lib/ui/widgets/charts/retirement_prediction_chart.dart +++ b/lib/ui/widgets/charts/retirement_prediction_chart.dart @@ -20,8 +20,8 @@ List _createSampleData() { return data; } -class RetirementPredicitionChart extends StatefulWidget { - const RetirementPredicitionChart({ +class RetirementPredictionChart extends StatefulWidget { + const RetirementPredictionChart({ required this.onCurrentSavings, super.key, this.showAreaElement = false, @@ -35,12 +35,11 @@ class RetirementPredicitionChart extends StatefulWidget { final double selectedPointRadius; @override - State createState() => - _RetirementPredicitionChartState(); + State createState() => + _RetirementPredictionChartState(); } -class _RetirementPredicitionChartState - extends State { +class _RetirementPredictionChartState extends State { late final List _sampleData; final _showTooltip = ValueNotifier(false); diff --git a/test/src/ui/widgets/charts/retirement_prediction_chart_test.dart b/test/src/ui/widgets/charts/retirement_prediction_chart_test.dart index bcd1fa6..35c6e6b 100644 --- a/test/src/ui/widgets/charts/retirement_prediction_chart_test.dart +++ b/test/src/ui/widgets/charts/retirement_prediction_chart_test.dart @@ -9,7 +9,7 @@ void main() { Widget buildSubject({ void Function(String)? onCurrentSavings, }) => - RetirementPredicitionChart( + RetirementPredictionChart( onCurrentSavings: onCurrentSavings ?? (_) {}, );