From 72b9bbbf3270b34ed5e10589026efbe2c7d9fec4 Mon Sep 17 00:00:00 2001 From: Jaime Date: Mon, 5 Aug 2024 11:54:04 +0200 Subject: [PATCH 1/8] feat: use monthly spending goal from bloc --- lib/demo/view/demo_page.dart | 5 ++++- lib/demo/widgets/app_one.dart | 2 +- lib/demo/widgets/app_three.dart | 2 +- lib/demo/widgets/monthly_goal.dart | 9 +++++---- test/demo/demo_page_test.dart | 13 +++++++++++++ test/helpers/pump_experience.dart | 9 +++++++++ 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/demo/view/demo_page.dart b/lib/demo/view/demo_page.dart index a0f3286..3b085ac 100644 --- a/lib/demo/view/demo_page.dart +++ b/lib/demo/view/demo_page.dart @@ -16,7 +16,10 @@ class DemoPage extends StatelessWidget { providers: [ BlocProvider(create: (_) => ThemeModeCubit()), BlocProvider(create: (_) => FlavorCubit()), - BlocProvider(create: (_) => FinancialDataBloc()), + BlocProvider( + create: (_) => + FinancialDataBloc()..add(const FinancialDataRequested()), + ), ], child: const DemoView(), ); diff --git a/lib/demo/widgets/app_one.dart b/lib/demo/widgets/app_one.dart index 49aa46c..dc3fa34 100644 --- a/lib/demo/widgets/app_one.dart +++ b/lib/demo/widgets/app_one.dart @@ -46,7 +46,7 @@ class AppOne extends StatelessWidget { ), const SizedBox(width: AppSpacing.xlg), const Expanded( - child: MonthlyGoal(amount: r'$3,125.00'), + child: MonthlyGoal(), ), ], ), diff --git a/lib/demo/widgets/app_three.dart b/lib/demo/widgets/app_three.dart index 7c9aae7..86ba650 100644 --- a/lib/demo/widgets/app_three.dart +++ b/lib/demo/widgets/app_three.dart @@ -48,7 +48,7 @@ class AppThree extends StatelessWidget { ), const SizedBox(width: AppSpacing.xlg), const Expanded( - child: MonthlyGoal(amount: r'$1,125.00'), + child: MonthlyGoal(), ), ], ), diff --git a/lib/demo/widgets/monthly_goal.dart b/lib/demo/widgets/monthly_goal.dart index b21e7d5..04c69d3 100644 --- a/lib/demo/widgets/monthly_goal.dart +++ b/lib/demo/widgets/monthly_goal.dart @@ -1,21 +1,22 @@ +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/l10n/l10n.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class MonthlyGoal extends StatelessWidget { const MonthlyGoal({ - required this.amount, super.key, }); - final String amount; - @override Widget build(BuildContext context) { final l10n = context.l10n; final theme = Theme.of(context); final textTheme = theme.textTheme; final colorScheme = theme.colorScheme; + final monthlySpendingGoal = context + .select((FinancialDataBloc bloc) => bloc.state.monthlySpendingGoal); return Card( elevation: 0, @@ -32,7 +33,7 @@ class MonthlyGoal extends StatelessWidget { fit: BoxFit.scaleDown, child: DefaultTextStyle( style: textTheme.displaySmall!, - child: Text(amount), + child: Text(monthlySpendingGoal.toCurrencyWithDecimals()), ), ), const SizedBox(height: AppSpacing.xxs), diff --git a/test/demo/demo_page_test.dart b/test/demo/demo_page_test.dart index 67ccecb..8f51079 100644 --- a/test/demo/demo_page_test.dart +++ b/test/demo/demo_page_test.dart @@ -1,4 +1,6 @@ +import 'package:bloc_test/bloc_test.dart'; import 'package:financial_dashboard/demo/demo.dart'; +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/flavor_button/flavor_button.dart'; import 'package:financial_dashboard/theme_button/theme_button.dart'; import 'package:flutter/material.dart'; @@ -7,6 +9,10 @@ import 'package:mocktail/mocktail.dart'; import '../helpers/helpers.dart'; +class _MockFinancialDataBloc + extends MockBloc + implements FinancialDataBloc {} + void main() { group('DemoPage', () { testWidgets('renders DemoView', (tester) async { @@ -18,12 +24,16 @@ void main() { group('DemoView', () { late FlavorCubit flavorCubit; late ThemeModeCubit themeModeCubit; + late FinancialDataBloc financialDataBloc; setUp(() { flavorCubit = MockFlavorCubit(); themeModeCubit = MockThemeModeCubit(); + financialDataBloc = _MockFinancialDataBloc(); when(() => themeModeCubit.state).thenReturn(ThemeMode.light); + when(() => financialDataBloc.state) + .thenReturn(const FinancialDataState()); }); testWidgets('renders AppOne when AppFlavor is one', (tester) async { @@ -32,6 +42,7 @@ void main() { const DemoView(), flavorCubit: flavorCubit, themeModeCubit: themeModeCubit, + financialDataBloc: financialDataBloc, ); expect(find.byType(AppOne), findsOneWidget); }); @@ -42,6 +53,7 @@ void main() { const DemoView(), flavorCubit: flavorCubit, themeModeCubit: themeModeCubit, + financialDataBloc: financialDataBloc, ); expect(find.byType(AppTwo), findsOneWidget); }); @@ -52,6 +64,7 @@ void main() { const DemoView(), flavorCubit: flavorCubit, themeModeCubit: themeModeCubit, + financialDataBloc: financialDataBloc, ); expect(find.byType(AppThree), findsOneWidget); }); diff --git a/test/helpers/pump_experience.dart b/test/helpers/pump_experience.dart index 10ab399..019a47e 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/financial_data/financial_data.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'; @@ -11,17 +12,25 @@ class MockThemeModeCubit extends MockCubit class MockFlavorCubit extends MockCubit implements FlavorCubit {} +class _MockFinancialDataBloc + extends MockBloc + implements FinancialDataBloc {} + extension PumpExperience on WidgetTester { Future pumpExperience( Widget widget, { ThemeModeCubit? themeModeCubit, FlavorCubit? flavorCubit, + FinancialDataBloc? financialDataBloc, }) { return pumpWidget( MultiBlocProvider( providers: [ BlocProvider(create: (_) => themeModeCubit ?? MockThemeModeCubit()), BlocProvider(create: (_) => flavorCubit ?? MockFlavorCubit()), + BlocProvider( + create: (_) => financialDataBloc ?? _MockFinancialDataBloc(), + ), ], child: MaterialApp( localizationsDelegates: AppLocalizations.localizationsDelegates, From 9bf900a65fccb3c3bc1a41ea945a39567cb858c1 Mon Sep 17 00:00:00 2001 From: Jaime Date: Mon, 5 Aug 2024 12:58:05 +0200 Subject: [PATCH 2/8] feat: use current savings data from bloc --- lib/demo/widgets/app_one.dart | 5 +- lib/demo/widgets/app_three.dart | 5 +- lib/demo/widgets/app_two.dart | 8 +- lib/demo/widgets/current_savings.dart | 20 ++--- lib/l10n/arb/app_en.arb | 6 ++ .../charts/line_chart/line_chart_body.dart | 4 +- .../charts/retirement_prediction_chart.dart | 84 ++++++++----------- .../retirement_prediction_chart_test.dart | 4 +- 8 files changed, 54 insertions(+), 82 deletions(-) diff --git a/lib/demo/widgets/app_one.dart b/lib/demo/widgets/app_one.dart index dc3fa34..13dd122 100644 --- a/lib/demo/widgets/app_one.dart +++ b/lib/demo/widgets/app_one.dart @@ -5,8 +5,6 @@ import 'package:flutter/material.dart'; class AppOne extends StatelessWidget { const AppOne({super.key}); - static final _currentSavings = ValueNotifier(null); - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -23,10 +21,9 @@ class AppOne extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CurrentSavings(savings: _currentSavings), + const CurrentSavings(), const SizedBox(height: AppSpacing.md), RetirementPredictionChart( - onCurrentSavings: (value) => _currentSavings.value = value, showAnnotations: true, ), ], diff --git a/lib/demo/widgets/app_three.dart b/lib/demo/widgets/app_three.dart index 86ba650..72a171a 100644 --- a/lib/demo/widgets/app_three.dart +++ b/lib/demo/widgets/app_three.dart @@ -5,8 +5,6 @@ import 'package:flutter/material.dart'; class AppThree extends StatelessWidget { const AppThree({super.key}); - static final _currentSavings = ValueNotifier(null); - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -24,12 +22,11 @@ class AppThree extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CurrentSavings(savings: _currentSavings), + const CurrentSavings(), const SizedBox(height: AppSpacing.md), RetirementPredictionChart( showAreaElement: true, selectedPointRadius: AppSpacing.xs, - onCurrentSavings: (value) => _currentSavings.value = value, ), ], ), diff --git a/lib/demo/widgets/app_two.dart b/lib/demo/widgets/app_two.dart index 1874c28..1c00cb2 100644 --- a/lib/demo/widgets/app_two.dart +++ b/lib/demo/widgets/app_two.dart @@ -5,8 +5,6 @@ import 'package:flutter/material.dart'; class AppTwo extends StatelessWidget { const AppTwo({super.key}); - static final _currentSavings = ValueNotifier(null); - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -30,9 +28,7 @@ class AppTwo extends StatelessWidget { padding: const EdgeInsets.symmetric( horizontal: AppSpacing.xlg, ), - child: RetirementPredictionChart( - onCurrentSavings: (value) => _currentSavings.value = value, - ), + child: RetirementPredictionChart(), ), Container( height: spacing, @@ -62,7 +58,7 @@ class AppTwo extends StatelessWidget { horizontal: AppSpacing.md, vertical: AppSpacing.lg, ), - child: CurrentSavings(savings: _currentSavings), + child: const CurrentSavings(), ), ), ), diff --git a/lib/demo/widgets/current_savings.dart b/lib/demo/widgets/current_savings.dart index 89f27da..a8e476d 100644 --- a/lib/demo/widgets/current_savings.dart +++ b/lib/demo/widgets/current_savings.dart @@ -1,32 +1,28 @@ +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/l10n/l10n.dart'; import 'package:financial_dashboard/ui/ui.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class CurrentSavings extends StatelessWidget { const CurrentSavings({ - required this.savings, super.key, }); - final ValueListenable savings; - @override Widget build(BuildContext context) { final l10n = context.l10n; final textTheme = Theme.of(context).textTheme; + final currentSavings = context.select( + (FinancialDataBloc bloc) => bloc.state.currentSavings, + ); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ValueListenableBuilder( - valueListenable: savings, - builder: (context, value, child) { - return Text( - '\$${value ?? ''}', - style: textTheme.displayMedium, - ); - }, + Text( + currentSavings.toCurrencyWithoutDecimal(), + style: textTheme.displayMedium, ), const SizedBox(height: AppSpacing.xs), Text( diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 3046863..cdaae73 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -40,5 +40,11 @@ "description": "Label for spendings transaction types", "type": "text", "placeholders": {} + }, + "noDataAvailable": "No data available", + "@noDataAvailable": { + "description": "Text shown when there is no data available", + "type": "text", + "placeholders": {} } } diff --git a/lib/ui/widgets/charts/line_chart/line_chart_body.dart b/lib/ui/widgets/charts/line_chart/line_chart_body.dart index e2e769a..2a5de12 100644 --- a/lib/ui/widgets/charts/line_chart/line_chart_body.dart +++ b/lib/ui/widgets/charts/line_chart/line_chart_body.dart @@ -48,7 +48,7 @@ class _LineChartBodyState extends State { }); _gestureStream = StreamController.broadcast(); - _gestureStream.stream.listen(_ongestureStreamUpdate); + _gestureStream.stream.listen(_onGestureStreamUpdate); } void _onSelectionChannelUpdate(Map>? selections) { @@ -60,7 +60,7 @@ class _LineChartBodyState extends State { } } - void _ongestureStreamUpdate(GestureEvent signal) { + void _onGestureStreamUpdate(GestureEvent signal) { final gesture = signal.gesture; final type = gesture.type; final position = gesture.localPosition; diff --git a/lib/ui/widgets/charts/retirement_prediction_chart.dart b/lib/ui/widgets/charts/retirement_prediction_chart.dart index bc9fc8b..47e39a3 100644 --- a/lib/ui/widgets/charts/retirement_prediction_chart.dart +++ b/lib/ui/widgets/charts/retirement_prediction_chart.dart @@ -1,80 +1,59 @@ -import 'dart:math'; - +import 'package:financial_dashboard/financial_data/financial_data.dart'; +import 'package:financial_dashboard/l10n/l10n.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -List _createSampleData() { - final data = []; - var value = 100000.0; - for (var age = 25; age <= 90; age++) { - final toAdd = Random().nextInt(13000); - value += toAdd; - data.add( - LineChartPoint( - age: age.toString(), - value: value, - ), - ); - } - return data; -} +import 'package:flutter_bloc/flutter_bloc.dart'; -class RetirementPredictionChart extends StatefulWidget { - const RetirementPredictionChart({ - required this.onCurrentSavings, +class RetirementPredictionChart extends StatelessWidget { + RetirementPredictionChart({ super.key, this.showAreaElement = false, this.showAnnotations = false, this.selectedPointRadius = AppSpacing.sm, }); - final ValueChanged onCurrentSavings; final bool showAreaElement; final bool showAnnotations; final double selectedPointRadius; - @override - State createState() => - _RetirementPredictionChartState(); -} - -class _RetirementPredictionChartState extends State { - late final List _sampleData; - final _showTooltip = ValueNotifier(false); final _selectedPoint = ValueNotifier(null); final _tooltipPosition = ValueNotifier(null); - @override - void initState() { - super.initState(); - _sampleData = _createSampleData(); - - final sampleValue = _sampleData[20].value; - final currentSavings = NumberFormat('###,###').format(sampleValue); - widget.onCurrentSavings(currentSavings); - } - - String _defaultValueFormat(double value) => - '\$${NumberFormat('###,###').format(value)}'; - @override Widget build(BuildContext context) { + final l10n = context.l10n; final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final data = context + .select((FinancialDataBloc bloc) => bloc.state.savingsDataPoints); + final mappedData = data + .map( + (e) => LineChartPoint( + age: e.age.toString(), + value: e.value, + ), + ) + .toList(); + + if (data.isEmpty) { + return SizedBox( + height: 200, + child: Center(child: Text(l10n.noDataAvailable)), + ); + } return Stack( children: [ SizedBox( height: 200, child: LineChartBody( - data: _sampleData, - showAreaElement: widget.showAreaElement, - showAnnotations: widget.showAnnotations, - selectedPointRadius: widget.selectedPointRadius, + data: mappedData, + showAreaElement: showAreaElement, + showAnnotations: showAnnotations, + selectedPointRadius: selectedPointRadius, onPointSelected: (value) { - final point = _sampleData.elementAt(value); + final point = mappedData.elementAt(value); _selectedPoint.value = point; }, onShowTooltip: ({required position, required show}) { @@ -116,9 +95,12 @@ class _RetirementPredictionChartState extends State { ), child: ValueListenableBuilder( valueListenable: _selectedPoint, - builder: (context, value, child) { + builder: (context, selectedPoint, child) { + if (selectedPoint == null) { + return const SizedBox.shrink(); + } return Text( - _defaultValueFormat(_selectedPoint.value?.value ?? 0), + selectedPoint.value.toCurrencyWithoutDecimal(), ); }, ), 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 35c6e6b..58d06d2 100644 --- a/test/src/ui/widgets/charts/retirement_prediction_chart_test.dart +++ b/test/src/ui/widgets/charts/retirement_prediction_chart_test.dart @@ -9,9 +9,7 @@ void main() { Widget buildSubject({ void Function(String)? onCurrentSavings, }) => - RetirementPredictionChart( - onCurrentSavings: onCurrentSavings ?? (_) {}, - ); + RetirementPredictionChart(); testWidgets('renders LineChartBody', (tester) async { await tester.pumpExperience(buildSubject()); From 743c9fa9d9afd42dc2ea78fe8e3a47091b00561f Mon Sep 17 00:00:00 2001 From: Jaime Date: Mon, 5 Aug 2024 13:45:00 +0200 Subject: [PATCH 3/8] feat: calculate progress from bloc data --- lib/demo/widgets/monthly_goal.dart | 9 +++++---- lib/demo/widgets/transactions_table.dart | 8 ++++---- .../bloc/financial_data_bloc.dart | 6 +++--- .../bloc/financial_data_state.dart | 19 ++++++++++++++----- lib/l10n/arb/app_en.arb | 13 +++++++------ lib/ui/widgets/goal_progress_indicator.dart | 12 ++++++++++++ .../bloc/financial_data_bloc_test.dart | 2 +- .../bloc/financial_data_state_test.dart | 4 ++-- .../demo/widgets/transactions_table_test.dart | 2 +- 9 files changed, 49 insertions(+), 26 deletions(-) diff --git a/lib/demo/widgets/monthly_goal.dart b/lib/demo/widgets/monthly_goal.dart index 04c69d3..6e9e97c 100644 --- a/lib/demo/widgets/monthly_goal.dart +++ b/lib/demo/widgets/monthly_goal.dart @@ -15,8 +15,9 @@ class MonthlyGoal extends StatelessWidget { final theme = Theme.of(context); final textTheme = theme.textTheme; final colorScheme = theme.colorScheme; - final monthlySpendingGoal = context - .select((FinancialDataBloc bloc) => bloc.state.monthlySpendingGoal); + final monthlySpendingLimitGoal = context.select( + (FinancialDataBloc bloc) => bloc.state.monthlySpendingLimitGoal, + ); return Card( elevation: 0, @@ -33,12 +34,12 @@ class MonthlyGoal extends StatelessWidget { fit: BoxFit.scaleDown, child: DefaultTextStyle( style: textTheme.displaySmall!, - child: Text(monthlySpendingGoal.toCurrencyWithDecimals()), + child: Text(monthlySpendingLimitGoal.toCurrencyWithDecimals()), ), ), const SizedBox(height: AppSpacing.xxs), Text( - l10n.monthlyGoalLabel, + l10n.monthlySpendingLimitGoal, style: textTheme.labelMedium, ), ], diff --git a/lib/demo/widgets/transactions_table.dart b/lib/demo/widgets/transactions_table.dart index 8cad02e..51de378 100644 --- a/lib/demo/widgets/transactions_table.dart +++ b/lib/demo/widgets/transactions_table.dart @@ -3,7 +3,7 @@ import 'package:financial_dashboard/l10n/l10n.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; -enum TransactionType { income, spendings } +enum TransactionType { income, expense } class Transaction extends Equatable { const Transaction({ @@ -35,12 +35,12 @@ class TransactionsTable extends StatelessWidget { amount: r'+$3,000', ), Transaction( - type: TransactionType.spendings, + type: TransactionType.expense, title: 'Rent', amount: r'-$1,000', ), Transaction( - type: TransactionType.spendings, + type: TransactionType.expense, title: 'Food', amount: r'-$800', ), @@ -75,7 +75,7 @@ class TransactionsTable extends StatelessWidget { ), child: transaction.type == TransactionType.income ? Text(l10n.incomeTransactionLabel) - : Text(l10n.spendingsTransactionLabel), + : Text(l10n.expenseTransactionLabel), ), trailing: Text( transaction.amount, diff --git a/lib/financial_data/bloc/financial_data_bloc.dart b/lib/financial_data/bloc/financial_data_bloc.dart index 8b5358e..9f707de 100644 --- a/lib/financial_data/bloc/financial_data_bloc.dart +++ b/lib/financial_data/bloc/financial_data_bloc.dart @@ -18,11 +18,11 @@ class FinancialDataBloc extends Bloc { state.copyWith( currentSavings: 234567.91, savingsDataPoints: createSampleData(), - monthlySpendingGoal: 3210.55, + monthlySpendingLimitGoal: 3210.55, transactions: [ const Transaction(name: 'Paycheck', amount: 3000), - const Transaction(name: 'Rent', amount: 1050.20), - const Transaction(name: 'Food', amount: 670.50), + const Transaction(name: 'Rent', amount: -1050.20), + const Transaction(name: 'Food', amount: -670.50), ], ), ); diff --git a/lib/financial_data/bloc/financial_data_state.dart b/lib/financial_data/bloc/financial_data_state.dart index 9060e15..6a9620a 100644 --- a/lib/financial_data/bloc/financial_data_state.dart +++ b/lib/financial_data/bloc/financial_data_state.dart @@ -4,25 +4,26 @@ class FinancialDataState extends Equatable { const FinancialDataState({ this.currentSavings = 0, this.savingsDataPoints = const [], - this.monthlySpendingGoal = 0, + this.monthlySpendingLimitGoal = 0, this.transactions = const [], }); final double currentSavings; final List savingsDataPoints; - final double monthlySpendingGoal; + final double monthlySpendingLimitGoal; final List transactions; FinancialDataState copyWith({ double? currentSavings, List? savingsDataPoints, - double? monthlySpendingGoal, + double? monthlySpendingLimitGoal, List? transactions, }) { return FinancialDataState( currentSavings: currentSavings ?? this.currentSavings, savingsDataPoints: savingsDataPoints ?? this.savingsDataPoints, - monthlySpendingGoal: monthlySpendingGoal ?? this.monthlySpendingGoal, + monthlySpendingLimitGoal: + monthlySpendingLimitGoal ?? this.monthlySpendingLimitGoal, transactions: transactions ?? this.transactions, ); } @@ -31,7 +32,7 @@ class FinancialDataState extends Equatable { List get props => [ currentSavings, savingsDataPoints, - monthlySpendingGoal, + monthlySpendingLimitGoal, transactions, ]; } @@ -61,3 +62,11 @@ class Transaction extends Equatable { @override List get props => [name, amount]; } + +extension TransactionListX on List { + List get expenses => + where((element) => element.amount < 0).toList(); + + double get spent => + expenses.fold(0, (value, element) => value + element.amount); +} diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index cdaae73..73ca6f3 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,4 +1,5 @@ { + "@@locale": "en", "appBarTitleText": "Theme Potential", "@appBarTitleText": { "description": "Text shown on appBar.", @@ -17,9 +18,9 @@ "type": "text", "placeholders": {} }, - "monthlyGoalLabel": "Monthly goal", - "@monthlyGoalLabel": { - "description": "Label for monthly goal", + "monthlySpendingLimitGoal": "Monthly spending limit goal", + "@monthlySpendingLimitGoal": { + "description": "Label for monthly spending limit goal", "type": "text", "placeholders": {} }, @@ -35,9 +36,9 @@ "type": "text", "placeholders": {} }, - "spendingsTransactionLabel": "Spendings", - "@spendingsTransactionLabel": { - "description": "Label for spendings transaction types", + "expenseTransactionLabel": "Expense", + "@expenseTransactionLabel": { + "description": "Label for expense transaction types", "type": "text", "placeholders": {} }, diff --git a/lib/ui/widgets/goal_progress_indicator.dart b/lib/ui/widgets/goal_progress_indicator.dart index cf5a526..ce14370 100644 --- a/lib/ui/widgets/goal_progress_indicator.dart +++ b/lib/ui/widgets/goal_progress_indicator.dart @@ -1,7 +1,9 @@ import 'dart:math' as math; +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class GoalProgressIndicator extends StatelessWidget { const GoalProgressIndicator({ @@ -22,7 +24,17 @@ class GoalProgressIndicator extends StatelessWidget { final theme = Theme.of(context); final textTheme = theme.textTheme; final coloScheme = theme.colorScheme; + final monthlySpendingLimitGoal = context.select( + (FinancialDataBloc bloc) => bloc.state.monthlySpendingLimitGoal, + ); + final transactions = context.select( + (FinancialDataBloc bloc) => bloc.state.transactions, + ); + var value = 0.0; + if (monthlySpendingLimitGoal != 0) { + value = transactions.spent.abs() / monthlySpendingLimitGoal; + } final displayValue = (value * 100).toInt(); return SizedBox( diff --git a/test/financial_data/bloc/financial_data_bloc_test.dart b/test/financial_data/bloc/financial_data_bloc_test.dart index 65d539a..d7ad3de 100644 --- a/test/financial_data/bloc/financial_data_bloc_test.dart +++ b/test/financial_data/bloc/financial_data_bloc_test.dart @@ -12,7 +12,7 @@ void main() { FinancialDataState( currentSavings: 234567.91, savingsDataPoints: createSampleData(), - monthlySpendingGoal: 3210.55, + monthlySpendingLimitGoal: 3210.55, transactions: [ const Transaction(name: 'Paycheck', amount: 3000), const Transaction(name: 'Rent', amount: 1050.20), diff --git a/test/financial_data/bloc/financial_data_state_test.dart b/test/financial_data/bloc/financial_data_state_test.dart index 19c3c8c..0a809fc 100644 --- a/test/financial_data/bloc/financial_data_state_test.dart +++ b/test/financial_data/bloc/financial_data_state_test.dart @@ -25,9 +25,9 @@ void main() { test('copies monthlySpendingGoal', () { final state = FinancialDataState(); - final newState = state.copyWith(monthlySpendingGoal: 123.45); + final newState = state.copyWith(monthlySpendingLimitGoal: 123.45); - expect(newState.monthlySpendingGoal, 123.45); + expect(newState.monthlySpendingLimitGoal, 123.45); }); test('copies transactions', () { diff --git a/test/src/demo/widgets/transactions_table_test.dart b/test/src/demo/widgets/transactions_table_test.dart index 2a57f26..b49f39a 100644 --- a/test/src/demo/widgets/transactions_table_test.dart +++ b/test/src/demo/widgets/transactions_table_test.dart @@ -15,7 +15,7 @@ void main() { amount: 'test-amount', ); const pointB = Transaction( - type: TransactionType.spendings, + type: TransactionType.expense, title: 'test-title-two', amount: 'test-amount-two', ); From c122b4aca7263952b3b97975cf752f5af346efe0 Mon Sep 17 00:00:00 2001 From: Jaime Date: Mon, 5 Aug 2024 14:01:32 +0200 Subject: [PATCH 4/8] feat: use transactions data from bloc --- lib/demo/widgets/transactions_table.dart | 47 ++++--------------- .../bloc/financial_data_bloc.dart | 6 +-- .../bloc/financial_data_state.dart | 6 +-- .../extensions/financial_formatter.dart | 4 +- .../bloc/financial_data_bloc_test.dart | 6 +-- .../bloc/financial_data_state_test.dart | 8 ++-- .../demo/widgets/transactions_table_test.dart | 27 ----------- 7 files changed, 23 insertions(+), 81 deletions(-) delete mode 100644 test/src/demo/widgets/transactions_table_test.dart diff --git a/lib/demo/widgets/transactions_table.dart b/lib/demo/widgets/transactions_table.dart index 51de378..7ce5b13 100644 --- a/lib/demo/widgets/transactions_table.dart +++ b/lib/demo/widgets/transactions_table.dart @@ -1,24 +1,8 @@ -import 'package:equatable/equatable.dart'; +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/l10n/l10n.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; - -enum TransactionType { income, expense } - -class Transaction extends Equatable { - const Transaction({ - required this.type, - required this.title, - required this.amount, - }); - - final TransactionType type; - final String title; - final String amount; - - @override - List get props => [type, title, amount]; -} +import 'package:flutter_bloc/flutter_bloc.dart'; class TransactionsTable extends StatelessWidget { const TransactionsTable({ @@ -28,30 +12,15 @@ class TransactionsTable extends StatelessWidget { final TextStyle? titleStyle; - static const _transactions = [ - Transaction( - type: TransactionType.income, - title: 'Paycheck', - amount: r'+$3,000', - ), - Transaction( - type: TransactionType.expense, - title: 'Rent', - amount: r'-$1,000', - ), - Transaction( - type: TransactionType.expense, - title: 'Food', - amount: r'-$800', - ), - ]; - @override Widget build(BuildContext context) { final l10n = context.l10n; final theme = Theme.of(context); final textTheme = theme.textTheme; final colorScheme = theme.colorScheme; + final transactions = context.select( + (FinancialDataBloc bloc) => bloc.state.transactions, + ); return Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -65,7 +34,7 @@ class TransactionsTable extends StatelessWidget { context: context, color: colorScheme.surfaceContainerHighest, tiles: [ - for (final transaction in _transactions) + for (final transaction in transactions) ListTile( contentPadding: EdgeInsets.zero, title: Text(transaction.title), @@ -73,12 +42,12 @@ class TransactionsTable extends StatelessWidget { style: textTheme.bodyMedium!.copyWith( color: colorScheme.onSurfaceVariant.withOpacity(0.8), ), - child: transaction.type == TransactionType.income + child: transaction.amount > 0 ? Text(l10n.incomeTransactionLabel) : Text(l10n.expenseTransactionLabel), ), trailing: Text( - transaction.amount, + transaction.amount.toCurrencyWithoutDecimal(), style: textTheme.bodyLarge?.copyWith( fontWeight: AppFontWeight.semiBold, ), diff --git a/lib/financial_data/bloc/financial_data_bloc.dart b/lib/financial_data/bloc/financial_data_bloc.dart index 9f707de..ea9467f 100644 --- a/lib/financial_data/bloc/financial_data_bloc.dart +++ b/lib/financial_data/bloc/financial_data_bloc.dart @@ -20,9 +20,9 @@ class FinancialDataBloc extends Bloc { savingsDataPoints: createSampleData(), monthlySpendingLimitGoal: 3210.55, transactions: [ - const Transaction(name: 'Paycheck', amount: 3000), - const Transaction(name: 'Rent', amount: -1050.20), - const Transaction(name: 'Food', amount: -670.50), + const Transaction(title: 'Paycheck', amount: 3000), + const Transaction(title: 'Rent', amount: -1050.20), + const Transaction(title: 'Food', amount: -670.50), ], ), ); diff --git a/lib/financial_data/bloc/financial_data_state.dart b/lib/financial_data/bloc/financial_data_state.dart index 6a9620a..cca431c 100644 --- a/lib/financial_data/bloc/financial_data_state.dart +++ b/lib/financial_data/bloc/financial_data_state.dart @@ -52,15 +52,15 @@ class SavingsDataPoint extends Equatable { class Transaction extends Equatable { const Transaction({ - required this.name, + required this.title, required this.amount, }); - final String name; + final String title; final double amount; @override - List get props => [name, amount]; + List get props => [title, amount]; } extension TransactionListX on List { diff --git a/lib/financial_data/extensions/financial_formatter.dart b/lib/financial_data/extensions/financial_formatter.dart index cfefea1..c3d8dad 100644 --- a/lib/financial_data/extensions/financial_formatter.dart +++ b/lib/financial_data/extensions/financial_formatter.dart @@ -2,8 +2,8 @@ import 'package:intl/intl.dart'; extension FinancialFormatterX on double { String toCurrencyWithoutDecimal() => - '\$${NumberFormat('###,###').format(this)}'; + NumberFormat.currency(decimalDigits: 0, symbol: r'$').format(this); String toCurrencyWithDecimals() => - '\$${NumberFormat('###,###.00').format(this)}'; + NumberFormat.currency(decimalDigits: 2, symbol: r'$').format(this); } diff --git a/test/financial_data/bloc/financial_data_bloc_test.dart b/test/financial_data/bloc/financial_data_bloc_test.dart index d7ad3de..961582b 100644 --- a/test/financial_data/bloc/financial_data_bloc_test.dart +++ b/test/financial_data/bloc/financial_data_bloc_test.dart @@ -14,9 +14,9 @@ void main() { savingsDataPoints: createSampleData(), monthlySpendingLimitGoal: 3210.55, transactions: [ - const Transaction(name: 'Paycheck', amount: 3000), - const Transaction(name: 'Rent', amount: 1050.20), - const Transaction(name: 'Food', amount: 670.50), + const Transaction(title: 'Paycheck', amount: 3000), + const Transaction(title: 'Rent', amount: 1050.20), + const Transaction(title: 'Food', amount: 670.50), ], ), ], diff --git a/test/financial_data/bloc/financial_data_state_test.dart b/test/financial_data/bloc/financial_data_state_test.dart index 0a809fc..a76680a 100644 --- a/test/financial_data/bloc/financial_data_state_test.dart +++ b/test/financial_data/bloc/financial_data_state_test.dart @@ -32,7 +32,7 @@ void main() { test('copies transactions', () { final state = FinancialDataState(); - final transaction = Transaction(name: 'test', amount: 123.45); + final transaction = Transaction(title: 'test', amount: 123.45); final newState = state.copyWith(transactions: [transaction]); expect(newState.transactions, equals([transaction])); @@ -53,9 +53,9 @@ void main() { group('Transaction', () { test('supports value equality', () { - const pointA = Transaction(name: 'test', amount: 100); - const secondPointA = Transaction(name: 'test', amount: 100); - const pointB = Transaction(name: 'test-two', amount: 200); + const pointA = Transaction(title: 'test', amount: 100); + const secondPointA = Transaction(title: 'test', amount: 100); + const pointB = Transaction(title: 'test-two', amount: 200); expect(pointA, equals(secondPointA)); expect(pointA, isNot(equals(pointB))); diff --git a/test/src/demo/widgets/transactions_table_test.dart b/test/src/demo/widgets/transactions_table_test.dart deleted file mode 100644 index b49f39a..0000000 --- a/test/src/demo/widgets/transactions_table_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:financial_dashboard/demo/demo.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('Transaction', () { - test('supports value equality', () { - const pointA = Transaction( - type: TransactionType.income, - title: 'test-title', - amount: 'test-amount', - ); - const secondPointA = Transaction( - type: TransactionType.income, - title: 'test-title', - amount: 'test-amount', - ); - const pointB = Transaction( - type: TransactionType.expense, - title: 'test-title-two', - amount: 'test-amount-two', - ); - - expect(pointA, equals(secondPointA)); - expect(pointA, isNot(equals(pointB))); - }); - }); -} From 0ceacfe3b81e4c612933219b8a548eae5421701f Mon Sep 17 00:00:00 2001 From: Jaime Date: Mon, 5 Aug 2024 14:02:31 +0200 Subject: [PATCH 5/8] fix: bloc test --- test/financial_data/bloc/financial_data_bloc_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/financial_data/bloc/financial_data_bloc_test.dart b/test/financial_data/bloc/financial_data_bloc_test.dart index 961582b..372e4cf 100644 --- a/test/financial_data/bloc/financial_data_bloc_test.dart +++ b/test/financial_data/bloc/financial_data_bloc_test.dart @@ -15,8 +15,8 @@ void main() { monthlySpendingLimitGoal: 3210.55, transactions: [ const Transaction(title: 'Paycheck', amount: 3000), - const Transaction(title: 'Rent', amount: 1050.20), - const Transaction(title: 'Food', amount: 670.50), + const Transaction(title: 'Rent', amount: -1050.20), + const Transaction(title: 'Food', amount: -670.50), ], ), ], From a5e2e9ad85bc7642a3162630872ad4693a716a9f Mon Sep 17 00:00:00 2001 From: Jaime Date: Mon, 5 Aug 2024 14:07:06 +0200 Subject: [PATCH 6/8] test: goal progress indicator --- .../widgets/goal_progress_indicator_test.dart | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/test/src/ui/widgets/goal_progress_indicator_test.dart b/test/src/ui/widgets/goal_progress_indicator_test.dart index dae5e43..ec8af9d 100644 --- a/test/src/ui/widgets/goal_progress_indicator_test.dart +++ b/test/src/ui/widgets/goal_progress_indicator_test.dart @@ -1,3 +1,5 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -12,22 +14,38 @@ class MockColorScheme extends Mock implements ColorScheme { } } +class _MockFinancialDataBloc + extends MockBloc + implements FinancialDataBloc {} + void main() { group('CircleProgressPainter', () { + late FinancialDataBloc financialDataBloc; + + setUp(() { + financialDataBloc = _MockFinancialDataBloc(); + + when(() => financialDataBloc.state).thenReturn(FinancialDataState()); + }); + group('$GoalProgressIndicator', () { testWidgets('renders without gradient', (tester) async { - await tester.pumpExperience(const GoalProgressIndicator(value: 1)); + await tester.pumpExperience( + GoalProgressIndicator(value: 1), + financialDataBloc: financialDataBloc, + ); await tester.pumpAndSettle(); + expect(find.byType(GoalProgressIndicator), findsOneWidget); }); testWidgets('renders with gradient', (tester) async { await tester.pumpExperience( - const GoalProgressIndicator( - isGradient: true, - ), + GoalProgressIndicator(isGradient: true), + financialDataBloc: financialDataBloc, ); await tester.pumpAndSettle(); + expect(find.byType(GoalProgressIndicator), findsOneWidget); }); }); From 34f0f065a912a8949b0b5bf9db82d9b890751acf Mon Sep 17 00:00:00 2001 From: Jaime Date: Mon, 5 Aug 2024 14:15:46 +0200 Subject: [PATCH 7/8] test: savings prediction chart --- .../retirement_prediction_chart_test.dart | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) 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 58d06d2..08c5a62 100644 --- a/test/src/ui/widgets/charts/retirement_prediction_chart_test.dart +++ b/test/src/ui/widgets/charts/retirement_prediction_chart_test.dart @@ -1,24 +1,44 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:financial_dashboard/financial_data/financial_data.dart'; import 'package:financial_dashboard/ui/ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; import '../../../../helpers/helpers.dart'; +class _MockFinancialDataBloc + extends MockBloc + implements FinancialDataBloc {} + void main() { group('RetirementPredictionChart', () { - Widget buildSubject({ - void Function(String)? onCurrentSavings, - }) => - RetirementPredictionChart(); + late FinancialDataBloc financialDataBloc; + + setUp(() { + financialDataBloc = _MockFinancialDataBloc(); + + when(() => financialDataBloc.state).thenReturn( + FinancialDataState( + savingsDataPoints: createSampleData(), + ), + ); + }); testWidgets('renders LineChartBody', (tester) async { - await tester.pumpExperience(buildSubject()); + await tester.pumpExperience( + RetirementPredictionChart(), + financialDataBloc: financialDataBloc, + ); expect(find.byType(LineChartBody), findsOneWidget); }); testWidgets('shows and hides tooltip on gestures', (tester) async { - await tester.pumpExperience(buildSubject()); + await tester.pumpExperience( + RetirementPredictionChart(), + financialDataBloc: financialDataBloc, + ); final keyFinder = find.byKey(const Key('chart_tooltip')); expect(keyFinder, findsNothing); From 15f951497a916f1f355234c3d1e1a039493578fa Mon Sep 17 00:00:00 2001 From: Jaime Date: Mon, 5 Aug 2024 14:31:34 +0200 Subject: [PATCH 8/8] test: use mock data --- test/demo/demo_page_test.dart | 14 ++++++++++++-- .../ui/widgets/goal_progress_indicator_test.dart | 11 ++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/test/demo/demo_page_test.dart b/test/demo/demo_page_test.dart index 8f51079..892566e 100644 --- a/test/demo/demo_page_test.dart +++ b/test/demo/demo_page_test.dart @@ -32,8 +32,18 @@ void main() { financialDataBloc = _MockFinancialDataBloc(); when(() => themeModeCubit.state).thenReturn(ThemeMode.light); - when(() => financialDataBloc.state) - .thenReturn(const FinancialDataState()); + when(() => financialDataBloc.state).thenReturn( + FinancialDataState( + currentSavings: 123456, + savingsDataPoints: createSampleData(), + monthlySpendingLimitGoal: 1000, + transactions: [ + const Transaction(title: 'Paycheck', amount: 3000), + const Transaction(title: 'Rent', amount: -1050.20), + const Transaction(title: 'Food', amount: -670.50), + ], + ), + ); }); testWidgets('renders AppOne when AppFlavor is one', (tester) async { diff --git a/test/src/ui/widgets/goal_progress_indicator_test.dart b/test/src/ui/widgets/goal_progress_indicator_test.dart index ec8af9d..a1608ae 100644 --- a/test/src/ui/widgets/goal_progress_indicator_test.dart +++ b/test/src/ui/widgets/goal_progress_indicator_test.dart @@ -25,7 +25,16 @@ void main() { setUp(() { financialDataBloc = _MockFinancialDataBloc(); - when(() => financialDataBloc.state).thenReturn(FinancialDataState()); + when(() => financialDataBloc.state).thenReturn( + FinancialDataState( + monthlySpendingLimitGoal: 1000, + transactions: [ + const Transaction(title: 'Paycheck', amount: 3000), + const Transaction(title: 'Rent', amount: -1050.20), + const Transaction(title: 'Food', amount: -670.50), + ], + ), + ); }); group('$GoalProgressIndicator', () {