katana_scoped

Last updated:

0 purchases

katana_scoped Image
katana_scoped Images
Add to Cart

Description:

katana scoped

Katana Scoped
















[GitHub] | [YouTube] | [Packages] | [Twitter] | [Threads] | [LinkedIn] | [mathru.net]

Introduction #
Flutter's status is divided into the following two categories as officially described.
https://docs.flutter.dev/development/data-and-backend/state-mgmt/ephemeral-vs-app

Ephemeral state

Closed state within a single widget. The current position of the navigation bar, the current input state of the text form, etc.


App state

State shared within an app. Used between multiple Widgets, e.g. data retrieved from DB, login status, user preferences, etc.



Ephemeral state is available as soon as a widget is created and should be destroyed as soon as the widget is destroyed. In contrast, App state is retained even after the widget is destroyed, and should be available to all widgets in the same way.
There are many state management methods such as State+StatefulWidget, Provider+ChangeNotifier, riverpod+StateNotifier, GetX, etc., but not many of them explicitly make such a division.
Therefore, I believe that they are consciously (or unconsciously) changing the way they handle states by being creative in their use of packages and by combining multiple state management methods.
(I used riverpod for App state and StatefulWidget for Ephemeral state)
Therefore, I further redefined the above state types as scopes as shown below and created a state management package that can be used to explicitly separate them.

Widget

Individual widget, synonymous with Ephemeral state
ScopedWidget<T> can take it
Manage closed states on individual widgets, such as show/hide toggles and current input state of text forms


Page

One screen of the application. Assumed to have single and multiple Widget
PageScopedWidget will be able to take it
Manage closed states on one screen, such as the current position of the navigation bar


App

The entire app, synonymous with App state
You can get it from anywhere.
Manage data retrieved from DB, login status, user preferences, and other status used throughout the application



Ephemeral state is divided into two types, Page and Widget, because there are many opportunities to manage the state of each screen across widgets, and we felt it would be more convenient.
In fact, when controlling a map, you want to use a single controller for each widget placed in the map, but if you want to discard a controller when you move the screen to save memory, you may want to prepare a controller closed in Page scope for easier handling. It may be easier to handle if you have a controller closed in the Page scope.
You can simply create a Widget by inheriting from a specific abstract class, like riverpod's ConsumerWidget.
You can manage the state like flutter_hooks without writing special boilerplate.
Samples of famous counter applications can be created with this simplicity.
// counter_page.dart

class CounterPage extends PageScopedWidget {
const CounterPage({super.key});

@override
Widget build(BuildContext context, PageRef ref) {
final counter = ref.page.watch((ref) => ValueNotifier(0));

return Scaffold(
appBar: AppBar(
title: const Text("Test App"),
),
body: Center(
child: Text("${counter.value}"),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
counter.value++;
},
child: const Icon(Icons.add),
),
);
}
}
copied to clipboard
I also make it easy to add custom methods like flutter_hooks.
By explicitly specifying the scope at that time, users will be able to use it without being aware of the scope.
// repository_value.dart

extension RepositoryAppRefExtension on AppScopedValueOrAppRef {
Repository repository(){
return getScopedValue(
(ref) => RepositoryValue(),
listen: true,
);
}
}

class Repository with ChangeNotifier {
~~~~~
}

class RepositoryValue extends ScopedValue<Repository> {
@override
ScopedValueState<Repository, RepositoryValue> createState() =>
RepositoryValueState();
}

class RepositoryValueState
extends ScopedValueState<Repository, RepositoryValue> {
late Repository repository;

@override
void initValue() {
super.initValue();
repository = Repository();
repository.addListener(_handeldOnUpdate);
}

void _handeldOnUpdate() {
setState(() {});
}

@override
void dispose() {
super.dispose();
repository.removeListener(_handeldOnUpdate);
repository.dispose();
}

@override
Repository build() => repository;
}
copied to clipboard
If the above implementation is done beforehand, it can be used as follows
// test_page.dart

class TestPage extends PageScopedWidget {
const TestPage();

@override
Widget build(BuildContext context, PageRef ref){
// DB Repository for app.
final respository = ref.app.repository();
~~~~
}
}
copied to clipboard
By applying the above mechanisms, it is possible not only to manage the state of the system, but also to do the following and more.

Manage page and widget lifecycle
Manual redraw of pages and widgets
Wait for Future to finish and redraw at the end
Redraw when Stream value is updated

Installation #
Import the following packages
flutter pub add katana_scoped
copied to clipboard
Implementation #
Advance preparation #
Be sure to create an AppRef and place the AppScoped widget near the root of the app.
// main.dart
import 'package:flutter/material.dart';
import 'package:katana_scoped/katana_scoped.dart';

void main() {
runApp(const MyApp());
}

final appRef = AppRef();

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return AppScoped(
appRef: appRef,
child: MaterialApp(
home: const ScopedTestPage(),
title: "Flutter Demo",
theme: ThemeData(
primarySwatch: Colors.blue,
),
),
);
}
}
copied to clipboard
Defining AppRef as a global variable makes it possible to retrieve the state even from outside the Widget.
Create a Page #
When creating a page, implement a widget that extends PageScopedWidget.
From PageRef, app and page can be obtained, and the status can be obtained in App scope and Page scope, respectively.
// counter_page.dart
import 'package:flutter/material.dart';
import 'package:katana_scoped/katana_scoped.dart';

class CounterPage extends PageScopedWidget {
const CounterPage({super.key});

@override
Widget build(BuildContext context, PageRef ref) {
final counter = ref.page.watch((ref) => ValueNotifier(0));

return Scaffold(
appBar: AppBar(
title: const Text("Test App"),
),
body: Center(
child: Text("${counter.value}"),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
counter.value++;
},
child: const Icon(Icons.add),
),
);
}
}
copied to clipboard
Create a Widget #
Widget under the page can be placed anything such as StatelessWidget or StatefulWidget, but if you want to manage the state, you can do so by creating a ScopedWidget.
From WidgetRef, app, page, and widget can be obtained, and their states can be obtained in App scope, Page scope, and Widget scope, respectively.
// scoped_test_page.dart
import 'package:flutter/material.dart';
import 'package:katana_scoped/katana_scoped.dart';

class ScopedTestPage extends PageScopedWidget {
const ScopedTestPage({super.key});

@override
Widget build(BuildContext context, PageRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text("Test App"),
),
body: ScopedTestContent(),
);
}
}

class ScopedTestContent extends ScopedWidget {
const ScopedTestContent({
super.key,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final widget = ref.widget.watch((ref) => ValueNotifier(0));
final page = ref.page.watch((ref) => ValueNotifier(0));
final app = ref.app.watch((ref) => ValueNotifier(0));

return Column(
children: [
ListTile(
title: Text(app.value.toString()),
onTap: () {
app.value++;
},
),
ListTile(
title: Text(page.value.toString()),
onTap: () {
page.value++;
},
),
ListTile(
title: Text(widget.value.toString()),
onTap: () {
widget.value++;
},
),
],
);
}
}
copied to clipboard
Available ScopedValueFunction #
When the state is acquired in App scope, Page scope, or Widget scope, it is acquired through ScopedValueFunction.
The default ScopedValueFunction defined in the package is

T cache<T>(T Function(Ref ref) callback, { List<Object> keys = const [], String? name })

Cache the value returned by callback.

The ref passed to callback is the Ref that was passed when this method was called.


If the value of keys is changed, callback is executed again to update the cached value.
name can be specified to save it as a different state.


T watch<T extends Listenable>( T Function(Ref ref) callback, { List<Object> keys = const [], String? name, bool disposal = true })

Cache the value returned by callback.

The ref passed to callback is the Ref that was passed when this method was called.


If notifyListners are executed inside T monitoring values on further executed Widget, the Widget will be redrawn.
If the value of keys is changed, callback is executed again to update the cached value.
name can be specified to save it as a different state.
If disposal is set to false, the given ChangeNotifier will not be destroyed if the reference is lost.


OnContext on({ FutureOr<void> Function()? initOrUpdate, VoidCallback? disposed, List<Object> keys = const [], String? name })

Only page scope and widget scope can be executed.
It is possible to execute each process on the life cycle of the executed widget.

initOrUpdate

Processing runs the first time the on method is executed and the first time it is executed with a different value for keys.
If the value is returned as Future, the end can be monitored and detected by OnContext.future returned from the on method.
It is possible to implement a CircularProgressIndicator that performs some kind of initialization process and displays the CircularProgressIndicator until it finishes.


disposed

Executed when the widget is destroyed.






void refresh()

When executed, the associated widget will be redrawn.


T query<T>(ScopedQuery<T> query)

Define a query that provides a global state like riverpod and read it to manage the state.
See below for details.



ScopedQuery #
By defining ScopedQuery separately, it is possible to issue and use queries that provide state globally like riverpod. T query<T>(ScopedQuery<T> query) of each scope can be used to manage the state.
It is also possible to load further other queries with ref available in the callback.
final valueNotifierQuery = ChangeNotifierScopedQuery(
(ref) => ValueNotifier(0),
);

class TestPage extends PageScopedWidget {
@override
Widget build(BuildContext context, PageRef ref) {
final valueNotifier = ref.page.query(valueNotifierQuery);

return Scaffold(
body: Center(child: Text("${valueNotifier.value}")),
);
}
}
copied to clipboard
The following types of ScopedQuery are available

ScopedQuery

Holds the value returned by the callback.
It has the same functionality as cache.


ChangeNotifierScopedQuery

Holds the value returned in the callback and monitors for value changes.
It has the same functionality as watch.



It is also possible to create ScopedQuery specific to each scope.

AppScopedQuery

Create a ScopedQuery limited to the App scope.


PageScopedQuery

Create a ScopedQuery limited to Page scope.


WidgetScopedQuery

Create a ScopedQuery limited to the Widget scope.



Add ScopedValueFunction #
Explicitly limited scope use #
A new ScopedValueFunction can be added using an existing ScopedValueFunction.
In such cases, it is possible to use a limited scope.
For example, suppose that a class for retrieving data from DB is created with XXRepository by inheriting ChangeNotifier.
I want to manage the data from the DB in App scope, so by default, I use the following.
final userRepository = ref.app.watch((ref) => UserRepository());
copied to clipboard
In this case, however, it can also be written as follows
final userRepository = ref.page.watch((ref) => UserRepository());
copied to clipboard
In this case, the state can be managed, but when the page is destroyed, the state is also destroyed.
So, add a separate ScopedValueFunction to force it to be managed as a scope of the application.
Extension is used for addition.
// user_repository_extension.dart

extension UserRepositoryAppRef on RefHasApp {
TRepository repository<TRepository extends Repository>(
TRepository source,
) {
return app.watch((ref) => source);
}
}
copied to clipboard
By implementing the above, it can be used as follows.
final userRepository = ref.repository(UserRepository());
copied to clipboard
ScopedValueFunction can be defined in various scopes by changing the class specified by on.

on Ref

ScopedValueFunction will be added to all AppRef, PageRef.app, PageRef.page, WidgetRef.app, WidgetRef.page, and WidgetRef.widget.


on AppRef

ScopedValueFunction is added to AppRef only.


on PageRef

ScopedValueFunction is added to PageRef only.


on WidgetRef

ScopedValueFunction is added to WidgetRef only.


on RefHasApp

ScopedValueFunction is added to references with .app, i.e. PageRef and WidgetRef. Only interfaces with .app are available.


on RefHasPage

ScopedValueFunction is added to references with .page, i.e. PageRef and WidgetRef. Only interfaces with .page are available.


on RefHasWidget

ScopedValueFunction is added to the reference with .widget, i.e. WidgetRef. Only interfaces with .widget are available.


on AppScopedValueRef

ScopedValueFunction is added to the .app itself, i.e. PageRef.app and WidgetRef.app.


on PageScopedValueRef

ScopedValueFunction is added to the .page itself, i.e. PageRef.page and WidgetRef.page.


on WidgetScopedValueRef

ScopedValueFunction is added to the .widget itself, i.e., WidgetRef.widget.


on AppScopedValueOrAppRef

AppRef and .app itself, i.e. AppRef, PageRef.app and WidgetRef.app, will all have ScopedValueFunction in their App scopes.


on PageOrWidgetScopedValueRef

ScopedValueFunction is added to the .page itself or the .widget itself, i.e. PageRef.page, WidgetRef.page, WidgetRef.widget.


on PageOrAppScopedValueOrAppRef

ScopedValueFunction will be added to AppRef and .app itself, i.e. AppRef and PageRef.app, PageRef.page, WidgetRef.app, WidgetRef.page.


on QueryScopedValueRef<TRef extends Ref>

ScopedValueFunction is added to the Ref passed to the ScopedQuery provider.



Create a new ScopedValue #
It is also possible to create a new ScopedValue and add a ScopedValueFunction with new functionality.
For example, if you want to implement a function like a so-called FutureBuilder that executes a process that returns a Future and redraws the screen when the Future is completed, create a new ScopedValue and ScopedValueState as shown below.
// future_value.dart

class FutureValue<T> extends ScopedValue<Future<T>> {
const FutureValue(this.future);

final Future<T> future;

@override
ScopedValueState<Future<T>, ScopedValue<Future<T>>> createState() =>
FutureValueState<T>();
}

class FutureValueState<T> extends ScopedValueState<Future<T>, FutureValue<T>> {
late final Future<T> _future;

@override
void initValue() {
super.initValue();
_future = value.future;
_future.then(
(value) => setState(() {}),
);
}

@override
Future<T> build() => _future;
}
copied to clipboard
If you want to add this as a ScopedValueFunction, write the following via the getScopedValue method.
extension FutureValueRefExtension on Ref {
Future<T> useFuture<T>(Future<T> Function() callback) {
return getScopedValue(
(ref) => FutureValue(callback.call()),
listen: true,
);
}
}
copied to clipboard
The actual use of this is as follows.
ref.page.useFuture(() => Future.delayed(const Duration(seconds: 5))); // Redraw after 5 seconds
copied to clipboard
In this case, I simply return the Future as is, but by creating a snapshot object with FutureValueState<T> and monitoring it, it is possible to implement a mechanism like FutureBuilder that allows the status of the snapshot to be monitored at any time.
GitHub Sponsors #
Sponsors are always welcome. Thank you for your support!
https://github.com/sponsors/mathrunet

License:

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Files In This Product:

Customer Reviews

There are no reviews.