A State Managament Code Base
(based on flutter_bloc) that helps effortlessly & easily implement BLoC pattern for production quality and maintenance.
There are various ways to implement flutter_bloc for a screen, sometime newcomers get lost into finding a best solution. Thus, this package aims to give simplest code base for implementating 3 differents layers in a screen (ex: Logic, View, Data/State) and easily use it without exploring in all flutter_bloc features, which we don't usually use all of them in production's cases.
This package is stably used in more than 20 author's enterprise applications and still counting.
This package is built to work with:
Here's some main features:
- State management (Of course!)
- Highly customized event based functions:
- Show loading page
- Show error dialog
- Show message dialog
- Listen to state changed
- Easily control rebuilding's conditions
A screen/feature in Smooth Bloc will contains 3 Dart
classes:
Cubit
: This class holds your logics that user interact on the View and you can control when/how the View should rebuild.
State
: This class includes properties displayed in View and could be changed overtime by Cubit class.
View
: This class holds all of your widgets, and will automatically rebuild whenever State is changed. The View usually call Cubit's functions.
dependencies:
# add copy_with_extension (Optional)
copy_with_extension:
# add smooth_bloc
smooth_bloc:
dev_dependencies:
# add copy_with_extension_gen (Optional)
copy_with_extension_gen:
Smooth Bloc allows customize Dialog & Loading Screen if you want.
void main() {
// Setup SmoothBloc
SmoothBloc().setUp(
appLoadingBuilder: (message) {
// Return your customized Widget here
},
appDialogBuilder: (message) {
// Return your customized Widget here
},
);
// Run App
runApp(const TestApp());
}
Classes that extends BaseState
need to implement getter value of stateComparisonProps
which declares props used to compare & identify when should the view need rebuild (when state are changed!)
You can use copy_with_extension to help generate copyWith() function. It's very useful in Cubit's logical functions.
//@CopyWith() is an optional annotation
@CopyWith()
class LoginState extends BaseState {
final bool isLoggedIn;
// Constructor need to declare default const for all props
LoginState({
this.isLoggedIn = false,
});
@override
List<Object?> get stateComparisonProps => [
isLoggedIn,
];
}
Classes that extends from BaseCubit
will inherit above functions and props.
import 'login_state.dart';
class LoginCubit extends BaseCubit<LoginState> {
LoginCubit() : super(LoginState());
void login(String email, password) {
try {
// Call to show loading screen
showLoading();
// Assumme that your have completed authenticate user
// Call emit function to change state and trigger screen rebuilding
emit(
state.copyWith(
isLoggedIn: true,
),
);
// You could show message to user
showMessage("You've been logged in!");
} catch (e) {
// Show error dialog on screen
handleError(e.toString());
} finally {
// Call to hide loading screen
hideLoading();
}
}
}
@override
Future<void> close() {
// Anything here you want to dispose/close/cancel with the cubit
return super.close();
}
Additionally, you can make use of eventStreamController
to fire any customized event class (extended from BaseEvent
), and the View can listen to it!.
For example, if you want to create an event that trigger view to navigate to another view.
/// Create an event class
class PushPageEvent extends BaseEvent {
final String routeName;
PushPageEvent({
required this.routeName,
});
}
/// Then use in Cubit
void signOut() {
// Signing user out
// Return to some screen
eventStreamController.add(PushPageEvent(routeName: "main"));
}
/// In View, hanlde like this
@override
onNewEvent(BaseEvent event) {
if (event is PushPageEvent) {
final routeName = event.routeName;
// Push new view
}
return super.onNewEvent(event);
}
Classes that extends from BaseView
will inherit above functions.
import 'login_cubit.dart';
import 'login_state.dart';
class LoginView extends StatefulWidget {
const LoginView({super.key});
@override
State<LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends BaseView<LoginState, LoginCubit, LoginView> {
@override
LoginCubit assignCubit() {
// Example return using dependency injection
return GetIt.instance<LoginCubit>();
// Or return as a constant value
return LoginCubit();
}
// If TRUE, the Cubit close() function will not be called when View is disposed
// Default is FALSE
@override
bool get shouldNotDisposeCubitAndState => false;
// This is from AutomaticKeepAliveClientMixin
@override
bool get wantKeepAlive => true;
@override
onNewEvent(BaseEvent event) {
// Handle your customized event here
if (event is PushPageEvent) {
final routeName = event.routeName;
// Ex: Push new view
}
return super.onNewEvent(event);
}
@override
bool shouldRebuild(LoginState previous, LoginState current) {
// Declare any conditions that allow or not allow view to rebuild
return super.shouldRebuild(previous, current);
}
@override
onStateChanged(LoginState previous, LoginState current) {
// Observe to state changes here!
if (previous.isLoggedIn != current.isLoggedIn && current.isLoggedIn) {
// Ex: Push to home screen
}
return super.onStateChanged(previous, current);
}
@override
String getErrorMessage(error) {
// You can customize error event (fired from Cubit) here
// Most common usage is localization
return super.getErrorMessage(error);
}
@override
String getMessage(msg) {
// You can customize message event (fired from Cubit) here
// Most common usage is localization
return super.getMessage(msg);
}
@override
Widget buildByState(BuildContext context, LoginState state) {
return Scaffold(
body: Center(
child: Column(
children: [
Text(
state.isLoggedIn ? "You're logged in" : "Please sign in!",
),
ElevatedButton(
onPressed: () {
cubit.login("email", "password");
},
child: const Text("Login"),
),
],
),
),
);
}
}