diff --git a/lib/main.dart b/lib/main.dart index 199d822..f57dfe3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,5 @@ import 'package:counter_workshop/src/app.dart'; -import 'package:counter_workshop/src/features/counter/data/datasources/local/counter.db.dart'; +import 'package:counter_workshop/src/features/counter/data/datasources/local/counter.database.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/src/mock/counter_fake.api.dart'; import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart'; import 'package:flutter/material.dart'; diff --git a/lib/src/features/counter/data/datasources/local/counter.database.dart b/lib/src/features/counter/data/datasources/local/counter.database.dart new file mode 100644 index 0000000..aa8aabd --- /dev/null +++ b/lib/src/features/counter/data/datasources/local/counter.database.dart @@ -0,0 +1,22 @@ +import 'package:counter_workshop/src/features/counter/domain/counter.model.dart'; + +/// Locale app database like SqlLite that providers a [CounterModel] +class CounterDatabase { + CounterModel _counter = CounterModel(value: 0); + final int databaseDelay = 200; + + Future getCounter() { + // Pretend it's a db call + return Future.delayed(Duration(milliseconds: databaseDelay), () => _counter); + } + + Future storeCounter(CounterModel counter) { + _counter = counter; + if (_counter.value == 10) { + throw Exception('Database read lock while updating Counter to ${_counter.value}.'); + } else { + // Pretend it's a db call + return Future.delayed(Duration(milliseconds: databaseDelay)); + } + } +} diff --git a/lib/src/features/counter/data/datasources/local/counter.db.dart b/lib/src/features/counter/data/datasources/local/counter.db.dart deleted file mode 100644 index 92e8e5c..0000000 --- a/lib/src/features/counter/data/datasources/local/counter.db.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:counter_workshop/src/features/counter/domain/counter.model.dart'; - -/// Locale app database like SqlLite that providers a [CounterModel] -class CounterDatabase { - CounterModel _counter = CounterModel(value: 0); - - CounterModel getCounter() { - return _counter; - } - - storeCounter(CounterModel counter) { - _counter = counter; - } -} diff --git a/lib/src/features/counter/data/repositories/counter.repository.dart b/lib/src/features/counter/data/repositories/counter.repository.dart index 6c7a9a0..be67d0c 100644 --- a/lib/src/features/counter/data/repositories/counter.repository.dart +++ b/lib/src/features/counter/data/repositories/counter.repository.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:counter_workshop/src/features/counter/data/datasources/local/counter.db.dart'; +import 'package:counter_workshop/src/features/counter/data/datasources/local/counter.database.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/counter.api.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/converters/counter_response.converter.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/dtos/counter_response.dto.dart'; @@ -25,16 +25,20 @@ class CounterRepository { CounterModel counterModel = CounterResponseConverter().toModel(counterResponseDto); // store model in database - counterDatabase.storeCounter(counterModel); + await counterDatabase.storeCounter(counterModel); } - CounterModel getCounter() { - return counterDatabase.getCounter(); + Future getCounter() async { + return await counterDatabase.getCounter(); } Future updateCounter({required CounterModel counterModel}) async { log('updating counter: ${counterModel.id} with value: $counterModel'); + + // store model in database + await counterDatabase.storeCounter(counterModel); + + // store model in api await counterApi.updateCounter(counterModel.id, counterModel.value); - return; } } diff --git a/lib/src/features/counter/presentation/bloc/counter.bloc.dart b/lib/src/features/counter/presentation/bloc/counter.bloc.dart index 280a56f..1380ed9 100644 --- a/lib/src/features/counter/presentation/bloc/counter.bloc.dart +++ b/lib/src/features/counter/presentation/bloc/counter.bloc.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart'; import 'package:counter_workshop/src/features/counter/presentation/bloc/counter.event.dart'; import 'package:counter_workshop/src/features/counter/presentation/bloc/counter.state.dart'; @@ -7,20 +9,48 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class CounterBloc extends Bloc { final CounterRepository counterRepository; - CounterBloc({required this.counterRepository}) : super(const CounterState(value: 0)) { + CounterBloc({required this.counterRepository}) : super(CounterLoadingState()) { + on(_onFetchData); on(_onIncrement); on(_onDecrement); } - void _onIncrement(CounterIncrementPressed event, Emitter emit) { - debugPrint('INCREMENT: ${state.value.toString()}'); - emit(CounterState(value: state.value + 1)); + Future> _onFetchData(CounterFetchData event, Emitter emit) async { + emit(CounterLoadingState()); + try { + final counter = await counterRepository.getCounter(); + emit(CounterDataState(counter)); + } catch (e) { + emit(CounterErrorState(e.toString())); + } + } + + Future _onIncrement(CounterIncrementPressed event, Emitter emit) async { + debugPrint('INCREMENT: ${event.counterModel.toString()}'); + emit(CounterLoadingState()); + try { + event.counterModel.value += 1; + await counterRepository.updateCounter(counterModel: event.counterModel); + emit(CounterDataState(event.counterModel)); + } catch (e) { + emit(CounterErrorState(e.toString())); + } } - void _onDecrement(CounterDecrementPressed event, Emitter emit) { - debugPrint('DECREMENT: ${state.value.toString()}'); - if (state.value > 0) { - emit(CounterState(value: state.value - 1)); + Future _onDecrement(CounterDecrementPressed event, Emitter emit) async { + debugPrint('DECREMENT: ${event.counterModel.toString()}'); + + if (event.counterModel.value == 0) { + return; + } + + emit(CounterLoadingState()); + try { + event.counterModel.value -= 1; + await counterRepository.updateCounter(counterModel: event.counterModel); + emit(CounterDataState(event.counterModel)); + } catch (e) { + emit(CounterErrorState(e.toString())); } } } diff --git a/lib/src/features/counter/presentation/bloc/counter.event.dart b/lib/src/features/counter/presentation/bloc/counter.event.dart index 635321c..484e786 100644 --- a/lib/src/features/counter/presentation/bloc/counter.event.dart +++ b/lib/src/features/counter/presentation/bloc/counter.event.dart @@ -1,12 +1,30 @@ +import 'package:counter_workshop/src/features/counter/domain/counter.model.dart'; import 'package:equatable/equatable.dart'; abstract class CounterEvent extends Equatable { + const CounterEvent(); + @override List get props => []; } -/// Notifies bloc to increment state. -class CounterIncrementPressed extends CounterEvent {} +/// Load data from repository +class CounterFetchData extends CounterEvent {} + +/// Notifies bloc to increment state +class CounterIncrementPressed extends CounterEvent { + const CounterIncrementPressed(this.counterModel); + final CounterModel counterModel; + + @override + List get props => [counterModel]; +} + +/// Notifies bloc to decrement state +class CounterDecrementPressed extends CounterEvent { + const CounterDecrementPressed(this.counterModel); + final CounterModel counterModel; -/// Notifies bloc to decrement state. -class CounterDecrementPressed extends CounterEvent {} + @override + List get props => [counterModel]; +} diff --git a/lib/src/features/counter/presentation/bloc/counter.state.dart b/lib/src/features/counter/presentation/bloc/counter.state.dart index 4567282..09ac43b 100644 --- a/lib/src/features/counter/presentation/bloc/counter.state.dart +++ b/lib/src/features/counter/presentation/bloc/counter.state.dart @@ -1,9 +1,31 @@ +import 'package:counter_workshop/src/features/counter/domain/counter.model.dart'; import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; -class CounterState extends Equatable { - const CounterState({required this.value}); - final int value; +@immutable +abstract class CounterState extends Equatable { + @override + List get props => []; +} + +/// Loading counter State +class CounterLoadingState extends CounterState {} + +/// Data counter State +class CounterDataState extends CounterState { + final CounterModel counterModel; + CounterDataState(this.counterModel); + + @override + List get props => [counterModel]; +} + +/// Error counter State +class CounterErrorState extends CounterState { + final String error; + + CounterErrorState(this.error); @override - List get props => [value]; + List get props => [error]; } diff --git a/lib/src/features/counter/presentation/view/counter.page.dart b/lib/src/features/counter/presentation/view/counter.page.dart index fc9a809..5c10af0 100644 --- a/lib/src/features/counter/presentation/view/counter.page.dart +++ b/lib/src/features/counter/presentation/view/counter.page.dart @@ -1,6 +1,7 @@ import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart'; import 'package:counter_workshop/src/features/counter/presentation/bloc/counter.bloc.dart'; import 'package:counter_workshop/src/features/counter/presentation/bloc/counter.event.dart'; +import 'package:counter_workshop/src/features/counter/presentation/bloc/counter.state.dart'; import 'package:counter_workshop/src/features/counter/presentation/view/widgets/counter_text.widget.dart'; import 'package:counter_workshop/src/features/counter/presentation/view/widgets/custom_circular_button.widget.dart'; import 'package:flutter/material.dart'; @@ -13,7 +14,10 @@ class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { final counterRepository = context.read(); - return BlocProvider(create: (_) => CounterBloc(counterRepository: counterRepository), child: const _CounterView()); + return BlocProvider( + create: (_) => CounterBloc(counterRepository: counterRepository)..add(CounterFetchData()), + child: const _CounterView(), + ); } } @@ -24,35 +28,69 @@ class _CounterView extends StatelessWidget { @override Widget build(BuildContext context) { final counterBloc = context.read(); + // final showButton = false; return Scaffold( extendBodyBehindAppBar: true, appBar: AppBar( title: const Text('Counter Page'), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - CounterText(), - ], - ), + body: BlocBuilder( + builder: (context, state) { + if (state is CounterLoadingState) { + // loading + return const Center( + child: CircularProgressIndicator(strokeWidth: 3), + ); + } else if (state is CounterDataState) { + // data + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CounterText(counterValue: state.counterModel.value), + ], + ), + ); + } else if (state is CounterErrorState) { + // error + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Es ist ein Fehler aufgetreten: ${state.error}', + style: const TextStyle(color: Colors.red), + ), + ), + ); + } + // state unknown, fallback to empty or return a common error + return const SizedBox(); + }, ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: Container( padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 40.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - CustomCircularButton( - icon: Icons.remove, - onPressed: () => counterBloc.add(CounterDecrementPressed()), - ), - CustomCircularButton( - icon: Icons.add, - onPressed: () => counterBloc.add(CounterIncrementPressed()), - ), - ], + child: BlocBuilder( + builder: (context, state) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomCircularButton( + icon: Icons.remove, + onPressed: state is CounterDataState + ? () => counterBloc.add(CounterDecrementPressed(state.counterModel)) + : null, + ), + CustomCircularButton( + icon: Icons.add, + onPressed: state is CounterDataState + ? () => counterBloc.add(CounterIncrementPressed(state.counterModel)) + : null, + ), + ], + ); + }, ), ), ); diff --git a/lib/src/features/counter/presentation/view/widgets/counter_text.widget.dart b/lib/src/features/counter/presentation/view/widgets/counter_text.widget.dart index aea3e1e..fba6ef6 100644 --- a/lib/src/features/counter/presentation/view/widgets/counter_text.widget.dart +++ b/lib/src/features/counter/presentation/view/widgets/counter_text.widget.dart @@ -1,14 +1,12 @@ -import 'package:counter_workshop/src/features/counter/presentation/bloc/counter.bloc.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class CounterText extends StatelessWidget { - const CounterText({super.key}); + const CounterText({super.key, required this.counterValue}); + final int counterValue; @override Widget build(BuildContext context) { final theme = Theme.of(context); - final counterValue = context.select((CounterBloc bloc) => bloc.state.value); return Text( '$counterValue', style: theme.textTheme.headlineLarge, diff --git a/lib/src/features/counter/presentation/view/widgets/custom_circular_button.widget.dart b/lib/src/features/counter/presentation/view/widgets/custom_circular_button.widget.dart index 76c0783..f0dfa62 100644 --- a/lib/src/features/counter/presentation/view/widgets/custom_circular_button.widget.dart +++ b/lib/src/features/counter/presentation/view/widgets/custom_circular_button.widget.dart @@ -12,6 +12,7 @@ class CustomCircularButton extends StatelessWidget { style: OutlinedButton.styleFrom( shape: const CircleBorder(), padding: const EdgeInsets.all(15), + disabledForegroundColor: Colors.black12, ), onPressed: onPressed, child: Icon( diff --git a/test/widget_test.dart b/test/widget_test.dart index 977ae37..7f83444 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -6,7 +6,7 @@ // tree, read text, and verify that the values of widget properties are correct. import 'package:counter_workshop/src/app.dart'; -import 'package:counter_workshop/src/features/counter/data/datasources/local/counter.db.dart'; +import 'package:counter_workshop/src/features/counter/data/datasources/local/counter.database.dart'; import 'package:counter_workshop/src/features/counter/data/datasources/remote/src/mock/counter_fake.api.dart'; import 'package:counter_workshop/src/features/counter/data/repositories/counter.repository.dart'; import 'package:flutter/material.dart'; @@ -20,10 +20,6 @@ void main() { const Duration(milliseconds: 300), // Because of FakeApi delay ); - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - // Tap the '-' icon and trigger a frame. await tester.tap(find.byIcon(Icons.remove)); await tester.pumpAndSettle();