-
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: initial impl of flutter_rearch
- Loading branch information
1 parent
bff328e
commit bdc68fe
Showing
16 changed files
with
912 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
79
packages/flutter_rearch/lib/src/side_effects/animation.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
54
packages/flutter_rearch/lib/src/side_effects/keep_alive.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
packages/flutter_rearch/lib/src/side_effects/text_editing_controller.dart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
} | ||
} |
Oops, something went wrong.