├── .gitignore ├── README.md ├── examples ├── infinite_list_state_machine │ ├── .dart_tool │ │ ├── version │ │ ├── package_config_subset │ │ └── package_config.json │ ├── lib │ │ ├── posts │ │ │ ├── models │ │ │ │ ├── models.dart │ │ │ │ └── post.dart │ │ │ ├── view │ │ │ │ ├── view.dart │ │ │ │ ├── posts_page.dart │ │ │ │ └── posts_list.dart │ │ │ ├── widgets │ │ │ │ ├── widgets.dart │ │ │ │ ├── bottom_loader.dart │ │ │ │ └── post_list_item.dart │ │ │ ├── posts.dart │ │ │ └── bloc │ │ │ │ ├── post_event.dart │ │ │ │ ├── post_state.dart │ │ │ │ └── post_bloc.dart │ │ ├── app.dart │ │ ├── main.dart │ │ └── simple_bloc_observer.dart │ ├── .metadata │ ├── test │ │ ├── posts │ │ │ ├── bloc │ │ │ │ ├── post_event_test.dart │ │ │ │ ├── post_state_test.dart │ │ │ │ └── post_bloc_test.dart │ │ │ ├── models │ │ │ │ └── post_test.dart │ │ │ └── view │ │ │ │ ├── posts_page_test.dart │ │ │ │ └── posts_list_test.dart │ │ └── app_test.dart │ ├── pubspec.yaml │ ├── README.md │ ├── .gitignore │ ├── analysis_options.yaml │ ├── .packages │ └── pubspec.lock ├── .DS_Store └── flutter_timer_state_machine │ ├── lib │ ├── timer │ │ ├── timer.dart │ │ ├── state_machine │ │ │ ├── timer_event.dart │ │ │ ├── timer_state.dart │ │ │ └── timer_state_machine.dart │ │ └── view │ │ │ └── timer_page.dart │ ├── main.dart │ ├── ticker.dart │ └── app.dart │ ├── test │ ├── ticker_test.dart │ ├── app_test.dart │ ├── widget_test.dart │ └── timer │ │ ├── bloc │ │ ├── timer_state_test.dart │ │ ├── timer_event_test.dart │ │ └── timer_bloc_test.dart │ │ └── view │ │ └── timer_page_test.dart │ ├── pubspec.yaml │ ├── README.md │ ├── .gitignore │ ├── .metadata │ ├── analysis_options.yaml │ └── pubspec.lock ├── docs └── assets │ ├── readme │ ├── analysis_options.yaml │ ├── simple_login_sm_code.png │ └── simple_login_sm_graph_horizontal.png │ └── state_machine_bloc_logo_full.png ├── packages └── state_machine_bloc │ ├── lib │ ├── state_machine_bloc.dart │ └── src │ │ ├── state_definition.dart │ │ ├── state_definition_builder.dart │ │ └── state_machine.dart │ ├── test │ ├── utils.dart │ ├── async_transition_test.dart │ ├── nested_async_transition_test.dart │ ├── lifecycle_test.dart │ ├── events_test.dart │ ├── deeply_nested_state_test.dart │ └── nested_state_test.dart │ ├── analysis_options.yaml │ ├── example │ ├── user_repository.dart │ ├── login_event.dart │ ├── login_state.dart │ └── simple_login_sm.dart │ ├── .metadata │ ├── pubspec.yaml │ ├── .gitignore │ ├── CHANGELOG.md │ ├── LICENSE │ └── README.md ├── .github └── workflows │ └── run-test.yml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | packages/state_machine_bloc/README.md -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/.dart_tool/version: -------------------------------------------------------------------------------- 1 | 3.24.3 -------------------------------------------------------------------------------- /docs/assets/readme/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | exclude: ["**.dart"] -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/models/models.dart: -------------------------------------------------------------------------------- 1 | export './post.dart'; 2 | -------------------------------------------------------------------------------- /examples/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierremrtn/state_machine_bloc/HEAD/examples/.DS_Store -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/view/view.dart: -------------------------------------------------------------------------------- 1 | export 'posts_list.dart'; 2 | export 'posts_page.dart'; 3 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/lib/state_machine_bloc.dart: -------------------------------------------------------------------------------- 1 | library state_machine_bloc; 2 | 3 | export 'src/state_machine.dart'; 4 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'bottom_loader.dart'; 2 | export 'post_list_item.dart'; 3 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/lib/timer/timer.dart: -------------------------------------------------------------------------------- 1 | export 'state_machine/timer_state_machine.dart'; 2 | export 'view/timer_page.dart'; 3 | -------------------------------------------------------------------------------- /docs/assets/readme/simple_login_sm_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierremrtn/state_machine_bloc/HEAD/docs/assets/readme/simple_login_sm_code.png -------------------------------------------------------------------------------- /docs/assets/state_machine_bloc_logo_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierremrtn/state_machine_bloc/HEAD/docs/assets/state_machine_bloc_logo_full.png -------------------------------------------------------------------------------- /docs/assets/readme/simple_login_sm_graph_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pierremrtn/state_machine_bloc/HEAD/docs/assets/readme/simple_login_sm_graph_horizontal.png -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/posts.dart: -------------------------------------------------------------------------------- 1 | export 'bloc/post_bloc.dart'; 2 | export 'models/models.dart'; 3 | export 'view/view.dart'; 4 | export 'widgets/widgets.dart'; 5 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_timer_state_machine/app.dart'; 3 | 4 | void main() => runApp(App()); 5 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/test/utils.dart: -------------------------------------------------------------------------------- 1 | Future tick() => Future.delayed(Duration.zero); 2 | 3 | const delay = Duration(milliseconds: 30); 4 | 5 | Future wait() => Future.delayed(delay); 6 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:infinite_list_state_machine/posts/posts.dart'; 3 | 4 | class App extends MaterialApp { 5 | App() : super(home: PostsPage()); 6 | } 7 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/lib/ticker.dart: -------------------------------------------------------------------------------- 1 | class Ticker { 2 | const Ticker(); 3 | Stream tick({required int ticks}) { 4 | return Stream.periodic(Duration(seconds: 1), (x) => ticks - x - 1) 5 | .take(ticks); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/example/user_repository.dart: -------------------------------------------------------------------------------- 1 | part of 'simple_login_sm.dart'; 2 | 3 | class UserRepository { 4 | Future login({ 5 | required String email, 6 | required String password, 7 | }) async { 8 | //login using api 9 | //return void or throw an error if login fail 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/models/post.dart: -------------------------------------------------------------------------------- 1 | import 'package:equatable/equatable.dart'; 2 | 3 | class Post extends Equatable { 4 | const Post({required this.id, required this.title, required this.body}); 5 | 6 | final int id; 7 | final String title; 8 | final String body; 9 | 10 | @override 11 | List get props => [id, title, body]; 12 | } 13 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: db747aa1331bd95bc9b3874c842261ca2d302cd5 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 7a005e1dcda665ace7241a24e79fae1a71f17b18 8 | channel: unknown 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/test/ticker_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:flutter_timer_state_machine/ticker.dart'; 3 | 4 | void main() { 5 | group('Ticker', () { 6 | const ticker = Ticker(); 7 | test('ticker emits 3 ticks from 2-0 every second', () { 8 | expectLater(ticker.tick(ticks: 3), emitsInOrder([2, 1, 0])); 9 | }); 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/widgets/bottom_loader.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class BottomLoader extends StatelessWidget { 4 | @override 5 | Widget build(BuildContext context) { 6 | return const Center( 7 | child: SizedBox( 8 | height: 24, 9 | width: 24, 10 | child: CircularProgressIndicator(strokeWidth: 1.5), 11 | ), 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | import 'package:flutter/widgets.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:infinite_list_state_machine/app.dart'; 5 | import 'package:infinite_list_state_machine/simple_bloc_observer.dart'; 6 | 7 | void main() { 8 | BlocOverrides.runZoned( 9 | () => runApp(App()), 10 | blocObserver: SimpleBlocObserver(), 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/test/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_timer_state_machine/app.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:flutter_timer_state_machine/timer/timer.dart'; 4 | 5 | void main() { 6 | group('App', () { 7 | testWidgets('renders TimerPage', (tester) async { 8 | await tester.pumpWidget(App()); 9 | expect(find.byType(TimerPage), findsOneWidget); 10 | }); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/test/posts/bloc/post_event_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:infinite_list_state_machine/posts/posts.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('PostEvent', () { 7 | group('PostFetched', () { 8 | test('supports value comparison', () { 9 | expect(PostFetchRequested(), PostFetchRequested()); 10 | }); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/simple_bloc_observer.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc/bloc.dart'; 2 | 3 | class SimpleBlocObserver extends BlocObserver { 4 | @override 5 | void onTransition(Bloc bloc, Transition transition) { 6 | super.onTransition(bloc, transition); 7 | print(transition); 8 | } 9 | 10 | @override 11 | void onError(BlocBase bloc, Object error, StackTrace stackTrace) { 12 | print(error); 13 | super.onError(bloc, error, stackTrace); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/test/app_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:infinite_list_state_machine/app.dart'; 2 | import 'package:infinite_list_state_machine/posts/posts.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('App', () { 7 | testWidgets('renders PostsPage', (tester) async { 8 | await tester.pumpWidget(App()); 9 | await tester.pumpAndSettle(); 10 | expect(find.byType(PostsPage), findsOneWidget); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/test/posts/models/post_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:infinite_list_state_machine/posts/models/models.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('Post', () { 7 | test('supports value comparison', () { 8 | expect( 9 | Post(id: 1, title: 'post title', body: 'post body'), 10 | Post(id: 1, title: 'post title', body: 'post body'), 11 | ); 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/test/posts/view/posts_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:infinite_list_state_machine/posts/posts.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('PostsPage', () { 7 | testWidgets('renders PostList', (tester) async { 8 | await tester.pumpWidget(MaterialApp(home: PostsPage())); 9 | await tester.pumpAndSettle(); 10 | expect(find.byType(PostsList), findsOneWidget); 11 | }); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/example/login_event.dart: -------------------------------------------------------------------------------- 1 | part of 'simple_login_sm.dart'; 2 | 3 | class LoginEvent { 4 | const LoginEvent(); 5 | } 6 | 7 | class LoginFormSubmitted extends LoginEvent { 8 | const LoginFormSubmitted({ 9 | required this.email, 10 | required this.password, 11 | }); 12 | 13 | final String email; 14 | final String password; 15 | } 16 | 17 | class LoginSucceeded extends LoginEvent {} 18 | 19 | class LoginFailed extends LoginEvent { 20 | const LoginFailed( 21 | this.reason, 22 | ); 23 | 24 | final String reason; 25 | } 26 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: state_machine_bloc 2 | description: state_machine_bloc is an extension to the bloc state management library which provide a convenient way to create finite state machines based blocs. 3 | version: 0.0.2 4 | homepage: https://github.com/Pierre2tm/state_machine_bloc 5 | repository: https://github.com/Pierre2tm/state_machine_bloc 6 | 7 | environment: 8 | sdk: ">=2.16.1 <4.0.0" 9 | 10 | dev_dependencies: 11 | lints: ^1.0.0 12 | test: ^1.20.2 13 | 14 | dependencies: 15 | bloc: ^8.0.2 16 | bloc_concurrency: ^0.2.0 17 | meta: ^1.7.0 18 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_timer_state_machine 2 | description: A new Flutter project. 3 | version: 1.0.0+1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | equatable: ^2.0.3 13 | bloc: ^8.0.2 14 | flutter_bloc: ^8.0.1 15 | state_machine_bloc: 16 | path: ../../packages/state_machine_bloc/ 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | bloc_test: ^9.0.0 22 | mocktail: ^0.3.0 23 | 24 | flutter: 25 | uses-material-design: true 26 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/test/posts/bloc/post_state_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: prefer_const_constructors 2 | import 'package:infinite_list_state_machine/posts/posts.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | void main() { 6 | group('PostState', () { 7 | test('supports value comparison', () { 8 | expect(PostInitial(), PostInitial()); 9 | expect( 10 | PostError(posts: [], error: "e"), 11 | PostError(posts: [], error: "e"), 12 | ); 13 | expect(PostSuccess(posts: []), PostSuccess(posts: [])); 14 | }); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_timer_state_machine/timer/timer.dart'; 3 | 4 | class App extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return MaterialApp( 8 | title: 'Flutter State Machine Timer', 9 | theme: ThemeData( 10 | primaryColor: Color.fromRGBO(109, 234, 255, 1), 11 | colorScheme: ColorScheme.light( 12 | secondary: Color.fromRGBO(72, 74, 126, 1), 13 | ), 14 | ), 15 | home: const TimerPage(), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/example/login_state.dart: -------------------------------------------------------------------------------- 1 | part of 'simple_login_sm.dart'; 2 | 3 | class LoginState { 4 | const LoginState(); 5 | } 6 | 7 | class WaitingFormSubmission extends LoginState {} 8 | 9 | class TryLoggingIn extends LoginState { 10 | const TryLoggingIn({ 11 | required this.email, 12 | required this.password, 13 | }); 14 | 15 | final String email; 16 | final String password; 17 | } 18 | 19 | class LoginSuccess extends LoginState {} 20 | 21 | class LoginError extends LoginState { 22 | const LoginError( 23 | this.error, 24 | ); 25 | 26 | final String error; 27 | } 28 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/view/posts_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:infinite_list_state_machine/posts/posts.dart'; 4 | import 'package:http/http.dart' as http; 5 | 6 | class PostsPage extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return Scaffold( 10 | appBar: AppBar(title: const Text('Posts')), 11 | body: BlocProvider( 12 | create: (_) => 13 | PostBloc(httpClient: http.Client())..add(PostFetchRequested()), 14 | child: PostsList(), 15 | ), 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: infinite_list_state_machine 2 | description: A new Flutter project. 3 | version: 1.0.0+1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: ">=3.0.0 <4.0.0" 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | http: ^0.13.0 13 | equatable: ^2.0.3 14 | stream_transform: ^2.0.0 15 | bloc_concurrency: ^0.2.0 16 | bloc: ^8.0.3 17 | flutter_bloc: ^8.0.1 18 | bloc_test: ^9.0.3 19 | state_machine_bloc: 20 | path: "../../packages/state_machine_bloc" 21 | 22 | dev_dependencies: 23 | flutter_test: 24 | sdk: flutter 25 | mocktail: ^0.2.0 26 | 27 | flutter: 28 | uses-material-design: true 29 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 25 | /pubspec.lock 26 | **/doc/api/ 27 | .dart_tool/ 28 | .packages 29 | build/ 30 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/bloc/post_event.dart: -------------------------------------------------------------------------------- 1 | part of 'post_bloc.dart'; 2 | 3 | abstract class PostEvent extends Equatable { 4 | const PostEvent(); 5 | @override 6 | List get props => []; 7 | } 8 | 9 | class PostFetchRequested extends PostEvent {} 10 | 11 | class PostFetchSuccess extends PostEvent { 12 | const PostFetchSuccess({ 13 | required this.posts, 14 | }); 15 | 16 | final List posts; 17 | 18 | @override 19 | List get props => [posts]; 20 | } 21 | 22 | class PostFetchError extends PostEvent { 23 | const PostFetchError({ 24 | required this.error, 25 | }); 26 | 27 | final String error; 28 | 29 | @override 30 | List get props => [error]; 31 | } 32 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/widgets/post_list_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:infinite_list_state_machine/posts/posts.dart'; 3 | 4 | class PostListItem extends StatelessWidget { 5 | const PostListItem({Key? key, required this.post}) : super(key: key); 6 | 7 | final Post post; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final textTheme = Theme.of(context).textTheme; 12 | return Material( 13 | child: ListTile( 14 | leading: Text('${post.id}', style: textTheme.bodySmall), 15 | title: Text(post.title), 16 | isThreeLine: true, 17 | subtitle: Text(post.body), 18 | dense: true, 19 | ), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/README.md: -------------------------------------------------------------------------------- 1 | [![build](https://github.com/felangel/bloc/workflows/build/badge.svg)](https://github.com/felangel/bloc/actions) 2 | 3 | # flutter_timer 4 | 5 | A new Flutter project. 6 | 7 | ## Getting Started 8 | 9 | This project is a starting point for a Flutter application. 10 | 11 | A few resources to get you started if this is your first Flutter project: 12 | 13 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 14 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 15 | 16 | For help getting started with Flutter, view our 17 | [online documentation](https://flutter.dev/docs), which offers tutorials, 18 | samples, guidance on mobile development, and a full API reference. 19 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/README.md: -------------------------------------------------------------------------------- 1 | [![build](https://github.com/felangel/bloc/workflows/build/badge.svg)](https://github.com/felangel/bloc/actions) 2 | 3 | # infinite_list_state_machine 4 | 5 | A new Flutter project. 6 | 7 | ## Getting Started 8 | 9 | This project is a starting point for a Flutter application. 10 | 11 | A few resources to get you started if this is your first Flutter project: 12 | 13 | - [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) 14 | - [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) 15 | 16 | For help getting started with Flutter, view our 17 | [online documentation](https://flutter.dev/docs), which offers tutorials, 18 | samples, guidance on mobile development, and a full API reference. 19 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.2 2 | * Support for async transitions 3 | * Bump max sdk version to 4.0.0 4 | 5 | ## 0.0.1 6 | * Initial release 🎉 7 | 8 | ## 0.0.1-dev.4 9 | * Improved documentation 10 | * Add doc-comments 11 | 12 | ## 0.0.1-dev.3 13 | * `StateMachine` now use `droppable` event transformer instead of `sequential` 14 | * Transition are now synchronous. 15 | * Fix: onEnter/onExit child state's side effects wasn't triggered when parent changed 16 | * Parents states are not forbidden states anymore 17 | * Improved documentation 18 | * Add unit tests 19 | * Add infinite List example 20 | 21 | ## 0.0.1-dev.2 22 | * `StateMachine` now extends `Bloc` instead of `BlocBase`. 23 | * Support for nested states. 24 | * Side effect rework 25 | 26 | ## 0.0.1-dev.1 27 | * proof of concept 28 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | **/ios/Flutter/.last_build_id 24 | .dart_tool/ 25 | .flutter-plugins 26 | .flutter-plugins-dependencies 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | build/ 31 | 32 | # Web related 33 | lib/generated_plugin_registrant.dart 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # Visual Studio Code related 19 | .vscode/ 20 | 21 | # Flutter/Dart/Pub related 22 | **/doc/api/ 23 | **/ios/Flutter/.last_build_id 24 | .dart_tool/ 25 | .flutter-plugins 26 | .flutter-plugins-dependencies 27 | .packages 28 | .pub-cache/ 29 | .pub/ 30 | /build/ 31 | 32 | # Web related 33 | lib/generated_plugin_registrant.dart 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | 46 | macos/ -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/lib/timer/state_machine/timer_event.dart: -------------------------------------------------------------------------------- 1 | part of 'timer_state_machine.dart'; 2 | 3 | abstract class TimerEvent extends Equatable { 4 | const TimerEvent(); 5 | 6 | @override 7 | List get props => []; 8 | } 9 | 10 | class TimerStarted extends TimerEvent { 11 | const TimerStarted({required this.duration}); 12 | final int duration; 13 | } 14 | 15 | class TimerPaused extends TimerEvent { 16 | const TimerPaused(); 17 | } 18 | 19 | class TimerResumed extends TimerEvent { 20 | const TimerResumed(); 21 | } 22 | 23 | class TimerReset extends TimerEvent { 24 | const TimerReset(); 25 | } 26 | 27 | class TimerTicked extends TimerEvent { 28 | const TimerTicked({required this.duration}); 29 | final int duration; 30 | 31 | @override 32 | List get props => [duration]; 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/run-test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | working-directory: [ 14 | packages/state_machine_bloc, 15 | examples/flutter_timer_state_machine, 16 | examples/infinite_list_state_machine 17 | ] 18 | 19 | runs-on: ubuntu-latest 20 | defaults: 21 | run: 22 | working-directory: ${{ matrix.working-directory }} 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Set up Flutter 28 | uses: subosito/flutter-action@v2 29 | with: 30 | channel: stable 31 | 32 | - name: Install dependencies 33 | run: flutter pub get 34 | 35 | - name: Verify formatting 36 | run: dart format --output=none --set-exit-if-changed . 37 | 38 | - name: Run tests 39 | run: flutter test -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/bloc/post_state.dart: -------------------------------------------------------------------------------- 1 | part of 'post_bloc.dart'; 2 | 3 | abstract class PostState extends Equatable { 4 | const PostState({required this.posts}); 5 | 6 | final List posts; 7 | 8 | @override 9 | List get props => [posts]; 10 | } 11 | 12 | class PostInitial extends PostState { 13 | const PostInitial() : super(posts: const []); 14 | } 15 | 16 | class PostSuccess extends PostState { 17 | const PostSuccess({required List posts}) : super(posts: posts); 18 | } 19 | 20 | class PostError extends PostState { 21 | const PostError({required List posts, required this.error}) 22 | : super(posts: posts); 23 | 24 | final String error; 25 | 26 | @override 27 | List get props => [posts, error]; 28 | } 29 | 30 | class PostFetchInProgress extends PostState { 31 | const PostFetchInProgress({required List posts}) : super(posts: posts); 32 | } 33 | 34 | class PostEndReached extends PostState { 35 | const PostEndReached({required List posts}) : super(posts: posts); 36 | } 37 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | import 'package:flutter_timer_state_machine/app.dart'; 11 | 12 | void main() { 13 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 14 | // Build our app and trigger a frame. 15 | await tester.pumpWidget(App()); 16 | 17 | // Verify that our counter starts at 0. 18 | expect(find.text('01:00'), findsOneWidget); 19 | 20 | // Tap the '+' icon and trigger a frame. 21 | await tester.tap(find.byIcon(Icons.play_arrow)); 22 | await tester.pump(); 23 | 24 | expect(find.text('1'), findsNothing); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "5874a72aa4c779a02553007c47dacbefba2374dc" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 17 | base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 18 | - platform: macos 19 | create_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 20 | base_revision: 5874a72aa4c779a02553007c47dacbefba2374dc 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Pierre2tm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/lib/timer/state_machine/timer_state.dart: -------------------------------------------------------------------------------- 1 | part of 'timer_state_machine.dart'; 2 | 3 | abstract class TimerState extends Equatable { 4 | final int duration; 5 | 6 | const TimerState(this.duration); 7 | 8 | @override 9 | List get props => [duration]; 10 | } 11 | 12 | class TimerInitial extends TimerState { 13 | const TimerInitial(int duration) : super(duration); 14 | 15 | @override 16 | String toString() => 'TimerInitial { duration: $duration }'; 17 | } 18 | 19 | abstract class TimerRun extends TimerState { 20 | const TimerRun(int duration) : super(duration); 21 | } 22 | 23 | class TimerRunPause extends TimerRun { 24 | const TimerRunPause(int duration) : super(duration); 25 | 26 | @override 27 | String toString() => 'TimerRunPause { duration: $duration }'; 28 | } 29 | 30 | class TimerRunInProgress extends TimerRun { 31 | const TimerRunInProgress(int duration) : super(duration); 32 | 33 | @override 34 | String toString() => 'TimerRunInProgress { duration: $duration }'; 35 | } 36 | 37 | class TimerRunComplete extends TimerRun { 38 | const TimerRunComplete() : super(0); 39 | } 40 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Pierre2tm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/test/timer/bloc/timer_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_timer_state_machine/timer/timer.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | group('TimerState', () { 6 | group('TimerInitial', () { 7 | test('supports value comparison', () { 8 | expect( 9 | const TimerInitial(60), 10 | const TimerInitial(60), 11 | ); 12 | }); 13 | }); 14 | group('TimerRunPause', () { 15 | test('supports value comparison', () { 16 | expect( 17 | const TimerRunPause(60), 18 | const TimerRunPause(60), 19 | ); 20 | }); 21 | }); 22 | group('TimerRunInProgress', () { 23 | test('supports value comparison', () { 24 | expect( 25 | const TimerRunInProgress(60), 26 | const TimerRunInProgress(60), 27 | ); 28 | }); 29 | }); 30 | group('TimerRunComplete', () { 31 | test('supports value comparison', () { 32 | expect( 33 | const TimerRunComplete(), 34 | const TimerRunComplete(), 35 | ); 36 | }); 37 | }); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/test/timer/bloc/timer_event_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_timer_state_machine/timer/timer.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | void main() { 5 | group('TimerEvent', () { 6 | group('TimerStarted', () { 7 | test('supports value comparison', () { 8 | expect( 9 | TimerStarted(duration: 60), 10 | TimerStarted(duration: 60), 11 | ); 12 | }); 13 | }); 14 | group('TimerPaused', () { 15 | test('supports value comparison', () { 16 | expect(TimerPaused(), TimerPaused()); 17 | }); 18 | }); 19 | group('TimerResumed', () { 20 | test('supports value comparison', () { 21 | expect(TimerResumed(), TimerResumed()); 22 | }); 23 | }); 24 | group('TimerReset', () { 25 | test('supports value comparison', () { 26 | expect(TimerReset(), TimerReset()); 27 | }); 28 | }); 29 | group('TimerTicked', () { 30 | test('supports value comparison', () { 31 | expect( 32 | TimerTicked(duration: 60), 33 | TimerTicked(duration: 60), 34 | ); 35 | }); 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/example/simple_login_sm.dart: -------------------------------------------------------------------------------- 1 | import 'package:state_machine_bloc/state_machine_bloc.dart'; 2 | 3 | part 'user_repository.dart'; 4 | part 'login_event.dart'; 5 | part 'login_state.dart'; 6 | 7 | class LoginStateMachine extends StateMachine { 8 | LoginStateMachine({ 9 | required this.userRepository, 10 | }) : super(WaitingFormSubmission()) { 11 | define( 12 | ($) => $..on(_toTryLoggingIn)); 13 | 14 | define(($) => $ 15 | ..onEnter(_login) 16 | ..on(_toSuccess) 17 | ..on(_toError)); 18 | 19 | define(); 20 | define(); 21 | } 22 | 23 | final UserRepository userRepository; 24 | 25 | TryLoggingIn _toTryLoggingIn(LoginFormSubmitted event, state) => 26 | TryLoggingIn(email: event.email, password: event.password); 27 | 28 | LoginSuccess _toSuccess(e, s) => LoginSuccess(); 29 | 30 | LoginError _toError(LoginFailed event, state) => LoginError(event.reason); 31 | 32 | /// Use state's data to try login-in using the API 33 | Future _login(TryLoggingIn state) async { 34 | try { 35 | await userRepository.login( 36 | email: state.email, 37 | password: state.password, 38 | ); 39 | add(LoginSucceeded()); 40 | } catch (e) { 41 | add(LoginFailed(e.toString())); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/test/async_transition_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:state_machine_bloc/state_machine_bloc.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils.dart'; 5 | 6 | abstract class Event {} 7 | 8 | class EventA extends Event {} 9 | 10 | class EventB extends Event {} 11 | 12 | abstract class State { 13 | const State(); 14 | } 15 | 16 | class StateA extends State { 17 | const StateA(); 18 | } 19 | 20 | class StateB extends State { 21 | const StateB(); 22 | } 23 | 24 | class StateC extends State { 25 | const StateC(); 26 | } 27 | 28 | class StateD extends State { 29 | const StateD(); 30 | } 31 | 32 | class DummyStateMachine extends StateMachine { 33 | DummyStateMachine([State? initial]) : super(initial ?? const StateA()) { 34 | define(($) => $ 35 | ..on((EventA e, s) async { 36 | await Future.delayed(Duration(milliseconds: 300)); 37 | return const StateB(); 38 | }) 39 | ..on((EventB e, s) { 40 | return const StateC(); 41 | })); 42 | define(); 43 | define(); 44 | } 45 | } 46 | 47 | void main() { 48 | group("event receiving tests", () { 49 | test("Transitions are awaited", () async { 50 | final sm = DummyStateMachine(const StateA()); 51 | sm.add(EventA()); 52 | 53 | await wait(); 54 | 55 | expect(sm.state, const StateA()); 56 | 57 | await Future.delayed(Duration(seconds: 1)); 58 | 59 | expect(sm.state, const StateB()); 60 | }); 61 | }); 62 | 63 | test("Transitions are evaluated sequentially", () async { 64 | final sm = DummyStateMachine(const StateA()); 65 | sm.add(EventA()); 66 | sm.add(EventB()); 67 | 68 | await wait(); 69 | 70 | expect(sm.state, const StateA()); 71 | 72 | await Future.delayed(Duration(seconds: 1)); 73 | 74 | expect(sm.state, const StateB()); 75 | }); 76 | } 77 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/test/nested_async_transition_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:state_machine_bloc/state_machine_bloc.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils.dart'; 5 | 6 | abstract class Event {} 7 | 8 | class EventA extends Event {} 9 | 10 | class EventB extends Event {} 11 | 12 | abstract class State { 13 | const State(); 14 | } 15 | 16 | class StateA extends State { 17 | const StateA(); 18 | } 19 | 20 | class StateB extends StateA { 21 | const StateB(); 22 | } 23 | 24 | class StateC extends State { 25 | const StateC(); 26 | } 27 | 28 | class StateD extends State { 29 | const StateD(); 30 | } 31 | 32 | class DummyStateMachine extends StateMachine { 33 | DummyStateMachine([State? initial]) : super(initial ?? const StateA()) { 34 | define( 35 | ($) => $ 36 | ..define(($) => $ 37 | ..on((EventA e, s) async { 38 | await Future.delayed(Duration(milliseconds: 300)); 39 | return const StateC(); 40 | }) 41 | ..on((EventB e, s) { 42 | return const StateD(); 43 | })), 44 | ); 45 | define(); 46 | define(); 47 | } 48 | } 49 | 50 | void main() { 51 | group("event receiving tests", () { 52 | test("Nested transitions are awaited", () async { 53 | final sm = DummyStateMachine(const StateB()); 54 | sm.add(EventA()); 55 | 56 | await wait(); 57 | 58 | expect(sm.state, const StateB()); 59 | 60 | await Future.delayed(Duration(seconds: 1)); 61 | 62 | expect(sm.state, const StateC()); 63 | }); 64 | }); 65 | 66 | test("Nested transitions are evaluated sequentially", () async { 67 | final sm = DummyStateMachine(const StateB()); 68 | sm.add(EventA()); 69 | sm.add(EventB()); 70 | 71 | await wait(); 72 | 73 | expect(sm.state, const StateB()); 74 | 75 | await Future.delayed(Duration(seconds: 1)); 76 | 77 | expect(sm.state, const StateC()); 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/view/posts_list.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:infinite_list_state_machine/posts/posts.dart'; 4 | 5 | class PostsList extends StatefulWidget { 6 | @override 7 | _PostsListState createState() => _PostsListState(); 8 | } 9 | 10 | class _PostsListState extends State { 11 | final _scrollController = ScrollController(); 12 | 13 | @override 14 | void initState() { 15 | super.initState(); 16 | _scrollController.addListener(_onScroll); 17 | } 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return BlocBuilder( 22 | builder: (context, state) { 23 | if (state is PostInitial) 24 | return const Center(child: CircularProgressIndicator()); 25 | 26 | if (state is PostError) 27 | return const Center(child: Text('failed to fetch posts')); 28 | 29 | if (state.posts.isEmpty) return const Center(child: Text('no posts')); 30 | 31 | return ListView.builder( 32 | itemBuilder: (BuildContext context, int index) { 33 | return index >= state.posts.length 34 | ? BottomLoader() 35 | : PostListItem(post: state.posts[index]); 36 | }, 37 | itemCount: state is PostEndReached 38 | ? state.posts.length 39 | : state.posts.length + 1, 40 | controller: _scrollController, 41 | ); 42 | }, 43 | ); 44 | } 45 | 46 | @override 47 | void dispose() { 48 | _scrollController 49 | ..removeListener(_onScroll) 50 | ..dispose(); 51 | super.dispose(); 52 | } 53 | 54 | void _onScroll() { 55 | if (_isBottom) context.read().add(PostFetchRequested()); 56 | } 57 | 58 | bool get _isBottom { 59 | if (!_scrollController.hasClients) return false; 60 | final maxScroll = _scrollController.position.maxScrollExtent; 61 | final currentScroll = _scrollController.offset; 62 | return currentScroll >= (maxScroll * 0.9); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-casts: false 4 | implicit-dynamic: false 5 | errors: 6 | close_sinks: ignore 7 | 8 | linter: 9 | rules: 10 | - annotate_overrides 11 | - avoid_empty_else 12 | - avoid_function_literals_in_foreach_calls 13 | - avoid_init_to_null 14 | - avoid_null_checks_in_equality_operators 15 | - avoid_relative_lib_imports 16 | - avoid_renaming_method_parameters 17 | - avoid_return_types_on_setters 18 | - avoid_types_as_parameter_names 19 | - avoid_unused_constructor_parameters 20 | - await_only_futures 21 | - camel_case_types 22 | - cancel_subscriptions 23 | - cascade_invocations 24 | - comment_references 25 | - constant_identifier_names 26 | - control_flow_in_finally 27 | - directives_ordering 28 | - empty_catches 29 | - empty_constructor_bodies 30 | - empty_statements 31 | - hash_and_equals 32 | - implementation_imports 33 | - library_names 34 | - library_prefixes 35 | - lines_longer_than_80_chars 36 | - no_adjacent_strings_in_list 37 | - no_duplicate_case_values 38 | - non_constant_identifier_names 39 | - null_closures 40 | - omit_local_variable_types 41 | - only_throw_errors 42 | - overridden_fields 43 | - package_api_docs 44 | - package_names 45 | - package_prefixed_library_names 46 | - prefer_adjacent_string_concatenation 47 | - prefer_collection_literals 48 | - prefer_conditional_assignment 49 | - prefer_const_constructors 50 | - prefer_contains 51 | - prefer_final_fields 52 | - prefer_initializing_formals 53 | - prefer_interpolation_to_compose_strings 54 | - prefer_is_empty 55 | - prefer_is_not_empty 56 | - prefer_single_quotes 57 | - prefer_typing_uninitialized_variables 58 | # - public_member_api_docs 59 | - recursive_getters 60 | - slash_for_doc_comments 61 | - sort_constructors_first 62 | - test_types_in_equals 63 | - throw_in_finally 64 | - type_init_formals 65 | - unawaited_futures 66 | - unnecessary_brace_in_string_interps 67 | - unnecessary_const 68 | - unnecessary_getters_setters 69 | - unnecessary_lambdas 70 | - unnecessary_new 71 | - unnecessary_null_aware_assignments 72 | - unnecessary_statements 73 | - unnecessary_this 74 | - unrelated_type_equality_checks 75 | - use_rethrow_when_possible 76 | - valid_regexps 77 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/lib/timer/state_machine/timer_state_machine.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:equatable/equatable.dart'; 3 | import 'package:flutter_timer_state_machine/ticker.dart'; 4 | import 'package:state_machine_bloc/state_machine_bloc.dart'; 5 | 6 | part 'timer_event.dart'; 7 | part 'timer_state.dart'; 8 | 9 | class TimerStateMachine extends StateMachine { 10 | TimerStateMachine({required Ticker ticker}) 11 | : _ticker = ticker, 12 | super(TimerInitial(_duration)) { 13 | define( 14 | (b) => b..on(_onTimerStarted), 15 | ); 16 | 17 | define( 18 | (b) => b 19 | //Reset Timer 20 | ..on(_onReset) 21 | 22 | //Timer running 23 | ..define((b) => b 24 | ..on(_onTicked) 25 | ..on(_onPaused)) 26 | 27 | //Timer paused 28 | ..define((b) => b 29 | ..on( 30 | _onResumed, 31 | )) 32 | 33 | //Timer Completed 34 | ..define(), 35 | ); 36 | } 37 | 38 | final Ticker _ticker; 39 | static const int _duration = 60; 40 | 41 | StreamSubscription? _tickerSubscription; 42 | 43 | @override 44 | Future close() { 45 | _tickerSubscription?.cancel(); 46 | return super.close(); 47 | } 48 | 49 | TimerRunInProgress _onTimerStarted(TimerStarted event, _) { 50 | _tickerSubscription?.cancel(); 51 | _tickerSubscription = _ticker.tick(ticks: event.duration).listen( 52 | (duration) { 53 | try { 54 | add(TimerTicked(duration: duration)); 55 | } catch (e) { 56 | print(e); 57 | } 58 | }, 59 | ); 60 | return TimerRunInProgress(event.duration); 61 | } 62 | 63 | TimerRunPause _onPaused(TimerPaused event, TimerRunInProgress state) { 64 | _tickerSubscription?.pause(); 65 | return TimerRunPause(state.duration); 66 | } 67 | 68 | TimerRunInProgress _onResumed(TimerResumed resume, TimerRunPause state) { 69 | _tickerSubscription?.resume(); 70 | return TimerRunInProgress(state.duration); 71 | } 72 | 73 | TimerInitial _onReset(TimerReset event, TimerRun state) { 74 | _tickerSubscription?.cancel(); 75 | return TimerInitial(_duration); 76 | } 77 | 78 | TimerRun _onTicked(TimerTicked event, TimerRunInProgress state) { 79 | return event.duration > 0 80 | ? TimerRunInProgress(event.duration) 81 | : TimerRunComplete(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/test/lifecycle_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:state_machine_bloc/state_machine_bloc.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils.dart'; 5 | 6 | abstract class Event {} 7 | 8 | class EventA extends Event {} 9 | 10 | class TriggerStateOnChange extends Event {} 11 | 12 | abstract class State { 13 | @override 14 | bool operator ==(Object value) => false; 15 | 16 | @override 17 | int get hashCode => 0; 18 | } 19 | 20 | class StateA extends State {} 21 | 22 | class StateB extends State {} 23 | 24 | class DummyStateMachine extends StateMachine { 25 | DummyStateMachine({ 26 | State? initialState, 27 | }) : super(initialState ?? StateA()) { 28 | define( 29 | ($) => $ 30 | ..onEnter((_) => onEnterCalls.add("StateA")) 31 | ..onExit((_) => onExitCalls.add("StateA")) 32 | ..onChange((_, __) => onChangeCalls.add("StateA")) 33 | ..on((_, __) => StateB()), 34 | ); 35 | 36 | define( 37 | ($) => $ 38 | ..onEnter((_) => onEnterCalls.add("StateB")) 39 | ..onExit((_) => onExitCalls.add("StateB")) 40 | ..onChange((_, __) => onChangeCalls.add("StateB")) 41 | ..on((_, __) => StateB()), 42 | ); 43 | } 44 | 45 | List onEnterCalls = []; 46 | List onExitCalls = []; 47 | List onChangeCalls = []; 48 | } 49 | 50 | void main() { 51 | group("Lifecycle", () { 52 | test("Initial state's onEnter is called at initialization", () { 53 | final sm = DummyStateMachine(); 54 | expect(sm.onEnterCalls, ["StateA"]); 55 | }); 56 | 57 | test("onEnter is called when entering new state", () async { 58 | final sm = DummyStateMachine(); 59 | sm.add(EventA()); 60 | 61 | await wait(); 62 | 63 | expect(sm.onEnterCalls, ["StateA", "StateB"]); 64 | }); 65 | 66 | test("onExit is called when exiting a state", () async { 67 | final sm = DummyStateMachine(); 68 | sm.add(EventA()); 69 | 70 | await wait(); 71 | 72 | expect(sm.onExitCalls, ["StateA"]); 73 | }); 74 | 75 | test("onChange is called when transiting to same state", () async { 76 | final sm = DummyStateMachine(initialState: StateB()); 77 | sm.add(TriggerStateOnChange()); 78 | 79 | await wait(); 80 | 81 | expect(sm.onChangeCalls, ["StateB"]); 82 | }); 83 | 84 | test("onChange is not called when transiting to an other state", () async { 85 | final sm = DummyStateMachine(); 86 | sm.add(EventA()); 87 | 88 | await wait(); 89 | 90 | expect(sm.onChangeCalls, []); 91 | }); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/test/events_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:state_machine_bloc/state_machine_bloc.dart'; 2 | import 'package:test/test.dart'; 3 | 4 | import 'utils.dart'; 5 | 6 | abstract class Event {} 7 | 8 | class EventA extends Event {} 9 | 10 | class EventB extends Event {} 11 | 12 | class EventC extends Event {} 13 | 14 | abstract class State { 15 | @override 16 | bool operator ==(Object value) => false; 17 | 18 | @override 19 | int get hashCode => 0; 20 | } 21 | 22 | class StateA extends State {} 23 | 24 | class StateB extends State {} 25 | 26 | class StateC extends State {} 27 | 28 | class DummyStateMachine extends StateMachine { 29 | DummyStateMachine([State? initial]) : super(initial ?? StateA()) { 30 | define(($) => $ 31 | ..on((EventA e, s) { 32 | _onEvent(e); 33 | return StateB(); 34 | }) 35 | ..on((EventA e, s) { 36 | _onEvent(e); 37 | return StateB(); 38 | })); 39 | define(($) => $ 40 | ..on((EventB e, s) { 41 | _onEvent(e); 42 | return StateA(); 43 | })); 44 | 45 | define(($) => $ 46 | ..on((EventC e, s) { 47 | _onEvent(e); 48 | return null; 49 | }) 50 | ..on((EventC e, s) { 51 | _onEvent(e); 52 | return StateA(); 53 | })); 54 | } 55 | 56 | void _onEvent(dynamic e) => eventsReceived.add(e.runtimeType.toString()); 57 | List eventsReceived = []; 58 | } 59 | 60 | void main() { 61 | group("event receiving computing tests", () { 62 | test("event handler that return null does not trigger a transition", 63 | () async { 64 | final sm = DummyStateMachine(StateC()); 65 | sm.add(EventC()); 66 | 67 | await wait(); 68 | 69 | expect(sm.eventsReceived, ["EventC", "EventC"]); 70 | }); 71 | 72 | test("events are received and evaluated sequentially", () async { 73 | final sm = DummyStateMachine(); 74 | sm.add(EventB()); 75 | sm.add(EventB()); 76 | sm.add(EventA()); 77 | sm.add(EventA()); 78 | sm.add(EventA()); 79 | sm.add(EventB()); 80 | 81 | await wait(); 82 | 83 | expect(sm.eventsReceived, ["EventA", "EventB"]); 84 | }); 85 | 86 | test("events are evaluated sequentially until a transition happen", 87 | () async { 88 | final sm = DummyStateMachine(); 89 | sm.add(EventA()); 90 | sm.add(EventA()); 91 | 92 | await wait(); 93 | 94 | expect(sm.eventsReceived, ["EventA"]); 95 | }); 96 | test( 97 | "if no event handler corresponding to the received event is registered for the current state, event is ignored", 98 | () async { 99 | final sm = DummyStateMachine(); 100 | sm.add(EventB()); 101 | 102 | await wait(); 103 | 104 | expect(sm.eventsReceived, []); 105 | }); 106 | }); 107 | } 108 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/test/posts/view/posts_list_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_bloc/flutter_bloc.dart'; 4 | import 'package:infinite_list_state_machine/posts/posts.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:mocktail/mocktail.dart'; 7 | 8 | class MockPostBloc extends MockBloc implements PostBloc {} 9 | 10 | extension on WidgetTester { 11 | Future pumpPostsList(PostBloc postBloc) { 12 | return pumpWidget( 13 | MaterialApp( 14 | home: BlocProvider.value( 15 | value: postBloc, 16 | child: PostsList(), 17 | ), 18 | ), 19 | ); 20 | } 21 | } 22 | 23 | void main() { 24 | final mockPosts = List.generate( 25 | 5, 26 | (i) => Post(id: i, title: 'post title', body: 'post body'), 27 | ); 28 | 29 | late PostBloc postBloc; 30 | 31 | setUp(() { 32 | postBloc = MockPostBloc(); 33 | }); 34 | 35 | group('PostsList', () { 36 | testWidgets( 37 | 'renders CircularProgressIndicator ' 38 | 'when post status is initial', (tester) async { 39 | when(() => postBloc.state).thenReturn(const PostInitial()); 40 | await tester.pumpPostsList(postBloc); 41 | expect(find.byType(CircularProgressIndicator), findsOneWidget); 42 | }); 43 | 44 | testWidgets( 45 | 'renders no posts text ' 46 | 'when post status is success but with 0 posts', (tester) async { 47 | when(() => postBloc.state).thenReturn(const PostSuccess( 48 | posts: [], 49 | )); 50 | await tester.pumpPostsList(postBloc); 51 | expect(find.text('no posts'), findsOneWidget); 52 | }); 53 | 54 | testWidgets( 55 | 'renders 5 posts and a bottom loader when post max is not reached yet', 56 | (tester) async { 57 | when(() => postBloc.state).thenReturn(PostSuccess( 58 | posts: mockPosts, 59 | )); 60 | await tester.pumpPostsList(postBloc); 61 | expect(find.byType(PostListItem), findsNWidgets(5)); 62 | expect(find.byType(BottomLoader), findsOneWidget); 63 | }); 64 | 65 | testWidgets('does not render bottom loader when post max is reached', 66 | (tester) async { 67 | when(() => postBloc.state).thenReturn(PostEndReached( 68 | posts: mockPosts, 69 | )); 70 | await tester.pumpPostsList(postBloc); 71 | expect(find.byType(BottomLoader), findsNothing); 72 | }); 73 | 74 | testWidgets('fetches more posts when scrolled to the bottom', 75 | (tester) async { 76 | when(() => postBloc.state).thenReturn( 77 | PostSuccess( 78 | posts: List.generate( 79 | 10, 80 | (i) => Post(id: i, title: 'post title', body: 'post body'), 81 | ), 82 | ), 83 | ); 84 | await tester.pumpPostsList(postBloc); 85 | await tester.drag(find.byType(PostsList), const Offset(0, -500)); 86 | verify(() => postBloc.add(PostFetchRequested())).called(1); 87 | }); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/test/timer/bloc/timer_bloc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:flutter_timer_state_machine/timer/timer.dart'; 3 | import 'package:flutter_timer_state_machine/ticker.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:mocktail/mocktail.dart'; 6 | 7 | class MockTicker extends Mock implements Ticker {} 8 | 9 | void main() { 10 | group('TimerStateMachine', () { 11 | late Ticker ticker; 12 | 13 | setUp(() { 14 | ticker = MockTicker(); 15 | 16 | when(() => ticker.tick(ticks: 5)).thenAnswer( 17 | (_) => Stream.fromIterable([5, 4, 3, 2, 1]), 18 | ); 19 | }); 20 | 21 | test('initial state is TimerInitial(60)', () { 22 | expect( 23 | TimerStateMachine(ticker: ticker).state, 24 | TimerInitial(60), 25 | ); 26 | }); 27 | 28 | blocTest( 29 | 'emits TickerRunInProgress 5 times after timer started', 30 | build: () => TimerStateMachine(ticker: ticker), 31 | act: (bloc) => bloc.add(const TimerStarted(duration: 5)), 32 | expect: () => [ 33 | TimerRunInProgress(5), 34 | TimerRunInProgress(4), 35 | TimerRunInProgress(3), 36 | TimerRunInProgress(2), 37 | TimerRunInProgress(1), 38 | ], 39 | verify: (_) => verify(() => ticker.tick(ticks: 5)).called(1), 40 | ); 41 | 42 | blocTest( 43 | 'emits [TickerRunPause(2)] when ticker is paused at 2', 44 | build: () => TimerStateMachine(ticker: ticker), 45 | seed: () => TimerRunInProgress(2), 46 | act: (bloc) => bloc.add(TimerPaused()), 47 | expect: () => [TimerRunPause(2)], 48 | ); 49 | 50 | blocTest( 51 | 'emits [TickerRunInProgress(5)] when ticker is resumed at 5', 52 | build: () => TimerStateMachine(ticker: ticker), 53 | seed: () => TimerRunPause(5), 54 | act: (bloc) => bloc.add(TimerResumed()), 55 | expect: () => [TimerRunInProgress(5)], 56 | ); 57 | 58 | blocTest( 59 | 'emits [TickerInitial(60)] when timer is restarted', 60 | build: () => TimerStateMachine(ticker: ticker), 61 | seed: () => TimerRunInProgress(3), 62 | act: (bloc) => bloc.add(TimerReset()), 63 | expect: () => [TimerInitial(60)], 64 | ); 65 | 66 | blocTest( 67 | 'emits [TimerRunInProgress(3)] when timer ticks to 3', 68 | build: () => TimerStateMachine(ticker: ticker), 69 | seed: () => TimerRunInProgress(4), 70 | act: (bloc) => bloc.add(TimerTicked(duration: 3)), 71 | expect: () => [TimerRunInProgress(3)], 72 | ); 73 | 74 | blocTest( 75 | 'emits [TimerRunComplete()] when timer ticks to 0', 76 | seed: () => TimerRunInProgress(1), 77 | build: () => TimerStateMachine(ticker: ticker), 78 | act: (bloc) => bloc.add(TimerTicked(duration: 0)), 79 | expect: () => [TimerRunComplete()], 80 | ); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/lib/posts/bloc/post_bloc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:equatable/equatable.dart'; 5 | import 'package:http/http.dart' as http; 6 | import 'package:infinite_list_state_machine/posts/posts.dart'; 7 | import 'package:state_machine_bloc/state_machine_bloc.dart'; 8 | 9 | part 'post_event.dart'; 10 | part 'post_state.dart'; 11 | 12 | const _postLimit = 20; 13 | const throttleDuration = Duration(milliseconds: 100); 14 | 15 | // EventTransformer throttleDroppable(Duration duration) { 16 | // return (events, mapper) { 17 | // return droppable().call(events.throttle(duration), mapper); 18 | // }; 19 | // } 20 | 21 | class PostBloc extends StateMachine { 22 | PostBloc({required this.httpClient}) : super(const PostInitial()) { 23 | define((b) => b 24 | ..on( 25 | _transitToFetchInProgress, 26 | )); 27 | 28 | define((b) => b 29 | ..on( 30 | _transitToFetchInProgress, 31 | )); 32 | 33 | define( 34 | (b) => b 35 | ..onEnter(_fetchPosts) 36 | ..on(_onPostFetchSuccess) 37 | ..on(_onPostFetchError), 38 | ); 39 | 40 | define(); 41 | 42 | define(); 43 | } 44 | 45 | final http.Client httpClient; 46 | 47 | PostFetchInProgress _transitToFetchInProgress( 48 | PostFetchRequested event, 49 | PostState state, 50 | ) => 51 | PostFetchInProgress(posts: state.posts); 52 | 53 | PostState _onPostFetchSuccess( 54 | PostFetchSuccess event, 55 | PostFetchInProgress state, 56 | ) { 57 | if (event.posts.isNotEmpty) { 58 | return PostSuccess(posts: List.of(state.posts)..addAll(event.posts)); 59 | } else { 60 | return PostEndReached(posts: state.posts); 61 | } 62 | } 63 | 64 | PostError _onPostFetchError( 65 | PostFetchError event, 66 | PostFetchInProgress state, 67 | ) => 68 | PostError( 69 | posts: state.posts, 70 | error: event.error, 71 | ); 72 | 73 | Future _fetchPosts(PostFetchInProgress state) async { 74 | final startIndex = state.posts.length; 75 | try { 76 | final response = await httpClient.get( 77 | Uri.https( 78 | 'jsonplaceholder.typicode.com', 79 | '/posts', 80 | {'_start': '$startIndex', '_limit': '$_postLimit'}, 81 | ), 82 | ); 83 | if (response.statusCode == 200) { 84 | final body = json.decode(response.body) as List; 85 | final posts = body 86 | .map((dynamic json) => Post( 87 | id: json['id'] as int, 88 | title: json['title'] as String, 89 | body: json['body'] as String, 90 | )) 91 | .toList(); 92 | add(PostFetchSuccess(posts: posts)); 93 | } 94 | add(PostFetchError(error: response.statusCode.toString())); 95 | } catch (e) { 96 | add(PostFetchError(error: e.toString())); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/lib/src/state_definition.dart: -------------------------------------------------------------------------------- 1 | part of 'state_machine.dart'; 2 | 3 | /// Signature of a function that may or may not emit a new state 4 | /// base on it's current state an an external event 5 | typedef EventTransition 6 | = FutureOr Function(Event, CurrentState); 7 | 8 | /// Signature of a callback function called by the state machine 9 | /// in various contexts that hasn't the ability to emit new state 10 | typedef SideEffect = void Function(CurrentState); 11 | 12 | typedef OnChangeSideEffect = void Function( 13 | CurrentState currentState, CurrentState nextState); 14 | 15 | /// An event handler for a given [DefinedState] 16 | /// created using on() api 17 | class _StateEventHandler { 19 | const _StateEventHandler({ 20 | required this.isType, 21 | required this.type, 22 | required this.transition, 23 | }); 24 | final bool Function(dynamic value) isType; 25 | final Type type; 26 | 27 | final EventTransition transition; 28 | 29 | FutureOr handle(SuperEvent e, SuperState s) => 30 | transition(e as DefinedEvent, s as DefinedState); 31 | } 32 | 33 | /// Definition of a state 34 | /// This class is intended to be constructed using 35 | /// [StateDefinitionBuilder] 36 | class _StateDefinition { 37 | const _StateDefinition({ 38 | List<_StateEventHandler> handlers = const [], 39 | SideEffect? onEnter, 40 | OnChangeSideEffect? onChange, 41 | SideEffect? onExit, 42 | List<_StateDefinition>? nestedStatesDefinitions, 43 | }) : _handlers = handlers, 44 | _onEnter = onEnter, 45 | _onChange = onChange, 46 | _onExit = onExit, 47 | _nestedStateDefinitions = nestedStatesDefinitions; 48 | 49 | const _StateDefinition.empty() 50 | : _handlers = const [], 51 | _onEnter = null, 52 | _onChange = null, 53 | _onExit = null, 54 | _nestedStateDefinitions = null; 55 | 56 | final List<_StateEventHandler> _handlers; 57 | 58 | /// Called whenever entering state. 59 | final SideEffect? _onEnter; 60 | 61 | /// Called whenever current state's data changed with the given updated state. 62 | final OnChangeSideEffect? _onChange; 63 | 64 | /// Called whenever exiting state. 65 | final SideEffect? _onExit; 66 | 67 | final List<_StateDefinition>? _nestedStateDefinitions; 68 | 69 | bool isType(dynamic object) => object is DefinedState; 70 | 71 | void onEnter(DefinedState state) { 72 | _onEnter?.call(state); 73 | _nestedStateDefinition(state)?.onEnter(state); 74 | } 75 | 76 | void onChange(DefinedState current, DefinedState next) { 77 | _onChange?.call(current, next); 78 | final currentDefinition = _nestedStateDefinition(current); 79 | final nextDefinition = _nestedStateDefinition(next); 80 | if (currentDefinition == nextDefinition) { 81 | currentDefinition?.onChange(current, next); 82 | } else { 83 | currentDefinition?.onExit(current); 84 | nextDefinition?.onEnter(next); 85 | } 86 | } 87 | 88 | void onExit(DefinedState state) { 89 | _onExit?.call(state); 90 | _nestedStateDefinition(state)?.onExit(state); 91 | } 92 | 93 | FutureOr add( 94 | Event event, 95 | DefinedState state, 96 | ) async { 97 | final stateHandlers = _handlers.where( 98 | (handler) => handler.isType(event), 99 | ); 100 | for (final handler in stateHandlers) { 101 | final nextState = (await handler.handle(event, state)) as SuperState?; 102 | if (nextState != null) return nextState; 103 | } 104 | final nestedDefinition = _nestedStateDefinition(state); 105 | if (nestedDefinition != null) { 106 | return nestedDefinition.add(event, state) as FutureOr; 107 | } 108 | return null; 109 | } 110 | 111 | _StateDefinition? _nestedStateDefinition(DefinedState state) { 112 | try { 113 | return _nestedStateDefinitions?.firstWhere((def) => def.isType(state)); 114 | } catch (_) { 115 | return null; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/lib/timer/view/timer_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_bloc/flutter_bloc.dart'; 3 | import 'package:flutter_timer_state_machine/ticker.dart'; 4 | import 'package:flutter_timer_state_machine/timer/timer.dart'; 5 | import 'package:flutter_timer_state_machine/timer/state_machine/timer_state_machine.dart'; 6 | 7 | class TimerPage extends StatelessWidget { 8 | const TimerPage({Key? key}) : super(key: key); 9 | @override 10 | Widget build(BuildContext context) { 11 | return BlocProvider( 12 | create: (_) => TimerStateMachine(ticker: Ticker()), 13 | child: const TimerView(), 14 | ); 15 | } 16 | } 17 | 18 | class TimerView extends StatelessWidget { 19 | const TimerView({Key? key}) : super(key: key); 20 | @override 21 | Widget build(BuildContext context) { 22 | return Scaffold( 23 | appBar: AppBar(title: const Text('Flutter Timer State Machine')), 24 | body: Stack( 25 | children: [ 26 | const Background(), 27 | Column( 28 | mainAxisAlignment: MainAxisAlignment.center, 29 | crossAxisAlignment: CrossAxisAlignment.center, 30 | children: const [ 31 | Padding( 32 | padding: EdgeInsets.symmetric(vertical: 100.0), 33 | child: Center(child: TimerText()), 34 | ), 35 | Actions(), 36 | ], 37 | ), 38 | ], 39 | ), 40 | ); 41 | } 42 | } 43 | 44 | class TimerText extends StatelessWidget { 45 | const TimerText({Key? key}) : super(key: key); 46 | @override 47 | Widget build(BuildContext context) { 48 | final duration = 49 | context.select((TimerStateMachine bloc) => bloc.state.duration); 50 | final minutesStr = 51 | ((duration / 60) % 60).floor().toString().padLeft(2, '0'); 52 | final secondsStr = (duration % 60).floor().toString().padLeft(2, '0'); 53 | return Text( 54 | '$minutesStr:$secondsStr', 55 | style: Theme.of(context).textTheme.titleLarge, 56 | ); 57 | } 58 | } 59 | 60 | class Actions extends StatelessWidget { 61 | const Actions({Key? key}) : super(key: key); 62 | @override 63 | Widget build(BuildContext context) { 64 | return BlocBuilder( 65 | buildWhen: (prev, state) => prev.runtimeType != state.runtimeType, 66 | builder: (context, state) { 67 | return Row( 68 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 69 | children: [ 70 | if (state is TimerInitial) ...[ 71 | FloatingActionButton( 72 | child: Icon(Icons.play_arrow), 73 | onPressed: () => context 74 | .read() 75 | .add(TimerStarted(duration: state.duration)), 76 | ), 77 | ], 78 | if (state is TimerRunInProgress) ...[ 79 | FloatingActionButton( 80 | child: Icon(Icons.pause), 81 | onPressed: () => 82 | context.read().add(TimerPaused()), 83 | ), 84 | FloatingActionButton( 85 | child: Icon(Icons.replay), 86 | onPressed: () => 87 | context.read().add(TimerReset()), 88 | ), 89 | ], 90 | if (state is TimerRunPause) ...[ 91 | FloatingActionButton( 92 | child: Icon(Icons.play_arrow), 93 | onPressed: () => 94 | context.read().add(TimerResumed()), 95 | ), 96 | FloatingActionButton( 97 | child: Icon(Icons.replay), 98 | onPressed: () => 99 | context.read().add(TimerReset()), 100 | ), 101 | ], 102 | if (state is TimerRunComplete) ...[ 103 | FloatingActionButton( 104 | child: Icon(Icons.replay), 105 | onPressed: () => 106 | context.read().add(TimerReset()), 107 | ), 108 | ] 109 | ], 110 | ); 111 | }, 112 | ); 113 | } 114 | } 115 | 116 | class Background extends StatelessWidget { 117 | const Background({Key? key}) : super(key: key); 118 | @override 119 | Widget build(BuildContext context) { 120 | return Container( 121 | decoration: BoxDecoration( 122 | gradient: LinearGradient( 123 | begin: Alignment.topCenter, 124 | end: Alignment.bottomCenter, 125 | colors: [ 126 | Colors.blue.shade50, 127 | Colors.blue.shade500, 128 | ], 129 | ), 130 | ), 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/test/timer/view/timer_page_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_timer_state_machine/timer/timer.dart'; 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:bloc_test/bloc_test.dart'; 5 | import 'package:flutter_bloc/flutter_bloc.dart'; 6 | import 'package:mocktail/mocktail.dart'; 7 | 8 | class MockTimerBloc extends MockBloc 9 | implements TimerStateMachine {} 10 | 11 | extension on WidgetTester { 12 | Future pumpTimerView(TimerStateMachine timerBloc) { 13 | return pumpWidget( 14 | MaterialApp( 15 | home: BlocProvider.value(value: timerBloc, child: TimerView()), 16 | ), 17 | ); 18 | } 19 | } 20 | 21 | void main() { 22 | late TimerStateMachine timerBloc; 23 | 24 | setUp(() { 25 | timerBloc = MockTimerBloc(); 26 | }); 27 | 28 | tearDown(() => reset(timerBloc)); 29 | 30 | group('TimerPage', () { 31 | testWidgets('renders TimerView', (tester) async { 32 | await tester.pumpWidget(MaterialApp(home: TimerPage())); 33 | expect(find.byType(TimerView), findsOneWidget); 34 | }); 35 | }); 36 | 37 | group('TimerView', () { 38 | testWidgets('renders initial Timer view', (tester) async { 39 | when(() => timerBloc.state).thenReturn(TimerInitial(60)); 40 | await tester.pumpTimerView(timerBloc); 41 | expect(find.text('01:00'), findsOneWidget); 42 | expect(find.byIcon(Icons.play_arrow), findsOneWidget); 43 | }); 44 | 45 | testWidgets('renders pause and reset button when timer is in progress', 46 | (tester) async { 47 | when(() => timerBloc.state).thenReturn(TimerRunInProgress(59)); 48 | await tester.pumpTimerView(timerBloc); 49 | expect(find.text('00:59'), findsOneWidget); 50 | expect(find.byIcon(Icons.pause), findsOneWidget); 51 | expect(find.byIcon(Icons.replay), findsOneWidget); 52 | }); 53 | 54 | testWidgets('renders play and reset button when timer is paused', 55 | (tester) async { 56 | when(() => timerBloc.state).thenReturn(TimerRunPause(600)); 57 | await tester.pumpTimerView(timerBloc); 58 | expect(find.text('10:00'), findsOneWidget); 59 | expect(find.byIcon(Icons.play_arrow), findsOneWidget); 60 | expect(find.byIcon(Icons.replay), findsOneWidget); 61 | }); 62 | 63 | testWidgets('renders replay button when timer is finished', (tester) async { 64 | when(() => timerBloc.state).thenReturn(TimerRunComplete()); 65 | await tester.pumpTimerView(timerBloc); 66 | expect(find.text('00:00'), findsOneWidget); 67 | expect(find.byIcon(Icons.replay), findsOneWidget); 68 | }); 69 | 70 | testWidgets('timer started when play arrow button is pressed', 71 | (tester) async { 72 | when(() => timerBloc.state).thenReturn(TimerInitial(60)); 73 | await tester.pumpTimerView(timerBloc); 74 | await tester.tap(find.byIcon(Icons.play_arrow)); 75 | verify(() => timerBloc.add(TimerStarted(duration: 60))).called(1); 76 | }); 77 | 78 | testWidgets( 79 | 'timer pauses when pause button is pressed ' 80 | 'while timer is in progress', (tester) async { 81 | when(() => timerBloc.state).thenReturn(TimerRunInProgress(30)); 82 | await tester.pumpTimerView(timerBloc); 83 | await tester.tap(find.byIcon(Icons.pause)); 84 | verify(() => timerBloc.add(TimerPaused())).called(1); 85 | }); 86 | 87 | testWidgets( 88 | 'timer resets when replay button is pressed ' 89 | 'while timer is in progress', (tester) async { 90 | when(() => timerBloc.state).thenReturn(TimerRunInProgress(30)); 91 | await tester.pumpTimerView(timerBloc); 92 | await tester.tap(find.byIcon(Icons.replay)); 93 | verify(() => timerBloc.add(TimerReset())).called(1); 94 | }); 95 | 96 | testWidgets( 97 | 'timer resumes when play arrow button is pressed ' 98 | 'while timer is paused', (tester) async { 99 | when(() => timerBloc.state).thenReturn(TimerRunPause(30)); 100 | await tester.pumpTimerView(timerBloc); 101 | await tester.tap(find.byIcon(Icons.play_arrow)); 102 | verify(() => timerBloc.add(TimerResumed())).called(1); 103 | }); 104 | 105 | testWidgets( 106 | 'timer resets when reset button is pressed ' 107 | 'while timer is paused', (tester) async { 108 | when(() => timerBloc.state).thenReturn(TimerRunPause(30)); 109 | await tester.pumpTimerView(timerBloc); 110 | await tester.tap(find.byIcon(Icons.replay)); 111 | verify(() => timerBloc.add(TimerReset())).called(1); 112 | }); 113 | 114 | testWidgets( 115 | 'timer resets when reset button is pressed ' 116 | 'when timer is finished', (tester) async { 117 | when(() => timerBloc.state).thenReturn(TimerRunComplete()); 118 | await tester.pumpTimerView(timerBloc); 119 | await tester.tap(find.byIcon(Icons.replay)); 120 | verify(() => timerBloc.add(TimerReset())).called(1); 121 | }); 122 | }); 123 | } 124 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/test/deeply_nested_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:state_machine_bloc/state_machine_bloc.dart'; 2 | import 'package:test/test.dart'; 3 | import 'utils.dart'; 4 | 5 | abstract class Event {} 6 | 7 | class TriggerNestedStateOnEnter extends Event {} 8 | 9 | class TriggerNestedStateOnChange extends Event {} 10 | 11 | class TriggerNestedStateOnExit extends Event {} 12 | 13 | abstract class State { 14 | @override 15 | bool operator ==(Object value) => false; 16 | 17 | @override 18 | int get hashCode => 0; 19 | } 20 | 21 | class ParentStateA extends State {} 22 | 23 | class AChildState1 extends ParentStateA {} 24 | 25 | class AChildState2 extends ParentStateA {} 26 | 27 | class AChildState3 extends ParentStateA {} 28 | 29 | class ParentStateB extends State {} 30 | 31 | class DummyStateMachine extends StateMachine { 32 | DummyStateMachine({ 33 | State? initialState, 34 | }) : super(initialState ?? AChildState1()) { 35 | define( 36 | ($) => $ 37 | ..define( 38 | ($) => $ 39 | ..onEnter((_) => onEnterCalls.add("ParentStateA")) 40 | ..onExit((_) => onExitCalls.add("ParentStateA")) 41 | ..onChange((_, __) => onChangeCalls.add("ParentStateA")) 42 | 43 | // Child State 1 44 | ..define(($) => $ 45 | ..onEnter((_) => onEnterCalls.add("AChildState1")) 46 | ..onExit((_) => onExitCalls.add("AChildState1")) 47 | ..onChange((_, __) => onChangeCalls.add("AChildState1")) 48 | ..on((e, s) => AChildState2()) 49 | ..on((e, s) => ParentStateB())) 50 | 51 | // Child State 2 52 | ..define(($) => $ 53 | ..onEnter((_) => onEnterCalls.add("AChildState2")) 54 | ..onExit((_) => onExitCalls.add("AChildState2")) 55 | ..onChange((_, __) => onChangeCalls.add("AChildState2")) 56 | ..on((e, s) => AChildState2())) 57 | 58 | // Child State 3 59 | ..define(($) => $ 60 | ..onEnter((_) => onEnterCalls.add("AChildState3")) 61 | ..onExit((_) => onExitCalls.add("AChildState3")) 62 | ..onChange((_, __) => onChangeCalls.add("AChildState3"))), 63 | ) 64 | ..define(($) => $ 65 | ..onEnter((_) => onEnterCalls.add("ParentStateB")) 66 | ..onExit((_) => onExitCalls.add("ParentStateB")) 67 | ..onChange((_, __) => onChangeCalls.add("ParentStateB"))), 68 | ); 69 | } 70 | 71 | List onEnterCalls = []; 72 | List onExitCalls = []; 73 | List onChangeCalls = []; 74 | } 75 | 76 | void main() { 77 | group("deeply nested state lifecycle test", () { 78 | test( 79 | "nested state onEnter called at initialization id it's the initial state", 80 | () async { 81 | final sm = DummyStateMachine(); 82 | 83 | await wait(); 84 | 85 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1"]); 86 | }); 87 | 88 | test("nested state onEnter called when entering sub state", () async { 89 | final sm = DummyStateMachine(); 90 | 91 | sm.add(TriggerNestedStateOnEnter()); 92 | 93 | await wait(); 94 | 95 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1", "AChildState2"]); 96 | expect(sm.onChangeCalls, ["ParentStateA"]); 97 | expect(sm.onExitCalls, ["AChildState1"]); 98 | }); 99 | 100 | test("nested state onExit called when entering sub state", () async { 101 | final sm = DummyStateMachine(); 102 | 103 | sm.add(TriggerNestedStateOnEnter()); 104 | 105 | await wait(); 106 | 107 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1", "AChildState2"]); 108 | expect(sm.onChangeCalls, ["ParentStateA"]); 109 | expect(sm.onExitCalls, ["AChildState1"]); 110 | }); 111 | 112 | test("nested state onChange called when transiting to same sub state", 113 | () async { 114 | final sm = DummyStateMachine(initialState: AChildState2()); 115 | 116 | sm.add(TriggerNestedStateOnChange()); 117 | 118 | await wait(); 119 | 120 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState2"]); 121 | expect(sm.onChangeCalls, ["ParentStateA", "AChildState2"]); 122 | expect(sm.onExitCalls, []); 123 | }); 124 | 125 | test("parent's onEnter called when transiting to one of its sub states", 126 | () async { 127 | final sm = DummyStateMachine(); 128 | 129 | await wait(); 130 | 131 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1"]); 132 | expect(sm.onChangeCalls, []); 133 | expect(sm.onExitCalls, []); 134 | }); 135 | 136 | test( 137 | "parent's onChange called when transiting to another of its sub states", 138 | () async { 139 | final sm = DummyStateMachine(); 140 | 141 | sm.add(TriggerNestedStateOnEnter()); 142 | 143 | await wait(); 144 | 145 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1", "AChildState2"]); 146 | expect(sm.onChangeCalls, ["ParentStateA"]); 147 | expect(sm.onExitCalls, ["AChildState1"]); 148 | }); 149 | 150 | test( 151 | "parent's onExit called when transiting to a state that is not one of its child", 152 | () async { 153 | final sm = DummyStateMachine(); 154 | 155 | sm.add(TriggerNestedStateOnExit()); 156 | 157 | await wait(); 158 | 159 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1", "ParentStateB"]); 160 | expect(sm.onChangeCalls, []); 161 | expect(sm.onExitCalls, ["ParentStateA", "AChildState1"]); 162 | }); 163 | }); 164 | } 165 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/.packages: -------------------------------------------------------------------------------- 1 | # This file is deprecated. Tools should instead consume 2 | # `.dart_tool/package_config.json`. 3 | # 4 | # For more info see: https://dart.dev/go/dot-packages-deprecation 5 | # 6 | # Generated by pub on 2022-04-10 17:43:19.694816. 7 | _fe_analyzer_shared:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/_fe_analyzer_shared-31.0.0/lib/ 8 | analyzer:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/analyzer-2.8.0/lib/ 9 | args:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/args-2.3.0/lib/ 10 | async:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/async-2.8.2/lib/ 11 | bloc:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/bloc-8.0.3/lib/ 12 | bloc_concurrency:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/bloc_concurrency-0.2.0/lib/ 13 | bloc_test:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/bloc_test-9.0.3/lib/ 14 | boolean_selector:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/boolean_selector-2.1.0/lib/ 15 | characters:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/characters-1.2.0/lib/ 16 | charcode:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/charcode-1.3.1/lib/ 17 | cli_util:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/cli_util-0.3.5/lib/ 18 | clock:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/clock-1.1.0/lib/ 19 | collection:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/collection-1.15.0/lib/ 20 | convert:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/convert-3.0.1/lib/ 21 | coverage:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/coverage-1.0.3/lib/ 22 | crypto:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/crypto-3.0.1/lib/ 23 | diff_match_patch:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/diff_match_patch-0.4.1/lib/ 24 | equatable:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/equatable-2.0.3/lib/ 25 | fake_async:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/fake_async-1.2.0/lib/ 26 | file:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/file-6.1.2/lib/ 27 | flutter:file:///Users/pm/Library/flutter/packages/flutter/lib/ 28 | flutter_bloc:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/flutter_bloc-8.0.1/lib/ 29 | flutter_test:file:///Users/pm/Library/flutter/packages/flutter_test/lib/ 30 | frontend_server_client:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/frontend_server_client-2.1.2/lib/ 31 | glob:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/glob-2.0.2/lib/ 32 | http:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/http-0.13.4/lib/ 33 | http_multi_server:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/http_multi_server-3.2.0/lib/ 34 | http_parser:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/http_parser-4.0.0/lib/ 35 | io:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/io-1.0.3/lib/ 36 | js:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/js-0.6.4/lib/ 37 | logging:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/logging-1.0.2/lib/ 38 | matcher:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/matcher-0.12.11/lib/ 39 | material_color_utilities:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/material_color_utilities-0.1.3/lib/ 40 | meta:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/meta-1.7.0/lib/ 41 | mime:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/mime-1.0.1/lib/ 42 | mocktail:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/mocktail-0.2.0/lib/ 43 | nested:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/nested-1.0.0/lib/ 44 | node_preamble:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/node_preamble-2.0.1/lib/ 45 | package_config:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/package_config-2.0.2/lib/ 46 | path:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/path-1.8.0/lib/ 47 | pool:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/pool-1.5.0/lib/ 48 | provider:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/provider-6.0.2/lib/ 49 | pub_semver:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/pub_semver-2.1.1/lib/ 50 | shelf:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/shelf-1.3.0/lib/ 51 | shelf_packages_handler:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/shelf_packages_handler-3.0.0/lib/ 52 | shelf_static:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/shelf_static-1.1.0/lib/ 53 | shelf_web_socket:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/shelf_web_socket-1.0.1/lib/ 54 | sky_engine:file:///Users/pm/Library/flutter/bin/cache/pkg/sky_engine/lib/ 55 | source_map_stack_trace:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/source_map_stack_trace-2.1.0/lib/ 56 | source_maps:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/source_maps-0.10.10/lib/ 57 | source_span:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/source_span-1.8.1/lib/ 58 | stack_trace:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/stack_trace-1.10.0/lib/ 59 | state_machine_bloc:../../packages/state_machine_bloc/lib/ 60 | stream_channel:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/stream_channel-2.1.0/lib/ 61 | stream_transform:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/stream_transform-2.0.0/lib/ 62 | string_scanner:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/string_scanner-1.1.0/lib/ 63 | term_glyph:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/term_glyph-1.2.0/lib/ 64 | test:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/test-1.19.5/lib/ 65 | test_api:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/test_api-0.4.8/lib/ 66 | test_core:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/test_core-0.4.9/lib/ 67 | typed_data:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/typed_data-1.3.0/lib/ 68 | vector_math:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/vector_math-2.1.1/lib/ 69 | vm_service:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/vm_service-7.5.0/lib/ 70 | watcher:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/watcher-1.0.1/lib/ 71 | web_socket_channel:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/web_socket_channel-2.1.0/lib/ 72 | webkit_inspection_protocol:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/webkit_inspection_protocol-1.0.0/lib/ 73 | yaml:file:///Users/pm/.pub-cache/hosted/pub.dartlang.org/yaml-3.1.0/lib/ 74 | infinite_list_state_machine:lib/ 75 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/test/nested_state_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:state_machine_bloc/state_machine_bloc.dart'; 2 | import 'package:test/test.dart'; 3 | import 'utils.dart'; 4 | 5 | abstract class Event {} 6 | 7 | class TriggerParentTransition extends Event {} 8 | 9 | class TriggerNestedStateOnEnter extends Event {} 10 | 11 | class TriggerNestedStateOnChange extends Event {} 12 | 13 | class TriggerNestedStateOnExit extends Event {} 14 | 15 | abstract class State { 16 | @override 17 | bool operator ==(Object value) => false; 18 | 19 | @override 20 | int get hashCode => 0; 21 | } 22 | 23 | class ParentStateA extends State {} 24 | 25 | class AChildState1 extends ParentStateA {} 26 | 27 | class AChildState2 extends ParentStateA {} 28 | 29 | class AChildState3 extends ParentStateA {} 30 | 31 | class ParentStateB extends State {} 32 | 33 | class DummyStateMachine extends StateMachine { 34 | DummyStateMachine({ 35 | State? initialState, 36 | }) : super(initialState ?? AChildState1()) { 37 | define( 38 | ($) => $ 39 | ..onEnter((_) => onEnterCalls.add("ParentStateA")) 40 | ..onExit((_) => onExitCalls.add("ParentStateA")) 41 | ..onChange((_, __) => onChangeCalls.add("ParentStateA")) 42 | 43 | // Child State 1 44 | ..define(($) => $ 45 | ..onEnter((_) => onEnterCalls.add("AChildState1")) 46 | ..onExit((_) => onExitCalls.add("AChildState1")) 47 | ..onChange((_, __) => onChangeCalls.add("AChildState1")) 48 | ..on((e, s) => AChildState2()) 49 | ..on((e, s) => ParentStateB())) 50 | 51 | // Child State 2 52 | ..define(($) => $ 53 | ..onEnter((_) => onEnterCalls.add("AChildState2")) 54 | ..onExit((_) => onExitCalls.add("AChildState2")) 55 | ..onChange((_, __) => onChangeCalls.add("AChildState2")) 56 | ..on((e, s) => AChildState2())) 57 | 58 | // Child State 3 59 | ..define(($) => $ 60 | ..onEnter((_) => onEnterCalls.add("AChildState3")) 61 | ..onExit((_) => onExitCalls.add("AChildState3")) 62 | ..onChange((_, __) => onChangeCalls.add("AChildState3"))), 63 | ); 64 | 65 | define(($) => $ 66 | ..onEnter((_) => onEnterCalls.add("ParentStateB")) 67 | ..onExit((_) => onExitCalls.add("ParentStateB")) 68 | ..onChange((_, __) => onChangeCalls.add("ParentStateB"))); 69 | } 70 | 71 | List onEnterCalls = []; 72 | List onExitCalls = []; 73 | List onChangeCalls = []; 74 | } 75 | 76 | void main() { 77 | group("nested state lifecycle test", () { 78 | test( 79 | "child states handlers are not called if state machine is in a parent state", 80 | () async { 81 | final sm = DummyStateMachine(initialState: ParentStateA()); 82 | 83 | sm.add(TriggerNestedStateOnEnter()); 84 | await wait(); 85 | 86 | expect(sm.onEnterCalls, ["ParentStateA"]); 87 | expect(sm.onChangeCalls, []); 88 | expect(sm.onExitCalls, []); 89 | }); 90 | 91 | test( 92 | "nested state onEnter called at initialization id it's the initial state", 93 | () async { 94 | final sm = DummyStateMachine(); 95 | 96 | await wait(); 97 | 98 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1"]); 99 | }); 100 | 101 | test("nested state onEnter called when entering sub state", () async { 102 | final sm = DummyStateMachine(); 103 | 104 | sm.add(TriggerNestedStateOnEnter()); 105 | 106 | await wait(); 107 | 108 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1", "AChildState2"]); 109 | expect(sm.onChangeCalls, ["ParentStateA"]); 110 | expect(sm.onExitCalls, ["AChildState1"]); 111 | }); 112 | 113 | test("nested state onExit called when entering sub state", () async { 114 | final sm = DummyStateMachine(); 115 | 116 | sm.add(TriggerNestedStateOnEnter()); 117 | 118 | await wait(); 119 | 120 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1", "AChildState2"]); 121 | expect(sm.onChangeCalls, ["ParentStateA"]); 122 | expect(sm.onExitCalls, ["AChildState1"]); 123 | }); 124 | 125 | test("nested state onChange called when transiting to same sub state", 126 | () async { 127 | final sm = DummyStateMachine(initialState: AChildState2()); 128 | 129 | sm.add(TriggerNestedStateOnChange()); 130 | 131 | await wait(); 132 | 133 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState2"]); 134 | expect(sm.onChangeCalls, ["ParentStateA", "AChildState2"]); 135 | expect(sm.onExitCalls, []); 136 | }); 137 | 138 | test("parent's onEnter called when transiting to one of its sub states", 139 | () async { 140 | final sm = DummyStateMachine(); 141 | 142 | await wait(); 143 | 144 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1"]); 145 | expect(sm.onChangeCalls, []); 146 | expect(sm.onExitCalls, []); 147 | }); 148 | 149 | test( 150 | "parent's onChange called when transiting to another of its sub states", 151 | () async { 152 | final sm = DummyStateMachine(); 153 | 154 | sm.add(TriggerNestedStateOnEnter()); 155 | 156 | await wait(); 157 | 158 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1", "AChildState2"]); 159 | expect(sm.onChangeCalls, ["ParentStateA"]); 160 | expect(sm.onExitCalls, ["AChildState1"]); 161 | }); 162 | 163 | test( 164 | "parent's onExit called when transiting to a state that is not one of its child", 165 | () async { 166 | final sm = DummyStateMachine(); 167 | 168 | sm.add(TriggerNestedStateOnExit()); 169 | 170 | await wait(); 171 | 172 | expect(sm.onEnterCalls, ["ParentStateA", "AChildState1", "ParentStateB"]); 173 | expect(sm.onChangeCalls, []); 174 | expect(sm.onExitCalls, ["ParentStateA", "AChildState1"]); 175 | }); 176 | }); 177 | } 178 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/test/posts/bloc/post_bloc_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:bloc_test/bloc_test.dart'; 2 | import 'package:infinite_list_state_machine/posts/bloc/post_bloc.dart'; 3 | import 'package:infinite_list_state_machine/posts/models/post.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | import 'package:http/http.dart' as http; 6 | import 'package:mocktail/mocktail.dart'; 7 | 8 | class MockClient extends Mock implements http.Client {} 9 | 10 | Uri _postsUrl({required int start}) { 11 | return Uri.https( 12 | 'jsonplaceholder.typicode.com', 13 | '/posts', 14 | {'_start': '$start', '_limit': '20'}, 15 | ); 16 | } 17 | 18 | void main() { 19 | group('PostBloc', () { 20 | const mockPosts = [Post(id: 1, title: 'post title', body: 'post body')]; 21 | const extraMockPosts = [ 22 | Post(id: 2, title: 'post title', body: 'post body') 23 | ]; 24 | 25 | late http.Client httpClient; 26 | 27 | setUpAll(() { 28 | registerFallbackValue(Uri()); 29 | }); 30 | 31 | setUp(() { 32 | httpClient = MockClient(); 33 | }); 34 | 35 | test('initial state is PostInitial()', () { 36 | expect(PostBloc(httpClient: httpClient).state, const PostInitial()); 37 | }); 38 | 39 | group('PostFetched', () { 40 | blocTest( 41 | 'emits nothing when posts has reached maximum amount', 42 | build: () => PostBloc(httpClient: httpClient), 43 | seed: () => const PostEndReached(posts: []), 44 | act: (bloc) => bloc.add(PostFetchRequested()), 45 | expect: () => [], 46 | ); 47 | 48 | blocTest( 49 | 'emits successful status when http fetches initial posts', 50 | setUp: () { 51 | when(() => httpClient.get(any())).thenAnswer((_) async { 52 | return http.Response( 53 | '[{ "id": 1, "title": "post title", "body": "post body" }]', 54 | 200, 55 | ); 56 | }); 57 | }, 58 | build: () => PostBloc(httpClient: httpClient), 59 | act: (bloc) => bloc.add(PostFetchRequested()), 60 | expect: () => const [ 61 | PostFetchInProgress( 62 | posts: [], 63 | ), 64 | PostSuccess( 65 | posts: mockPosts, 66 | ) 67 | ], 68 | verify: (_) { 69 | verify(() => httpClient.get(_postsUrl(start: 0))).called(1); 70 | }, 71 | ); 72 | 73 | blocTest( 74 | 'drops new events when processing current event', 75 | setUp: () { 76 | when(() => httpClient.get(any())).thenAnswer((_) async { 77 | return http.Response( 78 | '[{ "id": 1, "title": "post title", "body": "post body" }]', 79 | 200, 80 | ); 81 | }); 82 | }, 83 | build: () => PostBloc(httpClient: httpClient), 84 | act: (bloc) => bloc 85 | ..add(PostFetchRequested()) 86 | ..add(PostFetchRequested()), 87 | expect: () => const [ 88 | PostFetchInProgress( 89 | posts: [], 90 | ), 91 | PostSuccess( 92 | posts: mockPosts, 93 | ) 94 | ], 95 | verify: (_) { 96 | verify(() => httpClient.get(any())).called(1); 97 | }, 98 | ); 99 | 100 | blocTest( 101 | 'throttles events', 102 | setUp: () { 103 | when(() => httpClient.get(any())).thenAnswer((_) async { 104 | await Future.delayed(Duration.zero); 105 | return http.Response( 106 | '[{ "id": 1, "title": "post title", "body": "post body" }]', 107 | 200, 108 | ); 109 | }); 110 | }, 111 | build: () => PostBloc(httpClient: httpClient), 112 | act: (bloc) async { 113 | bloc.add(PostFetchRequested()); 114 | await Future.delayed(Duration.zero); 115 | bloc.add(PostFetchRequested()); 116 | }, 117 | expect: () => const [ 118 | PostFetchInProgress( 119 | posts: [], 120 | ), 121 | PostSuccess( 122 | posts: mockPosts, 123 | ) 124 | ], 125 | verify: (_) { 126 | verify(() => httpClient.get(any())).called(1); 127 | }, 128 | ); 129 | 130 | blocTest( 131 | 'emits failure status when http fetches posts and throw exception', 132 | setUp: () { 133 | when(() => httpClient.get(any())).thenAnswer( 134 | (_) async => http.Response('', 500), 135 | ); 136 | }, 137 | build: () => PostBloc(httpClient: httpClient), 138 | act: (bloc) => bloc.add(PostFetchRequested()), 139 | expect: () => const [ 140 | PostFetchInProgress( 141 | posts: [], 142 | ), 143 | PostError(posts: [], error: "500"), 144 | ], 145 | verify: (_) { 146 | verify(() => httpClient.get(_postsUrl(start: 0))).called(1); 147 | }, 148 | ); 149 | 150 | blocTest( 151 | 'emits successful status and reaches max posts when ' 152 | '0 additional posts are fetched', 153 | setUp: () { 154 | when(() => httpClient.get(any())).thenAnswer( 155 | (_) async => http.Response('[]', 200), 156 | ); 157 | }, 158 | build: () => PostBloc(httpClient: httpClient), 159 | seed: () => const PostSuccess( 160 | posts: mockPosts, 161 | ), 162 | act: (bloc) => bloc.add(PostFetchRequested()), 163 | expect: () => const [ 164 | PostFetchInProgress( 165 | posts: mockPosts, 166 | ), 167 | PostEndReached( 168 | posts: mockPosts, 169 | ) 170 | ], 171 | verify: (_) { 172 | verify(() => httpClient.get(_postsUrl(start: 1))).called(1); 173 | }, 174 | ); 175 | 176 | blocTest( 177 | 'emits successful status and does not reach max posts' 178 | 'when additional posts are fetched', 179 | setUp: () { 180 | when(() => httpClient.get(any())).thenAnswer((_) async { 181 | return http.Response( 182 | '[{ "id": 2, "title": "post title", "body": "post body" }]', 183 | 200, 184 | ); 185 | }); 186 | }, 187 | build: () => PostBloc(httpClient: httpClient), 188 | seed: () => const PostSuccess( 189 | posts: mockPosts, 190 | ), 191 | act: (bloc) => bloc.add(PostFetchRequested()), 192 | expect: () => const [ 193 | PostFetchInProgress( 194 | posts: mockPosts, 195 | ), 196 | PostSuccess( 197 | posts: [...mockPosts, ...extraMockPosts], 198 | ) 199 | ], 200 | verify: (_) { 201 | verify(() => httpClient.get(_postsUrl(start: 1))).called(1); 202 | }, 203 | ); 204 | }); 205 | }); 206 | } 207 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/lib/src/state_definition_builder.dart: -------------------------------------------------------------------------------- 1 | part of 'state_machine.dart'; 2 | 3 | /// Object used to describe state machine's states 4 | /// 5 | /// [StateDefinitionBuilder] provides methods to register defined state's event handlers. 6 | /// and side effects as well nested states. 7 | /// * [on] method is used to register event handlers. 8 | /// * [onEnter], [onChange], [onExit] methods are used to register side effects. 9 | /// * [define] method is used to defined nested state. 10 | /// 11 | /// ```dart 12 | /// define((StateDefinitionBuilder b) => b 13 | /// ..onEnter(...) 14 | /// ..onChange(...) 15 | /// ..onExit(...) 16 | /// ..on(...) 17 | /// ..define(...) 18 | /// ); 19 | /// ``` 20 | class StateDefinitionBuilder { 21 | final List _definedStates = []; 22 | final List<_StateEventHandler> _handlers = []; 23 | final List<_StateDefinition> _nestedStateDefinitions = []; 24 | SideEffect? _onEnter; 25 | SideEffect? _onExit; 26 | OnChangeSideEffect? _onChange; 27 | 28 | /// Register [onEnterCallback] function as onEnter side effect for [DefinedState] 29 | /// 30 | /// [onEnter] should only be called once per unique state. 31 | /// 32 | /// [onEnterCallback] will be called when the state machine enters 33 | /// [DefinedState] or one of its nested states **for the first** time. 34 | /// It will not be called if the previous state is [DefinedState] or one of its 35 | /// nested states. 36 | /// 37 | /// [onEnterCallback] callback could be async but **will not** be awaited. 38 | /// 39 | /// ```dart 40 | /// define(($) => $ 41 | /// ..onEnter((ParentState state) { 42 | /// // Custom code called when entering the state 43 | /// // Generally a good place to start async computation 44 | /// }) 45 | /// ); 46 | /// ``` 47 | void onEnter(SideEffect onEnterCallback) { 48 | assert(() { 49 | if (_onEnter != null) { 50 | throw StateError( 51 | 'onEnter was called multiple times.' 52 | 'There should only be a single onEnter side effect registered per state.', 53 | ); 54 | } 55 | return true; 56 | }()); 57 | _onEnter = onEnterCallback; 58 | } 59 | 60 | /// Register [onExitCallback] function as onExit side effect for [DefinedState] 61 | /// 62 | /// [onExit] should only be called once per unique state. 63 | /// 64 | /// [onExitCallback] will be called when the state machine exit [DefinedState]. 65 | /// It will not be called if the next state is one of [DefinedState]'s nested states. 66 | /// 67 | /// [onExitCallback] callback could be async but **will not** be awaited. 68 | /// 69 | /// ```dart 70 | /// define(($) => $ 71 | /// ..onExit((ParentState state) { 72 | /// // Custom code called when exiting the state 73 | /// }) 74 | /// ); 75 | /// ``` 76 | void onExit(SideEffect onExitCallback) { 77 | assert(() { 78 | if (_onExit != null) { 79 | throw StateError( 80 | 'onExit was called multiple times.' 81 | 'There should only be a single onExit side effect registered per state.', 82 | ); 83 | } 84 | return true; 85 | }()); 86 | _onExit = onExitCallback; 87 | } 88 | 89 | /// Register [onChangeCallback] function as onChange side effect for [DefinedState]. 90 | /// 91 | /// [onChange] should only be called once per unique state. 92 | /// 93 | /// [onChangeCallback] will be called when the state machine enter 94 | /// [DefinedState] or one of its nested states **and** previous state **was** 95 | /// [DefinedState] or one of its nested states. 96 | /// 97 | /// [onChangeCallback] It **will not** be called the first time state machine 98 | /// enter [DefinedState] or one of its nested states. 99 | /// 100 | /// 🚨 State machine discard any state update where `currentState == nextState`, 101 | /// so make sure you've implemented [operator==] for your state class, 102 | /// otherwise, onChange will not be called. 103 | /// 104 | /// [onChangeCallback] callback could be async but **will not** be awaited. 105 | /// 106 | /// ```dart 107 | /// define(($) => $ 108 | /// ..onChange((ParentState currentState, ParentState nextState) { 109 | /// // Custom code called when this state changed 110 | /// }) 111 | /// ); 112 | /// ``` 113 | void onChange(OnChangeSideEffect onChangeCallback) { 114 | assert(() { 115 | if (_onChange != null) { 116 | throw StateError( 117 | 'onChange was called multiple times.' 118 | 'There should only be a single side effect onChange effect per state.', 119 | ); 120 | } 121 | return true; 122 | }()); 123 | _onChange = onChangeCallback; 124 | } 125 | 126 | /// Register [transition] function as one of [DefinedState]'s event handler 127 | /// for [DefinedEvent] 128 | /// 129 | /// - If [transition] return a [State], the state machine stops any further 130 | /// event handlers evaluation and transition to the returned state. 131 | /// - If [transition] returns null, the state machine will continue other event 132 | /// handlers' evaluation. 133 | /// 134 | /// You can have multiple event handlers registered for the same [DefinedEvent]. 135 | /// When an event is received, every handler that matches the event will be evaluated 136 | /// **in their definition order**. Parent states' event handlers are always 137 | /// evaluated before nested states handlers so if a parent transit to a new 138 | /// state, children handlers will not be evaluated. 139 | /// 140 | /// ```dart 141 | /// define(($) => $ 142 | /// ..on((DefinedEvent event, ParentState state) { 143 | /// // return next state or null 144 | /// }) 145 | /// ..on((DefinedEvent event, ParentState state) { 146 | /// // multiple handlers for a give event type 147 | /// return NextState(); 148 | /// }) 149 | /// ..on((OtherEvent event, ParentState state) { 150 | /// // use event and current state's data to assemble next state 151 | /// return NextState(event.data + state.data); 152 | /// }) 153 | /// ); 154 | /// ``` 155 | void on( 156 | EventTransition transition, 157 | ) => 158 | _handlers.add( 159 | _StateEventHandler( 160 | transition: transition, 161 | isType: (dynamic e) => e is DefinedEvent, 162 | type: DefinedEvent, 163 | ), 164 | ); 165 | 166 | /// Register [NestedState] as one of [DefinedState]'s nested states. 167 | /// 168 | /// Works the same way as top-level define calls. 169 | /// The [definitionBuilder] function takes an [StateDefinitionBuilder] object as a parameter 170 | /// and should return it. [StateDefinitionBuilder] is used to register event 171 | /// handlers and side effects for the defined [NestedState]. 172 | /// 173 | /// Nested states should always be sub-classes of their parent state. 174 | /// 175 | /// To enter a nested state, you should explicitly transition to it. 176 | /// The state machine will not consider that you've entered the child state 177 | /// when you transit to one of its parents. 178 | /// The inverse is not true the state machine considers entering a parent 179 | /// state if you transition to one of its nested states. 180 | /// 181 | /// There is no depth limit in state nesting but you should never define a 182 | /// state more than ones. 183 | /// 184 | /// Nested state's event handlers and side effects are always evaluated after 185 | /// parent's ones. 186 | /// 187 | /// ```dart 188 | /// define(($) => $ 189 | /// ..define() 190 | /// ..define(($) => $ 191 | /// ..onEnter(...) 192 | /// ..onChange(...) 193 | /// ..onExit(...) 194 | /// ..on(...) 195 | /// ..define(($) => $ 196 | /// ... 197 | /// ) // Child2 198 | /// ) // Child1 199 | /// ); 200 | /// ``` 201 | /// 202 | /// See also: 203 | /// 204 | /// * [StateDefinitionBuilder] for more information about defining states. 205 | void define([ 206 | StateDefinitionBuilder Function( 207 | StateDefinitionBuilder)? 208 | definitionBuilder, 209 | ]) { 210 | late _StateDefinition definition; 211 | if (definitionBuilder != null) { 212 | definition = definitionBuilder 213 | .call(StateDefinitionBuilder()) 214 | ._build(); 215 | } else { 216 | definition = _StateDefinition.empty(); 217 | } 218 | 219 | assert(() { 220 | if (_definedStates.contains(NestedState)) { 221 | throw "$NestedState has been defined multiple times. States should only be defined once."; 222 | } 223 | _definedStates.add(NestedState); 224 | return true; 225 | }()); 226 | 227 | _nestedStateDefinitions.add(definition); 228 | } 229 | 230 | _StateDefinition _build() => _StateDefinition( 231 | handlers: _handlers, 232 | nestedStatesDefinitions: 233 | _nestedStateDefinitions.isNotEmpty ? _nestedStateDefinitions : null, 234 | onEnter: _onEnter, 235 | onExit: _onExit, 236 | onChange: _onChange, 237 | ); 238 | } 239 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/lib/src/state_machine.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bloc/bloc.dart'; 4 | import 'package:bloc_concurrency/bloc_concurrency.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | part 'state_definition.dart'; 8 | part 'state_definition_builder.dart'; 9 | 10 | /// {@template state_machine} 11 | /// A Bloc that provides facilities methods to create state machines 12 | /// 13 | /// The state machine uses `Bloc`'s `on` method under the hood with a 14 | /// custom event dispatcher that will in turn call your methods and callbacks. 15 | /// 16 | /// State machine's states should be defined with the 17 | /// `StateMachine`'s `define` methods inside the constructor. You should 18 | /// never try to transit to a state that hasn't been explicitly defined. 19 | /// If the state machine detects a transition to an undefined state, 20 | /// it will throw an error. 21 | /// 22 | /// Each state has its own set of event handlers and side effects callbacks: 23 | /// * **Event handlers** react to an incoming event and can emit the next 24 | /// machine's state. We call this a _transition_. 25 | /// * **Side effects** are callback functions called depending on state 26 | /// lifecycle. You have access to three different side effects: `onEnter`, `onExit`, and `onChange`. 27 | /// 28 | /// When an event is received, the state machine will first search 29 | /// for the actual state definition. Each current state's event handler 30 | /// that matches the received event type will be evaluated. 31 | /// If multiple events handlers match the event type, they will be evaluated 32 | /// in their **definition order**. As soon as an event handler returns 33 | /// a non-null state (we call this _entering a transition_), the state 34 | /// machine stops evaluating events handlers and transit to the new 35 | /// state immediately. 36 | /// 37 | /// ```dart 38 | /// class MyStateMachine extends StateMachine { 39 | /// MyStateMachine() : super(InitialState()) { 40 | /// define(($) => $ 41 | /// ..onEnter((InitialState state) { /** ... **/ }) 42 | /// ..onChange((InitialState state, InitialState nextState) { /** ... **/ }) 43 | /// ..onExit((InitialState state) { /** ... **/ }) 44 | /// ..on((SomeEvent event, InitialState state) => OtherState()) 45 | /// ); 46 | /// define(); 47 | /// } 48 | /// } 49 | /// ``` 50 | /// 51 | /// See also: 52 | /// 53 | /// * [Bloc] class for more information about general blocs behavior 54 | /// {@endtemplate} 55 | abstract class StateMachine extends Bloc { 56 | /// {@macro state_machine} 57 | StateMachine( 58 | State initial, { 59 | /// Used to change how the state machine process incoming events. 60 | /// The default event transformer is [droppable] by default, meaning it 61 | /// processes only one event and ignores (drop) any new events until the 62 | /// next event-loop iteration. 63 | EventTransformer? transformer, 64 | }) : super(initial) { 65 | super.on(_mapEventToState, transformer: transformer ?? droppable()); 66 | } 67 | 68 | final List _definedStates = []; 69 | final List<_StateDefinition> _stateDefinitions = []; 70 | bool _closed = false; 71 | 72 | /// Register [DefinedState] as one of the allowed machine's states. 73 | /// 74 | /// The define method should be called once for the allowed state 75 | /// **inside the class constructor**. Defined states should 76 | /// always be sub-classes of the [State] class. 77 | /// 78 | /// The define method takes an optional [definitionBuilder] function as 79 | /// a parameter that gives the opportunity to register events handler and 80 | /// transitions for the [DefinedState] thanks to a [StateDefinitionBuilder] 81 | /// passed as a parameter to the builder function. 82 | /// The [StateDefinitionBuilder] provides all necessary methods to register 83 | /// event handlers, side effects, and nested states. The [definitionBuilder] 84 | /// should call needed [StateDefinitionBuilder]'s object methods to describe 85 | /// the [DefinedState] and then return it. 86 | /// 87 | /// ```dart 88 | /// class MyStateMachine extends StateMachine { 89 | /// MyStateMachine() : super(InitialState()) { 90 | /// define(($) => $ 91 | /// ..onEnter((InitialState state) { /** ... **/ }) 92 | /// ..onChange((InitialState state, InitialState nextState) { /** ... **/ }) 93 | /// ..onExit((InitialState state) { /** ... **/ }) 94 | /// ..on((SomeEvent event, InitialState state) => OtherState()) 95 | /// ); 96 | /// define(); 97 | /// } 98 | /// } 99 | /// ``` 100 | /// 101 | /// See also: 102 | /// 103 | /// * [StateDefinitionBuilder] for more information about defining states. 104 | void define([ 105 | StateDefinitionBuilder Function( 106 | StateDefinitionBuilder, 107 | )? definitionBuilder, 108 | ]) { 109 | late final _StateDefinition definition; 110 | if (definitionBuilder != null) { 111 | definition = definitionBuilder 112 | .call(StateDefinitionBuilder()) 113 | ._build(); 114 | } else { 115 | definition = _StateDefinition.empty(); 116 | } 117 | 118 | assert(() { 119 | if (_definedStates.contains(DefinedState)) { 120 | throw "$DefinedState has been defined multiple times. States should only be defined once."; 121 | } 122 | _definedStates.add(DefinedState); 123 | return true; 124 | }()); 125 | 126 | _stateDefinitions.add(definition); 127 | 128 | if (state is DefinedState) { 129 | definition.onEnter(state); 130 | } 131 | } 132 | 133 | /// [on] function should never be used inside [StateMachine]. 134 | /// Use [define] method instead. 135 | @nonVirtual 136 | @protected 137 | @override 138 | void on( 139 | EventHandler handler, { 140 | EventTransformer? transformer, 141 | }) { 142 | throw "Invalid use of StateMachine.on(). You should use StateMachine.define() instead."; 143 | } 144 | 145 | void _mapEventToState(Event event, Emitter emit) async { 146 | final definition = _stateDefinitions.firstWhere((def) => def.isType(state)); 147 | 148 | final nextState = (await definition.add(event, state)) as State?; 149 | if (nextState != null) { 150 | emit(nextState); 151 | } 152 | } 153 | 154 | /// Notifies the [Bloc] of a new [event] which triggers 155 | /// all corresponding [EventHandler] instances. 156 | /// 157 | /// * A [StateError] will be thrown if there is no event handler 158 | /// registered for the incoming [event]. 159 | /// 160 | /// * A [StateError] will be thrown if the bloc is closed and the 161 | /// [event] will not be processed. 162 | @override 163 | @mustCallSuper 164 | void add(Event event) { 165 | if (_closed) { 166 | // Since StateMachine handles onEnter/onExit/onChange methods 167 | // in the [Bloc.onChange] handler, these side effects cannot be awaited. 168 | // Due to this, the bloc has no understanding of when these effects may be 169 | // completed, and they aren't treated like normal event handlers 170 | // that are run to completion before a bloc is closed. 171 | // 172 | // In this case, the bloc will be closed for adding events, but [isClosed] 173 | // will be false, because the bloc state controller is not yet closed. 174 | // Overriding add here allows us to ignore events from side effects if the 175 | // bloc has been closed. 176 | return; 177 | } 178 | super.add(event); 179 | } 180 | 181 | /// Called whenever a [change] occurs with the given [change]. 182 | /// A [change] occurs when a new `state` is emitted. 183 | /// [onChange] is called before the `state` of the `cubit` is updated. 184 | /// [onChange] is a great spot to add logging/analytics for a specific `cubit`. 185 | /// 186 | /// **Note: `super.onChange` should always be called first.** 187 | /// ```dart 188 | /// @override 189 | /// void onChange(Change change) { 190 | /// // Always call super.onChange with the current change 191 | /// super.onChange(change); 192 | /// 193 | /// // Custom onChange logic goes here 194 | /// } 195 | /// ``` 196 | /// 197 | /// See also: 198 | /// 199 | /// * [BlocObserver] for observing [Cubit] behavior globally. 200 | @protected 201 | @mustCallSuper 202 | @override 203 | void onChange(Change change) { 204 | super.onChange(change); 205 | final currentDefinition = _definition(change.currentState); 206 | final nextDefinition = _definition(change.nextState); 207 | if (currentDefinition == nextDefinition) { 208 | currentDefinition.onChange(change.currentState, change.nextState); 209 | } else { 210 | currentDefinition.onExit(change.currentState); 211 | nextDefinition.onEnter(change.nextState); 212 | } 213 | } 214 | 215 | /// Closes the `event` and `state` `Streams`. 216 | /// This method should be called when a [Bloc] is no longer needed. 217 | /// Once [close] is called, `events` that are [add]ed will not be 218 | /// processed. 219 | /// In addition, if [close] is called while `events` are still being 220 | /// processed, the [Bloc] will finish processing the pending `events`. 221 | @mustCallSuper 222 | @override 223 | Future close() async { 224 | _closed = true; 225 | super.close(); 226 | } 227 | 228 | _StateDefinition _definition(State state) => 229 | _stateDefinitions.firstWhere((def) => def.isType(state)); 230 | } 231 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/.dart_tool/package_config_subset: -------------------------------------------------------------------------------- 1 | _fe_analyzer_shared 2 | 3.0 3 | file:///Users/pm/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-67.0.0/ 4 | file:///Users/pm/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-67.0.0/lib/ 5 | analyzer 6 | 3.0 7 | file:///Users/pm/.pub-cache/hosted/pub.dev/analyzer-6.4.1/ 8 | file:///Users/pm/.pub-cache/hosted/pub.dev/analyzer-6.4.1/lib/ 9 | args 10 | 2.12 11 | file:///Users/pm/.pub-cache/hosted/pub.dev/args-2.3.0/ 12 | file:///Users/pm/.pub-cache/hosted/pub.dev/args-2.3.0/lib/ 13 | async 14 | 2.18 15 | file:///Users/pm/.pub-cache/hosted/pub.dev/async-2.11.0/ 16 | file:///Users/pm/.pub-cache/hosted/pub.dev/async-2.11.0/lib/ 17 | bloc 18 | 2.12 19 | file:///Users/pm/.pub-cache/hosted/pub.dev/bloc-8.0.3/ 20 | file:///Users/pm/.pub-cache/hosted/pub.dev/bloc-8.0.3/lib/ 21 | bloc_concurrency 22 | 2.12 23 | file:///Users/pm/.pub-cache/hosted/pub.dev/bloc_concurrency-0.2.0/ 24 | file:///Users/pm/.pub-cache/hosted/pub.dev/bloc_concurrency-0.2.0/lib/ 25 | bloc_test 26 | 2.12 27 | file:///Users/pm/.pub-cache/hosted/pub.dev/bloc_test-9.0.3/ 28 | file:///Users/pm/.pub-cache/hosted/pub.dev/bloc_test-9.0.3/lib/ 29 | boolean_selector 30 | 2.17 31 | file:///Users/pm/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1/ 32 | file:///Users/pm/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1/lib/ 33 | characters 34 | 2.12 35 | file:///Users/pm/.pub-cache/hosted/pub.dev/characters-1.3.0/ 36 | file:///Users/pm/.pub-cache/hosted/pub.dev/characters-1.3.0/lib/ 37 | charcode 38 | 2.12 39 | file:///Users/pm/.pub-cache/hosted/pub.dev/charcode-1.3.1/ 40 | file:///Users/pm/.pub-cache/hosted/pub.dev/charcode-1.3.1/lib/ 41 | clock 42 | 2.12 43 | file:///Users/pm/.pub-cache/hosted/pub.dev/clock-1.1.1/ 44 | file:///Users/pm/.pub-cache/hosted/pub.dev/clock-1.1.1/lib/ 45 | collection 46 | 2.18 47 | file:///Users/pm/.pub-cache/hosted/pub.dev/collection-1.18.0/ 48 | file:///Users/pm/.pub-cache/hosted/pub.dev/collection-1.18.0/lib/ 49 | convert 50 | 2.12 51 | file:///Users/pm/.pub-cache/hosted/pub.dev/convert-3.0.1/ 52 | file:///Users/pm/.pub-cache/hosted/pub.dev/convert-3.0.1/lib/ 53 | coverage 54 | 3.0 55 | file:///Users/pm/.pub-cache/hosted/pub.dev/coverage-1.8.0/ 56 | file:///Users/pm/.pub-cache/hosted/pub.dev/coverage-1.8.0/lib/ 57 | crypto 58 | 2.12 59 | file:///Users/pm/.pub-cache/hosted/pub.dev/crypto-3.0.1/ 60 | file:///Users/pm/.pub-cache/hosted/pub.dev/crypto-3.0.1/lib/ 61 | diff_match_patch 62 | 2.12 63 | file:///Users/pm/.pub-cache/hosted/pub.dev/diff_match_patch-0.4.1/ 64 | file:///Users/pm/.pub-cache/hosted/pub.dev/diff_match_patch-0.4.1/lib/ 65 | equatable 66 | 2.12 67 | file:///Users/pm/.pub-cache/hosted/pub.dev/equatable-2.0.3/ 68 | file:///Users/pm/.pub-cache/hosted/pub.dev/equatable-2.0.3/lib/ 69 | fake_async 70 | 2.12 71 | file:///Users/pm/.pub-cache/hosted/pub.dev/fake_async-1.3.1/ 72 | file:///Users/pm/.pub-cache/hosted/pub.dev/fake_async-1.3.1/lib/ 73 | file 74 | 2.12 75 | file:///Users/pm/.pub-cache/hosted/pub.dev/file-6.1.2/ 76 | file:///Users/pm/.pub-cache/hosted/pub.dev/file-6.1.2/lib/ 77 | flutter_bloc 78 | 2.12 79 | file:///Users/pm/.pub-cache/hosted/pub.dev/flutter_bloc-8.0.1/ 80 | file:///Users/pm/.pub-cache/hosted/pub.dev/flutter_bloc-8.0.1/lib/ 81 | frontend_server_client 82 | 3.0 83 | file:///Users/pm/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0/ 84 | file:///Users/pm/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0/lib/ 85 | glob 86 | 2.14 87 | file:///Users/pm/.pub-cache/hosted/pub.dev/glob-2.0.2/ 88 | file:///Users/pm/.pub-cache/hosted/pub.dev/glob-2.0.2/lib/ 89 | http 90 | 2.14 91 | file:///Users/pm/.pub-cache/hosted/pub.dev/http-0.13.4/ 92 | file:///Users/pm/.pub-cache/hosted/pub.dev/http-0.13.4/lib/ 93 | http_multi_server 94 | 2.12 95 | file:///Users/pm/.pub-cache/hosted/pub.dev/http_multi_server-3.2.0/ 96 | file:///Users/pm/.pub-cache/hosted/pub.dev/http_multi_server-3.2.0/lib/ 97 | http_parser 98 | 2.12 99 | file:///Users/pm/.pub-cache/hosted/pub.dev/http_parser-4.0.0/ 100 | file:///Users/pm/.pub-cache/hosted/pub.dev/http_parser-4.0.0/lib/ 101 | io 102 | 2.12 103 | file:///Users/pm/.pub-cache/hosted/pub.dev/io-1.0.3/ 104 | file:///Users/pm/.pub-cache/hosted/pub.dev/io-1.0.3/lib/ 105 | js 106 | 2.16 107 | file:///Users/pm/.pub-cache/hosted/pub.dev/js-0.6.4/ 108 | file:///Users/pm/.pub-cache/hosted/pub.dev/js-0.6.4/lib/ 109 | leak_tracker 110 | 3.2 111 | file:///Users/pm/.pub-cache/hosted/pub.dev/leak_tracker-10.0.5/ 112 | file:///Users/pm/.pub-cache/hosted/pub.dev/leak_tracker-10.0.5/lib/ 113 | leak_tracker_flutter_testing 114 | 3.2 115 | file:///Users/pm/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.5/ 116 | file:///Users/pm/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.5/lib/ 117 | leak_tracker_testing 118 | 3.2 119 | file:///Users/pm/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1/ 120 | file:///Users/pm/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1/lib/ 121 | logging 122 | 2.12 123 | file:///Users/pm/.pub-cache/hosted/pub.dev/logging-1.0.2/ 124 | file:///Users/pm/.pub-cache/hosted/pub.dev/logging-1.0.2/lib/ 125 | matcher 126 | 3.0 127 | file:///Users/pm/.pub-cache/hosted/pub.dev/matcher-0.12.16+1/ 128 | file:///Users/pm/.pub-cache/hosted/pub.dev/matcher-0.12.16+1/lib/ 129 | material_color_utilities 130 | 2.17 131 | file:///Users/pm/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1/ 132 | file:///Users/pm/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1/lib/ 133 | meta 134 | 2.12 135 | file:///Users/pm/.pub-cache/hosted/pub.dev/meta-1.15.0/ 136 | file:///Users/pm/.pub-cache/hosted/pub.dev/meta-1.15.0/lib/ 137 | mime 138 | 2.12 139 | file:///Users/pm/.pub-cache/hosted/pub.dev/mime-1.0.1/ 140 | file:///Users/pm/.pub-cache/hosted/pub.dev/mime-1.0.1/lib/ 141 | mocktail 142 | 2.12 143 | file:///Users/pm/.pub-cache/hosted/pub.dev/mocktail-0.2.0/ 144 | file:///Users/pm/.pub-cache/hosted/pub.dev/mocktail-0.2.0/lib/ 145 | nested 146 | 2.12 147 | file:///Users/pm/.pub-cache/hosted/pub.dev/nested-1.0.0/ 148 | file:///Users/pm/.pub-cache/hosted/pub.dev/nested-1.0.0/lib/ 149 | node_preamble 150 | 2.12 151 | file:///Users/pm/.pub-cache/hosted/pub.dev/node_preamble-2.0.1/ 152 | file:///Users/pm/.pub-cache/hosted/pub.dev/node_preamble-2.0.1/lib/ 153 | package_config 154 | 2.12 155 | file:///Users/pm/.pub-cache/hosted/pub.dev/package_config-2.0.2/ 156 | file:///Users/pm/.pub-cache/hosted/pub.dev/package_config-2.0.2/lib/ 157 | path 158 | 3.0 159 | file:///Users/pm/.pub-cache/hosted/pub.dev/path-1.9.0/ 160 | file:///Users/pm/.pub-cache/hosted/pub.dev/path-1.9.0/lib/ 161 | pool 162 | 2.12 163 | file:///Users/pm/.pub-cache/hosted/pub.dev/pool-1.5.0/ 164 | file:///Users/pm/.pub-cache/hosted/pub.dev/pool-1.5.0/lib/ 165 | provider 166 | 2.12 167 | file:///Users/pm/.pub-cache/hosted/pub.dev/provider-6.0.2/ 168 | file:///Users/pm/.pub-cache/hosted/pub.dev/provider-6.0.2/lib/ 169 | pub_semver 170 | 2.17 171 | file:///Users/pm/.pub-cache/hosted/pub.dev/pub_semver-2.1.4/ 172 | file:///Users/pm/.pub-cache/hosted/pub.dev/pub_semver-2.1.4/lib/ 173 | shelf 174 | 2.16 175 | file:///Users/pm/.pub-cache/hosted/pub.dev/shelf-1.3.0/ 176 | file:///Users/pm/.pub-cache/hosted/pub.dev/shelf-1.3.0/lib/ 177 | shelf_packages_handler 178 | 2.12 179 | file:///Users/pm/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.0/ 180 | file:///Users/pm/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.0/lib/ 181 | shelf_static 182 | 2.12 183 | file:///Users/pm/.pub-cache/hosted/pub.dev/shelf_static-1.1.0/ 184 | file:///Users/pm/.pub-cache/hosted/pub.dev/shelf_static-1.1.0/lib/ 185 | shelf_web_socket 186 | 2.12 187 | file:///Users/pm/.pub-cache/hosted/pub.dev/shelf_web_socket-1.0.1/ 188 | file:///Users/pm/.pub-cache/hosted/pub.dev/shelf_web_socket-1.0.1/lib/ 189 | source_map_stack_trace 190 | 2.12 191 | file:///Users/pm/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.0/ 192 | file:///Users/pm/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.0/lib/ 193 | source_maps 194 | 2.12 195 | file:///Users/pm/.pub-cache/hosted/pub.dev/source_maps-0.10.10/ 196 | file:///Users/pm/.pub-cache/hosted/pub.dev/source_maps-0.10.10/lib/ 197 | source_span 198 | 2.18 199 | file:///Users/pm/.pub-cache/hosted/pub.dev/source_span-1.10.0/ 200 | file:///Users/pm/.pub-cache/hosted/pub.dev/source_span-1.10.0/lib/ 201 | stack_trace 202 | 2.18 203 | file:///Users/pm/.pub-cache/hosted/pub.dev/stack_trace-1.11.1/ 204 | file:///Users/pm/.pub-cache/hosted/pub.dev/stack_trace-1.11.1/lib/ 205 | stream_channel 206 | 2.19 207 | file:///Users/pm/.pub-cache/hosted/pub.dev/stream_channel-2.1.2/ 208 | file:///Users/pm/.pub-cache/hosted/pub.dev/stream_channel-2.1.2/lib/ 209 | stream_transform 210 | 2.12 211 | file:///Users/pm/.pub-cache/hosted/pub.dev/stream_transform-2.0.0/ 212 | file:///Users/pm/.pub-cache/hosted/pub.dev/stream_transform-2.0.0/lib/ 213 | string_scanner 214 | 2.18 215 | file:///Users/pm/.pub-cache/hosted/pub.dev/string_scanner-1.2.0/ 216 | file:///Users/pm/.pub-cache/hosted/pub.dev/string_scanner-1.2.0/lib/ 217 | term_glyph 218 | 2.12 219 | file:///Users/pm/.pub-cache/hosted/pub.dev/term_glyph-1.2.1/ 220 | file:///Users/pm/.pub-cache/hosted/pub.dev/term_glyph-1.2.1/lib/ 221 | test 222 | 3.2 223 | file:///Users/pm/.pub-cache/hosted/pub.dev/test-1.25.7/ 224 | file:///Users/pm/.pub-cache/hosted/pub.dev/test-1.25.7/lib/ 225 | test_api 226 | 3.2 227 | file:///Users/pm/.pub-cache/hosted/pub.dev/test_api-0.7.2/ 228 | file:///Users/pm/.pub-cache/hosted/pub.dev/test_api-0.7.2/lib/ 229 | test_core 230 | 3.2 231 | file:///Users/pm/.pub-cache/hosted/pub.dev/test_core-0.6.4/ 232 | file:///Users/pm/.pub-cache/hosted/pub.dev/test_core-0.6.4/lib/ 233 | typed_data 234 | 2.12 235 | file:///Users/pm/.pub-cache/hosted/pub.dev/typed_data-1.3.0/ 236 | file:///Users/pm/.pub-cache/hosted/pub.dev/typed_data-1.3.0/lib/ 237 | vector_math 238 | 2.14 239 | file:///Users/pm/.pub-cache/hosted/pub.dev/vector_math-2.1.4/ 240 | file:///Users/pm/.pub-cache/hosted/pub.dev/vector_math-2.1.4/lib/ 241 | vm_service 242 | 3.3 243 | file:///Users/pm/.pub-cache/hosted/pub.dev/vm_service-14.2.5/ 244 | file:///Users/pm/.pub-cache/hosted/pub.dev/vm_service-14.2.5/lib/ 245 | watcher 246 | 3.0 247 | file:///Users/pm/.pub-cache/hosted/pub.dev/watcher-1.1.0/ 248 | file:///Users/pm/.pub-cache/hosted/pub.dev/watcher-1.1.0/lib/ 249 | web_socket_channel 250 | 2.12 251 | file:///Users/pm/.pub-cache/hosted/pub.dev/web_socket_channel-2.1.0/ 252 | file:///Users/pm/.pub-cache/hosted/pub.dev/web_socket_channel-2.1.0/lib/ 253 | webkit_inspection_protocol 254 | 2.12 255 | file:///Users/pm/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.0.0/ 256 | file:///Users/pm/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.0.0/lib/ 257 | yaml 258 | 2.12 259 | file:///Users/pm/.pub-cache/hosted/pub.dev/yaml-3.1.0/ 260 | file:///Users/pm/.pub-cache/hosted/pub.dev/yaml-3.1.0/lib/ 261 | sky_engine 262 | 3.2 263 | file:///Users/pm/Dev/flutter/bin/cache/pkg/sky_engine/ 264 | file:///Users/pm/Dev/flutter/bin/cache/pkg/sky_engine/lib/ 265 | flutter 266 | 3.3 267 | file:///Users/pm/Dev/flutter/packages/flutter/ 268 | file:///Users/pm/Dev/flutter/packages/flutter/lib/ 269 | flutter_test 270 | 3.3 271 | file:///Users/pm/Dev/flutter/packages/flutter_test/ 272 | file:///Users/pm/Dev/flutter/packages/flutter_test/lib/ 273 | infinite_list_state_machine 274 | 3.0 275 | file:///Users/pm/Project/state_machine_bloc/examples/infinite_list_state_machine/ 276 | file:///Users/pm/Project/state_machine_bloc/examples/infinite_list_state_machine/lib/ 277 | state_machine_bloc 278 | 2.16 279 | file:///Users/pm/Project/state_machine_bloc/packages/state_machine_bloc/ 280 | file:///Users/pm/Project/state_machine_bloc/packages/state_machine_bloc/lib/ 281 | 2 282 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/.dart_tool/package_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "configVersion": 2, 3 | "packages": [ 4 | { 5 | "name": "_fe_analyzer_shared", 6 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/_fe_analyzer_shared-67.0.0", 7 | "packageUri": "lib/", 8 | "languageVersion": "3.0" 9 | }, 10 | { 11 | "name": "analyzer", 12 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/analyzer-6.4.1", 13 | "packageUri": "lib/", 14 | "languageVersion": "3.0" 15 | }, 16 | { 17 | "name": "args", 18 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/args-2.3.0", 19 | "packageUri": "lib/", 20 | "languageVersion": "2.12" 21 | }, 22 | { 23 | "name": "async", 24 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/async-2.11.0", 25 | "packageUri": "lib/", 26 | "languageVersion": "2.18" 27 | }, 28 | { 29 | "name": "bloc", 30 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/bloc-8.0.3", 31 | "packageUri": "lib/", 32 | "languageVersion": "2.12" 33 | }, 34 | { 35 | "name": "bloc_concurrency", 36 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/bloc_concurrency-0.2.0", 37 | "packageUri": "lib/", 38 | "languageVersion": "2.12" 39 | }, 40 | { 41 | "name": "bloc_test", 42 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/bloc_test-9.0.3", 43 | "packageUri": "lib/", 44 | "languageVersion": "2.12" 45 | }, 46 | { 47 | "name": "boolean_selector", 48 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/boolean_selector-2.1.1", 49 | "packageUri": "lib/", 50 | "languageVersion": "2.17" 51 | }, 52 | { 53 | "name": "characters", 54 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/characters-1.3.0", 55 | "packageUri": "lib/", 56 | "languageVersion": "2.12" 57 | }, 58 | { 59 | "name": "charcode", 60 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/charcode-1.3.1", 61 | "packageUri": "lib/", 62 | "languageVersion": "2.12" 63 | }, 64 | { 65 | "name": "clock", 66 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/clock-1.1.1", 67 | "packageUri": "lib/", 68 | "languageVersion": "2.12" 69 | }, 70 | { 71 | "name": "collection", 72 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/collection-1.18.0", 73 | "packageUri": "lib/", 74 | "languageVersion": "2.18" 75 | }, 76 | { 77 | "name": "convert", 78 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/convert-3.0.1", 79 | "packageUri": "lib/", 80 | "languageVersion": "2.12" 81 | }, 82 | { 83 | "name": "coverage", 84 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/coverage-1.8.0", 85 | "packageUri": "lib/", 86 | "languageVersion": "3.0" 87 | }, 88 | { 89 | "name": "crypto", 90 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/crypto-3.0.1", 91 | "packageUri": "lib/", 92 | "languageVersion": "2.12" 93 | }, 94 | { 95 | "name": "diff_match_patch", 96 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/diff_match_patch-0.4.1", 97 | "packageUri": "lib/", 98 | "languageVersion": "2.12" 99 | }, 100 | { 101 | "name": "equatable", 102 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/equatable-2.0.3", 103 | "packageUri": "lib/", 104 | "languageVersion": "2.12" 105 | }, 106 | { 107 | "name": "fake_async", 108 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/fake_async-1.3.1", 109 | "packageUri": "lib/", 110 | "languageVersion": "2.12" 111 | }, 112 | { 113 | "name": "file", 114 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/file-6.1.2", 115 | "packageUri": "lib/", 116 | "languageVersion": "2.12" 117 | }, 118 | { 119 | "name": "flutter", 120 | "rootUri": "file:///Users/pm/Dev/flutter/packages/flutter", 121 | "packageUri": "lib/", 122 | "languageVersion": "3.3" 123 | }, 124 | { 125 | "name": "flutter_bloc", 126 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/flutter_bloc-8.0.1", 127 | "packageUri": "lib/", 128 | "languageVersion": "2.12" 129 | }, 130 | { 131 | "name": "flutter_test", 132 | "rootUri": "file:///Users/pm/Dev/flutter/packages/flutter_test", 133 | "packageUri": "lib/", 134 | "languageVersion": "3.3" 135 | }, 136 | { 137 | "name": "frontend_server_client", 138 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/frontend_server_client-4.0.0", 139 | "packageUri": "lib/", 140 | "languageVersion": "3.0" 141 | }, 142 | { 143 | "name": "glob", 144 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/glob-2.0.2", 145 | "packageUri": "lib/", 146 | "languageVersion": "2.14" 147 | }, 148 | { 149 | "name": "http", 150 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/http-0.13.4", 151 | "packageUri": "lib/", 152 | "languageVersion": "2.14" 153 | }, 154 | { 155 | "name": "http_multi_server", 156 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/http_multi_server-3.2.0", 157 | "packageUri": "lib/", 158 | "languageVersion": "2.12" 159 | }, 160 | { 161 | "name": "http_parser", 162 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/http_parser-4.0.0", 163 | "packageUri": "lib/", 164 | "languageVersion": "2.12" 165 | }, 166 | { 167 | "name": "io", 168 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/io-1.0.3", 169 | "packageUri": "lib/", 170 | "languageVersion": "2.12" 171 | }, 172 | { 173 | "name": "js", 174 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/js-0.6.4", 175 | "packageUri": "lib/", 176 | "languageVersion": "2.16" 177 | }, 178 | { 179 | "name": "leak_tracker", 180 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/leak_tracker-10.0.5", 181 | "packageUri": "lib/", 182 | "languageVersion": "3.2" 183 | }, 184 | { 185 | "name": "leak_tracker_flutter_testing", 186 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/leak_tracker_flutter_testing-3.0.5", 187 | "packageUri": "lib/", 188 | "languageVersion": "3.2" 189 | }, 190 | { 191 | "name": "leak_tracker_testing", 192 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/leak_tracker_testing-3.0.1", 193 | "packageUri": "lib/", 194 | "languageVersion": "3.2" 195 | }, 196 | { 197 | "name": "logging", 198 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/logging-1.0.2", 199 | "packageUri": "lib/", 200 | "languageVersion": "2.12" 201 | }, 202 | { 203 | "name": "matcher", 204 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/matcher-0.12.16+1", 205 | "packageUri": "lib/", 206 | "languageVersion": "3.0" 207 | }, 208 | { 209 | "name": "material_color_utilities", 210 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/material_color_utilities-0.11.1", 211 | "packageUri": "lib/", 212 | "languageVersion": "2.17" 213 | }, 214 | { 215 | "name": "meta", 216 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/meta-1.15.0", 217 | "packageUri": "lib/", 218 | "languageVersion": "2.12" 219 | }, 220 | { 221 | "name": "mime", 222 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/mime-1.0.1", 223 | "packageUri": "lib/", 224 | "languageVersion": "2.12" 225 | }, 226 | { 227 | "name": "mocktail", 228 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/mocktail-0.2.0", 229 | "packageUri": "lib/", 230 | "languageVersion": "2.12" 231 | }, 232 | { 233 | "name": "nested", 234 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/nested-1.0.0", 235 | "packageUri": "lib/", 236 | "languageVersion": "2.12" 237 | }, 238 | { 239 | "name": "node_preamble", 240 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/node_preamble-2.0.1", 241 | "packageUri": "lib/", 242 | "languageVersion": "2.12" 243 | }, 244 | { 245 | "name": "package_config", 246 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/package_config-2.0.2", 247 | "packageUri": "lib/", 248 | "languageVersion": "2.12" 249 | }, 250 | { 251 | "name": "path", 252 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/path-1.9.0", 253 | "packageUri": "lib/", 254 | "languageVersion": "3.0" 255 | }, 256 | { 257 | "name": "pool", 258 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/pool-1.5.0", 259 | "packageUri": "lib/", 260 | "languageVersion": "2.12" 261 | }, 262 | { 263 | "name": "provider", 264 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/provider-6.0.2", 265 | "packageUri": "lib/", 266 | "languageVersion": "2.12" 267 | }, 268 | { 269 | "name": "pub_semver", 270 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/pub_semver-2.1.4", 271 | "packageUri": "lib/", 272 | "languageVersion": "2.17" 273 | }, 274 | { 275 | "name": "shelf", 276 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/shelf-1.3.0", 277 | "packageUri": "lib/", 278 | "languageVersion": "2.16" 279 | }, 280 | { 281 | "name": "shelf_packages_handler", 282 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/shelf_packages_handler-3.0.0", 283 | "packageUri": "lib/", 284 | "languageVersion": "2.12" 285 | }, 286 | { 287 | "name": "shelf_static", 288 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/shelf_static-1.1.0", 289 | "packageUri": "lib/", 290 | "languageVersion": "2.12" 291 | }, 292 | { 293 | "name": "shelf_web_socket", 294 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/shelf_web_socket-1.0.1", 295 | "packageUri": "lib/", 296 | "languageVersion": "2.12" 297 | }, 298 | { 299 | "name": "sky_engine", 300 | "rootUri": "file:///Users/pm/Dev/flutter/bin/cache/pkg/sky_engine", 301 | "packageUri": "lib/", 302 | "languageVersion": "3.2" 303 | }, 304 | { 305 | "name": "source_map_stack_trace", 306 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/source_map_stack_trace-2.1.0", 307 | "packageUri": "lib/", 308 | "languageVersion": "2.12" 309 | }, 310 | { 311 | "name": "source_maps", 312 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/source_maps-0.10.10", 313 | "packageUri": "lib/", 314 | "languageVersion": "2.12" 315 | }, 316 | { 317 | "name": "source_span", 318 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/source_span-1.10.0", 319 | "packageUri": "lib/", 320 | "languageVersion": "2.18" 321 | }, 322 | { 323 | "name": "stack_trace", 324 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/stack_trace-1.11.1", 325 | "packageUri": "lib/", 326 | "languageVersion": "2.18" 327 | }, 328 | { 329 | "name": "state_machine_bloc", 330 | "rootUri": "../../../packages/state_machine_bloc", 331 | "packageUri": "lib/", 332 | "languageVersion": "2.16" 333 | }, 334 | { 335 | "name": "stream_channel", 336 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/stream_channel-2.1.2", 337 | "packageUri": "lib/", 338 | "languageVersion": "2.19" 339 | }, 340 | { 341 | "name": "stream_transform", 342 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/stream_transform-2.0.0", 343 | "packageUri": "lib/", 344 | "languageVersion": "2.12" 345 | }, 346 | { 347 | "name": "string_scanner", 348 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/string_scanner-1.2.0", 349 | "packageUri": "lib/", 350 | "languageVersion": "2.18" 351 | }, 352 | { 353 | "name": "term_glyph", 354 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/term_glyph-1.2.1", 355 | "packageUri": "lib/", 356 | "languageVersion": "2.12" 357 | }, 358 | { 359 | "name": "test", 360 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/test-1.25.7", 361 | "packageUri": "lib/", 362 | "languageVersion": "3.2" 363 | }, 364 | { 365 | "name": "test_api", 366 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/test_api-0.7.2", 367 | "packageUri": "lib/", 368 | "languageVersion": "3.2" 369 | }, 370 | { 371 | "name": "test_core", 372 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/test_core-0.6.4", 373 | "packageUri": "lib/", 374 | "languageVersion": "3.2" 375 | }, 376 | { 377 | "name": "typed_data", 378 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/typed_data-1.3.0", 379 | "packageUri": "lib/", 380 | "languageVersion": "2.12" 381 | }, 382 | { 383 | "name": "vector_math", 384 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/vector_math-2.1.4", 385 | "packageUri": "lib/", 386 | "languageVersion": "2.14" 387 | }, 388 | { 389 | "name": "vm_service", 390 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/vm_service-14.2.5", 391 | "packageUri": "lib/", 392 | "languageVersion": "3.3" 393 | }, 394 | { 395 | "name": "watcher", 396 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/watcher-1.1.0", 397 | "packageUri": "lib/", 398 | "languageVersion": "3.0" 399 | }, 400 | { 401 | "name": "web_socket_channel", 402 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/web_socket_channel-2.1.0", 403 | "packageUri": "lib/", 404 | "languageVersion": "2.12" 405 | }, 406 | { 407 | "name": "webkit_inspection_protocol", 408 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/webkit_inspection_protocol-1.0.0", 409 | "packageUri": "lib/", 410 | "languageVersion": "2.12" 411 | }, 412 | { 413 | "name": "yaml", 414 | "rootUri": "file:///Users/pm/.pub-cache/hosted/pub.dev/yaml-3.1.0", 415 | "packageUri": "lib/", 416 | "languageVersion": "2.12" 417 | }, 418 | { 419 | "name": "infinite_list_state_machine", 420 | "rootUri": "../", 421 | "packageUri": "lib/", 422 | "languageVersion": "3.0" 423 | } 424 | ], 425 | "generated": "2024-10-07T02:53:31.718067Z", 426 | "generator": "pub", 427 | "generatorVersion": "3.5.3", 428 | "flutterRoot": "file:///Users/pm/Dev/flutter", 429 | "flutterVersion": "3.24.3", 430 | "pubCache": "file:///Users/pm/.pub-cache" 431 | } 432 | -------------------------------------------------------------------------------- /packages/state_machine_bloc/README.md: -------------------------------------------------------------------------------- 1 |

2 | State machine Bloc 3 |

4 | 5 |

6 | Star on Github 7 | style: effective dart 8 | License: MIT 9 | Bloc Library 10 |

11 |

⚠️ state_machine_bloc is not an official bloc package ⚠️

12 | 13 | state_machine_bloc is an extension to the bloc state management library which provide a `StateMachine` utility class to create finite-states machines based blocs. Event routing and filtering are done under the hood for you so you can focus on building state machine logic. 14 | 15 | This package uses a flexible declarative API to conveniently describe simple to complex state machines. 16 | 17 | **state_machine_bloc supports:** 18 | * ✅ Easy state machine definition 19 | * ✅ Shared or per-state data 20 | * ✅ States lifecycle events 21 | * ✅ Guard conditions on transitions 22 | * ✅ Nested states without depth limit 23 | 24 | # Index 25 | * How to use 26 | * StateMachine vs Bloc 27 | * When to use StateMachine? 28 | * Documentation 29 | * The state machine 30 | * Events concurrency 31 | * Defining states 32 | * Event handlers 33 | * Side effects 34 | * Nesting states 35 | * Nested states event handlers 36 | * Nested state side effects 37 | * Examples 38 | * Issues and feature requests 39 | * Additional resources 40 | 41 | # How to use 42 | State machines are created by extending `StateMachine`, a new class introduced by this package. `StateMachine` itself inherits from `Bloc` class, meaning states machines created using this package **are** blocs and therefore compatible with the entire bloc's ecosystem. 43 | 44 | `StateMachine` class has been designed to be as lightweight as possible to avoid interfering with `Bloc` inner behavior. Under the hood, `StateMachine` uses the `Bloc`'s `on` method with a custom event mapper to call your callbacks based on the state machine's definition you've provided. 45 | 46 | State machine's states and transitions are defined using a new method, `define`, which is similar to `Bloc`'s `on`. By calling `define`, you register `State` as part of the machine's set of allowed states. Each state can have its own set of events handlers, lifecycle events, and transitions. 47 | 48 | The following state machine represents a login page's bloc that first wait for the user to submit the form, then try to log in using the API, and finally change its state to success or error based on API return. Bellow, you can see its graph representation and the corresponding code. 49 | 50 | 51 | 52 | 53 | 56 | 59 | 60 | 61 |
54 | Login state machine code 55 | 57 | Login state machine graph 58 |
62 | 63 | `StateMachine` **is** a `Bloc`, so you could use it in the same way as `Bloc`: 64 | 65 | ```dart 66 | BlocProvider( 67 | create: (_) => LoginStateMachine(), 68 | child: ..., 69 | ); 70 | 71 | ... 72 | 73 | BlocBuilder( 74 | builder: ..., 75 | ); 76 | ``` 77 | 78 | # StateMachine vs Bloc 79 | State machines are very close to what bloc already do. The main differences reside in the number of states you can have and how events are computed. 80 | 81 | With `Bloc`, you register a set of event handlers that can emit new states when corresponding events are received. Event handlers are processed no matter the current bloc's state and they can emit as many new states they want as long they inherit from the base `State` class. 82 | 83 | With StateMachine, you register a set of states, each one with its own set of event handlers. StateMachine can never be in a state that it hasn't been explicitly defined and it will throw an error if you try to. When an event is received, `StateMachine` searches for corresponding events handlers registered for the current state. If no event handler is found, the event is discarded. Handlers can return a state to indicate `StateMachine` should transit to this new state or `null`, to indicate no transition should happen. Event handlers are processed sequentially until one of them returns a new state or they have all been evaluated. 84 | 85 | These differences in design bring some benefits as well some disadvantages: 86 | 87 | **pros** 88 | - The state machine pattern eliminates bugs and weird situations because it won't let the UI transition to a state which we don’t know about. 89 | - It eliminates the need for code that protects other code from execution because the state machine is not accepting events that are not explicitly defined as acceptable for the current state. 90 | - Business logic rules are written down explicitly in the state machine definition, which makes it easy to understand and maintain. 91 | 92 | **cons** 93 | - The state machine is less flexible than Bloc. Sometimes state machine is not adapted to express certain problems. 94 | - State machine trend to be more verbose than blocs. 95 | 96 | # When to use `StateMachine`? 97 | Generally, it's recommended to use `StateMachine` where you can because it will make your code clean and robust. 98 | 99 | StateMachine is well suited if you can identify a set of states and easily identify what event belongs to what state. If you need to use complex event transformers or if states are too intricated so it's difficult to distinguish them, you should probably use a `Bloc`. 100 | 101 | # Documentation 102 | ## The state machine 103 | The state machine uses `Bloc`'s `on` method under the hood with a custom event dispatcher that will in turn call your methods and callbacks. 104 | 105 | State machine's states should be defined with the `StateMachine`'s `define` methods inside the constructor. You should never try to transit to a state that hasn't been explicitly defined. If the state machine detects a transition to an undefined state, it will throw an error. 106 | 107 | > 🚨 You should **NEVER** use `on` method inside a StateMachine. 108 | 109 | Each state has its own set of event handlers and side effects callbacks: 110 | * **Event handlers** react to an incoming event and can emit the next machine's state. We call this a _transition_. 111 | * **Side effects** are callback functions called depending on state lifecycle. You have access to three different side effects: `onEnter`, `onExit`, and `onChange`. 112 | 113 | When an event is received, the state machine will first search for the actual state definition. Each current state's event handler that matches the received event type will be evaluated. If multiple events handlers match the event type, they will be evaluated in their **definition order**. As soon as an event handler returns a non-null state (we call this _entering a transition_), the state machine stops evaluating events handlers and transit to the new state immediately. 114 | 115 | ### Events concurrency 116 | By default, if multiple incoming events are received during **the same event loop**, the first one is processed and every other is dropped. You can override this behavior by passing a `transformer` to the `StateMachine`'s constructor. 117 | 118 | ```dart 119 | class MyStateMachine extends StateMachine { 120 | MyStateMachine() : super(Initial(), transformer: /** custom transformer **/) {} 121 | } 122 | ``` 123 | 124 | ## Defining states 125 | State machine states are defined using `StateMachine`'s `define` method inside the constructor. Define should be called ones for each available state. Every defined state for a given `StateMachine` should inherit from `` base class and should only be defined once. 126 | 127 | ```dart 128 | class MyStateMachine extends StateMachine { 129 | MyStateMachine() : super(InitialState()) { 130 | 131 | // InitialState definition 132 | define(($) => $ 133 | // onEnter side effect 134 | ..onEnter((InitialState state) { /** ... **/ }) 135 | 136 | // onChange side effect 137 | ..onChange((InitialState state, InitialState nextState) { /** ... **/ }) 138 | 139 | // onExit side effect 140 | ..onExit((InitialState state) { /** ... **/ }) 141 | 142 | // transition to OtherState when receiving SomeEvent 143 | ..on((SomeEvent event, InitialState state) => OtherState()) 144 | ); 145 | 146 | // OtherState definition 147 | define(); 148 | } 149 | } 150 | ``` 151 | 152 | `define` method takes an optional builder function as parameter that could be used to register event handlers and side effect callbacks for the defined state. 153 | 154 | The builder function takes a `StateDefinitionBuilder` as parameter and should return it. `StateDefinitionBuilder` exposes methods necessary to register the defined state's transitions and side effects callbacks. 155 | 156 | ```dart 157 | define((StateDefinitionBuilder builder) { 158 | builder.onEnter((State state) { /* Side effect */ }) 159 | builder.on((Event event, State state) => NextState()); //transition to NextState 160 | return builder; 161 | }); 162 | ``` 163 | 164 | This syntax is very verbose but hopefully thanks to the dart [cascade](https://dart.dev/guides/language/language-tour#cascade-notation) notation you could write it like so: 165 | ```dart 166 | define(($) => $ 167 | ..onEnter((State state) {}) 168 | ..on((Event event, State state) => NextState()); 169 | ``` 170 | 171 | ### Event handlers 172 | Event handlers are registered for a given state using `StateDefinitionBuilder`'s `on` method. For a given `StateMachine`, every registered event should inherit from `` base class. You can register as many handlers you want for a given state. You can also register multiple handlers for the same event. 173 | 174 | Event handlers have the following signature: 175 | ```dart 176 | State? Function(DefinedEvent, DefinedState); 177 | ``` 178 | If the returned state is not null, it is considered a _transition_ and the state machine will transit immediately to this new state. Otherwise, no transition append, and the next event handler is evaluated. 179 | 180 | > 🚨 Event handlers are only evaluated if the event is received while the state machine is in the state for which the handler is registered. 181 | 182 | > 🚨 **If a new state is returned from a transition where `newState == state`, the new state will be ignored**. If you're using a state that contains data, make sure you've implemented `==` operator. You could use `freezed` or `equatable` packages for this purpose. 183 | 184 | **Example of three event handlers registered for `InitialState`.** 185 | ```dart 186 | class MyStateMachine extends StateMachine { 187 | MyStateMachine() : super(InitialState()) { 188 | define(($) => $ 189 | ..on((SomeEvent e, InitialState s) => null) 190 | ..on((SomeEvent e, InitialState s) => SecondState()) 191 | ..on((SomeEvent e, InitialState s) => ThirdState()) 192 | ); 193 | 194 | define(); 195 | define(); 196 | } 197 | } 198 | ``` 199 | ### Side effects 200 | Side effects are callback functions that you can register to react to the state's lifecycle events. They are generally a good place to request APIs or start async computations. 201 | You have access to 3 different side effects: 202 | **onEnter** is called when State Machine enters a state. If `State` is the initial `StateMachine`'s state, `onEnter` will be called during state machine initialization. 203 | **onChange** is called when a state transitions to itself. `onChange` **is not** called if `state == nextState` or when the state machine enters this state for the first time. 204 | **onExit** is called before State Machine's exit a state. 205 | You can register a side effect for a given state using `StateDefinitionBuilder`'s `onEnter`, `onChange` or `onExit` methods. 206 | 207 | ```dart 208 | define((b) => b 209 | ..onEnter((State state) { /* called when entering State */ }) 210 | ..onChange((State current, State next) { /* called when State data changed */ }) 211 | ..onExit((State state) { /* called when exiting State */ }) 212 | ``` 213 | 214 | You can give async function as parameter for side effects, but remember they will **not** be awaited. 215 | ```dart 216 | define((b) => b 217 | ..onEnter((State state) async { /* not awaited */ }) 218 | ``` 219 | 220 | ## Nesting states 221 | `StateMachine` supports state nesting. This is convenient for defining common event handlers or side effects for a group of states. 222 | You can define a nested state using `StateDefinitionBuilder`'s `define` method. This method behaves the same way as top-level define calls. 223 | You could register event handlers and side effects for the nested state like any normal state. 224 | 225 | The only restriction you have when defining a nested state is that the child state should be a sub-class of the parent state. 226 | 227 | ```dart 228 | define(($) => $ 229 | ..on(_transitToTryLoggingIn) 230 | ..define() 231 | ); 232 | ``` 233 | 234 | In the example above, the `LoginError` state is a child state of `WaitingFormSubmission`. `WaitingFormSubmission` and `LoginError` are both valid states that can transit to the `TryLoginIn` state, but `LoginError` carries an additional error the UI will display. 235 | 236 | A state can have any number of child states as long they are only defined once. Nested states have access to a `StateDefinitionBuilder` like normal states so they can register their event handlers, side effects, or in turn, nested states. 237 | 238 | ```dart 239 | define(($) => $ 240 | ..define(($) => $ 241 | ..onEnter(...) 242 | ..onChange(...) 243 | ..onExit(...) 244 | ..on(...) 245 | ..define(($) => $ 246 | ... 247 | ) // Child2 248 | ) // Child1 249 | ); // Parent 250 | ``` 251 | 252 | ### Nested states event handlers 253 | When an event is received, the state machine searches the current state definition. If it's a nested state, the state machine will evaluate each parent state, from the higher ones in the hierarchy to the lowest ones before evaluating the current state. If a parent state event handler handles an event and returns a new state, the child's event handlers will not be evaluated. 254 | 255 | ### Nested state side effects 256 | Parent's side effects can be triggered when the state machine enters, move or exit from one of its child's state. 257 | The table below describes how side effects are triggered for the parent and child states. 258 | 259 | > 🚨 A state is considered a child as long as it's below the parent state in the state hierarchy. 260 | 261 | | | Parent | Child | 262 | |----------|---------------------------------------------------------------------------------------|-----------------------------------------| 263 | | onEnter | Called when entering this state or any of its children | Called when entering the state | 264 | | onChange | Called transitioning from itself or one of its child to itself or one of its children | Called transitioning to itself | 265 | | onExit | Called when next state isn't this state or one of its children | Called when next state isn't this state | 266 | 267 | # Examples 268 | You can find usage examples in the repository's [example folder](https://github.com/Pierre2tm/state_machine_bloc/tree/main/examples). These examples are re-implementations of bloc's examples using state machines. New examples will be added over time. 269 | * [Timer](https://github.com/Pierre2tm/state_machine_bloc/tree/main/examples/flutter_timer_state_machine) 270 | * [Infinite list](https://github.com/Pierre2tm/state_machine_bloc/tree/main/examples/infinite_list_state_machine) 271 | 272 | # Issues and feature requests 273 | If you find a bug or want to see an additional feature, please open an issue on [github](https://github.com/Pierre2tm/state_machine_bloc/issues/new). 274 | 275 | ## Additional resources 276 | * [You are managing state? Think twice.](https://krasimirtsonev.com/blog/article/managing-state-in-javascript-with-state-machines-stent) 277 | * [The rise of state machine](https://www.smashingmagazine.com/2018/01/rise-state-machines/) 278 | * [Robust React User Interfaces with Finite State Machines](https://css-tricks.com/robust-react-user-interfaces-with-finite-state-machines/) 279 | -------------------------------------------------------------------------------- /examples/flutter_timer_state_machine/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "67.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.4.1" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.3.0" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | bloc: 37 | dependency: "direct main" 38 | description: 39 | name: bloc 40 | sha256: "318e6cc6803d93b8d2de5f580e452ca565bcaa44f724d5156c71961426b88e03" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "8.0.3" 44 | bloc_concurrency: 45 | dependency: transitive 46 | description: 47 | name: bloc_concurrency 48 | sha256: "0af76bfdbdcf21d897c743bb376fbe0d356e2b9e76dc787397077391074d985d" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "0.2.0" 52 | bloc_test: 53 | dependency: "direct dev" 54 | description: 55 | name: bloc_test 56 | sha256: "17423ebac5cfa606be656d312f262e423d3737f2b7b82c46a0ef38988d96f0ca" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "9.0.3" 60 | boolean_selector: 61 | dependency: transitive 62 | description: 63 | name: boolean_selector 64 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.1.1" 68 | characters: 69 | dependency: transitive 70 | description: 71 | name: characters 72 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.3.0" 76 | charcode: 77 | dependency: transitive 78 | description: 79 | name: charcode 80 | sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.3.1" 84 | clock: 85 | dependency: transitive 86 | description: 87 | name: clock 88 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.1.1" 92 | collection: 93 | dependency: transitive 94 | description: 95 | name: collection 96 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "1.18.0" 100 | convert: 101 | dependency: transitive 102 | description: 103 | name: convert 104 | sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "3.0.1" 108 | coverage: 109 | dependency: transitive 110 | description: 111 | name: coverage 112 | sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "1.8.0" 116 | crypto: 117 | dependency: transitive 118 | description: 119 | name: crypto 120 | sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "3.0.1" 124 | diff_match_patch: 125 | dependency: transitive 126 | description: 127 | name: diff_match_patch 128 | sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "0.4.1" 132 | equatable: 133 | dependency: "direct main" 134 | description: 135 | name: equatable 136 | sha256: c6094fd1efad3046334a9c40bee022147e55c25401ccd89b94e373e3edadd375 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.0.3" 140 | fake_async: 141 | dependency: transitive 142 | description: 143 | name: fake_async 144 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.3.1" 148 | file: 149 | dependency: transitive 150 | description: 151 | name: file 152 | sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "6.1.2" 156 | flutter: 157 | dependency: "direct main" 158 | description: flutter 159 | source: sdk 160 | version: "0.0.0" 161 | flutter_bloc: 162 | dependency: "direct main" 163 | description: 164 | name: flutter_bloc 165 | sha256: "7b84d9777db3e30a5051c6718331be57e4cfc0c2424be82ac1771392cad7dbe8" 166 | url: "https://pub.dev" 167 | source: hosted 168 | version: "8.0.1" 169 | flutter_test: 170 | dependency: "direct dev" 171 | description: flutter 172 | source: sdk 173 | version: "0.0.0" 174 | frontend_server_client: 175 | dependency: transitive 176 | description: 177 | name: frontend_server_client 178 | sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 179 | url: "https://pub.dev" 180 | source: hosted 181 | version: "4.0.0" 182 | glob: 183 | dependency: transitive 184 | description: 185 | name: glob 186 | sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" 187 | url: "https://pub.dev" 188 | source: hosted 189 | version: "2.0.2" 190 | http_multi_server: 191 | dependency: transitive 192 | description: 193 | name: http_multi_server 194 | sha256: ab298ef2b2acd283bd36837df7801dcf6e6b925f8da6e09efb81111230aa9037 195 | url: "https://pub.dev" 196 | source: hosted 197 | version: "3.2.0" 198 | http_parser: 199 | dependency: transitive 200 | description: 201 | name: http_parser 202 | sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 203 | url: "https://pub.dev" 204 | source: hosted 205 | version: "4.0.0" 206 | io: 207 | dependency: transitive 208 | description: 209 | name: io 210 | sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" 211 | url: "https://pub.dev" 212 | source: hosted 213 | version: "1.0.3" 214 | js: 215 | dependency: transitive 216 | description: 217 | name: js 218 | sha256: a5e201311cb08bf3912ebbe9a2be096e182d703f881136ec1e81a2338a9e120d 219 | url: "https://pub.dev" 220 | source: hosted 221 | version: "0.6.4" 222 | leak_tracker: 223 | dependency: transitive 224 | description: 225 | name: leak_tracker 226 | sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" 227 | url: "https://pub.dev" 228 | source: hosted 229 | version: "10.0.5" 230 | leak_tracker_flutter_testing: 231 | dependency: transitive 232 | description: 233 | name: leak_tracker_flutter_testing 234 | sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" 235 | url: "https://pub.dev" 236 | source: hosted 237 | version: "3.0.5" 238 | leak_tracker_testing: 239 | dependency: transitive 240 | description: 241 | name: leak_tracker_testing 242 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 243 | url: "https://pub.dev" 244 | source: hosted 245 | version: "3.0.1" 246 | logging: 247 | dependency: transitive 248 | description: 249 | name: logging 250 | sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" 251 | url: "https://pub.dev" 252 | source: hosted 253 | version: "1.0.2" 254 | matcher: 255 | dependency: transitive 256 | description: 257 | name: matcher 258 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 259 | url: "https://pub.dev" 260 | source: hosted 261 | version: "0.12.16+1" 262 | material_color_utilities: 263 | dependency: transitive 264 | description: 265 | name: material_color_utilities 266 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 267 | url: "https://pub.dev" 268 | source: hosted 269 | version: "0.11.1" 270 | meta: 271 | dependency: transitive 272 | description: 273 | name: meta 274 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 275 | url: "https://pub.dev" 276 | source: hosted 277 | version: "1.15.0" 278 | mime: 279 | dependency: transitive 280 | description: 281 | name: mime 282 | sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 283 | url: "https://pub.dev" 284 | source: hosted 285 | version: "1.0.1" 286 | mocktail: 287 | dependency: "direct dev" 288 | description: 289 | name: mocktail 290 | sha256: "80a996cd9a69284b3dc521ce185ffe9150cde69767c2d3a0720147d93c0cef53" 291 | url: "https://pub.dev" 292 | source: hosted 293 | version: "0.3.0" 294 | nested: 295 | dependency: transitive 296 | description: 297 | name: nested 298 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 299 | url: "https://pub.dev" 300 | source: hosted 301 | version: "1.0.0" 302 | node_preamble: 303 | dependency: transitive 304 | description: 305 | name: node_preamble 306 | sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" 307 | url: "https://pub.dev" 308 | source: hosted 309 | version: "2.0.1" 310 | package_config: 311 | dependency: transitive 312 | description: 313 | name: package_config 314 | sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 315 | url: "https://pub.dev" 316 | source: hosted 317 | version: "2.0.2" 318 | path: 319 | dependency: transitive 320 | description: 321 | name: path 322 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 323 | url: "https://pub.dev" 324 | source: hosted 325 | version: "1.9.0" 326 | pool: 327 | dependency: transitive 328 | description: 329 | name: pool 330 | sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" 331 | url: "https://pub.dev" 332 | source: hosted 333 | version: "1.5.0" 334 | provider: 335 | dependency: transitive 336 | description: 337 | name: provider 338 | sha256: "7896193cf752c40ba7f7732a95264319a787871e5d628225357f5c909182bc06" 339 | url: "https://pub.dev" 340 | source: hosted 341 | version: "6.0.2" 342 | pub_semver: 343 | dependency: transitive 344 | description: 345 | name: pub_semver 346 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 347 | url: "https://pub.dev" 348 | source: hosted 349 | version: "2.1.4" 350 | shelf: 351 | dependency: transitive 352 | description: 353 | name: shelf 354 | sha256: "4592f6cb6c417632ebdfb63e4db42a7e3ad49d1bd52d9f93b6eb883035ddc0c3" 355 | url: "https://pub.dev" 356 | source: hosted 357 | version: "1.3.0" 358 | shelf_packages_handler: 359 | dependency: transitive 360 | description: 361 | name: shelf_packages_handler 362 | sha256: e0b44ebddec91e70a713e13adf93c1b2100821303b86a18e1ef1d082bd8bd9b8 363 | url: "https://pub.dev" 364 | source: hosted 365 | version: "3.0.0" 366 | shelf_static: 367 | dependency: transitive 368 | description: 369 | name: shelf_static 370 | sha256: "4a0d12cd512aa4fc55fed5f6280f02ef183f47ba29b4b0dfd621b1c99b7e6361" 371 | url: "https://pub.dev" 372 | source: hosted 373 | version: "1.1.0" 374 | shelf_web_socket: 375 | dependency: transitive 376 | description: 377 | name: shelf_web_socket 378 | sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d 379 | url: "https://pub.dev" 380 | source: hosted 381 | version: "1.0.1" 382 | sky_engine: 383 | dependency: transitive 384 | description: flutter 385 | source: sdk 386 | version: "0.0.99" 387 | source_map_stack_trace: 388 | dependency: transitive 389 | description: 390 | name: source_map_stack_trace 391 | sha256: "8c463326277f68a628abab20580047b419c2ff66756fd0affd451f73f9508c11" 392 | url: "https://pub.dev" 393 | source: hosted 394 | version: "2.1.0" 395 | source_maps: 396 | dependency: transitive 397 | description: 398 | name: source_maps 399 | sha256: "52de2200bb098de739794c82d09c41ac27b2e42fd7e23cce7b9c74bf653c7296" 400 | url: "https://pub.dev" 401 | source: hosted 402 | version: "0.10.10" 403 | source_span: 404 | dependency: transitive 405 | description: 406 | name: source_span 407 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 408 | url: "https://pub.dev" 409 | source: hosted 410 | version: "1.10.0" 411 | stack_trace: 412 | dependency: transitive 413 | description: 414 | name: stack_trace 415 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 416 | url: "https://pub.dev" 417 | source: hosted 418 | version: "1.11.1" 419 | state_machine_bloc: 420 | dependency: "direct main" 421 | description: 422 | path: "../../packages/state_machine_bloc" 423 | relative: true 424 | source: path 425 | version: "0.0.1" 426 | stream_channel: 427 | dependency: transitive 428 | description: 429 | name: stream_channel 430 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 431 | url: "https://pub.dev" 432 | source: hosted 433 | version: "2.1.2" 434 | stream_transform: 435 | dependency: transitive 436 | description: 437 | name: stream_transform 438 | sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e 439 | url: "https://pub.dev" 440 | source: hosted 441 | version: "2.0.0" 442 | string_scanner: 443 | dependency: transitive 444 | description: 445 | name: string_scanner 446 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 447 | url: "https://pub.dev" 448 | source: hosted 449 | version: "1.2.0" 450 | term_glyph: 451 | dependency: transitive 452 | description: 453 | name: term_glyph 454 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 455 | url: "https://pub.dev" 456 | source: hosted 457 | version: "1.2.1" 458 | test: 459 | dependency: transitive 460 | description: 461 | name: test 462 | sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" 463 | url: "https://pub.dev" 464 | source: hosted 465 | version: "1.25.7" 466 | test_api: 467 | dependency: transitive 468 | description: 469 | name: test_api 470 | sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" 471 | url: "https://pub.dev" 472 | source: hosted 473 | version: "0.7.2" 474 | test_core: 475 | dependency: transitive 476 | description: 477 | name: test_core 478 | sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" 479 | url: "https://pub.dev" 480 | source: hosted 481 | version: "0.6.4" 482 | typed_data: 483 | dependency: transitive 484 | description: 485 | name: typed_data 486 | sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" 487 | url: "https://pub.dev" 488 | source: hosted 489 | version: "1.3.0" 490 | vector_math: 491 | dependency: transitive 492 | description: 493 | name: vector_math 494 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 495 | url: "https://pub.dev" 496 | source: hosted 497 | version: "2.1.4" 498 | vm_service: 499 | dependency: transitive 500 | description: 501 | name: vm_service 502 | sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" 503 | url: "https://pub.dev" 504 | source: hosted 505 | version: "14.2.5" 506 | watcher: 507 | dependency: transitive 508 | description: 509 | name: watcher 510 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 511 | url: "https://pub.dev" 512 | source: hosted 513 | version: "1.1.0" 514 | web_socket_channel: 515 | dependency: transitive 516 | description: 517 | name: web_socket_channel 518 | sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" 519 | url: "https://pub.dev" 520 | source: hosted 521 | version: "2.1.0" 522 | webkit_inspection_protocol: 523 | dependency: transitive 524 | description: 525 | name: webkit_inspection_protocol 526 | sha256: "5adb6ab8ed14e22bb907aae7338f0c206ea21e7a27004e97664b16c120306f00" 527 | url: "https://pub.dev" 528 | source: hosted 529 | version: "1.0.0" 530 | yaml: 531 | dependency: transitive 532 | description: 533 | name: yaml 534 | sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" 535 | url: "https://pub.dev" 536 | source: hosted 537 | version: "3.1.0" 538 | sdks: 539 | dart: ">=3.3.0 <4.0.0" 540 | flutter: ">=3.18.0-18.0.pre.54" 541 | -------------------------------------------------------------------------------- /examples/infinite_list_state_machine/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | _fe_analyzer_shared: 5 | dependency: transitive 6 | description: 7 | name: _fe_analyzer_shared 8 | sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "67.0.0" 12 | analyzer: 13 | dependency: transitive 14 | description: 15 | name: analyzer 16 | sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "6.4.1" 20 | args: 21 | dependency: transitive 22 | description: 23 | name: args 24 | sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "2.3.0" 28 | async: 29 | dependency: transitive 30 | description: 31 | name: async 32 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "2.11.0" 36 | bloc: 37 | dependency: "direct main" 38 | description: 39 | name: bloc 40 | sha256: "318e6cc6803d93b8d2de5f580e452ca565bcaa44f724d5156c71961426b88e03" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "8.0.3" 44 | bloc_concurrency: 45 | dependency: "direct main" 46 | description: 47 | name: bloc_concurrency 48 | sha256: "0af76bfdbdcf21d897c743bb376fbe0d356e2b9e76dc787397077391074d985d" 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "0.2.0" 52 | bloc_test: 53 | dependency: "direct main" 54 | description: 55 | name: bloc_test 56 | sha256: "17423ebac5cfa606be656d312f262e423d3737f2b7b82c46a0ef38988d96f0ca" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "9.0.3" 60 | boolean_selector: 61 | dependency: transitive 62 | description: 63 | name: boolean_selector 64 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 65 | url: "https://pub.dev" 66 | source: hosted 67 | version: "2.1.1" 68 | characters: 69 | dependency: transitive 70 | description: 71 | name: characters 72 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 73 | url: "https://pub.dev" 74 | source: hosted 75 | version: "1.3.0" 76 | charcode: 77 | dependency: transitive 78 | description: 79 | name: charcode 80 | sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 81 | url: "https://pub.dev" 82 | source: hosted 83 | version: "1.3.1" 84 | clock: 85 | dependency: transitive 86 | description: 87 | name: clock 88 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 89 | url: "https://pub.dev" 90 | source: hosted 91 | version: "1.1.1" 92 | collection: 93 | dependency: transitive 94 | description: 95 | name: collection 96 | sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a 97 | url: "https://pub.dev" 98 | source: hosted 99 | version: "1.18.0" 100 | convert: 101 | dependency: transitive 102 | description: 103 | name: convert 104 | sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d 105 | url: "https://pub.dev" 106 | source: hosted 107 | version: "3.0.1" 108 | coverage: 109 | dependency: transitive 110 | description: 111 | name: coverage 112 | sha256: "3945034e86ea203af7a056d98e98e42a5518fff200d6e8e6647e1886b07e936e" 113 | url: "https://pub.dev" 114 | source: hosted 115 | version: "1.8.0" 116 | crypto: 117 | dependency: transitive 118 | description: 119 | name: crypto 120 | sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c 121 | url: "https://pub.dev" 122 | source: hosted 123 | version: "3.0.1" 124 | diff_match_patch: 125 | dependency: transitive 126 | description: 127 | name: diff_match_patch 128 | sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "0.4.1" 132 | equatable: 133 | dependency: "direct main" 134 | description: 135 | name: equatable 136 | sha256: c6094fd1efad3046334a9c40bee022147e55c25401ccd89b94e373e3edadd375 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "2.0.3" 140 | fake_async: 141 | dependency: transitive 142 | description: 143 | name: fake_async 144 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "1.3.1" 148 | file: 149 | dependency: transitive 150 | description: 151 | name: file 152 | sha256: b69516f2c26a5bcac4eee2e32512e1a5205ab312b3536c1c1227b2b942b5f9ad 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "6.1.2" 156 | flutter: 157 | dependency: "direct main" 158 | description: flutter 159 | source: sdk 160 | version: "0.0.0" 161 | flutter_bloc: 162 | dependency: "direct main" 163 | description: 164 | name: flutter_bloc 165 | sha256: "7b84d9777db3e30a5051c6718331be57e4cfc0c2424be82ac1771392cad7dbe8" 166 | url: "https://pub.dev" 167 | source: hosted 168 | version: "8.0.1" 169 | flutter_test: 170 | dependency: "direct dev" 171 | description: flutter 172 | source: sdk 173 | version: "0.0.0" 174 | frontend_server_client: 175 | dependency: transitive 176 | description: 177 | name: frontend_server_client 178 | sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 179 | url: "https://pub.dev" 180 | source: hosted 181 | version: "4.0.0" 182 | glob: 183 | dependency: transitive 184 | description: 185 | name: glob 186 | sha256: "8321dd2c0ab0683a91a51307fa844c6db4aa8e3981219b78961672aaab434658" 187 | url: "https://pub.dev" 188 | source: hosted 189 | version: "2.0.2" 190 | http: 191 | dependency: "direct main" 192 | description: 193 | name: http 194 | sha256: "2ed163531e071c2c6b7c659635112f24cb64ecbebf6af46b550d536c0b1aa112" 195 | url: "https://pub.dev" 196 | source: hosted 197 | version: "0.13.4" 198 | http_multi_server: 199 | dependency: transitive 200 | description: 201 | name: http_multi_server 202 | sha256: ab298ef2b2acd283bd36837df7801dcf6e6b925f8da6e09efb81111230aa9037 203 | url: "https://pub.dev" 204 | source: hosted 205 | version: "3.2.0" 206 | http_parser: 207 | dependency: transitive 208 | description: 209 | name: http_parser 210 | sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 211 | url: "https://pub.dev" 212 | source: hosted 213 | version: "4.0.0" 214 | io: 215 | dependency: transitive 216 | description: 217 | name: io 218 | sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" 219 | url: "https://pub.dev" 220 | source: hosted 221 | version: "1.0.3" 222 | js: 223 | dependency: transitive 224 | description: 225 | name: js 226 | sha256: a5e201311cb08bf3912ebbe9a2be096e182d703f881136ec1e81a2338a9e120d 227 | url: "https://pub.dev" 228 | source: hosted 229 | version: "0.6.4" 230 | leak_tracker: 231 | dependency: transitive 232 | description: 233 | name: leak_tracker 234 | sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" 235 | url: "https://pub.dev" 236 | source: hosted 237 | version: "10.0.5" 238 | leak_tracker_flutter_testing: 239 | dependency: transitive 240 | description: 241 | name: leak_tracker_flutter_testing 242 | sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" 243 | url: "https://pub.dev" 244 | source: hosted 245 | version: "3.0.5" 246 | leak_tracker_testing: 247 | dependency: transitive 248 | description: 249 | name: leak_tracker_testing 250 | sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" 251 | url: "https://pub.dev" 252 | source: hosted 253 | version: "3.0.1" 254 | logging: 255 | dependency: transitive 256 | description: 257 | name: logging 258 | sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" 259 | url: "https://pub.dev" 260 | source: hosted 261 | version: "1.0.2" 262 | matcher: 263 | dependency: transitive 264 | description: 265 | name: matcher 266 | sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb 267 | url: "https://pub.dev" 268 | source: hosted 269 | version: "0.12.16+1" 270 | material_color_utilities: 271 | dependency: transitive 272 | description: 273 | name: material_color_utilities 274 | sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec 275 | url: "https://pub.dev" 276 | source: hosted 277 | version: "0.11.1" 278 | meta: 279 | dependency: transitive 280 | description: 281 | name: meta 282 | sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 283 | url: "https://pub.dev" 284 | source: hosted 285 | version: "1.15.0" 286 | mime: 287 | dependency: transitive 288 | description: 289 | name: mime 290 | sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 291 | url: "https://pub.dev" 292 | source: hosted 293 | version: "1.0.1" 294 | mocktail: 295 | dependency: "direct dev" 296 | description: 297 | name: mocktail 298 | sha256: dd85ca5229cf677079fd9ac740aebfc34d9287cdf294e6b2ba9fae25c39e4dc2 299 | url: "https://pub.dev" 300 | source: hosted 301 | version: "0.2.0" 302 | nested: 303 | dependency: transitive 304 | description: 305 | name: nested 306 | sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" 307 | url: "https://pub.dev" 308 | source: hosted 309 | version: "1.0.0" 310 | node_preamble: 311 | dependency: transitive 312 | description: 313 | name: node_preamble 314 | sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" 315 | url: "https://pub.dev" 316 | source: hosted 317 | version: "2.0.1" 318 | package_config: 319 | dependency: transitive 320 | description: 321 | name: package_config 322 | sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 323 | url: "https://pub.dev" 324 | source: hosted 325 | version: "2.0.2" 326 | path: 327 | dependency: transitive 328 | description: 329 | name: path 330 | sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" 331 | url: "https://pub.dev" 332 | source: hosted 333 | version: "1.9.0" 334 | pool: 335 | dependency: transitive 336 | description: 337 | name: pool 338 | sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" 339 | url: "https://pub.dev" 340 | source: hosted 341 | version: "1.5.0" 342 | provider: 343 | dependency: transitive 344 | description: 345 | name: provider 346 | sha256: "7896193cf752c40ba7f7732a95264319a787871e5d628225357f5c909182bc06" 347 | url: "https://pub.dev" 348 | source: hosted 349 | version: "6.0.2" 350 | pub_semver: 351 | dependency: transitive 352 | description: 353 | name: pub_semver 354 | sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" 355 | url: "https://pub.dev" 356 | source: hosted 357 | version: "2.1.4" 358 | shelf: 359 | dependency: transitive 360 | description: 361 | name: shelf 362 | sha256: "4592f6cb6c417632ebdfb63e4db42a7e3ad49d1bd52d9f93b6eb883035ddc0c3" 363 | url: "https://pub.dev" 364 | source: hosted 365 | version: "1.3.0" 366 | shelf_packages_handler: 367 | dependency: transitive 368 | description: 369 | name: shelf_packages_handler 370 | sha256: e0b44ebddec91e70a713e13adf93c1b2100821303b86a18e1ef1d082bd8bd9b8 371 | url: "https://pub.dev" 372 | source: hosted 373 | version: "3.0.0" 374 | shelf_static: 375 | dependency: transitive 376 | description: 377 | name: shelf_static 378 | sha256: "4a0d12cd512aa4fc55fed5f6280f02ef183f47ba29b4b0dfd621b1c99b7e6361" 379 | url: "https://pub.dev" 380 | source: hosted 381 | version: "1.1.0" 382 | shelf_web_socket: 383 | dependency: transitive 384 | description: 385 | name: shelf_web_socket 386 | sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d 387 | url: "https://pub.dev" 388 | source: hosted 389 | version: "1.0.1" 390 | sky_engine: 391 | dependency: transitive 392 | description: flutter 393 | source: sdk 394 | version: "0.0.99" 395 | source_map_stack_trace: 396 | dependency: transitive 397 | description: 398 | name: source_map_stack_trace 399 | sha256: "8c463326277f68a628abab20580047b419c2ff66756fd0affd451f73f9508c11" 400 | url: "https://pub.dev" 401 | source: hosted 402 | version: "2.1.0" 403 | source_maps: 404 | dependency: transitive 405 | description: 406 | name: source_maps 407 | sha256: "52de2200bb098de739794c82d09c41ac27b2e42fd7e23cce7b9c74bf653c7296" 408 | url: "https://pub.dev" 409 | source: hosted 410 | version: "0.10.10" 411 | source_span: 412 | dependency: transitive 413 | description: 414 | name: source_span 415 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 416 | url: "https://pub.dev" 417 | source: hosted 418 | version: "1.10.0" 419 | stack_trace: 420 | dependency: transitive 421 | description: 422 | name: stack_trace 423 | sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" 424 | url: "https://pub.dev" 425 | source: hosted 426 | version: "1.11.1" 427 | state_machine_bloc: 428 | dependency: "direct main" 429 | description: 430 | path: "../../packages/state_machine_bloc" 431 | relative: true 432 | source: path 433 | version: "0.0.1" 434 | stream_channel: 435 | dependency: transitive 436 | description: 437 | name: stream_channel 438 | sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 439 | url: "https://pub.dev" 440 | source: hosted 441 | version: "2.1.2" 442 | stream_transform: 443 | dependency: "direct main" 444 | description: 445 | name: stream_transform 446 | sha256: ed464977cb26a1f41537e177e190c67223dbd9f4f683489b6ab2e5d211ec564e 447 | url: "https://pub.dev" 448 | source: hosted 449 | version: "2.0.0" 450 | string_scanner: 451 | dependency: transitive 452 | description: 453 | name: string_scanner 454 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 455 | url: "https://pub.dev" 456 | source: hosted 457 | version: "1.2.0" 458 | term_glyph: 459 | dependency: transitive 460 | description: 461 | name: term_glyph 462 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 463 | url: "https://pub.dev" 464 | source: hosted 465 | version: "1.2.1" 466 | test: 467 | dependency: transitive 468 | description: 469 | name: test 470 | sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" 471 | url: "https://pub.dev" 472 | source: hosted 473 | version: "1.25.7" 474 | test_api: 475 | dependency: transitive 476 | description: 477 | name: test_api 478 | sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" 479 | url: "https://pub.dev" 480 | source: hosted 481 | version: "0.7.2" 482 | test_core: 483 | dependency: transitive 484 | description: 485 | name: test_core 486 | sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" 487 | url: "https://pub.dev" 488 | source: hosted 489 | version: "0.6.4" 490 | typed_data: 491 | dependency: transitive 492 | description: 493 | name: typed_data 494 | sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" 495 | url: "https://pub.dev" 496 | source: hosted 497 | version: "1.3.0" 498 | vector_math: 499 | dependency: transitive 500 | description: 501 | name: vector_math 502 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 503 | url: "https://pub.dev" 504 | source: hosted 505 | version: "2.1.4" 506 | vm_service: 507 | dependency: transitive 508 | description: 509 | name: vm_service 510 | sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" 511 | url: "https://pub.dev" 512 | source: hosted 513 | version: "14.2.5" 514 | watcher: 515 | dependency: transitive 516 | description: 517 | name: watcher 518 | sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" 519 | url: "https://pub.dev" 520 | source: hosted 521 | version: "1.1.0" 522 | web_socket_channel: 523 | dependency: transitive 524 | description: 525 | name: web_socket_channel 526 | sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" 527 | url: "https://pub.dev" 528 | source: hosted 529 | version: "2.1.0" 530 | webkit_inspection_protocol: 531 | dependency: transitive 532 | description: 533 | name: webkit_inspection_protocol 534 | sha256: "5adb6ab8ed14e22bb907aae7338f0c206ea21e7a27004e97664b16c120306f00" 535 | url: "https://pub.dev" 536 | source: hosted 537 | version: "1.0.0" 538 | yaml: 539 | dependency: transitive 540 | description: 541 | name: yaml 542 | sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" 543 | url: "https://pub.dev" 544 | source: hosted 545 | version: "3.1.0" 546 | sdks: 547 | dart: ">=3.3.0 <4.0.0" 548 | flutter: ">=3.18.0-18.0.pre.54" 549 | --------------------------------------------------------------------------------