cubes 1.2.1 cubes: ^1.2.1 copied to clipboard
Simple State Manager (Focusing on simplicity and rebuilding only the necessary)
Cubes #
Simple State Manager with dependency injection and no code generation required.
With Cubes, manage the state of the application in a simple and objective way and reconstructing in your widget tree only where necessary!
No uses RxDart, Cubes
uses ChangeNotifier because it is a feature already available in Flutter and for its simplicity.
Install #
To use this plugin, add cubes
as a dependency in your pubspec.yaml file.
Counter Example #
import 'package:cubes/cubes.dart';
import 'package:flutter/material.dart';
void main() {
Cubes.registerDependency((i) => CounterCube());
runApp(MaterialApp(
title: 'Cube Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: CounterScreen(),
),
);
}
class CounterCube extends Cube {
final count = 0.obsValue;
void increment() {
count.modify((value) => value + 1); // or count.update(newValue);
}
}
class CounterScreen extends CubeWidget<CounterCube> {
@override
Widget buildView(BuildContext context, CounterCube cube) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('You have pushed the button this many times:'),
SizedBox(height: 20),
cube.count.build<int>((value) {
return Text(value.toString());
}),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: cube.increment,
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Usage #
Creating a Cube #
Cube is the classe responsible to center business logic of the your view.
For create your cube, just create a class and extends of Cube
like this:
class CounterCube extends Cube {
}
In Cubes
you controll elements in the view using ObservableValues
. To create once is easy:
class CounterCube extends Cube {
final count = 0.obsValue;
// final myList = <MyModel>[].obsValue;
// final viewModel = ViewMidel().obsValue;
}
Ready, now we can modify these ObservableValues
and your view will react to these changes. For example:
class CounterCube extends Cube {
final count = 0.obsValue;
void increment() {
count.modify((value) => value + 1); // or count.update(newValue);
}
}
It's normal to want to do a query in an API or do something else once the View is ready.
In Cubes
it is super simple to do this. You can do overrride of the methos onReady
, this is called when your View is a ready.
class CounterCube extends Cube {
final count = 0.obsValue;
void increment() {
count.modify((value) => value + 1); // or count.update(newValue);
}
@override
void onReady(Object arguments) {
// do anything when view is ready
super.ready(arguments);
}
}
The arguments
variable we get is passed by the view, and if we don't pass this variable it gets from ModalRoute.of(context).settings.arguments;
Creating a View #
We widget that represents the View is very simple. just create a class and extends of CubeWidget<CubeName>
passing the Cube
name that this view will uses. For example:
class CounterScreen extends CubeWidget<CounterCube> {
}
Your IDE will force you to implement a mandatory method called buildView
. getting like this:
class CounterScreen extends CubeWidget<CounterCube> {
@override
Widget buildView(BuildContext context, CounterCube cube) {
// TODO: implement buildView
throw UnimplementedError();
}
}
It is similar to the 'build' method already known to you from StatelessWidget
and State
. Where you will return your widget tree and will already have access to Cube
to listen to the ObservableValues
.
The end result is this:
class CounterScreen extends CubeWidget<CounterCube> {
@override
Widget buildView(BuildContext context, CounterCube cube) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
cube.count.build<int>((value) {
return Text(value.toString());
}),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: cube.increment,
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Note that listening to the ObservableValue
was very simple. Simply:
cube.count.build<int>((value) {
return Text(value.toString());
})
This way we listening to the ObservableValue
count
, and every time this variable is changed, the View
is notified by running the code block again:
return Text(value.toString());
This guarantees that in the whole widget tree of your screen, only the necessary is rebuilt.
Registering Cubes and dependencies #
Did you notice that we never created an instance of CounterCube
?
This is because Cubes
works with dependency injection. So for everything to work properly we have to register the Cube
used and its dependencies, if any.
import 'package:cubes/cubes.dart';
import 'package:flutter/material.dart';
void main() {
// register cube
Cubes.registerDependency((i) => CounterCube());
// Example register singleton Cube
// Cubes.registerDependency(
// (i) => CounterCube(),
// type: DependencyRegisterType.singleton,
// );
// Example register repositories or anything
// Cubes.registerDependency((i) => SingletonRepository(i.getDependency());
runApp(MaterialApp(
title: 'Cubes Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Home(),
));
}
For those of you who don't like to attach a package to your project too much, there are other ways to work the View part. Look:
- You can use
CubeBuilder
widget, see this example; - To work with
StatefulWidget
you can use the mixinCubeStateMixin<StatefulWidget,Cube>
see this example; - If you want to use it in a more minimalist way you can use the
SimpleCube
, see this example.
Listening observable variables #
You can listen to observables in two ways, using the extension build
as in the example above or using the Observer
widget:
Extension 'build' #
cube.count.build<int>(
(value) => Text(value.toString()), // Here you build the widget and it will be rebuilt every time the variable is modified and will leave the conditions of `when`.
animate: true, // Setting to `true`, fadeIn animation will be performed between widget changes.
when: (last, next) => last != next, // You can decide when rebuild widget using previous and next value. (For a good functioning of this feature use immutable variables)
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, // Here you can modify the default animation which is FadeIn.
duration: Duration(milliseconds: 300), // Sets the duration of the animation.
),
Widget CObserver #
return CObserver<int>(
observable: cube.count,
builder: (value)=> Text(value.toString()),
when: (last, next) => true,
animate:true,
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
duration: Duration(milliseconds: 300),
);
Provider #
To get the Cube by the children of CubeBuilder
, CubeWidget
you can use Cubes.of<MyCube>(context)
;
Methods inner Cube #
onAction #
onAction
is used to send any type of action or message to a view. You simply create an 'action' extending from CubeAction
.
class NavigationAction extends CubeAction {
final String route;
NavigationAction({this.route});
}
In your cube
class MyCube extends Cube {
void navToGome(){
// sending action
onAction(NavigationAction(route: "/home"));
}
}
you will receive this action in the View
through the method:
class MyScreen extends CubeWidget<MyCube> {
@override
Widget buildView(BuildContext context, MyCube cube) {
return ...;
}
@override
void onAction(BuildContext context, MyCube cube, CubeAction action) {
if(action is NavigationAction) Navigator.pushNamed(context, (action as NavigationAction).route);
super.onAction(context, cube, data);
}
}
this will be useful for navigation, to start some more complex animation, among other needs that View
has to perform.
runDebounce #
This method will help you to debounce
the execution of something.
runDebounce(
'increment', // identify
() => print(count.value),
duration: Duration(seconds: 1),
);
listen #
Use to listen ObservableValue.
listen(count,(value){
// do anything
});
listenActions #
Use to listen to Action
sent to view.
listenActions((action){
// do anything
});
Useful Widgets #
CAnimatedList #
This is a version of AnimatedList that simplifies its use for the Cube context.
CAnimatedList<String>(
observable: cube.todoList,
itemBuilder: (context, item, animation, type) {
return ScaleTransition(
scale: animation,
child: _buildItem(item),
);
},
)
Full usage example here.
CFeedBackManager #
Use this widget if you want to reactively control your Dialog
, BottomSheet
and SnackBar
using an ObservableValue.
Creating observable to control:
final bottomSheetControl = CFeedBackControl(data:'test').obsValue;
final dialogControl = CFeedBackControl(data:'test').obsValue;
final snackBarControl = CFeedBackControl<String>().obsValue;
Now just add the widget to your tree and settings:
FeedBackManager(
dialogControllers:[ // You can add as many different dialogs as you like
CDialogController<String>(
observable: cube.dialogControl,
// dismissible: bool,
// barrierColor: Color,
// routeSettings: RouteSettings,
// useRootNavigator: bool,
// useSafeArea: bool,
builder: (data, context) {
return Container(height: 200, child: Center(child: Text('Dialog: $data')));
},
),
],
bottomSheetControllers: [ // You can add as many different BottomSheets as you like
CBottomSheetController<String>(
observable: cube.bottomSheetControl,
// dismissible: bool,
// useRootNavigator: bool,
// routeSettings: RouteSettings,
// barrierColor: Color,
// backgroundColor: Color,
// elevation: double,
// shape: ShapeBorder,
// clipBehavior: Clip,
// enableDrag: bool,
// isScrollControlled: bool,
// useSafeArea: bool,
builder: (data, context) {
return Container(height: 200, child: Center(child: Text('BottomSheet: $data')));
},
),
],
snackBarControllers: [
CSnackBarController<String>(
observable: cube.snackBarControl,
builder: (data, context) {
return SnackBar(content: Text(data));
},
),
],
child: ...
)
To show or hide:
bottomSheetControl.show(); // or hide();
dialogControl.show(); // or hide();
snackBarControl.show(data: 'Success');
Full usage example here.
CTextFormField #
Widget created to use TextFormField
with ObservableValue
.
With it you can work reactively with your TextFormField
. Being able to modify and read its value, set error, enable and disable.
/// code in Cube
final textFieldControl = CTextFormFieldControl(text: '').obsValue;
// final text = textFieldControl.text; // get text
// textFieldControl.text = 'New text'; // change text
// textFieldControl.error = 'error example'; // set error
// textFieldControl.enable = true; // enable or disable
// textFieldControl.enableObscureText = true; // enable or disable obscureText
// code in Widget
CTextFormField(
observable: cube.textFieldControl,
obscureTextButtonConfiguration: CObscureTextButtonConfiguration( // use to configure the hide and show content icon in case of obscureText = true.
align: CObscureTextAlign.right,
iconHide: Icon(Icons.visibility_off_outlined),
iconShow: Icon(Icons.visibility_outlined),
),
decoration: InputDecoration(hintText: 'Type something'),
// ... All other TextFormField attributes
),
It is exactly the same as the conventional TextFormField
with two more fields, the observable
and obscureTextButtonConfiguration
.
Full usage example here.
Internationalization support #
With Cubes you can configure internationalization in your application. in a simple way using .json files.
Using #
Create a folder named lang
and put your files with name location. This way:
Add path in your pubspec.yaml
:
# To add assets to your application, add an assets section, like this:
assets:
- lang/
In your MaterialApp
you can configure the CubesLocalizationDelegate
:
final cubeLocation = CubesLocalizationDelegate(
[
Locale('en', 'US'),
Locale('pt', 'BR'),
],
);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My app',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
localizationsDelegates: cubeLocation.delegates, // see here
supportedLocales: cubeLocation.supportedLocations, // see here
home: Home(),
);
}
Ready!!! Your application already supports internationalization. Get the strings as follows:
String text = Cubes.getString('welcome');
You can replace part of the string using params
. Like this:
// String in json file:
// {'welcome':'Hello {name}! Welcome to my app!'}
String text = Cubes.getString('welcome',params:{'{name}':'Kevin'});
Custom dependency injection #
By default, we use get_it to manage dependencies. if you want to use another one you can overwrite the Injector:
class MyInjector extends CInjector {
@override
T getDependency<T>({String dependencyName}) {
// your implementation
}
@override
void registerDependency<T>(DependencyInjectorBuilder<T> builder, {String dependencyName, bool isSingleton = false}) {
// your implementation
}
@override
void reset() {
// your implementation
}
}
Cubes().injector = MyInjector();
Useful extensions #
// BuildContextExtensions
context.goTo(Widget());
context.goToReplacement(Widget());
context.goToAndRemoveUntil(Widget(),RoutePredicate);
context.mediaQuery; // MediaQuery.of(context);
context.padding; // MediaQuery.of(context).padding;
context.viewInsets; // MediaQuery.of(context).viewInsets;
context.sizeScreen; // MediaQuery.of(context).size;
context.widthScreen; // MediaQuery.of(context).size.width;
context.heightScreen; // MediaQuery.of(context).size.height;
context.theme;
context.scaffold;
context.showSnackBar(SnackBar());
context.arguments;
context.getCube<MyCube>(); // get Cube from provided
Testing #
import 'package:flutter_test/flutter_test.dart';
void main() {
CounterCube cube;
setUp(() {
cube = CounterCube();
});
tearDown(() {
cube?.dispose();
});
test('initial value', () {
expect(cube.count.value, 0);
});
test('increment value', () {
cube.increment();
expect(cube.count.value, 1);
});
test('increment value 3 times', () {
cube.increment();
cube.increment();
cube.increment();
expect(cube.count.value, 3);
});
}
Example with asynchronous call here
Example widget test using Robot
here
Any questions see our example.