From 065b59a5191caaf60387a1ed6f6122e9da7e4ba2 Mon Sep 17 00:00:00 2001 From: Stefan Schaller Date: Thu, 12 Mar 2026 23:15:53 +0100 Subject: [PATCH 1/4] rename logger --- lib/src/base_notifier.dart | 12 ++++++------ ...ation_error_logger.dart => operation_logger.dart} | 2 +- lib/tapped_riverpod.dart | 2 +- test/base_notifier_test.dart | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) rename lib/src/{operation_error_logger.dart => operation_logger.dart} (79%) diff --git a/lib/src/base_notifier.dart b/lib/src/base_notifier.dart index 0c58883..a8b477c 100644 --- a/lib/src/base_notifier.dart +++ b/lib/src/base_notifier.dart @@ -34,10 +34,10 @@ abstract class BaseNotifier extends Notifier { /// This can be overridden in: /// ProviderScope( - /// overrides: BaseNotifier.errorLogger.overrideWithValue(myNewLogger), + /// overrides: BaseNotifier.logger.overrideWithValue(myNewLogger), /// ... /// ) - static Provider get errorLogger => _errorLogger; + static Provider get logger => _logger; Map> get operations => _activeOperations; @@ -98,7 +98,7 @@ abstract class BaseNotifier extends Notifier { setState(result); } - final errorLogger = ref.read(BaseNotifier.errorLogger); + final errorLogger = ref.read(BaseNotifier.logger); // cancel existing operation with same identifier unawaited(_activeOperations[identifier]?.cancel()); @@ -190,11 +190,11 @@ abstract class BaseNotifier extends Notifier { } } -final _errorLogger = Provider((ref) { - return _DummyOperationErrorLogger(); +final _logger = Provider((ref) { + return _DummyOperationLogger(); }); -class _DummyOperationErrorLogger implements OperationErrorLogger { +class _DummyOperationLogger implements OperationLogger { @override void logError(DisplayableError error, Type providerType, String identifier) {} } diff --git a/lib/src/operation_error_logger.dart b/lib/src/operation_logger.dart similarity index 79% rename from lib/src/operation_error_logger.dart rename to lib/src/operation_logger.dart index d2b93f0..ac8a4f2 100644 --- a/lib/src/operation_error_logger.dart +++ b/lib/src/operation_logger.dart @@ -1,5 +1,5 @@ import 'package:tapped_riverpod/src/error/displayble_error.dart'; -abstract class OperationErrorLogger { +abstract class OperationLogger { void logError(DisplayableError error, Type providerType, String identifier); } diff --git a/lib/tapped_riverpod.dart b/lib/tapped_riverpod.dart index 74f1b03..e7ebd54 100644 --- a/lib/tapped_riverpod.dart +++ b/lib/tapped_riverpod.dart @@ -5,7 +5,7 @@ export 'src/error/displayble_error.dart'; export 'src/base_notifier.dart'; export 'src/result_filter_not_null_notifier.dart'; export 'src/result.dart'; -export 'src/operation_error_logger.dart'; +export 'src/operation_logger.dart'; // endregion diff --git a/test/base_notifier_test.dart b/test/base_notifier_test.dart index c94ccfa..3d3a131 100644 --- a/test/base_notifier_test.dart +++ b/test/base_notifier_test.dart @@ -135,8 +135,8 @@ void main() { final container = ProviderContainer( overrides: [ - BaseNotifier.errorLogger.overrideWithValue( - _CallbackErrorLogger( + BaseNotifier.logger.overrideWithValue( + _CallbackOperationLogger( onLog: (err, type, id) { emittedError = err.exception.toString(); notifierType = type; @@ -172,8 +172,8 @@ void main() { final container = ProviderContainer( overrides: [ - BaseNotifier.errorLogger.overrideWithValue( - _CallbackErrorLogger( + BaseNotifier.logger.overrideWithValue( + _CallbackOperationLogger( onLog: (err, _, _) => emittedError = err.exception.toString(), ), ), @@ -212,7 +212,7 @@ class _BaseTestNotifier extends BaseNotifier { } } -class _CallbackErrorLogger extends OperationErrorLogger { +class _CallbackOperationLogger extends OperationLogger { final void Function( DisplayableError error, Type providerType, @@ -220,7 +220,7 @@ class _CallbackErrorLogger extends OperationErrorLogger { ) onLog; - _CallbackErrorLogger({required this.onLog}); + _CallbackOperationLogger({required this.onLog}); @override void logError(DisplayableError error, Type providerType, String identifier) { From 3f7e544e652f4a1d90b5d19a9e702567c44d980a Mon Sep 17 00:00:00 2001 From: Stefan Schaller Date: Thu, 12 Mar 2026 23:39:48 +0100 Subject: [PATCH 2/4] add strong linter --- analysis_options.yaml | 7 ++-- lib/src/base_notifier.dart | 11 +++++- lib/src/operation_logger.dart | 2 + pubspec.yaml | 4 ++ test/base_notifier_test.dart | 69 ++++++++++++++++++++++++++++++++--- 5 files changed, 83 insertions(+), 10 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1..4daa9ac 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,5 @@ -include: package:flutter_lints/flutter.yaml +include: package:tapped_lints/flutter-3.29.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +analyzer: + exclude: + - "**.freezed.dart" diff --git a/lib/src/base_notifier.dart b/lib/src/base_notifier.dart index a8b477c..bd0cce6 100644 --- a/lib/src/base_notifier.dart +++ b/lib/src/base_notifier.dart @@ -181,7 +181,13 @@ abstract class BaseNotifier extends Notifier { operation.then( (_) => _activeOperations.remove(identifier), - onCancel: () => _activeOperations.remove(identifier), + onCancel: () { + _activeOperations.remove(identifier); + + ref + .read(BaseNotifier.logger) + .logOperationCanceled(runtimeType, identifier); + }, onError: (_, _) => _activeOperations.remove(identifier), ); @@ -197,4 +203,7 @@ final _logger = Provider((ref) { class _DummyOperationLogger implements OperationLogger { @override void logError(DisplayableError error, Type providerType, String identifier) {} + + @override + void logOperationCanceled(Type providerType, String identifier) {} } diff --git a/lib/src/operation_logger.dart b/lib/src/operation_logger.dart index ac8a4f2..8199ec1 100644 --- a/lib/src/operation_logger.dart +++ b/lib/src/operation_logger.dart @@ -1,5 +1,7 @@ import 'package:tapped_riverpod/src/error/displayble_error.dart'; abstract class OperationLogger { + void logOperationCanceled(Type providerType, String identifier); + void logError(DisplayableError error, Type providerType, String identifier); } diff --git a/pubspec.yaml b/pubspec.yaml index a7a7751..e0c99cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,10 @@ dev_dependencies: json_serializable: ^6.11.1 freezed: ^3.2.3 flutter_lints: ^6.0.0 + tapped_lints: + git: + url: https://github.com/tappeddev/tapped_lints.git + ref: master test: ^1.26.3 diff --git a/test/base_notifier_test.dart b/test/base_notifier_test.dart index 3d3a131..1ab26b5 100644 --- a/test/base_notifier_test.dart +++ b/test/base_notifier_test.dart @@ -126,6 +126,54 @@ void main() { expect(actualResult, const ResultSuccess(2)); }); + test( + "when the same action is called twice, the second call's Future completes with the second result", + () async { + final container = ProviderContainer(); + + final notifier = container.read(_testNotifierProvider.notifier); + + final identifier = "test"; + + bool secondActionStarted = false; + + unawaited( + notifier.runCatching( + () async { + await Future.delayed(const Duration(seconds: 2)); + + return 1; + }, + identifier: identifier, + setState: (result) { + expect( + secondActionStarted, + false, + reason: + "After second action started, this should not be called, because the first action is not completed", + ); + }, + ), + ); + + await Future.delayed(const Duration(milliseconds: 50)); + + final secondResult = await notifier.runCatching( + () async { + secondActionStarted = true; + await Future.delayed(const Duration(milliseconds: 100)); + return 2; + }, + identifier: identifier, + setState: (_) {}, + ); + + await Future.delayed(const Duration(seconds: 5)); + + expect(secondResult, 2); + }, + ); + test( "OperationErrorLogger.logError should be called when error occur", () async { @@ -137,7 +185,7 @@ void main() { overrides: [ BaseNotifier.logger.overrideWithValue( _CallbackOperationLogger( - onLog: (err, type, id) { + onLogError: (err, type, id) { emittedError = err.exception.toString(); notifierType = type; requestId = id; @@ -174,7 +222,8 @@ void main() { overrides: [ BaseNotifier.logger.overrideWithValue( _CallbackOperationLogger( - onLog: (err, _, _) => emittedError = err.exception.toString(), + onLogError: (err, _, _) => + emittedError = err.exception.toString(), ), ), ], @@ -217,13 +266,21 @@ class _CallbackOperationLogger extends OperationLogger { DisplayableError error, Type providerType, String identifier, - ) - onLog; + )? + onLogError; - _CallbackOperationLogger({required this.onLog}); + final void Function(Type providerType, String identifier)? + onLogOperationCanceled; + + _CallbackOperationLogger({this.onLogError, this.onLogOperationCanceled}); @override void logError(DisplayableError error, Type providerType, String identifier) { - onLog(error, providerType, identifier); + onLogError?.call(error, providerType, identifier); + } + + @override + void logOperationCanceled(Type providerType, String identifier) { + onLogOperationCanceled?.call(providerType, identifier); } } From 1da6b217a792c939df2ae83f119aadb4b70dbe80 Mon Sep 17 00:00:00 2001 From: Stefan Schaller Date: Fri, 13 Mar 2026 07:21:28 +0100 Subject: [PATCH 3/4] add some more tests --- lib/src/base_notifier.dart | 14 ++-- test/base_notifier_test.dart | 74 ++++++++++++++++++- test/lifecycle_base_notifier_test.dart | 2 +- .../result_filter_not_null_notifier_test.dart | 38 +++++----- 4 files changed, 100 insertions(+), 28 deletions(-) diff --git a/lib/src/base_notifier.dart b/lib/src/base_notifier.dart index bd0cce6..4062f28 100644 --- a/lib/src/base_notifier.dart +++ b/lib/src/base_notifier.dart @@ -43,7 +43,7 @@ abstract class BaseNotifier extends Notifier { // endregion - final Map _activeOperations = {}; + final Map> _activeOperations = {}; @mustCallSuper @override @@ -98,7 +98,7 @@ abstract class BaseNotifier extends Notifier { setState(result); } - final errorLogger = ref.read(BaseNotifier.logger); + final logger = ref.read(BaseNotifier.logger); // cancel existing operation with same identifier unawaited(_activeOperations[identifier]?.cancel()); @@ -131,7 +131,7 @@ abstract class BaseNotifier extends Notifier { stackTrace: stacktrace, ); - errorLogger.logError(displayableError, runtimeType, identifier); + logger.logError(displayableError, runtimeType, identifier); setStateWhenMounted(ResultFailure(displayableError)); } @@ -142,7 +142,7 @@ abstract class BaseNotifier extends Notifier { /// Cancel and clean up **all** running operations. /// ⚠️ This need to handled by the creator of the instance Future cancelAllOperations() async { - final list = List.from(_activeOperations.values); + final list = List>.from(_activeOperations.values); _activeOperations.clear(); @@ -174,6 +174,8 @@ abstract class BaseNotifier extends Notifier { required String identifier, FutureOr Function()? onCancel, }) { + final logger = ref.read(BaseNotifier.logger); + final operation = CancelableOperation.fromFuture( result, onCancel: () => onCancel?.call(), @@ -184,9 +186,7 @@ abstract class BaseNotifier extends Notifier { onCancel: () { _activeOperations.remove(identifier); - ref - .read(BaseNotifier.logger) - .logOperationCanceled(runtimeType, identifier); + logger.logOperationCanceled(runtimeType, identifier); }, onError: (_, _) => _activeOperations.remove(identifier), ); diff --git a/test/base_notifier_test.dart b/test/base_notifier_test.dart index 1ab26b5..9c396a5 100644 --- a/test/base_notifier_test.dart +++ b/test/base_notifier_test.dart @@ -174,6 +174,76 @@ void main() { }, ); + test( + "OperationErrorLogger.logOperationCanceled should be called when action is cancelled or overridden", + () async { + int operationCanceledCounter = 0; + + final container = ProviderContainer( + overrides: [ + BaseNotifier.logger.overrideWithValue( + _CallbackOperationLogger( + onLogOperationCanceled: (type, id) { + expect( + type, + _BaseTestNotifier, + reason: "The type should be correct", + ); + + expect(id, "test", reason: "The id should be correct"); + + operationCanceledCounter++; + }, + ), + ), + ], + ); + + final notifier = container.read(_testNotifierProvider.notifier); + + unawaited( + notifier.runCatching( + () async { + await Future.delayed(const Duration(seconds: 2)); + + fail('Cancelled operation should not run to completion'); + }, + identifier: "test", + setState: (result) {}, + ), + ); + + notifier.cancelOperationBy(identifier: "test"); + + expect(operationCanceledCounter, 1); + + await Future.delayed(Duration.zero); + + unawaited( + notifier.runCatching( + () async { + await Future.delayed(const Duration(seconds: 2)); + + fail('Superseded operation should not run to completion'); + }, + identifier: "test", + setState: (result) {}, + ), + ); + + await notifier.runCatching( + () async { + await Future.delayed(const Duration(milliseconds: 300)); + return 2; + }, + identifier: "test", + setState: (result) {}, + ); + + expect(operationCanceledCounter, 2); + }, + ); + test( "OperationErrorLogger.logError should be called when error occur", () async { @@ -199,7 +269,7 @@ void main() { await notifier.runCatching( () async { - await Future.delayed(const Duration(milliseconds: 23)); + await Future.delayed(const Duration(milliseconds: 23)); throw Exception("Expected error"); }, @@ -233,7 +303,7 @@ void main() { await notifier.runCatching( () async { - await Future.delayed(const Duration(milliseconds: 23)); + await Future.delayed(const Duration(milliseconds: 23)); return 0; }, diff --git a/test/lifecycle_base_notifier_test.dart b/test/lifecycle_base_notifier_test.dart index f984f91..dd4640a 100644 --- a/test/lifecycle_base_notifier_test.dart +++ b/test/lifecycle_base_notifier_test.dart @@ -9,7 +9,7 @@ void main() { test("the lifecycle of the provider should be correct", () async { final container = ProviderContainer(); - expect(lifeCycle, []); + expect(lifeCycle, []); final notifier = container.read(_testNotifierProvider.notifier); diff --git a/test/result_filter_not_null_notifier_test.dart b/test/result_filter_not_null_notifier_test.dart index 8674330..eab6c51 100644 --- a/test/result_filter_not_null_notifier_test.dart +++ b/test/result_filter_not_null_notifier_test.dart @@ -5,24 +5,24 @@ void main() { group('ResultFilterNotNullNotifier', () { _test( testName: "initial value should be correct", - initialState: ResultSuccess("Initial-Data"), + initialState: const ResultSuccess("Initial-Data"), fireUpdated: (_) {}, expectedOutputs: ["Initial-Data"], ); _test( testName: "initial value should be null if there nothing else specified", - initialState: ResultInitial(), + initialState: const ResultInitial(), fireUpdated: (_) {}, expectedOutputs: [null], ); _test( testName: "should not change state when filterMap returns null", - initialState: ResultSuccess("Initial-Data"), + initialState: const ResultSuccess("Initial-Data"), fireUpdated: (prov) { prov - ..setResult(Result.loading()) + ..setResult(const Result.loading()) ..setResult( Result.failure( DisplayableError( @@ -37,34 +37,36 @@ void main() { _test( testName: "should not change state when same value is reported again", - initialState: ResultSuccess("Data"), + initialState: const ResultSuccess("Data"), fireUpdated: (prov) { prov - ..setResult(Result.success("Data")) - ..setResult(Result.success("Data")); + ..setResult(const Result.success("Data")) + ..setResult(const Result.success("Data")); }, expectedOutputs: ["Data"], ); _test( testName: "should emit changes", - initialState: ResultSuccess("Initial-Data"), + initialState: const ResultSuccess("Initial-Data"), fireUpdated: (provider) { provider - ..setResult(Result.loading()) - ..setResult(Result.success("Data-2")) - ..setResult(Result.loading()) - ..setResult(Result.success("Data-3")) - ..setResult(Result.success("Data-3")) - ..setResult(Result.loading()) - ..setResult(Result.success("Data-4")); + ..setResult(const Result.loading()) + ..setResult(const Result.success("Data-2")) + ..setResult(const Result.loading()) + ..setResult(const Result.success("Data-3")) + ..setResult(const Result.success("Data-3")) + ..setResult(const Result.loading()) + ..setResult(const Result.success("Data-4")); }, expectedOutputs: ["Initial-Data", "Data-2", "Data-3", "Data-4"], ); }); test("Map data with different generic types", () { - final inner = NotifierProvider(() => _TestBaseNotifier(ResultInitial())); + final inner = NotifierProvider( + () => _TestBaseNotifier(const ResultInitial()), + ); final provider = NotifierProvider( () => ResultFilterNotNullNotifier( @@ -86,7 +88,7 @@ void main() { fireImmediately: true, ); - container.read(inner.notifier).setResult(ResultLoading()); + container.read(inner.notifier).setResult(const ResultLoading()); container .read(inner.notifier) .setResult( @@ -98,7 +100,7 @@ void main() { ), ); - container.read(inner.notifier).setResult(ResultSuccess("Logged in")); + container.read(inner.notifier).setResult(const ResultSuccess("Logged in")); expect(events, [false, true]); }); From 51ad4e0d4a4696217cef120be4d21f21a036cb11 Mon Sep 17 00:00:00 2001 From: Stefan Schaller Date: Fri, 13 Mar 2026 07:23:40 +0100 Subject: [PATCH 4/4] improve test --- test/base_notifier_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/base_notifier_test.dart b/test/base_notifier_test.dart index 9c396a5..171d650 100644 --- a/test/base_notifier_test.dart +++ b/test/base_notifier_test.dart @@ -142,7 +142,7 @@ void main() { () async { await Future.delayed(const Duration(seconds: 2)); - return 1; + fail('Cancelled operation should not run to completion'); }, identifier: identifier, setState: (result) {