Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -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"
29 changes: 19 additions & 10 deletions lib/src/base_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ abstract class BaseNotifier<T> extends Notifier<T> {

/// This can be overridden in:
/// ProviderScope(
/// overrides: BaseNotifier.errorLogger.overrideWithValue(myNewLogger),
/// overrides: BaseNotifier.logger.overrideWithValue(myNewLogger),
/// ...
/// )
static Provider<OperationErrorLogger> get errorLogger => _errorLogger;
static Provider<OperationLogger> get logger => _logger;

Map<String, CancelableOperation<void>> get operations => _activeOperations;

// endregion

final Map<String, CancelableOperation> _activeOperations = {};
final Map<String, CancelableOperation<void>> _activeOperations = {};

@mustCallSuper
@override
Expand Down Expand Up @@ -98,7 +98,7 @@ abstract class BaseNotifier<T> extends Notifier<T> {
setState(result);
}

final errorLogger = ref.read(BaseNotifier.errorLogger);
final logger = ref.read(BaseNotifier.logger);

// cancel existing operation with same identifier
unawaited(_activeOperations[identifier]?.cancel());
Expand Down Expand Up @@ -131,7 +131,7 @@ abstract class BaseNotifier<T> extends Notifier<T> {
stackTrace: stacktrace,
);

errorLogger.logError(displayableError, runtimeType, identifier);
logger.logError(displayableError, runtimeType, identifier);

setStateWhenMounted(ResultFailure<R>(displayableError));
}
Expand All @@ -142,7 +142,7 @@ abstract class BaseNotifier<T> extends Notifier<T> {
/// Cancel and clean up **all** running operations.
/// ⚠️ This need to handled by the creator of the instance
Future<void> cancelAllOperations() async {
final list = List<CancelableOperation>.from(_activeOperations.values);
final list = List<CancelableOperation<void>>.from(_activeOperations.values);

_activeOperations.clear();

Expand Down Expand Up @@ -174,14 +174,20 @@ abstract class BaseNotifier<T> extends Notifier<T> {
required String identifier,
FutureOr<void> Function()? onCancel,
}) {
final logger = ref.read(BaseNotifier.logger);

final operation = CancelableOperation<O>.fromFuture(
result,
onCancel: () => onCancel?.call(),
);

operation.then(
(_) => _activeOperations.remove(identifier),
onCancel: () => _activeOperations.remove(identifier),
onCancel: () {
_activeOperations.remove(identifier);

logger.logOperationCanceled(runtimeType, identifier);
},
onError: (_, _) => _activeOperations.remove(identifier),
);

Expand All @@ -190,11 +196,14 @@ abstract class BaseNotifier<T> extends Notifier<T> {
}
}

final _errorLogger = Provider<OperationErrorLogger>((ref) {
return _DummyOperationErrorLogger();
final _logger = Provider<OperationLogger>((ref) {
return _DummyOperationLogger();
});

class _DummyOperationErrorLogger implements OperationErrorLogger {
class _DummyOperationLogger implements OperationLogger {
@override
void logError(DisplayableError error, Type providerType, String identifier) {}

@override
void logOperationCanceled(Type providerType, String identifier) {}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:tapped_riverpod/src/error/displayble_error.dart';

abstract class OperationErrorLogger {
abstract class OperationLogger {
void logOperationCanceled(Type providerType, String identifier);

void logError(DisplayableError error, Type providerType, String identifier);
}
2 changes: 1 addition & 1 deletion lib/tapped_riverpod.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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


153 changes: 140 additions & 13 deletions test/base_notifier_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,124 @@ 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<int>(
() async {
await Future<void>.delayed(const Duration(seconds: 2));

fail('Cancelled operation should not run to completion');
},
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<void>.delayed(const Duration(milliseconds: 50));

final secondResult = await notifier.runCatching<int>(
() async {
secondActionStarted = true;
await Future<void>.delayed(const Duration(milliseconds: 100));
return 2;
},
identifier: identifier,
setState: (_) {},
);

await Future<void>.delayed(const Duration(seconds: 5));

expect(secondResult, 2);
},
);

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<int>(
() async {
await Future<void>.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<void>.delayed(Duration.zero);

unawaited(
notifier.runCatching<int>(
() async {
await Future<void>.delayed(const Duration(seconds: 2));

fail('Superseded operation should not run to completion');
},
identifier: "test",
setState: (result) {},
),
);

await notifier.runCatching<int>(
() async {
await Future<void>.delayed(const Duration(milliseconds: 300));
return 2;
},
identifier: "test",
setState: (result) {},
);

expect(operationCanceledCounter, 2);
},
);

test(
"OperationErrorLogger.logError should be called when error occur",
() async {
Expand All @@ -135,9 +253,9 @@ void main() {

final container = ProviderContainer(
overrides: [
BaseNotifier.errorLogger.overrideWithValue(
_CallbackErrorLogger(
onLog: (err, type, id) {
BaseNotifier.logger.overrideWithValue(
_CallbackOperationLogger(
onLogError: (err, type, id) {
emittedError = err.exception.toString();
notifierType = type;
requestId = id;
Expand All @@ -151,7 +269,7 @@ void main() {

await notifier.runCatching<int>(
() async {
await Future.delayed(const Duration(milliseconds: 23));
await Future<void>.delayed(const Duration(milliseconds: 23));

throw Exception("Expected error");
},
Expand All @@ -172,9 +290,10 @@ void main() {

final container = ProviderContainer(
overrides: [
BaseNotifier.errorLogger.overrideWithValue(
_CallbackErrorLogger(
onLog: (err, _, _) => emittedError = err.exception.toString(),
BaseNotifier.logger.overrideWithValue(
_CallbackOperationLogger(
onLogError: (err, _, _) =>
emittedError = err.exception.toString(),
),
),
],
Expand All @@ -184,7 +303,7 @@ void main() {

await notifier.runCatching<int>(
() async {
await Future.delayed(const Duration(milliseconds: 23));
await Future<void>.delayed(const Duration(milliseconds: 23));

return 0;
},
Expand Down Expand Up @@ -212,18 +331,26 @@ class _BaseTestNotifier extends BaseNotifier<String> {
}
}

class _CallbackErrorLogger extends OperationErrorLogger {
class _CallbackOperationLogger extends OperationLogger {
final void Function(
DisplayableError error,
Type providerType,
String identifier,
)
onLog;
)?
onLogError;

final void Function(Type providerType, String identifier)?
onLogOperationCanceled;

_CallbackErrorLogger({required this.onLog});
_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);
}
}
2 changes: 1 addition & 1 deletion test/lifecycle_base_notifier_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ void main() {
test("the lifecycle of the provider should be correct", () async {
final container = ProviderContainer();

expect(lifeCycle, []);
expect(lifeCycle, <String>[]);

final notifier = container.read(_testNotifierProvider.notifier);

Expand Down
Loading
Loading