lib_x 0.0.4 lib_x: ^0.0.4 copied to clipboard
lib_x is a simple library which includes some well tested, lightweight, and commonly needed packages. And on top of those packages, there're some preconfigured solutions and simplifications to help yo [...]
lib_x #
lib_x is a simple library which includes some well tested, lightweight, and commonly needed packages. And on top of those packages, there're some preconfigured solutions and simplifications to help you quickly focus on the scaffold within few steps.
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: #
- routemaster
- overlay_support
- url_strategy
- back_button_interceptor
- google_fonts
- font_awesome_flutter
- flutter_svg
- mime
- http
- intl
- shimmer
- crypto
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
- DataController & ReBuilder
- ValueController & ReactiveBuilder
- 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.
Widget 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.
abstract class X
#
It's the 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 the declaration of materialApp, routeMap, and 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
that holds the data for its descendant widgets to access.static of(context)
method that returns 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
DataController & 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 View Layer, and put each layer separately, like in MVC design model. That's why this solution is divided in 2 separate classes, a view class "widget", and data controller class.
-
DataController
: It's an extension ofChangeNotifier
with a better name. This would be the controller class of aRebuilder
widget. This class should encapsulte all the data logic separately from the view logic. When data changes, and the@protected update()
method is called, theReBuilder
widget will rebuild to reflect the 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 a widget built on top ofAnimatedBuilder
widget that will rebuild when the controller changes. and it takes 2 named parameters:DataController controller
: an instance of a DataController object.Function builder
: a function that returns aWidget
, which will rebuild when the controller changes.
E.g.
// Instead of StatefulWidget and changing the data with setState(). we'll create 2 separate layers:
// 1. data model that extends DataController
class UserModel extends DataController {
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 rebuild
}
}
// 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 DataController
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 an external 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 objects. ValueController<T>
to control a value of some type, and ReactiveBuilder
that refelcts the change of that value.
-
ValueController<T>
: It's a value controller object built on top ofValueNotifier
. It has a the following 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 aStatelessWidget
on top ofValueListenableBuilder
that rebuilds when the value of the controller updates. 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,
),
);
}
}
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 thanbarrierDismissible
in the native functionshowDialog()
.
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.