An easy Flutter state management library, EbloX
Introduction to State Management Libraries in Flutter
A library is a collection of subprograms that are utilized in the development of software. Libraries contain supplementary code and data that provide stand-alone program services, allowing for modular code and data sharing and modification.
A collection of procedures is referred to as a library in a programming language (set of programming instructions). Dart comes with a collection of built-in libraries for storing frequently used functions. Classes, constants, functions, typedefs, properties, and exceptions are all part of a Dart library.
One of the most critical and crucial processes in the life cycle of an application is managing state.
Let’s have a look at a basic shopping cart application:
- The user will enter the application using their credentials.
- Once a user has logged in, the application should keep track of their information across all screens.
- When a user adds a product to a cart and saves it, the cart information should be preserved between pages until the user checks out the cart.
- The state of the program at any given time refers to the information about the user and their cart.
Based on how long a state lasts in an application, state management can be separated into two types.
Ephemeral State Management: Last for a few seconds, such as the present state of an animation or a single page, such as a product’s current rating. StatefulWidget in Flutter makes it possible.
App State State Management: Flutter provides scoped model for the entire application, including logged in user details, cart information, and so on.
EbloX
An easy Flutter state management library.It is similar to Bloc, but it uses a lot of annotations and separates business logic from UI through the concepts of Action and State.
Simpler, more reliable, easier to test !
Example
An example of a common counter.
Add dependency:
dependencies: eblox: eblox_annotation: dev_dependencies: build_runner: eblox_generator:
New counter_view_model.dart
import 'package:eblox/eblox.dart'; import 'package:eblox_annotation/eblox_annotation.dart'; import 'package:flutter/cupertino.dart'; part 'counter_view_model.g.dart'; @bloX class _CounterVModel extends Blox{ @StateX(name:'CounterState') int _counter = 0; @ActionX(bind: 'CounterState') void _add() async{ _counter ++; } @ActionX(bind: 'CounterState') void _sub(){ _counter--; } @override void dispose() { super.dispose(); debugPrint('CounterVModel dispose...'); } }
Execute flutter pub run build_runner watch --delete-conflicting-outputs
command will generate the counter_view_model.g.dart
file in the current directory.
It will automatically generate Action and State for us. Next, write the UI and use these Actions.
import 'package:eblox/blox.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'blox/counter_view_model.dart'; class CounterPage extends StatelessWidget { const CounterPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child:BloxBuilder<CounterVModel,CounterState>( create:()=>CounterVModel(), builder: (count) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text("$count"), ElevatedButton( onPressed: () { AddAction().to<CounterVModel>(); }, child: const Text("+"), ), ElevatedButton( onPressed: () { SubAction().to<CounterVModel>(); }, child: const Text("-"), ), ], ), ); }), ), ); } }
So easy!
Usage
Derive a class from Blox, where we write business logic. This is the ViewModel in MVVM. Note that in order to facilitate annotation to generate code, the class must use the _
prefix. Finally, we used the @bloX
annotation on this class.
@StateX
is used to decorate the state we need, it will automatically generate a State class with a specified name for packaging the modified data:
// ************************************************************************** // BloxStateGenerator // ************************************************************************** class CounterState<T> extends BloxSingleState<T> { CounterState(data) : super(data); }
If you do not specify a name, the state class will be generated according to the default rules. E.g:
@StateX() Color _color = Colors.white;
Will generate ColorState
.
@ActionX
is used to generate the Action class, and the name can also be specified. The bind
is used to specify which State class to associate this Action with. In addition, it also associates the decorated method with the generated Action, and this method is called when the Action is sent.
An @AsyncX
annotation is also currently provided to decorate asynchronous state:
part 'search_view_model.g.dart'; @bloX class _SearchVModel extends Blox{ @AsyncX(name: 'SongListState') SongListModel _songModel = SongListModel(); @bindAsync @ActionX(bind: 'SongListState') BloxAsyncTask<SongListModel> _search(String name){ return (){ return SearchService.search(name); }; } }
@ActionX
decorated method can also declare parameters, the generated class will automatically include:
// ************************************************************************** // BloxActionGenerator // ************************************************************************** class SearchAction extends BloxAction { SearchAction(String name) : super.argsByPosition([name]); }
To associate an Action method with an asynchronous state, you need to add another annotation @bindAsync
. The method annotated by @bindAsync
must return the type BloxAsyncTask<T>
, and the generic T
is the data we need to load asynchronously type.
In the UI, you can use BloxView
to handle asynchronous states:
class SearchPage extends StatelessWidget { SearchPage({Key? key}) : super(key: key); final TextEditingController _controller = TextEditingController(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Song search'),), body: SafeArea( child: Column( children: [ TextField( controller: _controller, decoration: InputDecoration( contentPadding: const EdgeInsets.symmetric(horizontal: 16), suffix: IconButton( icon: const Icon(Icons.search_rounded), onPressed: (){ if(_controller.text.isNotEmpty) { SearchAction(_controller.text).to<SearchVModel>(); } }, )), ), Flexible( child: BloxView<SearchVModel, SongListState<SongListModel>>( create: () => SearchVModel(), onLoading: () => const Center(child: CircularProgressIndicator()), onEmpty: ()=> const Center(child: Text("Empty")), builder: (state) { return ListView.builder( itemCount: state.data.songs.length, itemBuilder: (ctx, i) { return Container( alignment: Alignment.center, height: 40, child: Text(state.data.songs[i],style: const TextStyle(color: Colors.blueGrey,fontSize: 20),), ); }); }, )), ], ), ), ); } }
BloxView
provides onLoading
, onEmpty
, onError
,builder
to handle the UI display during and after loading.
Note that if you want onEmpty
to be valid, then your custom data type should mixin BloxData
:
class SongListModel with BloxData{ SongListModel({UnmodifiableListView<String>? songs}){ if(songs !=null) this.songs = songs; } UnmodifiableListView<String> songs = UnmodifiableListView([]); @override bool get isEmpty => songs.isEmpty; }
Please check here for detailed examples.
GitHub
Source Code: EbloX.