Skip to content

Commit

Permalink
Merge pull request #15 from jinyus/0.16.0
Browse files Browse the repository at this point in the history
v0.16.0
  • Loading branch information
jinyus authored Jan 3, 2024
2 parents eed87b0 + 9a1ee66 commit 3b42a70
Show file tree
Hide file tree
Showing 40 changed files with 2,018 additions and 1,769 deletions.
1 change: 1 addition & 0 deletions .github/workflows/dart.yml → .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
# branches: [ "main" ]
pull_request:
branches: ["main"]
types: [ready_for_review, review_requested] # Restrict to non-draft PRs

jobs:
build:
Expand Down
7 changes: 7 additions & 0 deletions state_beacon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 0.16.0

- beacon.toStream() now returns a broadcast stream
- Add lastData. isLoading and valueOrNull getters to AsyncValue
- Add optional beacon parameter to tryCatch
- Add WritableBeacon<AsyncValue>.tryCatch extension for handling asynchronous values

## 0.15.0

- Beacon.untracked() now only hide the update/access from encompassing effects.
Expand Down
57 changes: 52 additions & 5 deletions state_beacon/lib/src/async_value.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
import 'package:state_beacon/src/base_beacon.dart';

sealed class AsyncValue<T> {
T? _oldData;

/// This is useful when manually hanlding async state
/// and you want to keep track of the last successful data.
/// You can use the `lastData` getter to retrieve the last successful data
/// when in [AsyncError] or [AsyncLoading] state.
void setLastData(T? value) {
_oldData = value;
}

/// If this is [AsyncData], returns it's value.
/// Otherwise returns `null`.
T? get valueOrNull {
if (this case AsyncData<T>(:final value)) {
return value;
}
return null;
}

/// Returns the last data that was successfully loaded
/// This is useful when the current state is [AsyncError] or [AsyncLoading]
T? get lastData => valueOrNull ?? _oldData;

/// Casts this [AsyncValue] to [AsyncData] and return it's value
/// or throws [CastError] if this is not [AsyncData].
T unwrapValue() {
return (this as AsyncData<T>).value;
}

/// Executes the future provided and returns `AsyncData` with the result if successful
/// or `AsyncError` if the future throws an exception.
/// Returns `true` if this is [AsyncLoading] or [AsyncIdle].
bool get isLoading => this is AsyncLoading || this is AsyncIdle;

/// Executes the future provided and returns [AsyncData] with the result if successful
/// or [AsyncError] if an exception is thrown.
///
/// Supply an optional [WritableBeacon] that will be set throughout the various states.
///
/// /// Example:
/// ```dart
/// Future<String> fetchUserData() {
Expand All @@ -16,6 +47,11 @@ sealed class AsyncValue<T> {
///
/// beacon.value = AsyncLoading();
/// beacon.value = await AsyncValue.tryCatch(fetchUserData);
///```
/// You can also pass the beacon as a parameter; `loading`,`data` and `error` states,
/// as well as the last successful data will be set automatically.
///```dart
/// await AsyncValue.tryCatch(fetchUserData, beacon: beacon);
/// ```
///
/// Without `tryCatch`, handling the potential error requires more boilerplate code:
Expand All @@ -27,11 +63,22 @@ sealed class AsyncValue<T> {
/// beacon.value = AsyncError(err, stacktrace);
/// }
/// ```
static Future<AsyncValue<T>> tryCatch<T>(Future<T> Function() future) async {
static Future<AsyncValue<T>> tryCatch<T>(
Future<T> Function() future, {
WritableBeacon<AsyncValue<T>>? beacon,
}) async {
final oldData = beacon?.peek().lastData;

beacon?.set(AsyncLoading()..setLastData(oldData));

try {
return AsyncData(await future());
final data = AsyncData(await future());
beacon?.set(data);
return data;
} catch (e, s) {
return AsyncError(e, s);
final error = AsyncError<T>(e, s)..setLastData(oldData);
beacon?.set(error);
return error;
}
}
}
Expand Down
6 changes: 6 additions & 0 deletions state_beacon/lib/src/base_beacon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ abstract class BaseBeacon<T> implements ValueListenable<T> {
bool get isDisposed => _isDisposed;

final _widgetSubscribers = <int>{};

// coverage:ignore-start
// requires a manual GC trigger to test
final Finalizer<void Function()> _finalizer = Finalizer((fn) => fn());
// coverage:ignore-end

T peek() => _value;

Expand Down Expand Up @@ -247,6 +251,7 @@ abstract class BaseBeacon<T> implements ValueListenable<T> {

unsub = subscribe(handleNewValue);

// coverage:ignore-start
// clean up if the widget is disposed
// and value is never modified again
_finalizer.attach(
Expand All @@ -257,6 +262,7 @@ abstract class BaseBeacon<T> implements ValueListenable<T> {
},
detach: context,
);
// coverage:ignore-end

return _value;
}
Expand Down
19 changes: 9 additions & 10 deletions state_beacon/lib/src/beacons/future.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ abstract class FutureBeacon<T> extends ReadableBeacon<AsyncValue<T>> {

final bool cancelRunning;
AsyncValue<T>? _previousAsyncValue;
T? _lastData;

/// The last data that was successfully loaded
/// This is useful when the current state is [AsyncError] or [AsyncLoading]
T? get lastData => _lastData;
/// Alias for peek().lastData. Returns the last data that was successfully loaded
T? get lastData => _value.lastData;

@override
AsyncValue<T>? get previousValue => _previousAsyncValue;
Expand All @@ -22,7 +20,9 @@ abstract class FutureBeacon<T> extends ReadableBeacon<AsyncValue<T>> {
/// Internal method to start loading
@protected
int $startLoading() {
_setValue(AsyncLoading());
_setValue(
AsyncLoading()..setLastData(lastData),
);
return ++_executionID;
}

Expand All @@ -34,15 +34,15 @@ abstract class FutureBeacon<T> extends ReadableBeacon<AsyncValue<T>> {
if (cancelRunning && exeID != _executionID) return;

if (value is AsyncData) {
if (_lastData != null) {
if (lastData != null) {
// if _lastData == null, then it's the first time we are getting data,
// so we wouldn't have a previous value.

// ignore: null_check_on_nullable_type_parameter
_previousAsyncValue = AsyncData(_lastData!);
_previousAsyncValue = AsyncData(lastData!);
}

_lastData = value.unwrapValue();
} else if (value is AsyncError) {
value.setLastData(lastData);
}
_setValue(value, force: true);
}
Expand Down Expand Up @@ -79,7 +79,6 @@ abstract class FutureBeacon<T> extends ReadableBeacon<AsyncValue<T>> {

@override
void dispose() {
_lastData = null;
_previousAsyncValue = null;
_cancelAwaitedSubscription?.call();
Awaited.remove(this);
Expand Down
1 change: 1 addition & 0 deletions state_beacon/lib/src/extensions/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:state_beacon/src/base_beacon.dart';
import 'package:state_beacon/state_beacon.dart';

part 'iterable.dart';
part 'readable.dart';
Expand Down
5 changes: 2 additions & 3 deletions state_beacon/lib/src/extensions/readable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ part of 'extensions.dart';

extension ReadableBeaconUtils<T> on ReadableBeacon<T> {
/// Converts a [ReadableBeacon] to [Stream]
/// The stream can only be canceled by calling [dispose]
Stream<T> toStream({
FutureOr<void> Function()? onCancel,
}) {
Expand All @@ -17,10 +18,8 @@ extension ReadableBeaconUtils<T> on ReadableBeacon<T> {
onCancel?.call();
}

controller.onCancel = cancel;

onDispose(cancel);

return controller.stream;
return controller.stream.asBroadcastStream();
}
}
29 changes: 29 additions & 0 deletions state_beacon/lib/src/extensions/writable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,32 @@ extension WritableBeaconUtils<T> on WritableBeacon<T> {

ReadableBeacon<T> freeze() => this;
}

extension WritableAsyncBeacon<T> on WritableBeacon<AsyncValue<T>> {
/// Executes the future provided and automatically sets the beacon to the appropriate state.
///
/// ie. [AsyncLoading] while the future is running, [AsyncData] if the future completes successfully or [AsyncError] if the future throws an error.
///
/// /// Example:
/// ```dart
/// Future<String> fetchUserData() {
/// // Imagine this is a network request that might throw an error
/// return Future.delayed(Duration(seconds: 1), () => 'User data');
/// }
///
/// await beacon.tryCatch(fetchUserData);
///```
///
/// Without `tryCatch`, handling the potential error requires more boilerplate code:
/// ```dart
/// beacon.value = AsyncLoading();
/// try {
/// beacon.value = AsyncData(await fetchUserData());
/// } catch (err,stacktrace) {
/// beacon.value = AsyncError(err, stacktrace);
/// }
/// ```
Future<void> tryCatch(Future<T> Function() future) async {
await AsyncValue.tryCatch(future, beacon: this);
}
}
2 changes: 1 addition & 1 deletion state_beacon/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: state_beacon
description: A reactive primitive and simple state managerment solution for dart and flutter
version: 0.15.0
version: 0.16.0
repository: https://github.com/jinyus/dart_beacon

environment:
Expand Down
Loading

0 comments on commit 3b42a70

Please sign in to comment.