Flutter App Manager (app_manager)
Flutter App Manager is a lightweight
Flutter package that simplifies
the customization
of Flutter apps.
Some of the things you can achieve with this package include:
- Easy management of your app's
theme
, allowing you to create custom themes. - Convenient management of your app's
language
settings.
And much more...
Explore the Example App to see practical usage examples.
You can show your support by starring it on GitHub and giving it a like on pub.dev. You can also check out my other packages on pub.dev.
Table of Contents
Getting Started
From this point onward, this article will provide a comprehensive, step-by-step guide covering everything you need to know about utilizing the app_manager
package.
Creating Cores
The app_manager
package utilizes subunits known as cores
. You can envision a core
as a mode handler. It stores modes and enables the app to use the currently active mode. This mode is changeable and can respond to system changes.
Creating a Core
Creating a core
is just simple as extening a class. To create a core just extend the AppManagerCore
class.
class YourCore extends AppManagerCore {}
The AppManagerCore
requires 3 overrides.
1. The modes
Getter
The modes
getter is where the AppManagerCore
stores its modes. Each mode
consists of a key and a value, therefore modes are stored in a map. The key
of a mode is defined as an enum
and is referred to as the mode key
, while the value of a mode is a model referred to as the mode model
.
To create your mode keys
, define an enum, and for your mode models
, create a model class.
enum YourCoreModes {
mode1,
mode2,
mode3,
}
class YourCoreModel {
final String yourString;
YourCoreModel({
required this.yourString,
});
}
Following that, you can create your modes by using the mode key
you've created as the key
, and the corresponding model as the value
within the modes
map.
class YourCore extends AppManagerCore {
@override
Map<YourCoreModes, YourCoreModel> get modes => {
YourCoreModes.mode1: YourCoreModel(
yourString: "mode1 strig",
),
YourCoreModes.mode2: YourCoreModel(
yourString: "mode2 strig",
),
YourCoreModes.mode3: YourCoreModel(
yourString: "mode3 strig",
),
};
}
Now that you've created modes for your core, before we proceed further, there is one more step to complete. We must always utilize the generics
of the AppManagerCore
class. The AppManagerCore
class contains two generic fields: the first one should correspond to the mode key
enum, and the second one should correspond to the mode module
class you've created.
class YourCore extends AppManagerCore<YourCoreModes, YourCoreModel> {
@override
Map<YourCoreModes, YourCoreModel> get modes => {
YourCoreModes.mode1: YourCoreModel(
yourString: "mode1 strig",
),
YourCoreModes.mode2: YourCoreModel(
yourString: "mode2 strig",
),
YourCoreModes.mode3: YourCoreModel(
yourString: "mode3 strig",
),
};
}
2. The defaultMode
Getter
The defaultMode
getter is used when no modes are stored locally. Therefore, set the mode key
of the mode you want to use as the default mode.
The defaultMode
must exist as a mode key
in the modes
map.
Note: Its usage changes when the core is bound to an utility.
class YourCore extends AppManagerCore<YourCoreModes, YourCoreModel> {
@override
YourCoreModes get defaultMode => YourCoreModes.mode1;
@override
Map<YourCoreModes, YourCoreModel> get modes => {
YourCoreModes.mode1: YourCoreModel(
yourString: "mode1 strig",
),
YourCoreModes.mode2: YourCoreModel(
yourString: "mode2 strig",
),
YourCoreModes.mode3: YourCoreModel(
yourString: "mode3 strig",
),
};
}
3. Binding Utilities with Cores
A utility acts as a middleman between the core and the system. Utilities listen the system and inform the core about the system's mode.
The core uses the system's mode if a utility is provided, and only if the current mode matches the provided system mode
while the utility is being provided.
The core searches for a mode that matches the system's mode. During this search, the core compares the names of the mode keys
, and if it finds a mode that matches the string returned from the utility as the system mode
, it uses that mode.
To bind a utility with a core, you should override the util
getter of the core. The util
getter takes an AppManagerUtilOptions
class, which has two parameters. The util
parameter accepts an instance of the AppManagerUtil
, while the system
parameter takes a mode key
.
The mode key
provided to the system
parameter is not just an ordinary mode key
. This mode key
serves as the system mode key
, and for this reason, it must not be used in the modes
map or as the default mode
. The system mode key
still allows the core to use a mode just like a regular mode key
, even if it doesn't exist in the modes
map. Instead of providing only one mode, it adapts and changes its mode depending on the system's mode. The mode chosen depending on the system's mode will be one of the modes
provided by you. If the core
finds a matching mode, it will use that mode. However, if it cannot find a mode that matches the system's mode
, it will use the default mode
instead.
Lastly, the core prefers to use the system mode
over the default mode
when no modes are stored locally. You can disable the use of the system mode
instead of the default mode
by setting the useSystemAsDefault
getter to false
. By default, it is set to true
.
You can provide either built-in or your own custom utilities to the util
getter. We will cover creating your own custom utilities in the Creating Custom Utilities chapter. As for the built-in utilities, we have the following options:
AppManagerUtil.theme
orAppManagerThemeUtil
AppManagerUtil.lang
orAppManagerLangUtil
The AppManagerUtil.theme
binds the core to the system's theme mode
. It looks for a mode key
(enum) named as dark
if the system uses dark mode, or as light
if it uses light mode. Just as explained earlier, these two modes must exist in the modes
map; otherwise, the default mode
will be used in place of a non-existing mode.
The AppManagerUtil.lang
binds the core to the system's language mode
. It searches for a mode key
named as the language code
of the current language. You can find the language codes in the IANA Language Subtag Registry.
Now, let's apply what we've covered in this chapter so far. First, we need to add an enum value to our mode key
enum that we won't use in the modes
map. This enum will serve as our system mode key
.
enum YourCoreModes {
system, // Adding this to use as the `system mode key`, we call it as system in our example but you can name it whatever you want.
mode1,
mode2,
mode3,
}
Following that, when we provide an AppManagerUtilOptions
to the util
parameter of the core, with our system mode key
set as the value for the system
parameter, and the utility we wish to use as the util
parameter's value getter, we are finished.
class YourCore extends AppManagerCore<YourCoreModes, YourCoreModel> {
@override
AppManagerUtilOptions<YourCoreModes>? get util => AppManagerUtilOptions(
util: AppManagerUtil.<util>, // Utility you want to use.
system: YourCoreModes.system, // The enum value you created to use as a system mode key.
);
@override
YourCoreModes get defaultMode => YourCoreModes.mode1;
@override
Map<YourCoreModes, YourCoreModel> get modes => {
YourCoreModes.mode1: YourCoreModel(
yourString: "mode1 strig",
),
YourCoreModes.mode2: YourCoreModel(
yourString: "mode2 strig",
),
YourCoreModes.mode3: YourCoreModel(
yourString: "mode3 strig",
),
};
}
You also have the option to use the useSystemAsDefault
getter and set its value to false
if you prefer to use the default mode
when no modes are stored locally.
class YourCore extends AppManagerCore<YourCoreModes, YourCoreModel> {
@override
AppManagerUtilOptions<YourCoreModes>? get util => AppManagerUtilOptions(
util: AppManagerUtil..<util>,
system: YourCoreModes.system,
);
@override
YourCoreModes get defaultMode => YourCoreModes.mode1;
@override
YourCoreModes get useSystemAsDefault => false; // The `default mode` will be used instead.
@override
Map<YourCoreModes, YourCoreModel> get modes => {
YourCoreModes.mode1: YourCoreModel(
yourString: "mode1 strig",
),
YourCoreModes.mode2: YourCoreModel(
yourString: "mode2 strig",
),
YourCoreModes.mode3: YourCoreModel(
yourString: "mode3 strig",
),
};
}
Creating a Style Core
A style core
functions similarly to a regular class. You can access its values, methods, and getters. In addition to these, a style core rebuilds the widget it's used in, and it also enables you to access the cores without the need for a context using its core
method.
Creating a style core
is even easier than creating a regular core. To create a style core, simply extend the AppManagerStyleCore
class.
class YourStyleCore extends AppManagerStyleCore {}
That's all there is to it. Now you can use it just like a regular class, with the added features it provides. For example, let's access the YourCore
core we created earlier.
class YourStyleCore extends AppManagerStyleCore {
YourCore get yourCoreCurrentMode => core<YourCore>().current; // Returns the currently active mode (You will learn more about accessing to cores later.)
}
Connecting the AppManager to Your App
Now that we've created our cores, it's important for our app to be aware of them. The AppManagerScope
facilitates access to its cores for the widgets under it, and we configure it by specifying an AppManagerConfig
as its config
parameter's value. Therefore, let's begin by discussing how to use the AppManagerConfig
.
Using AppManagerConfig
To create a config just extend the AppManagerConfig
class.
class YourConfig extends AppManagerConfig {}
Now, you can configure the cores that you intend to use in your app. These cores will only function if they are included in the list provided to the cores
getter. Attempting to use a core that is not included in the cores
list will result in an error.
class YourConfig extends AppManagerConfig {
@override
List<AppManagerBaseCore>? get cores => [
YourCore(),
YourStyleCore(),
];
}
Using AppManagerScope
The final step before using your cores is to ensure that you place the AppManagerScope
above the location where you intend to access your cores. We recommend placing it above the MaterialApp
widget to ensure that all pages can access it. Once you've done that, pass the config class you created to the config
parameter. With that, you're all set to begin using your cores within your app.
void main() {
runApp(const YourApp());
}
class YourApp extends StatelessWidget {
const YourApp({super.key});
@override
Widget build(BuildContext context) {
return AppManagerScope(
config: YourConfig(), /// Use your config here
child: const MaterialApp(),
);
}
}
Using Cores
Now, let's explore how you can use your cores within your app.
Accessing Cores
Accessing your cores is simple. You can either call the core
method of the AppManager
class or use the core
method from the context
extension.
Accessing cores using AppManager
:
AppManager.core<YourCore>(context);
Accessing cores using context
:
context.core<YourCore>();
As mentioned earlier, style cores
are similar to regular classes, and as such, you can use their getters, methods, or variables along with additional features. For instance, when a core's mode is updated, they will rebuild the widget they were called from. This is because their values might change as well.
context.core<YourStyleCore>().<yourGetter>;
To access the currently active mode of a core, use its current
getter.
context.core<YourCore>().current;
It will provide access to the fields of the mode model
of the currently active mode.
context.core<YourCore>().current.yourString; // Accessing the `yourString` field of the currently active mode's YourCoreModel (mode model).
Changing a Core's Mode
To change the mode of a core, call its changeMode
method and provide the mode key
of the desired mode you want to switch to.
context.core<YourCore>().changeMode(YourCoreModes.mode2);
After that, when you call the current
method of the core, you will access the new mode model
instead.
context.core<YourCore>().current; // Now you will access the mode model of the `YourCoreModes.mode2`.
If you wish to either "save" or "not save" the current mode locally, you can use the saveChanges
argument of the changeMode
method. This argument is optional, and its default value is set to true
.
This might be useful if you want to show a preview of a mode.
context.core<YourCore>().changeMode(YourCoreModes.yourMode, false); // This mode won't be saved locally and the last saved mode will be used when the app starts again.
Creating Custom Utilities
You can create your own utilities by extending the AppManagerUtil
class.
class YourUtil extends AppManagerUtil {}
The AppManagerUtil
requires only one override, which is the systemMode
getter.
The systemMode
getter accepts a string to inform the core about the system's current mode. The returned string will be used to search for the corresponding mode in the modes
map. The core will use a mode if the name of its mode key
(enum) matches the returned string. If no matches are found, it will use the default mode
instead.
class YourUtil extends AppManagerUtil {
@override
String get systemMode => isMode1Active ? "mode1" : "mode2";
}
After that, you need to inform the core by calling the onSystemChange
method of the AppManagerUtil
. This method informs the core, and after that, the core will use the systemMode
getter to access the name of the current mode of the system. Before calling the onSystemChange
method, always use the isCoreBound
getter to check if it is safe to use onSystemChange
.
class YourUtil extends AppManagerUtil {
@override
String get systemMode => isMode1Active ? "mode1" : "mode2";
void didChangePlatformMode() {
if (isCoreBound) onSystemChange!(); // Use it like this
}
}
Lastly, you can use your utility to bind with a core by passing it to the util
parameter of its utility options, as we've discussed in the Binding Utilities with Cores chapter.