Created
September 4, 2022 09:56
-
-
Save SuperPenguin/e7c61cea1607feebc2f3eea474f8c95d to your computer and use it in GitHub Desktop.
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/foundation.dart'; | |
import 'package:flutter/material.dart'; | |
///This emulates a server that counts the number of times it has been called. | |
class CountServerService { | |
int _counter = 0; | |
Future<int> getCallCount() => | |
Future<int>.delayed(const Duration(seconds: 1), () async { | |
_counter++; | |
return _counter; | |
}); | |
} | |
///This is a base class for implementing view models | |
///that refresh when you tell them to | |
abstract class ViewModel { | |
void Function(VoidCallback fn)? _setState; | |
///Pass in the setState function from your state here on [initState] | |
void attach(Function(VoidCallback fn) setState) => _setState = setState; | |
///This does the same thing as calling [setState] in your widget's | |
///state class | |
void setState(VoidCallback fn) { | |
assert(_setState != null, 'You must call attach'); | |
_setState!(fn); | |
} | |
} | |
class ViewModelPropagator<T extends ViewModel> extends InheritedWidget { | |
const ViewModelPropagator({ | |
required this.viewModel, | |
required Widget child, | |
final Key? key, | |
}) : super(key: key, child: child); | |
final T viewModel; | |
static ViewModelPropagator<T> of<T extends ViewModel>(BuildContext context) { | |
final result = | |
context.dependOnInheritedWidgetOfExactType<ViewModelPropagator<T>>(); | |
assert(result != null, 'No ViewModel of type $T found in context'); | |
return result!; | |
} | |
@override | |
bool updateShouldNotify(covariant final InheritedWidget oldWidget) => | |
viewModel != (oldWidget as ViewModelPropagator<T>).viewModel; | |
@override | |
void debugFillProperties(DiagnosticPropertiesBuilder properties) { | |
super.debugFillProperties(properties); | |
properties.add(DiagnosticsProperty<T>('state', viewModel)); | |
} | |
} | |
///This app's view model | |
class AppViewModel extends ViewModel { | |
int _callCount = 0; | |
bool _isProcessing = false; | |
bool _displayWidgets = true; | |
int get callCount => _callCount; | |
bool get isProcessing => _isProcessing; | |
bool get displayWidgets => _displayWidgets; | |
final CountServerService countServerService; | |
AppViewModel(this.countServerService); | |
void hideWidgets() { | |
_displayWidgets = false; | |
setState(() { | |
_displayWidgets = false; | |
}); | |
} | |
Future<void> callGetCount() async { | |
setState(() { | |
_isProcessing = true; | |
}); | |
final callCount = await countServerService.getCallCount(); | |
setState(() { | |
_isProcessing = false; | |
_callCount = callCount; | |
}); | |
} | |
} | |
void main() { | |
//Register services and the view model with an IoC container | |
final builder = IocContainerBuilder() | |
..addSingletonService(CountServerService()) | |
..addSingleton( | |
(container) => AppViewModel(container.get<CountServerService>())); | |
runApp(MyApp(builder.toContainer())); | |
} | |
class MyApp extends StatelessWidget { | |
final IocContainer container; | |
const MyApp(this.container, {super.key}); | |
@override | |
Widget build(BuildContext context) { | |
return MaterialApp( | |
debugShowCheckedModeBanner: false, | |
title: 'Flutter Demo', | |
theme: ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
home: ViewModelPropagator<AppViewModel>( | |
viewModel: container.get<AppViewModel>(), | |
child: const Home(), | |
), | |
); | |
} | |
} | |
class Home extends StatefulWidget { | |
const Home({super.key}); | |
@override | |
State<Home> createState() => _HomeState(); | |
} | |
class _HomeState extends State<Home> { | |
@override | |
void didChangeDependencies() { | |
super.didChangeDependencies(); | |
ViewModelPropagator.of<AppViewModel>(context).viewModel.attach(setState); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final viewModel = ViewModelPropagator.of<AppViewModel>(context).viewModel; | |
return Scaffold( | |
appBar: AppBar( | |
title: | |
const Text("Managing Up The State with Management-like Managers"), | |
), | |
body: Stack(children: [ | |
viewModel.displayWidgets | |
? Wrap(children: const [ | |
CounterDisplay(), | |
CounterDisplay(), | |
CounterDisplay(), | |
CounterDisplay(), | |
]) | |
: Text('X', style: Theme.of(context).textTheme.headline1), | |
Align( | |
alignment: Alignment.bottomRight, | |
child: Row(children: [ | |
FloatingActionButton( | |
onPressed: () => viewModel.callGetCount(), | |
tooltip: 'Increment', | |
child: const Icon(Icons.add), | |
), | |
FloatingActionButton( | |
onPressed: () => viewModel.hideWidgets(), | |
tooltip: 'X', | |
child: const Icon(Icons.close), | |
) | |
]), | |
), | |
]), | |
); | |
} | |
} | |
class CounterDisplay extends StatelessWidget { | |
const CounterDisplay({ | |
Key? key, | |
}) : super(key: key); | |
@override | |
Widget build(BuildContext context) { | |
final viewModel = ViewModelPropagator.of<AppViewModel>(context).viewModel; | |
return Padding( | |
padding: const EdgeInsets.all(10), | |
child: Container( | |
height: 200, | |
width: 200, | |
color: const Color(0xFFEEEEEE), | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: <Widget>[ | |
const Text( | |
'You have pushed the button this many times:', | |
), | |
if (viewModel.isProcessing) | |
const CircularProgressIndicator() | |
else | |
Text( | |
'${viewModel.callCount}', | |
style: Theme.of(context).textTheme.headline4, | |
), | |
], | |
), | |
), | |
); | |
} | |
} | |
/* Sorry but I have to cram the ioc_container package in here because | |
Dartpad doesn't support it | |
https://pub.dev/packages/ioc_container | |
*/ | |
///Defines a factory for the service and whether or not it is a singleton. | |
@immutable | |
class ServiceDefinition<T> { | |
///If true, only once instance of the service will be created and shared for | |
///for the lifespan of the app | |
final bool isSingleton; | |
///The factory that creates the instance of the service and can access other | |
///services in this container | |
final T Function(IocContainer container) factory; | |
const ServiceDefinition(this.factory, {this.isSingleton = false}); | |
} | |
///A built Ioc Container. To create a new IocContainer, use | |
///[IocContainerBuilder]. To get a service from the container, call [get]. | |
///Builders create immutable containers unless you specify the | |
///isLazy option on toContainer(). You can build your own container by injecting | |
///service definitions and singletons here | |
@immutable | |
class IocContainer { | |
///This is only here for testing and you should not use this in your code | |
@visibleForTesting | |
final Map<Type, ServiceDefinition> serviceDefinitionsByType; | |
///This is only here for testing and you should not use this in your code | |
@visibleForTesting | |
final Map<Type, Object> singletons; | |
const IocContainer(this.serviceDefinitionsByType, this.singletons); | |
///Get an instance of the service by type | |
T get<T>() => singletons.containsKey(T) | |
? singletons[T] as T | |
: serviceDefinitionsByType.containsKey(T) | |
? (serviceDefinitionsByType[T]!.isSingleton | |
? singletons.putIfAbsent( | |
T, | |
() => | |
serviceDefinitionsByType[T]!.factory(this) as Object) as T | |
: serviceDefinitionsByType[T]!.factory(this)) | |
: throw Exception('Service not found'); | |
} | |
///A builder for creating an [IocContainer]. | |
@immutable | |
class IocContainerBuilder { | |
final Map<Type, ServiceDefinition> _serviceDefinitionsByType = {}; | |
///Throw an error if a service is added more than once. Set this to true when | |
///you want to add mocks to set of services for a test. | |
final bool allowOverrides; | |
IocContainerBuilder({this.allowOverrides = false}); | |
///Add a factory to the container. | |
void addServiceDefinition<T>( | |
///Add a factory and whether or not this service is a singleton | |
ServiceDefinition<T> serviceDefinition) { | |
if (_serviceDefinitionsByType.containsKey(T)) { | |
if (allowOverrides) { | |
_serviceDefinitionsByType.remove(T); | |
} else { | |
throw Exception('Service already exists'); | |
} | |
} | |
_serviceDefinitionsByType.putIfAbsent(T, () => serviceDefinition); | |
} | |
///Create an [IocContainer] from the [IocContainerBuilder]. | |
///This will create an instance of each singleton service and store it | |
///in an immutable list unless you specify [isLazy] as true. | |
IocContainer toContainer( | |
{ | |
///If this is true the services will be created when they are requested | |
///and this container will not technically be immutable. | |
bool isLazy = false}) { | |
if (!isLazy) { | |
final singletons = <Type, Object>{}; | |
final tempContainer = IocContainer(_serviceDefinitionsByType, singletons); | |
_serviceDefinitionsByType.forEach((type, serviceDefinition) { | |
if (serviceDefinition.isSingleton) { | |
singletons.putIfAbsent( | |
type, () => serviceDefinition.factory(tempContainer)); | |
} | |
}); | |
return IocContainer( | |
Map<Type, ServiceDefinition>.unmodifiable(_serviceDefinitionsByType), | |
Map<Type, Object>.unmodifiable(singletons)); | |
} | |
return IocContainer( | |
Map<Type, ServiceDefinition>.unmodifiable(_serviceDefinitionsByType), | |
//Note: this case allows the singletons to be mutable | |
// ignore: prefer_const_literals_to_create_immutables | |
<Type, Object>{}); | |
} | |
} | |
extension Extensions on IocContainerBuilder { | |
///Add a singleton service to the container. | |
void addSingletonService<T>(T service) => addServiceDefinition( | |
ServiceDefinition<T>((i) => service, isSingleton: true)); | |
///Add a singleton factory to the container. The container | |
///will only call this once throughout the lifespan of the app | |
void addSingleton<T>(T Function(IocContainer container) factory) => | |
addServiceDefinition(ServiceDefinition(factory, isSingleton: true)); | |
///Add a factory to the container. | |
void add<T>(T Function(IocContainer container) factory) => | |
addServiceDefinition(ServiceDefinition(factory)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment