0 purchases
cubes
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.
Install #
To use this plugin, add cubes as a dependency in your pubspec.yaml file.
Example #
All Examples
Counter example #
import 'package:cubes/cubes.dart';
import 'package:flutter/material.dart';
void main() {
Cubes.registerFactory((i) => CounterCube());
runApp(MaterialApp(
title: 'Cube Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: CounterScreen(),
),
);
}
class CounterCube extends Cube {
final count = 0.obs;
void increment() {
count.modify((value) => value + 1); // or count.update(newValue); or count.value = newValue;
}
}
class CounterScreen extends CubeWidget<CounterCube> {
const CounterScreen({Key? key}) : super(key: key);
@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.
);
}
}
copied to clipboard
Explaining Counter example #
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 {
}
copied to clipboard
In Cubes, you control elements in the view using ObservableValues. Creating such variables is easy:
class CounterCube extends Cube {
final count = 0.obs;
// final myList = <MyModel>[].obs;
// final viewModel = ViewModel().obs;
}
copied to clipboard
You can modify these ObservableValues and then your view will react to these changes. For example:
class CounterCube extends Cube {
final count = 0.obs;
void increment() {
count.modify((value) => value + 1); // or count.update(newValue);
}
}
copied to clipboard
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.obs;
void increment() {
count.modify((value) => value + 1); // or count.update(newValue);
}
@override
void onReady(Object? arguments) {
// do anything when view is ready
}
}
copied to clipboard
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> {
}
copied to clipboard
Your IDE will force you to implement a mandatory method called buildView, just like this:
class CounterScreen extends CubeWidget<CounterCube> {
const CounterScreen({Key? key}) : super(key: key);
@override
Widget buildView(BuildContext context, CounterCube cube) {
// TODO: implement buildView
throw UnimplementedError();
}
}
copied to clipboard
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.
);
}
}
copied to clipboard
Note that listening to an ObservableValue is very simple:
cube.count.build<int>((value) {
return Text(value.toString());
})
copied to clipboard
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());
copied to clipboard
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.registerFactory((i) => CounterCube());
// Example: register a singleton Cube
// Cubes.registerSingleton(CounterCube());
// Example: register repositories or something else
// Cubes.registerFactory((i) => SingletonRepository(i.get());
// Example: get any dependency
// Cubes.get<MyDependency>();
runApp(MaterialApp(
title: 'Cubes Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Home(),
));
}
copied to clipboard
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 mixin CubeStateMixin<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.
),
copied to clipboard
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),
);
copied to clipboard
Provider #
To get the reference of a specific Cube from CubeConsumer or CubeWidget above of the widget tree, you can use Cubes.of
Methods: Inner Cube #
sendAction #
sendAction is used to send any type of action or message to a view. You simply create an 'action' extending from CubeAction.
class EventAction extends CubeAction {
String eventName;
EventAction(this.eventName);
@override
void onExecute(BuildContext context){
// You can do anything when this action to arrive in the view. Per example show Dialog, show snackBar, etc
}
}
copied to clipboard
Then, inside your cube:
class MyCube extends Cube {
void sendEvent(){
// sending action
sendAction(EventAction());
}
}
copied to clipboard
You can listen this action in the View through the method onAction.(If you implemented onExecute method is not necessary do this)
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 EventAction){
// do anything
}
super.onAction(context, cube, data);
}
}
copied to clipboard
This approach will be useful for complex animations among other features that the View may need to perform.
Example that create navigation system using CubeActions : navigator_action
Navigation #
You can use this feature to your own navigation system. But you don't have to do anything manually, we've already done that for you. Just use the CubeNavigation mixin.
class MyCube extends Cube with CubeNavigation{
void navToAnOtherScreen(){
navToNamed('/home');
// You can use:
// - navToNamed
// - navToNamedAndRemoveUntil
// - navToNamedReplacement
// - navPop
}
}
copied to clipboard
runDebounce #
This method will help you to debounce the execution of something.
runDebounce(
'increment', // identify
() => print(count.value),
duration: Duration(seconds: 1),
);
copied to clipboard
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),
);
},
)
copied to clipboard
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;
copied to clipboard
Now just add the widget to your tree and its settings:
CFeedBackManager(
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: ...
)
copied to clipboard
To show or hide:
bottomSheetControl.show(); // or hide();
dialogControl.show(); // or hide();
snackBarControl.show(data: 'Success');
copied to clipboard
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
),
copied to clipboard
It is exactly the same as the conventional TextFormField with two more fields, the observable and obscureTextButtonConfiguration.
Full usage example here.
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
void registerLazySingleton<T extends Object>(
CDependencyInjectorBuilder<T> builder, {
String? dependencyName,
}) {
// your implementation
}
@override
void registerFactory<T extends Object>(
CDependencyInjectorBuilder<T> builder, {
String? dependencyName,
}) {
// your implementation
}
@override
void registerSingleton<T extends Object>(
T value, {
String? dependencyName,
}) {
// your implementation
}
@override
void registerFactoryAsync<T extends Object>(
CDependencyInjectorAsyncBuilder<T> builder, {
String? dependencyName,
}) {
// your implementation
}
@override
void registerSingletonAsync<T extends Object>(
CDependencyInjectorAsyncBuilder<T> builder, {
String? dependencyName,
}) {
// your implementation
}
@override
void registerSingletonAsync<T extends Object>(
CDependencyInjectorAsyncBuilder<T> builder, {
String? dependencyName,
}) {
// your implementation
}
@override
T get<T extends Object>({String? dependencyName}){
// your implementation
}
Future<T> getAsync<T extends Object>({String? dependencyName}){
// your implementation
}
Future<void> reset({bool dispose = false}){
// your implementation
}
}
Cubes().injector = MyInjector();
copied to clipboard
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;
copied to clipboard
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);
});
}
copied to clipboard
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.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.