Last active
August 20, 2023 03:01
-
-
Save Andrious/da8348b60f81bb5e49c5dd5623d88b4c to your computer and use it in GitHub Desktop.
Counter App Example using the Pub.dev package, state_extended
This file contains hidden or 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
// | |
import 'package:flutter/material.dart'; | |
import 'package:state_extended/state_extended.dart'; | |
void main() => runApp(const MyApp(key: Key('MyApp'))); | |
/// README.md example app | |
class MyApp extends StatefulWidget { | |
/// | |
const MyApp({super.key, this.title = 'StateX Demo App'}); | |
/// Title of the screen | |
// Fields in a StatefulWidget should always be "final". | |
final String title; | |
/// This is the App's State object | |
@override | |
State createState() => _MyAppState(); | |
} | |
class _MyAppState extends AppStateX<MyApp> { | |
factory _MyAppState() => _this ??= _MyAppState._(); | |
_MyAppState._() : super(controller: AppController()) { | |
/// Acquire a reference to the passed Controller. | |
con = controller as AppController; | |
} | |
static _MyAppState? _this; | |
late AppController con; | |
/// Place a breakpoint here to see what's going on 'under the hood.' | |
@override | |
Widget build(BuildContext context) => super.build(context); | |
/// Define the 'look and fell' of the overall app. | |
/// The body: property takes in a separate widget for the 'home' page. | |
@override | |
Widget buildIn(BuildContext context) => MaterialApp( | |
debugShowCheckedModeBanner: false, | |
home: MyHomePage(title: widget.title), | |
); | |
} | |
/// The Home page | |
class MyHomePage extends StatefulWidget { | |
/// | |
const MyHomePage({super.key, this.title}); | |
/// Title of the screen | |
// Fields in a StatefulWidget should always be "final". | |
final String? title; | |
@override | |
State createState() => _MyHomePageState(); | |
} | |
/// This is a subclass of the State class. | |
/// This subclass is linked to the App's lifecycle using [WidgetsBindingObserver] | |
class _MyHomePageState extends StateX<MyHomePage> { | |
/// Let the 'business logic' run in a Controller | |
_MyHomePageState() : super(controller: HomeController(), useInherited: true) { | |
con = controller as HomeController; | |
} | |
late HomeController con; | |
@override | |
void initState() { | |
/// Look inside the parent function and see it calls | |
/// all it's Controllers if any. | |
super.initState(); | |
/// Retrieve the 'app level' State object | |
appState = rootState!; | |
/// You're able to retrieve the Controller(s) from other State objects. | |
final con = appState.controller; | |
/// Another way to retrieve the 'app level' State object | |
appState = con?.state!.startState as AppStateX; | |
/// You can retrieve by type as well | |
appState = stateByType<AppStateX>()!; | |
} | |
late AppStateX appState; | |
/// Place a breakpoint here to see what's going on 'under the hood.' | |
@override | |
Widget build(BuildContext context) => super.build(context); | |
/// Place a breakpoint here to see what's going on 'under the hood.' | |
@override | |
Widget buildF(BuildContext context) => super.buildF(context); | |
@override | |
Widget buildIn(BuildContext context) => Scaffold( | |
appBar: AppBar( | |
title: Text(widget.title ?? ''), | |
// popup menu button | |
actions: [con.popupMenuButton], | |
), | |
body: Center( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
Text( | |
'You have pushed the button this many times:', | |
style: Theme.of(context).textTheme.bodyMedium, | |
), | |
/// Linked to the built-in InheritedWidget. | |
/// A Text widget to display the counter is in here. | |
/// ONLY THIS WIDGET is updated with every press of the button. | |
const CounterWidget(), | |
], | |
), | |
), | |
floatingActionButton: FloatingActionButton( | |
key: const Key('+'), | |
// rebuilds only the Text widget containing the counter. | |
onPressed: () => con.onPressed(), | |
child: const Icon(Icons.add), | |
), | |
); | |
} | |
/// Demonstrating the InheritedWidget's ability to spontaneously rebuild | |
/// its dependent widgets. | |
class CounterWidget extends StatefulWidget { | |
/// Pass along the State Object Controller to make this widget | |
/// dependent on the App's InheritedWidget. | |
const CounterWidget({super.key}); | |
@override | |
State<StatefulWidget> createState() => _CounterState(); | |
} | |
class _CounterState extends State<CounterWidget> { | |
@override | |
Widget build(BuildContext context) { | |
/// Making this widget dependent will cause the build() function below | |
/// to run again if and when the App's InheritedWidget calls its notifyClients() function. | |
final con = HomeController(); | |
con.dependOnInheritedWidget(context); | |
return Text( | |
con.data, | |
style: Theme.of(context).textTheme.headlineMedium, | |
); | |
} | |
} | |
/// Everything a State object can do, this Controller can do as well! | |
class HomeController extends StateXController { | |
/// Utilizing the Singleton pattern is a good programming practice | |
factory HomeController() => _this ??= HomeController._(); | |
// This constructor is hidden with the underscore. | |
HomeController._() | |
: _model = Model(), | |
_letters = AlphabetLetters(), | |
_primes = PrimeNumbers(); | |
static HomeController? _this; | |
final Model _model; | |
final AlphabetLetters _letters; | |
final PrimeNumbers _primes; | |
/// Note, each count comes from a separate class. | |
String get data { | |
String data; | |
switch (_countType) { | |
case CountType.prime: | |
data = _primes.primeNumber.toString(); | |
break; | |
case CountType.alphabet: | |
data = _letters.current; | |
break; | |
default: | |
data = _model.integer.toString(); | |
} | |
return data; | |
} | |
CountType _countType = CountType.integer; | |
/// The Controller deals with the event handling and business logic. | |
void onPressed() { | |
switch (_countType) { | |
case CountType.prime: | |
_primes.next(); | |
break; | |
case CountType.alphabet: | |
_letters.read(); | |
break; | |
default: | |
_model.incrementCounter(); | |
} | |
// | |
notifyClients(); | |
} | |
/// Supply an 'error handler' routine if something goes wrong | |
/// in initAsync() routine above. | |
@override | |
bool onAsyncError(FlutterErrorDetails details) => false; | |
/// Provide a menu to this simple app. | |
PopupMenuButton get popupMenuButton => PopupMenuButton<CountType>( | |
itemBuilder: (context) => [ | |
PopupMenuItem( | |
value: CountType.integer, | |
child: Row( | |
children: [ | |
if (_countType == CountType.integer) | |
const Icon(Icons.star_rounded, color: Colors.black), | |
const Text("Integers") | |
], | |
), | |
), | |
PopupMenuItem( | |
value: CountType.alphabet, | |
child: Row( | |
children: [ | |
if (_countType == CountType.alphabet) | |
const Icon(Icons.star_rounded, color: Colors.black), | |
const Text("Alphabet") | |
], | |
), | |
), | |
PopupMenuItem( | |
value: CountType.prime, | |
child: Row( | |
children: [ | |
if (_countType == CountType.prime) | |
const Icon(Icons.star_rounded, color: Colors.black), | |
const Text("Prime Numbers") | |
], | |
), | |
), | |
], | |
onSelected: (value) { | |
switch (value) { | |
case CountType.prime: | |
_countType = value; | |
break; | |
case CountType.alphabet: | |
_countType = value; | |
break; | |
default: | |
// In case the enumeration class was unknowingly changed | |
// Default to integer | |
_countType = CountType.integer; | |
} | |
// 'Refresh' the home screen to show the new count option | |
setState(() {}); | |
// var state = this.state; // The controller's current State object | |
// // If you're not confident the its the intended State class. | |
// state = stateOf<MyHomePage>(); | |
// state = ofState<_MyHomePageState>(); | |
// state?.setState(() {}); | |
}, | |
offset: const Offset(0, 40), | |
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), | |
elevation: 14, | |
); | |
/// Like the State object, the Flutter framework will call this method exactly once. | |
/// Only when the [StateX] object is first created. | |
@override | |
void initState() { | |
super.initState(); | |
/// A State object can reference it's 'current' State object controller. | |
var thisController = state?.controller; | |
/// The same controller can be retrieved by its unique identifier if you know it. | |
/// You then don't have to know the type or the type is private with a leading underscore. | |
/// Note, it has to be a Controller explicitly added to the State object at some time. | |
thisController = state?.controllerById(thisController?.identifier); | |
assert(thisController == this, | |
'Just demonstrating the means to retrieve a Controller.'); | |
/// You can retrieve a Controller's state object by its StatefulWidget | |
/// Good if the state class type is unknown or private with a leading underscore. | |
//ignore: unused_local_variable | |
var stateObj = stateOf<MyHomePage>(); | |
/// Retrieve the 'app level' State object | |
final appState = rootState; | |
assert(appState is _MyAppState, | |
"Every Controller has access to the 'first' State object."); | |
/// The 'app level' State object has *all* the Stat objects running in the App | |
/// at any one point of time. | |
stateObj = appState?.stateByType<_MyHomePageState>(); | |
/// Retrieve the State object's controller. | |
final appController = appState?.controller; | |
/// You're able to retrieve the Controller(s) from other State objects. | |
/// if you know their unique identifier. | |
final con = appState?.controllerById(appController?.identifier); | |
assert(appController == con, 'They should be the same object.'); | |
} | |
/// The framework calls this method whenever it removes this [StateX] object | |
/// from the tree. | |
@override | |
void deactivate() { | |
super.deactivate(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: deactivate in HomeController'); | |
} | |
} | |
/// Called when this object is reinserted into the tree after having been | |
/// removed via [deactivate]. | |
@override | |
void activate() { | |
super.activate(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: activate in HomeController'); | |
} | |
} | |
/// The framework calls this method when this [StateX] object will never | |
/// build again. | |
/// Note: THERE IS NO GUARANTEE THIS METHOD WILL RUN in the Framework. | |
@override | |
void dispose() { | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: dispose in HomeController'); | |
} | |
super.dispose(); | |
} | |
/// Called when the corresponding [StatefulWidget] is recreated. | |
@override | |
void didUpdateWidget(StatefulWidget oldWidget) { | |
/// The framework always calls build() after calling [didUpdateWidget], which | |
/// means any calls to [setState] in [didUpdateWidget] are redundant. | |
super.didUpdateWidget(oldWidget); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didUpdateWidget in HomeController'); | |
} | |
} | |
/// Called when this [StateX] object is first created immediately after [initState]. | |
/// Otherwise called only if this [State] object's Widget | |
/// is a dependency of [InheritedWidget]. | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didChangeDependencies in HomeController'); | |
} | |
} | |
/// Called whenever the application is reassembled during debugging, for | |
/// example during hot reload. | |
@override | |
void reassemble() { | |
super.reassemble(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: reassemble in HomeController'); | |
} | |
} | |
/// Called when the system tells the app to pop the current route. | |
/// For example, on Android, this is called when the user presses | |
/// the back button. | |
/// | |
/// Observers are notified in registration order until one returns | |
/// true. If none return true, the application quits. | |
/// This method exposes the `popRoute` notification from | |
// ignore: comment_references | |
/// [SystemChannels.navigation]. | |
@override | |
Future<bool> didPopRoute() async { | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didPopRoute in HomeController'); | |
} | |
return super.didPopRoute(); | |
} | |
/// Called when the host tells the app to push a new route onto the | |
/// navigator. | |
/// This method exposes the `pushRoute` notification from | |
// ignore: comment_references | |
/// [SystemChannels.navigation]. | |
@override | |
Future<bool> didPushRoute(String route) async { | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didPushRoute in HomeController'); | |
} | |
return super.didPushRoute(route); | |
} | |
/// Called when the host tells the application to push a new | |
/// [RouteInformation] and a restoration state onto the router. | |
/// This method exposes the `popRoute` notification from | |
// ignore: comment_references | |
/// [SystemChannels.navigation]. | |
/// | |
/// The default implementation is to call the [didPushRoute] directly with the | |
/// [RouteInformation.location]. | |
@override | |
Future<bool> didPushRouteInformation(RouteInformation routeInformation) { | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didPushRouteInformation in HomeController'); | |
} | |
return super.didPushRouteInformation(routeInformation); | |
} | |
/// Called when the application's dimensions change. For example, | |
/// when a phone is rotated. | |
@override | |
void didChangeMetrics() { | |
super.didChangeMetrics(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didChangeMetrics in HomeController'); | |
} | |
} | |
/// Called when the platform's text scale factor changes. | |
@override | |
void didChangeTextScaleFactor() { | |
super.didChangeTextScaleFactor(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didChangeTextScaleFactor in HomeController'); | |
} | |
} | |
/// Brightness changed. | |
@override | |
void didChangePlatformBrightness() { | |
super.didChangePlatformBrightness(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print( | |
'############ Event: didChangePlatformBrightness in HomeController'); | |
} | |
} | |
/// Called when the system tells the app that the user's locale has changed. | |
@override | |
void didChangeLocales(List<Locale>? locales) { | |
super.didChangeLocales(locales); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didChangeLocales in HomeController'); | |
} | |
} | |
/// Called when the system puts the app in the background or returns the app to the foreground. | |
@override | |
void didChangeAppLifecycleState(AppLifecycleState state) { | |
/// Passing these possible values: | |
/// AppLifecycleState.inactive (may be paused at any time) | |
/// AppLifecycleState.paused (may enter the suspending state at any time) | |
/// AppLifecycleState.detach | |
/// AppLifecycleState.resumed | |
super.didChangeAppLifecycleState(state); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didChangeAppLifecycleState in HomeController'); | |
} | |
} | |
/// The application is in an inactive state and is not receiving user input. | |
/// | |
/// On iOS, this state corresponds to an app or the Flutter host view running | |
/// in the foreground inactive state. Apps transition to this state when in | |
/// a phone call, responding to a TouchID request, when entering the app | |
/// switcher or the control center, or when the UIViewController hosting the | |
/// Flutter app is transitioning. | |
/// | |
/// On Android, this corresponds to an app or the Flutter host view running | |
/// in the foreground inactive state. Apps transition to this state when | |
/// another activity is focused, such as a split-screen app, a phone call, | |
/// a picture-in-picture app, a system dialog, or another window. | |
/// | |
/// Apps in this state should assume that they may be [pausedLifecycleState] at any time. | |
@override | |
void inactiveLifecycleState() { | |
super.inactiveLifecycleState(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: inactiveLifecycleState in HomeController'); | |
} | |
} | |
/// The application is not currently visible to the user, not responding to | |
/// user input, and running in the background. | |
@override | |
void pausedLifecycleState() { | |
super.pausedLifecycleState(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: pausedLifecycleState in HomeController'); | |
} | |
} | |
/// Either be in the progress of attaching when the engine is first initializing | |
/// or after the view being destroyed due to a Navigator pop. | |
@override | |
void detachedLifecycleState() { | |
super.detachedLifecycleState(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: detachedLifecycleState in HomeController'); | |
} | |
} | |
/// The application is visible and responding to user input. | |
@override | |
void resumedLifecycleState() { | |
super.resumedLifecycleState(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: resumedLifecycleState in HomeController'); | |
} | |
} | |
/// Called when the system is running low on memory. | |
@override | |
void didHaveMemoryPressure() { | |
super.didHaveMemoryPressure(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print('############ Event: didHaveMemoryPressure in HomeController'); | |
} | |
} | |
/// Called when the system changes the set of active accessibility features. | |
@override | |
void didChangeAccessibilityFeatures() { | |
super.didChangeAccessibilityFeatures(); | |
if (inDebugMode) { | |
//ignore: avoid_print | |
print( | |
'############ Event: didChangeAccessibilityFeatures in HomeController'); | |
} | |
} | |
} | |
// The means 'to talk' between the Controller and the Model | |
enum CountType { integer, prime, alphabet } | |
/// A separate class that contains the data. | |
class Model { | |
int _integer = 0; | |
// The external property transferring the value to the outside world. | |
int get integer => _integer; | |
/// The business logic involves incrementing something. | |
void incrementCounter() => ++_integer; | |
} | |
/// Goes through the alphabet. | |
class AlphabetLetters { | |
// Used for incrementing the alphabet | |
int start = "a".codeUnitAt(0); | |
int end = "z".codeUnitAt(0); | |
late int letter = start; | |
// The external property transferring the value to the outside world. | |
String get current => String.fromCharCode(letter); | |
/// The business logic involves incrementing something. | |
void read() { | |
letter++; | |
if (letter > end) { | |
letter = start; | |
} | |
} | |
} | |
/// Another class. A complete different type of data conceived. | |
class PrimeNumbers { | |
PrimeNumbers({int? start, int? end}) { | |
start = start ?? 1; | |
end = end ?? 1000; | |
if (start < 0) { | |
start = 1; | |
} | |
if (end <= start) { | |
end = 1000; | |
} | |
initPrimeNumbers(start, end); | |
} | |
final List<int> _numbers = []; | |
int _cnt = 0; | |
int get primeNumber => _numbers[_cnt]; | |
void next() { | |
_cnt++; | |
if (_cnt > _numbers.length) { | |
_cnt = 0; | |
} | |
} | |
void initPrimeNumbers(int M, int N) { | |
a: | |
for (var k = M; k <= N; ++k) { | |
for (var i = 2; i <= k / i; ++i) { | |
if (k % i == 0) { | |
continue a; | |
} | |
} | |
_numbers.add(k); | |
} | |
} | |
} | |
/// Everything a State object can do, this Controller can do as well! | |
class AppController extends StateXController { | |
factory AppController() => _this ??= AppController._(); | |
AppController._(); | |
static AppController? _this; | |
/// Used for long asynchronous operations that need to be done | |
/// before the app can be fully available to the user. | |
/// e.g. Opening Databases, accessing Web servers, etc. | |
@override | |
Future<bool> initAsync() async { | |
// Simply wait for 10 seconds at startup. | |
/// In production, this is where databases are accessed, web services opened, login attempts, etc. | |
return Future.delayed(const Duration(seconds: 10), () { | |
return true; | |
}); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment