lib_x 0.1.3 lib_x: ^0.1.3 copied to clipboard
lib_x is an Object-Oriented approach to Flutter for a better architecture design. It provides Solutions for Routing, State Management, Data Provider & more.
lib_x #
lib_x is a simple library that includes some well tested, and commonly needed packages. And on top of those packages, there're some preconfigured solutions and simplifications for a better architecture design.
lib_x is designed to complement 2 things: #
- The Object-Oriented nature of Dart by separating between the
View
logic & theData
logic in a Flutter application. - The Flutter Framework methods with some simplifications. So we're making things the Flutter way.
The purpose of this library: #
- Grouping some basic packages into one.
- Avoiding some of the commonly needed boilerplate.
- Providing simple solutions for [Routing, State Management, Data Providers].
- Helping with the Design Pattern & Separation Of Concerns.
- Semantic & Self-Explanatory Naming.
Packages included in this liberary: #
By installing lib_x
, you have these awesome packages already installed, and you can directly import them in your application. Plus, you can also use one of the following solutions.
Solutions #
- MaterialX & X Controller
- ScaffoldX
- DataProvider
- StatefulData & ReBuilder
- ValueController & ReactiveBuilder
- XUtils
- Bonus Widgets
MaterialX & X Controller
#
We often need more than just push()
& pop()
which the simple MaterialApp provides. For a real world application, we need navigation via url, deep linking, routing without depending on context
... And for that we need to configure Navigator 2.0 via MaterialApp.router()
or MaterialX()
with the controller class X
that separates & encapsulates all the routing and themeing concerns.
MaterialX
#
Takes these 2 named parameters and builds a MaterialApp.router()
to be used in the runApp()
function:
MaterialApp materialApp
: MaterialApp that contains the themes, and locale concerns. Not the routing options.RouteMap routeMap
: A map between the routing patterns and their correspondingScaffold
s.
E.g.
lib/main.dart
void main() {
runApp(const MyApp());
}
lib/src/views/const/route_map.dart
const String LoginPath = '/login';
const String RootPath = '/';
const String UserPath = '/user/';
const String ContactPath = '/contact/';
const String PostPath = '/post/';
// if authentication, guarded routes
bool isLoggedIn = true;
final RouteMap routeMap = RouteMap(
routes: {
LoginPath: (info) => const MaterialPage(child: LoginPage()), // without Guarding
RootPath: (info) => isLoggedIn // with Guarding
? const MaterialPage(child: HomePage())
: const Redirect(LoginPath),
UserPath + ':username': (RouteData info) => isLoggedIn
? MaterialPage(child: UserPage(username: info.pathParameters['username']!))
: const Redirect(LoginPath),
UserPath + ':username' + ContactPath : (RouteData info) => isLoggedIn
? MaterialPage(child: ContactUserPage(username: info.pathParameters['username']!))
: const Redirect(LoginPath),
PostPath + ':postId': (RouteData info) => isLoggedIn
? MaterialPage(child: PostPage(postId: info.pathParameters['postId']!))
: const Redirect(LoginPath),
PostPath + ':postId/:commentId': (RouteData info) => isLoggedIn
? MaterialPage(
child: CommentPage(
postId: info.pathParameters['postId']!,
commentId: info.pathParameters['commentId']!,
))
: const Redirect(LoginPath),
// ... and so on
},
onUnknownRoute: (_) => const MaterialPage(child: NotFoundPage()), // fallback route
);
lib/src/views/const/material_app.dart
final MaterialApp materialApp = MaterialApp(
title: 'Contacts App',
debugShowCheckedModeBanner: false,
theme: myLightTheme,
darkTheme: myDarkTheme,
themeMode: ThemeMode.system,
);
lib/src/views/my_app.dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialX(
materialApp: materialApp,
routeMap: routeMap,
);
}
}
// now from anywhere in your app you can navigate like this:
// X.to(UserPath + '_username_');
For more info about RouteMap type and the package routemaster.
X
#
It's an abstract controller class for the MaterialX
widget, and it has the following interface:
Notes:
abstract class
in Dart simply means it's not to be instantiated, and can have abstract methods.- Abstract Method in Dart is a method to be implemented. It's a function without a body inside an
abstract class
e.g. the famousbuild
method in theabstract class StatelessWidget
.
Theme Management
-
ThemeData X.theme
=> returnsThemeData
object of the current context -
MediaQueryData X.mediaQuery
=> returnsMediaQueryData
object of the current context -
ValueController<ThemeMode> X.themeMode
=> is the themeMode controller ofMaterialX
. If you provided both light and dark themes in the materialApp. MaterialX will change automatically with system themeMode changes, unless you specified a themeMode with a different value thanThemeMode.system
. -
void X.switchTheme({ThemeMode? to})
=> it takes one of these values[ThemeMode.system, ThemeMode.dark, ThemeMode.light]
and updates the value ofX.themeMode
. It also updates the stausBarcolor
andbrightness
accordingly. If you didn't pass a value of themeMode, it will just switch the current themeMode to the opposite, and it will stop listening to the system themeMode changes. To change theme with system again, use
X.switchTheme(to: ThemeMode.system);
-
void X.setStatusBar({Color? color, Brightness? brightness})
=> to set the color and brightness of the statusBar. -
void X.forcePortrait()
=> to force Portait Orientation. -
void X.forceLandscape()
=> to force Landscape Orientation. -
void X.allowAutoOrientation()
=> to allow both Portait & Landscape Orientations.
Route Management
-
String X.currentPath
=> returns aString
of the current path -
Map<String, String>? X.currentPathParameters
=> returns aMap
of the current path parameters, e.g. if the current path is 'user/12345', it will return{id: '12345'}
-
Map<String, String>? X.currentQueryParameters
=> returns aMap
of the current path query parameters, e.g. 'user?id=12345' returns{id: '12345'}
-
void X.to({required String path})
=> navigate to path, that is defined in theRouteMap
. -
void X.offTo({required String path})
=> navigate to path and prevents back. -
void X.back()
=> go back to previous route chronologically. -
void X.backTo({required String path})
=> pop all routes till the provided path. -
void X.openDrawer()
=> open Drawer programmatically. -
void X.dissmissKeyboard()
=> close device keyboard if it's open. -
void X.showSnackBar({required SnackBar snackBar})
=> show snackBar. -
void X.showNotification({})
=> show notification overlay. It takes the following named parameters:
X.showNotification({
required Widget widget,
bool dismissable = true,
VoidCallback? onDismiss,
VoidCallback? onTap,
Duration duration = const Duration(seconds: 5),
NotificationPosition position = NotificationPosition.top,
})
-
void X.showBottomSheet({required Widget child})
=> show this widget in bottomSheet. -
void X.showModal({})
=> open modal route with this dialoug widget. It takes the following named parameters:
X.showModal({
required Widget widget,
bool safeArea = true,
bool dismissable = true,
Color? barrierColor,
})
-
bool X.isOpenModal
=> to check if modal is open. -
void X.pop()
=> pop modal, bottomSheet, drawer, or keyboard.
ScaffoldX #
Right after finishing MyApp, we need to create the pages that contains a Scaffold
widget and corresponds with the defined paths in the routeMap
. ScaffoldX
is a quick way to compose a scaffold with default configurations. It has the following named parameters:
Widget ScaffoldX({
required Widget body,
Color? bgColor,
DecorationImage? bgDecorationImage,
Widget? appBar,
double appBarHeight = 60,
Widget? drawer,
Widget? bottomNavigationBar,
Widget? bottomSheet,
Widget? fab, // FloatingActionButton
FloatingActionButtonLocation? fabLocation,
BoxConstraints? constraints,
TextStyle textStyle = const TextStyle(color: black, fontSize: 16),
VoidCallback? onInit,
bool safeArea = true,
bool scrollView = false,
})
E.g.
class UserPage extends StatelessWidget {
final String username;
const UserPage({Key? key, required this.username}) : super(key: key);
@override
Widget build(BuildContext context) {
return ScaffoldX(
onInit: () => debugPrint(username),
appBar: const MyAppBar(),
bottomNavigationBar: const MyNavBar(),
bgDecorationImage: const DecorationImage(
image: AssetImage('assets/images/bg.png'),
fit: BoxFit.fill,
opacity: .3,
),
body: Center(
child: Text('Hello $username'),
),
);
}
}
Note: If you're not going to use ScaffoldX, make sure to implement the BackButtonInterceptor
to handle the back guesture.
E.g.
class UserPage extends StatefulWidget {
final String username;
const UserPage({Key? key, required this.username}) : super(key: key);
@override
State<UserPage> createState() => _UserPageState();
}
class _UserPageState extends State<UserPage> {
@override
void initState() {
BackButtonInterceptor.add(myInterceptor);
super.initState();
}
@override
void dispose() {
BackButtonInterceptor.remove(myInterceptor);
super.dispose();
}
FutureOr<bool> myInterceptor(bool stopDefaultButtonEvent, RouteInfo info) {
X.back();
return true;
}
@override
Widget build(BuildContext context) {
return Scaffold(...);
}
}
DataProvider #
Widget DataProvider<T>
Instead of passing the data from parent widget to children to sub-children... via parameters, which gets messy very quickly, we need the concept of data provider class. And for that we can either use InheritedWidget
or DataProvider
to assign the desired data to a widget, and all and only the descendant children & sub-children widgets will have access to that data via the static method of(context)
.
DataProvider
needs 3 arguments:
- Data Object
data
of any typeT
, likeUserModel
,List<String>
, ... etc. Widget child
is a data access point for its descendant widgets.static of(context)
to return the provider class instance of this context.
E.g. models/user_model.dart
// If we have a user model class like this
class UserModel {
final String id;
final String username;
UserModel({required this.id, required this.username});
}
// To create a userModel provider
class UserProvider extends DataProvider<UserModel> {
// Declare the desired data you want the class to provide
final UserModel userModel;
const UserProvider({
super.key,
required this.userModel, // first argument: require the declared data object
required super.child, // second argument: require & pass child to the super class
}) : super(data: userModel); // pass the data to the super class
// third argument: without it the class is useless
// Declare a static method that returns the provider instance of(context)
static UserProvider of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<UserProvider>()!;
// just copy the method and change the class name.
}
views/pages/user_page.dart
// Now the user page could be like this
class UserPage extends StatelessWidget {
final String username;
const UserPage({Key? key, required this.username}) : super(key: key);
@override
Widget build(BuildContext context) {
return ScaffoldX(
body: UserProvider(
userModel: UserModel(
id: 'id',
username: username
),
child: ProfileWidget(),
),
);
}
}
class ProfileWidget extends StatelessWidget {
const ProfileWidget({super.key});
@override
Widget build(BuildContext context) {
// you can access userModel of context
final UserModel userModel = UserProvider.of(context).userModel;
return Text(userModel.username);
}
}
// And all the descendant widgets of ProfileWidget have access to userModel data
StatefulData & ReBuilder #
StatefulWidget
is a very useful widget in a lot of situations, except when it comes to data management. In a real world application, we need to decouple the Data Layer from the Render Layer, and put each layer separately, like in MVC design. That's why this solution is divided in 2 separate classes, a view class Widget
, and a StatefulData
controller class.
StatefulData
: #
It's an extension of ChangeNotifier
with a better name. It's the controller class of a Rebuilder
widget. This class should encapsulte all the data logic separately from the view logic. When data changes, and the @protected update()
method is called, the ReBuilder
widget will rebuild to reflect changes of data.
Notes:
update()
is protected by design to force separation of concers.@protected
method in Dart means: It cannot be called from outside the class. So if you're going to do crud operations on your data model, it must be inside the class.
ReBuilder
: #
It's an AnimatedBuilder abstracted from context. It will rebuild when the state of data changes using the protected method update()
. and it takes 2 named parameters:
StatefulData controller
: an instance of a StatefulData object.Function builder
: a function that returns aWidget
, which will rebuild when the controller say so.
E.g.
// Instead of StatefulWidget and changing the state of data with setState(). we'll create 2 separate layers:
// 1. data model that extends StatefulData
class UserModel extends StatefulData {
final String id;
String username;
UserModel({required this.id, required this.username});
// we manage the data state here inside the data class
void changeUsername(String newName) {
username = newName;
update(); // triggers Rebuilder to render changes
}
}
// 2. view model 'widget' that reflect the state of the data model using ReBuilder
class ProfileWidget extends StatelessWidget {
const ProfileWidget({super.key});
@override
Widget build(BuildContext context) {
final UserModel userModel = UserProvider.of(context).userModel;
return ReBuilder(
controller: userModel, // first parameter: an instance of StatefulData
builder: () { // second paramter: a function that returns a widget
// whenever the username changes and update has been called, this widget will reflect these changes
return Text(userModel.username);
}
);
}
}
- Note:
ReBuilder
widget is still a Statefull widget deep under the hood, but with a controller that triggers setState() for us.
ValueController & ReactiveBuilder #
Sometimes we only have one independent value that we need to listen to its state. Again, we'll create 2 separate classes: view class & value controller class
ValueController<T>
: #
It's a value controller object built on top of ValueNotifier
. It has a the following public interface:
value
=> returns the current value.ValueListenable listenable
=> listenable object that could be used withValueListenableBuilder
.void update(T v)
=> to update the value property of typeT
.set onChange(VoidCallback callback)
=> a setter method if you want to attach a callback function that runs whenever the value changes.void dispose()
=> valueController cannot be used after calling this method.
ReactiveBuilder
#
It's a ValueListenableBuilder
abstracted from context. It rebuilds when controller.update(value)
invoked. And it takes 2 named parameters:
ValueController<T>
.Function builder(T value)
: a function with value argument that returns a widget, that rebuilds when the value of controller changes.
E.g.
final ValueController<ThemeMode> themeModeController = ValueController<ThemeMode>(ThemeMode.dark);
class SwitchThemeButton extends StatelessWidget {
const SwitchThemeButton({super.key});
@override
Widget build(BuildContext context) {
ThemeMode value() => themeModeController.value == ThemeMode.dark
? ThemeMode.light
: ThemeMode.dark;
// when this button pressed, the AdaptiveText widget will change the text color,
// because it has ReactiveBuilder which is controlled by themeModeController
return TextButton(
onPressed: () => themeModeController.update(value()),
child: const Text('Change Theme'),
);
}
}
class AdaptiveText extends StatelessWidget {
final String text;
final Color? lightModeC;
final Color? darkModeC;
final double? fontSize;
const AdaptiveText(
this.text, {
super.key,
this.lightModeC,
this.darkModeC,
this.fontSize,
});
@override
Widget build(BuildContext context) {
return ReactiveBuilder(
controller: themeModeController,
builder: (ThemeMode mode) => Text(
text,
style: TextStyle(
color: mode == ThemeMode.dark
? darkModeC ?? white
: lightModeC ?? black,
fontSize: fontSize,
),
textAlign: TextAlign.center,
),
);
}
}
XUtils #
It's an abstract class
that provides some handy quick solutions. And it has the following static interface:
bool XUtils.isUrl(String string)
=> check if string is urlbool XUtils.isAsset(String path)
=> check if path starts with"assets/"
bool XUtils.isSVG(String path)
=> check if path contains".svg"
bool getter XUtils.isSysDarkMode
=> returnstrue
if system is in dark mode ||false
ThemeMode getter XUtils.sysThemeMode
=> returns the system's currentThemeMode
valueint getter XUtils.now
=> returns the int value of now timestamp in secondsString XUtils.formatTimestamp(int timestamp, {bool shortMonthFormat = true})
=> convert timestamp to readable format.- if today: returns [Hours:Minutes AM/PM] e.g. 5:30 PM
- if yesterday: returns [Yesterday - Hour AM/PM] e.g. Yesterday - 8 AM
- if same year: returns [Month Day] e.g. May 29
- else: returns [Month Day Year] e.g. Jan. 25 2011
- default month format is short e.g. January becomes Jan.
bool XUtils.isNumeric(String str)
=> check if a string is a number e.g. '123' returnstrue
, '+1' returnsfalse
bool XUtils.isEmail(String email)
check if string is a valid email format based on the HTML5 email validation specsString XUtils.genString({int length = 16})
=> generate random string with default length value of 16String XUtils.genNum({int length = 16})
=> generate random number string with default length value of 16String XUtils.genId({int length = 16})
=> generate timestamp based id string
Widgets #
Widget PersistStateWidget({required Widget child})
It could be useful if you have e.g. ScrollView and you want to maintain its state like scoll position when navigating to other tabs.
E.g.
class MyListView extends StatelessWidget {
const MyListView({super.key});
@override
Widget build(BuildContext context) {
final List<Widget> myList = List.generate(100, (index) => Text(index.toString())).toList();
// wrap ListView with PersistStateWidget to maintain its scroll state
return PersistStateWidget(
child: ListView.builder(
itemBuilder: (context, index) => myList[index],
),
);
}
}
Widget DismissModalWidget({required Widget child})
If you will push a modal route and you want it to pop when clicked outside of the dialog widget, wrap the dialog widget with DismissModalWidget
.It's more reliable than barrierDismissible
in the native function showDialog()
.
E.g.
class MyDialog extends StatelessWidget {
const MyDialog({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('Dialog'));
}
}
class ShowMyDialogButton extends StatelessWidget {
const ShowMyDialogButton({super.key});
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () => showDialog(
context: context,
barrierDismissible: true, // not reliable
builder: (context) {
return const DismissableModal(child: MyDialog());
},
),
child: const Text('Show Dismissable Dialog'),
);
}
}
- Note: The dismissable behavior is the default if you're gonna use
X.showModal(child: MyDialog())
instead of showDialog().
P.S. #
- These are my personal implementations, which I don't know if it will be useful for others or not, but I hope it will be.
- Feel free to suggest more simplifications or optimizations by pulling a request on Github.
- Feel free extending it or modifying it as you wish.