diff --git a/packages/wyatt_crud_bloc/example/lib/advanced_cubit_view.dart b/packages/wyatt_crud_bloc/example/lib/advanced_cubit_view.dart index 1cee2df2..9671663e 100644 --- a/packages/wyatt_crud_bloc/example/lib/advanced_cubit_view.dart +++ b/packages/wyatt_crud_bloc/example/lib/advanced_cubit_view.dart @@ -33,11 +33,11 @@ class AdvancedCubitView extends StatelessWidget { ), body: BlocProvider( create: (context) => - UserAdvancedCubit(context.read>())..getAll(), + UserAdvancedCubit(context.read>()) + ..streaming(), child: Builder(builder: (context) { return Column( children: [ - const Text("Data:"), BlocBuilder( buildWhen: (previous, current) { if (current is CrudLoading && current is! CrudReading) { @@ -117,31 +117,6 @@ class AdvancedCubitView extends StatelessWidget { }, ), ), - ElevatedButton( - onPressed: () { - context.read().getAll(); - }, - child: BlocBuilder( - buildWhen: (previous, current) { - if (current is CrudLoading && current is! CrudReading) { - return false; - } - return true; - }, - builder: (context, state) { - return state is CrudReading - ? const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - color: Colors.white, - ), - ) - : const Text("GetAll"); - }, - ), - ), - const SizedBox(height: 20), ], ); }), diff --git a/packages/wyatt_crud_bloc/example/lib/app.dart b/packages/wyatt_crud_bloc/example/lib/app.dart index 51e6a7d6..088f1324 100644 --- a/packages/wyatt_crud_bloc/example/lib/app.dart +++ b/packages/wyatt_crud_bloc/example/lib/app.dart @@ -16,6 +16,7 @@ import 'package:crud_bloc_example/advanced_cubit_view.dart'; import 'package:crud_bloc_example/basic_cubit_view.dart'; +import 'package:crud_bloc_example/streaming_cubit_view.dart'; import 'package:crud_bloc_example/user_entity.dart'; import 'package:crud_bloc_example/user_model.dart'; import 'package:flutter/material.dart'; @@ -65,32 +66,47 @@ class MyHomePage extends StatelessWidget { appBar: AppBar( title: const Text('Flutter Demo Home Page'), ), - body: Column( - children: [ - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const BasicCubitView(), - ), - ); - }, - child: const Text('Basic example'), - ), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const AdvancedCubitView(), - ), - ); - }, - child: const Text('Advanced example'), - ), - ], + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const BasicCubitView(), + ), + ); + }, + child: const Text('Basic example'), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const StreamingCubitView(), + ), + ); + }, + child: const Text('Streaming example'), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const AdvancedCubitView(), + ), + ); + }, + child: const Text('Advanced example'), + ), + ], + ), ), ); } diff --git a/packages/wyatt_crud_bloc/example/lib/streaming_cubit_view.dart b/packages/wyatt_crud_bloc/example/lib/streaming_cubit_view.dart new file mode 100644 index 00000000..8f1df40d --- /dev/null +++ b/packages/wyatt_crud_bloc/example/lib/streaming_cubit_view.dart @@ -0,0 +1,125 @@ +// Copyright (C) 2023 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'dart:math'; + +import 'package:crud_bloc_example/user_entity.dart'; +import 'package:crud_bloc_example/user_streaming_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:wyatt_crud_bloc/wyatt_crud_bloc.dart'; + +class StreamingCubitView extends StatelessWidget { + const StreamingCubitView({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Streaming Cubit"), + ), + body: BlocProvider( + create: (context) => + UserStreamingCubit(context.read>())..read(), + child: Builder(builder: (context) { + return Column( + children: [ + BlocBuilder( + buildWhen: (previous, current) { + if (current is CrudLoading && current is! CrudReading) { + return false; + } + return true; + }, + builder: (context, state) { + return Expanded( + child: CrudBuilder.typed>( + state: state, + builder: ((context, state) { + return ListView.builder( + itemCount: state.data.length, + itemBuilder: (context, index) { + final user = state.data.elementAt(index); + return ListTile( + title: Text(user?.name ?? 'Error'), + subtitle: Text(user?.email ?? 'Error'), + onTap: () { + context + .read() + .delete(id: (user?.id)!); + }, + onLongPress: () { + context.read().update( + single: UpdateParameters( + id: user?.id ?? '', + raw: { + 'email': '${user?.id}@updated.io', + }), + ); + }, + ); + }, + ); + }), + initialBuilder: (context, state) => + const Center(child: CircularProgressIndicator()), + loadingBuilder: (context, state) => + const Center(child: CircularProgressIndicator()), + errorBuilder: (context, state) => Text("Error: $state"), + ), + ); + }, + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + final r = Random().nextInt(1000); + context.read().create( + User( + id: '$r', + name: 'Wyatt $r', + email: '$r@wyattapp.io', + phone: '06$r', + ), + ); + }, + child: BlocBuilder( + buildWhen: (previous, current) { + if (current is CrudLoading && current is! CrudCreating) { + return false; + } + return true; + }, + builder: (context, state) { + return state is CrudCreating + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ) + : const Text("Create"); + }, + ), + ), + ], + ); + }), + ), + ); + } +} diff --git a/packages/wyatt_crud_bloc/example/lib/user_advanced_cubit.dart b/packages/wyatt_crud_bloc/example/lib/user_advanced_cubit.dart index 5ebbf0df..b61c3144 100644 --- a/packages/wyatt_crud_bloc/example/lib/user_advanced_cubit.dart +++ b/packages/wyatt_crud_bloc/example/lib/user_advanced_cubit.dart @@ -46,6 +46,9 @@ class UserAdvancedCubit extends CrudAdvancedCubit { @override Search? get crudSearch => Search(crudRepository); + @override + Streaming? get crudStreaming => Streaming(crudRepository); + @override Update? get crudUpdate => Update(crudRepository); diff --git a/packages/wyatt_crud_bloc/example/lib/user_streaming_cubit.dart b/packages/wyatt_crud_bloc/example/lib/user_streaming_cubit.dart new file mode 100644 index 00000000..45387137 --- /dev/null +++ b/packages/wyatt_crud_bloc/example/lib/user_streaming_cubit.dart @@ -0,0 +1,42 @@ +// Copyright (C) 2022 WYATT GROUP +// Please see the AUTHORS file for details. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import 'package:crud_bloc_example/user_entity.dart'; +import 'package:wyatt_crud_bloc/wyatt_crud_bloc.dart'; + +/// A [CrudCubit] for [User]. +class UserStreamingCubit extends CrudCubit { + final CrudRepository crudRepository; + + UserStreamingCubit(this.crudRepository); + + @override + DefaultCreate? get createOperation => Create(crudRepository); + + @override + DefaultDelete? get deleteOperation => Delete(crudRepository); + + @override + DefaultRead? get readOperation => Streaming(crudRepository); + + @override + DefaultUpdate? get updateOperation => Update(crudRepository); + + @override + ModelIdentifier get modelIdentifier => ModelIdentifier( + getIdentifier: (user) => user.id ?? '', + ); +} diff --git a/packages/wyatt_crud_bloc/lib/src/data/data_sources/crud_data_source_in_memory_impl.dart b/packages/wyatt_crud_bloc/lib/src/data/data_sources/crud_data_source_in_memory_impl.dart index d5ebd3f0..c5bff608 100644 --- a/packages/wyatt_crud_bloc/lib/src/data/data_sources/crud_data_source_in_memory_impl.dart +++ b/packages/wyatt_crud_bloc/lib/src/data/data_sources/crud_data_source_in_memory_impl.dart @@ -16,6 +16,7 @@ import 'dart:async'; +import 'package:rxdart/subjects.dart'; import 'package:wyatt_crud_bloc/src/core/enums/operation_type.dart'; import 'package:wyatt_crud_bloc/src/core/enums/where_query_type.dart'; import 'package:wyatt_crud_bloc/src/data/data_sources/crud_data_source.dart'; @@ -58,8 +59,8 @@ class CrudDataSourceInMemoryImpl extends CrudDataSource { }) : _data = data ?? {}; final Map> _data; - final StreamController?>> _streamData = - StreamController(); + final BehaviorSubject?>> _streamData = + BehaviorSubject?>>.seeded([]); @override Future create(Map object, {String? id}) async { @@ -142,7 +143,7 @@ class CrudDataSourceInMemoryImpl extends CrudDataSource { } return res; - }).asBroadcastStream(); + }); @override Future update( diff --git a/packages/wyatt_crud_bloc/lib/src/domain/usecases/stream.dart b/packages/wyatt_crud_bloc/lib/src/domain/usecases/streaming.dart similarity index 93% rename from packages/wyatt_crud_bloc/lib/src/domain/usecases/stream.dart rename to packages/wyatt_crud_bloc/lib/src/domain/usecases/streaming.dart index 68fcec8c..0a8184d6 100644 --- a/packages/wyatt_crud_bloc/lib/src/domain/usecases/stream.dart +++ b/packages/wyatt_crud_bloc/lib/src/domain/usecases/streaming.dart @@ -20,14 +20,14 @@ import 'package:wyatt_crud_bloc/src/domain/repositories/crud_repository.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/usecases.dart'; import 'package:wyatt_type_utils/wyatt_type_utils.dart'; -/// {@template stream} +/// {@template streaming} /// A use case that streams the object models. /// {@endtemplate} -class Stream +class Streaming extends Usecase>> with ReadOperation>> { - /// {@macro stream} - const Stream(this.crudRepository); + /// {@macro streaming} + const Streaming(this.crudRepository); final CrudRepository crudRepository; diff --git a/packages/wyatt_crud_bloc/lib/src/domain/usecases/usecases.dart b/packages/wyatt_crud_bloc/lib/src/domain/usecases/usecases.dart index cc0bc9d6..6f292edd 100644 --- a/packages/wyatt_crud_bloc/lib/src/domain/usecases/usecases.dart +++ b/packages/wyatt_crud_bloc/lib/src/domain/usecases/usecases.dart @@ -26,6 +26,7 @@ export 'get.dart'; export 'get_all.dart'; export 'params/params.dart'; export 'search.dart'; +export 'streaming.dart'; export 'update.dart'; export 'update_all.dart'; diff --git a/packages/wyatt_crud_bloc/lib/src/features/crud/blocs/crud_advanced_cubit.dart b/packages/wyatt_crud_bloc/lib/src/features/crud/blocs/crud_advanced_cubit.dart index 3f2d3bda..8c6fe57f 100644 --- a/packages/wyatt_crud_bloc/lib/src/features/crud/blocs/crud_advanced_cubit.dart +++ b/packages/wyatt_crud_bloc/lib/src/features/crud/blocs/crud_advanced_cubit.dart @@ -16,6 +16,7 @@ import 'dart:async'; +import 'package:wyatt_architecture/wyatt_architecture.dart'; import 'package:wyatt_crud_bloc/src/core/enums/where_query_type.dart'; import 'package:wyatt_crud_bloc/src/core/model_identifier.dart'; import 'package:wyatt_crud_bloc/src/domain/entities/query.dart'; @@ -24,11 +25,14 @@ import 'package:wyatt_crud_bloc/src/domain/usecases/delete.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/delete_all.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/get.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/get_all.dart'; +import 'package:wyatt_crud_bloc/src/domain/usecases/params/stream_parameters.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/params/update_parameters.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/search.dart'; +import 'package:wyatt_crud_bloc/src/domain/usecases/streaming.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/update.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/update_all.dart'; import 'package:wyatt_crud_bloc/src/features/crud/blocs/crud_base_cubit/crud_base_cubit.dart'; +import 'package:wyatt_type_utils/wyatt_type_utils.dart'; /// {@template crud_cubit_advanced} /// Cubit that handles CRUD operations with more granularity. @@ -43,6 +47,7 @@ abstract class CrudAdvancedCubit extends CrudBaseCubit { GetAll? get crudGetAll; Get? get crudGet; Search? get crudSearch; + Streaming? get crudStreaming; UpdateAll? get crudUpdateAll; Update? get crudUpdate; @@ -50,6 +55,8 @@ abstract class CrudAdvancedCubit extends CrudBaseCubit { /// Used to identify a model. ModelIdentifier get modelIdentifier; + StreamSubscription, AppException>>? _streamSubscription; + FutureOr create(Model model) async { final crud = crudCreate; if (crud == null) { @@ -59,6 +66,10 @@ abstract class CrudAdvancedCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudCreating()); final result = await crud.call(model); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( result.fold( (_) { @@ -97,6 +108,10 @@ abstract class CrudAdvancedCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudDeleting()); final result = await crud.call(id); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( result.fold( (_) { @@ -139,6 +154,10 @@ abstract class CrudAdvancedCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudDeleting()); final result = await crud.call(null); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( result.fold( (_) { @@ -202,6 +221,31 @@ abstract class CrudAdvancedCubit extends CrudBaseCubit { ); } + FutureOr streaming({ + String? id, + List? conditions, + }) async { + final crud = crudStreaming; + if (crud == null) { + return; + } + final result = await crud.call( + StreamParameters( + id: id, + conditions: conditions, + ), + ); + + _streamSubscription = result.ok?.listen((event) { + emit( + event.fold( + CrudListLoaded.new, + (error) => CrudError(error.toString()), + ), + ); + }); + } + FutureOr update(UpdateParameters param) async { final crud = crudUpdate; if (crud == null) { @@ -211,6 +255,10 @@ abstract class CrudAdvancedCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudUpdating()); final result = await crud.call(param); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( await result.foldAsync( (_) async { @@ -284,6 +332,10 @@ abstract class CrudAdvancedCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudUpdating()); final result = await crud.call(param); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( await result.foldAsync( (_) async { @@ -334,4 +386,10 @@ abstract class CrudAdvancedCubit extends CrudBaseCubit { ), ); } + + @override + Future close() async { + await _streamSubscription?.cancel(); + return super.close(); + } } diff --git a/packages/wyatt_crud_bloc/lib/src/features/crud/blocs/crud_cubit.dart b/packages/wyatt_crud_bloc/lib/src/features/crud/blocs/crud_cubit.dart index 80c8b19f..6495abba 100644 --- a/packages/wyatt_crud_bloc/lib/src/features/crud/blocs/crud_cubit.dart +++ b/packages/wyatt_crud_bloc/lib/src/features/crud/blocs/crud_cubit.dart @@ -16,6 +16,7 @@ import 'dart:async'; +import 'package:wyatt_architecture/wyatt_architecture.dart'; import 'package:wyatt_crud_bloc/src/core/enums/where_query_type.dart'; import 'package:wyatt_crud_bloc/src/core/mixins/operation.dart'; import 'package:wyatt_crud_bloc/src/core/model_identifier.dart'; @@ -25,11 +26,14 @@ import 'package:wyatt_crud_bloc/src/domain/usecases/delete.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/delete_all.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/get.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/get_all.dart'; +import 'package:wyatt_crud_bloc/src/domain/usecases/params/stream_parameters.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/params/update_parameters.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/search.dart'; +import 'package:wyatt_crud_bloc/src/domain/usecases/streaming.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/update.dart'; import 'package:wyatt_crud_bloc/src/domain/usecases/update_all.dart'; import 'package:wyatt_crud_bloc/src/features/crud/blocs/crud_base_cubit/crud_base_cubit.dart'; +import 'package:wyatt_type_utils/wyatt_type_utils.dart'; typedef DefaultCreate = CreateOperation; typedef DefaultRead = ReadOperation; @@ -63,6 +67,8 @@ abstract class CrudCubit extends CrudBaseCubit { /// Used to identify a model. ModelIdentifier get modelIdentifier; + StreamSubscription, AppException>>? _streamSubscription; + Expected? _checkOperation(dynamic operation) { if (operation == null) { return null; @@ -90,9 +96,11 @@ abstract class CrudCubit extends CrudBaseCubit { conditions != null) { return _search(conditions); } - if (_checkOperation>(readOperation) != null && - conditions == null) { - return _getAll(); + if (_checkOperation>(readOperation) != null) { + return _streaming( + id: id, + conditions: conditions, + ); } } @@ -129,6 +137,11 @@ abstract class CrudCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudCreating()); final result = await crud.call(model); + final crudStreaming = _checkOperation>(readOperation); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( result.fold( (_) { @@ -167,6 +180,11 @@ abstract class CrudCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudDeleting()); final result = await crud.call(id); + final crudStreaming = _checkOperation>(readOperation); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( result.fold( (_) { @@ -209,6 +227,11 @@ abstract class CrudCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudDeleting()); final result = await crud.call(null); + final crudStreaming = _checkOperation>(readOperation); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( result.fold( (_) { @@ -256,6 +279,31 @@ abstract class CrudCubit extends CrudBaseCubit { ); } + FutureOr _streaming({ + String? id, + List? conditions, + }) async { + final crud = _checkOperation>(readOperation); + if (crud == null) { + return; + } + final result = await crud.call( + StreamParameters( + id: id, + conditions: conditions, + ), + ); + + _streamSubscription = result.ok?.listen((event) { + emit( + event.fold( + CrudListLoaded.new, + (error) => CrudError(error.toString()), + ), + ); + }); + } + FutureOr _search(List conditions) async { final crud = _checkOperation>(readOperation); if (crud == null) { @@ -281,6 +329,11 @@ abstract class CrudCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudUpdating()); final result = await crud.call(param); + final crudStreaming = _checkOperation>(readOperation); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( await result.foldAsync( (_) async { @@ -360,6 +413,11 @@ abstract class CrudCubit extends CrudBaseCubit { final stateCopy = state; emit(const CrudUpdating()); final result = await crud.call(param); + final crudStreaming = _checkOperation>(readOperation); + if (crudStreaming != null && _streamSubscription != null && result.isOk) { + // If streaming is available, we don't need to update stateCopy. + return; + } emit( await result.foldAsync( (_) async { @@ -410,4 +468,10 @@ abstract class CrudCubit extends CrudBaseCubit { ), ); } + + @override + Future close() async { + await _streamSubscription?.cancel(); + return super.close(); + } } diff --git a/packages/wyatt_crud_bloc/pubspec.yaml b/packages/wyatt_crud_bloc/pubspec.yaml index 6f0174ae..cd9ec7eb 100644 --- a/packages/wyatt_crud_bloc/pubspec.yaml +++ b/packages/wyatt_crud_bloc/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: wyatt_type_utils: hosted: https://git.wyatt-studio.fr/api/packages/Wyatt-FOSS/pub/ version: ^0.0.5 + rxdart: ^0.27.7 dev_dependencies: flutter_test: { sdk: flutter }