cubes 1.3.0 cubes: ^1.3.0 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.
About #
Manage the state of your Flutter application in a simple and objective way, rebuilding the widget tree only where necessary!
Cubes
makes use of ChangeNotifier since it is a feature already available in Flutter and for its simplicity. Cubes
doesn't rely on RxDart.
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 class responsible for handling the business logic of your view.
To create your Cube, just make a class that extends from Cube
as follows:
class CounterCube extends Cube {
}
In Cubes
, you control elements in the view using ObservableValues
. Creating such variables is easy:
class CounterCube extends Cube {
final count = 0.obsValue;
// final myList = <MyModel>[].obsValue;
// final viewModel = ViewMidel().obsValue;
}
You can modify these ObservableValues
and then 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 a common practice to query an API or do something else once the View is ready.
In Cubes
, this is super simple to achieve. Just override the method onReady
and your code will be called once the View is 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
property is taken from the view and, if ommited, it will be taken from ModalRoute.of(context).settings.arguments;
Creating a View #
Creating a widget that represents a View is very simple. Make a class that extends from CubeWidget<CubeName>
passing the Cube
name that this view will use. For example:
class CounterScreen extends CubeWidget<CounterCube> {
}
Your IDE will force you to implement a mandatory method called buildView
, just like this:
class CounterScreen extends CubeWidget<CounterCube> {
@override
Widget buildView(BuildContext context, CounterCube cube) {
// TODO: implement buildView
throw UnimplementedError();
}
}
This method is similar to the 'build' method from StatelessWidget
and State
. There you will return your widget tree and will have access to Cube
for listening your ObservableValues
.
The final result looks like 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 an ObservableValue
is very simple:
cube.count.build<int>((value) {
return Text(value.toString());
})
By listening to the ObservableValue
count
, every time this variable is changed the View
is notified by running the following code again:
return Text(value.toString());
This guarantees that only the necessary is rebuilt in the whole widget tree.
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 inside main()
.
import 'package:cubes/cubes.dart';
import 'package:flutter/material.dart';
void main() {
// Register your Cube
Cubes.registerDependency((i) => CounterCube());
// Example: register a singleton Cube
// Cubes.registerDependency(
// (i) => CounterCube(),
// type: DependencyRegisterType.singleton,
// );
// Example: register repositories or something else
// 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 depend your projects too much in a package, there are other ways to work with it:
- You can use the
CubeConsumer
widget, see this example; - To work with
StatefulWidget
you can use the mixinCubeStateMixin<StatefulWidget,Cube>
. See this example; - For a minimalist approach, you can use
SimpleCube
. See this example.
Listening observable variables #
You can listen to observables in two ways: using the extension build
as mentioned earlier or using the CObserver
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 reference of a specific Cube from CubeConsumer
or 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});
}
Then, inside your cube:
class MyCube extends Cube {
void navToGome(){
// sending action
onAction(NavigationAction(route: "/home"));
}
}
Finally, 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 approach will be useful for navigation, for complex animations among other features that the View
may need 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 it to listen ObservableValues.
listen(count,(value){
// do anything
});
listenActions #
Use it to listen to the Action
sent to the 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.
Create the 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 its 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 it.
/// 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
in the root folder of your project and put your files named by the language and locale, just like this:
Add the 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(),
);
}
All done! Your application already supports internationalization. Now you can retrieve the strings as follows:
String text = Cubes.getString('welcome');
// or
String text = 'welcome'.tr();
You can replace parts of the string using params
, just like this:
// String in json file:
// {'welcome':'Hello @name! Welcome to my app!'}
String text = Cubes.getString('welcome',params:{'name':'Kevin'});
// or
String text = 'welcome'.tr(params:{'name':'Kevin'});
Custom dependency injection #
By default, Cubes
uses 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
If there are still doubts, you should be able to find what you're looking for in the full example.