ChangeNotifier
Controlling Widgets with ChangeNotifier
When You Need to Control a Widget from Outside
State management solutions are incredibly convenient, but sometimes we need pure Flutter solutions without relying on third-party libraries.
In some cases, we have shared widgets that require a built-in state management tool from Flutter itself. This is where ChangeNotifier becomes a powerful option.
We can think of ChangeNotifier as a way to create controllers similar to familiar built-in Flutter controllers like:
TextEditingController – used to control text fields.
ScrollController – used to control scrolling behavior.
Just like these, ChangeNotifier allows us to create custom controllers for widgets while keeping everything within Flutter’s ecosystem.
Quickly Implementing ChangeNotifier
Using ChangeNotifier requires initialization and writing a significant amount of code. To simplify this process, I use Mason to generate a boilerplate template for faster development.
If you're unfamiliar with Mason, check out this guide:
MasonYou can also explore my Mason template for ChangeNotifier here: 🔗 dr_change_notifier
Controller Section
This part defines the controller responsible for managing the widget’s state.
/// Part II: Controller
class {{name.pascalCase()}}Controller with ChangeNotifier implements Listenable {
/// Learn more: url_to_document_of_package
int _example = 0;
int get example => _example;
// USE | controller.example = example;
set example(int index){
_example = index;
notifyListeners();
}
// @override
// void dispose() {
// super.dispose();
//
// }
}
This section contains variables for storing data. If you want to update the screen that uses this controller, call notifyListeners();
at the end of the function. This will trigger a UI rebuild.
UI Section
This part defines how the user interface (UI) interacts with the ChangeNotifier controller.
/// Part I: Screen
class {{name.pascalCase()}} extends StatefulWidget {
const {{name.pascalCase()}}({super.key, this.controller, this.autoDispose = true});
final {{name.pascalCase()}}Controller? controller;
final bool autoDispose;
@override
State<{{name.pascalCase()}}> createState() => _{{name.pascalCase()}}State();
}
class _{{name.pascalCase()}}State extends State<{{name.pascalCase()}}> {
late {{name.pascalCase()}}Controller controller;
@override
void initState() {
super.initState();
controller = widget.controller ?? {{name.pascalCase()}}Controller();
controller.addListener(() {
if (mounted) {
setState(() {});
}
});
}
// update once properties is changed
@override
void didUpdateWidget({{name.pascalCase()}} oldWidget) {
super.didUpdateWidget(oldWidget);
// if (widget.path != oldWidget.path) {
//
// }
}
@override
void dispose() {
if(widget.autoDispose) controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// final ratio = context.dsgRatio;
return const Text("ok");
}
}
In this UI section, I have pre-configured the following:
Automatic controller initialization if no controller is passed in
initState()
.Automatic disposal of the controller when the widget is destroyed in
dispose()
.Listening for
notifyListeners()
calls from the controller and updating the UI accordingly.
controller.addListener(() {
if (mounted) {
setState(() {});
}
});
Real-World Usage

Assuming I have a ChangeNotifier structure for the camera as shown in the image, the initialization process will be as follows:
Step 1: Initialize the Controller
CameraWidgetController controllerCamera = CameraWidgetController();
Make sure to dispose of it when it's no longer needed.
Step 2: Initialize the UI

Pass the controller into the widget that needs to be controlled externally.
Step 3: Usage

Now, you can use it anywhere in your project as needed.
This is my approach to controlling widgets externally. You can use this as a reference and create your own code template with Mason, customizing it to fit your coding style and preferences.
Last updated