Skip to content

Commit

Permalink
feat: initial impl of flutter_rearch
Browse files Browse the repository at this point in the history
  • Loading branch information
GregoryConrad committed Jul 11, 2023
1 parent bff328e commit bdc68fe
Show file tree
Hide file tree
Showing 16 changed files with 912 additions and 10 deletions.
41 changes: 41 additions & 0 deletions packages/flutter_rearch/lib/flutter_rearch.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_rearch/src/widgets.dart';
import 'package:rearch/rearch.dart';

export 'src/side_effects.dart';
export 'src/widgets.dart';

/// The API exposed to [CapsuleConsumer]s to extend their functionality.
abstract interface class WidgetSideEffectApi implements SideEffectApi {
/// The [BuildContext] of the associated [CapsuleConsumer].
BuildContext get context;

/// Adds a deactivate lifecycle listener.
void addDeactivateListener(SideEffectApiCallback callback);

/// Removes the specified deactivate lifecycle listener.
void removeDeactivateListener(SideEffectApiCallback callback);
}

/// Defines what a [WidgetSideEffect] should look like (a [Function]
/// that consumes a [WidgetSideEffectApi] and returns something).
///
/// If your side effect is more advanced or requires parameters,
/// simply make a callable class instead of just a regular [Function]!
typedef WidgetSideEffect<T> = T Function(WidgetSideEffectApi);

/// Represents an object that can [register] [WidgetSideEffect]s.
abstract interface class WidgetSideEffectRegistrar
implements SideEffectRegistrar {
@override
T register<T>(WidgetSideEffect<T> sideEffect);
}

/// The [WidgetHandle] is to [Widget]s what a [CapsuleHandle] is to
/// [Capsule]s.
///
/// [WidgetHandle]s provide a mechanism to watch [Capsule]s and
/// register [SideEffect]s, so all Capsule-specific methodologies
/// carry over.
abstract interface class WidgetHandle
implements CapsuleReader, WidgetSideEffectRegistrar {}
69 changes: 69 additions & 0 deletions packages/flutter_rearch/lib/src/side_effects.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_rearch/flutter_rearch.dart';
import 'package:rearch/rearch.dart';

part 'side_effects/animation.dart';
part 'side_effects/keep_alive.dart';
part 'side_effects/text_editing_controller.dart';

/// A collection of the builtin [WidgetSideEffect]s.
extension BuiltinWidgetSideEffects on WidgetSideEffectRegistrar {
/// The [WidgetSideEffectApi] backing this [WidgetSideEffectRegistrar].
WidgetSideEffectApi api() => register((api) => api);

/// The [BuildContext] associated with this [WidgetSideEffectRegistrar].
BuildContext context() => register((api) => api.context);

/// Provides a way to easily get a copy of a [TextEditingController].
///
/// Pass an optional `initialText` to set the controller's initial text.
TextEditingController textEditingController({String? initialText}) =>
_textEditingController(this, initialText: initialText);

/// Creates a single use [TickerProvider].
/// Used by [animationController] when `vsync` is not set.
TickerProvider singleTickerProvider() => _singleTickerProvider(this);

/// Provides a way to easily get a copy of an [AnimationController].
///
/// When [vsync] is not given, one is created automatically via
/// [singleTickerProvider].
/// You *may not* change [vsync] after the first build.
///
/// All other fields are ignored after the first build except for [duration]
/// and [reverseDuration], whose new values will be updated in the
/// [AnimationController].
AnimationController animationController({
Duration? duration,
Duration? reverseDuration,
String? debugLabel,
double initialValue = 0,
double lowerBound = 0,
double upperBound = 1,
TickerProvider? vsync,
AnimationBehavior animationBehavior = AnimationBehavior.normal,
}) =>
_animationController(
this,
vsync: vsync,
duration: duration,
reverseDuration: reverseDuration,
debugLabel: debugLabel,
lowerBound: lowerBound,
upperBound: upperBound,
animationBehavior: animationBehavior,
initialValue: initialValue,
);

/// Prevents the associated [CapsuleConsumer] from being disposed when it
/// normally would be by its lazy list container (such as in a [ListView]).
///
/// Acts similar to [AutomaticKeepAlive].
///
/// When using [automaticKeepAlive], you *must* use it in a new
/// [CapsuleConsumer] that is a child of the container;
/// otherwise, the widget might still be disposed.
void automaticKeepAlive({bool keepAlive = true}) =>
_automaticKeepAlive(this, keepAlive: keepAlive);
}
79 changes: 79 additions & 0 deletions packages/flutter_rearch/lib/src/side_effects/animation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
part of '../side_effects.dart';

TickerProvider _singleTickerProvider(
WidgetSideEffectRegistrar use,
) {
final context = use.context();
final provider = use.memo(() => _SingleTickerProvider(context));
use.effect(
() {
// During dispose, ensure Ticker was disposed.
return () {
assert(
provider._ticker == null || !provider._ticker!.isActive,
'A Ticker created with use.singleTickerProvider() was not disposed, '
'causing the Ticker to leak. You need to call dispose() on the '
'AnimationController that consumes the Ticker to prevent this leak.',
);
};
},
[provider],
);

provider._ticker?.muted = !TickerMode.of(context);

return provider;
}

final class _SingleTickerProvider implements TickerProvider {
_SingleTickerProvider(this.context);

final BuildContext context;
Ticker? _ticker;

@override
Ticker createTicker(TickerCallback onTick) {
assert(
_ticker == null,
'${context.widget.runtimeType} attempted to create multiple '
'Tickers with a single use.singleTickerProvider(). '
'If you need multiple Tickers, call use.singleTickerProvider() '
'multiple times.',
);
return _ticker = Ticker(onTick, debugLabel: 'Created by $context');
}
}

AnimationController _animationController(
WidgetSideEffectRegistrar use, {
Duration? duration,
Duration? reverseDuration,
String? debugLabel,
double initialValue = 0,
double lowerBound = 0,
double upperBound = 1,
TickerProvider? vsync,
AnimationBehavior animationBehavior = AnimationBehavior.normal,
}) {
vsync ??= use.singleTickerProvider();

final controller = use.memo(
() => AnimationController(
vsync: vsync!,
duration: duration,
reverseDuration: reverseDuration,
debugLabel: debugLabel,
lowerBound: lowerBound,
upperBound: upperBound,
animationBehavior: animationBehavior,
value: initialValue,
),
);
use.effect(() => controller.dispose, [controller]);

controller
..duration = duration
..reverseDuration = reverseDuration;

return controller;
}
54 changes: 54 additions & 0 deletions packages/flutter_rearch/lib/src/side_effects/keep_alive.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
part of '../side_effects.dart';

// See https://github.com/flutter/flutter/issues/123712
class _CustomKeepAliveHandle with ChangeNotifier {
void removeKeepAlive() => super.notifyListeners();
}

void _automaticKeepAlive(
WidgetSideEffectRegistrar use, {
bool keepAlive = true,
}) {
final api = use.api();

final handle = use.memo(_CustomKeepAliveHandle.new);
use.effect(() => handle.dispose, [handle]);

// Dirty tracks whether or not we will need to request a new keep alive
// in case keepAlive == true
final (getDirty, setDirty) = use.rawValueWrapper(() => true);
final requestKeepAlive = use.memo(
() => () {
// It is only safe to dispatch a notification when dirty is true
if (getDirty()) {
setDirty(false);
KeepAliveNotification(handle).dispatch(api.context);
}
},
[getDirty, setDirty, handle, api.context],
);
final removeKeepAlive = use.memo(
() => () {
// It is always safe to remove keep alives
handle.removeKeepAlive();
setDirty(true);
},
[handle, setDirty],
);

// Remove keep alives on deactivate to prevent leaks per documentation
use.effect(
() {
api.addDeactivateListener(removeKeepAlive);
return () => api.removeDeactivateListener(removeKeepAlive);
},
[api, removeKeepAlive],
);

// Request/remove the keep alive as necessary
if (keepAlive) {
requestKeepAlive();
} else {
removeKeepAlive();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
part of '../side_effects.dart';

TextEditingController _textEditingController(
WidgetSideEffectRegistrar use, {
String? initialText,
}) {
final controller = use.memo(() => TextEditingController(text: initialText));
use.effect(() => controller.dispose, [controller]);
return controller;
}
8 changes: 8 additions & 0 deletions packages/flutter_rearch/lib/src/widgets.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_rearch/flutter_rearch.dart';
import 'package:rearch/rearch.dart';

part 'widgets/capsule_consumer.dart';
part 'widgets/capsule_container_provider.dart';
part 'widgets/capsule_warm_up.dart';
part 'widgets/bootstrap.dart';
45 changes: 45 additions & 0 deletions packages/flutter_rearch/lib/src/widgets/bootstrap.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
part of '../widgets.dart';

/// {@template rearch.bootstrapper}
/// Bootstraps rearch for use in Flutter.
///
/// Provides a [CapsuleContainer] to the rest of the [Widget] tree via
/// [CapsuleContainerProvider].
/// {@endtemplate}
class RearchBootstrapper extends StatefulWidget {
/// {@macro rearch.bootstrapper}
const RearchBootstrapper({
required this.child,
super.key,
});

/// The child of this [RearchBootstrapper].
final Widget child;

@override
State<RearchBootstrapper> createState() => _RearchBootstrapperState();
}

class _RearchBootstrapperState extends State<RearchBootstrapper> {
late final CapsuleContainer container;

@override
void initState() {
super.initState();
container = CapsuleContainer();
}

@override
void dispose() {
super.dispose();
container.dispose();
}

@override
Widget build(BuildContext context) {
return CapsuleContainerProvider(
container: container,
child: widget.child,
);
}
}
Loading

0 comments on commit bdc68fe

Please sign in to comment.