flutter_inherited_model
ENGLISH | 한국어
A state management package that helps you easily use Flutter's InheritedModel based on code generation.
You can leverage the powerful features of InheritedModel with simple annotation (@) usage without complex setup, and optimize app performance by rebuilding only the necessary widgets according to state changes.
This package must be used with flutter_inherited_model_builder.
Key Features
- Concise Syntax: Automatically generates an
InheritedModelwidget with a single@FlutterInheritedModelannotation. - Performance Optimization: Automatically detects state fields and rebuilds only the widgets that depend on a specific state.
- Granular State Management: You can define and manage another sub-state within the model using
@FlutterInheritedState. This allows you to organize and separate complex state logic more systematically. - Lifecycle Management: You can directly manage widget lifecycle events such as
onInitState,onDispose, andonDidChangeAppLifecycleStatewithin the model class. - Event Handling: You can easily handle interactions between the model and the UI (e.g., showing a SnackBar, navigating pages) through the
Eventsystem. - Asynchronous Task Support: You can safely perform asynchronous tasks within the model with the
useAsyncWorkeroption.
Installation
Add the following dependencies to your pubspec.yaml file.
dependencies:
flutter_inherited_model: ^0.9.5 # Please check the latest version on pub.dev.
dev_dependencies:
flutter_inherited_model_builder: ^0.9.5 # Please check the latest version.
build_runner: ^2.5.4 # The build_runner version may be adjusted for compatibility.
Basic Usage
1. Create a Model Class
Create a class to manage the state and add the @FlutterInheritedModel annotation.
- You must add the
part 'your_file.g.dart';statement at the top of the file. - Add the
@inheritedModelStateannotation to the state fields you want to manage.
lib/main_model.dart
import 'package:flutter_inherited_model/flutter_inherited_model.dart';
part 'main_model.g.dart';
// 1. Define the FlutterInheritedModel annotation
@FlutterInheritedModel(
name: 'MainInheritedModel',
event: MainModelEvent,
)
// 2. Add the generated Mixin with the 'with' keyword.
class MainModel with $MainModel {
// 3. Create a private constructor and a factory constructor that connects to the generated class.
MainModel._();
factory MainModel() = _$MainModel;
// 4. Specify the state to manage with the @inheritedModelState annotation.
@inheritedModelState
late int count;
// Lifecycle callback for model initialization
@override
void onInitState() {
super.onInitState();
count = 0;
}
// Lifecycle callback for model disposal
@override
void onDispose() {
super.onDispose();
}
// Method to change the model state
void onIncrement() {
count++;
}
// Deliver UI Event
void onShowSnapBar(String message) {
emitEvent(MainModelSnackBarEvent(message));
}
}
// Define events for model-UI interaction
sealed class MainModelEvent {}
class MainModelSnackBarEvent implements MainModelEvent {
final String message;
MainModelSnackBarEvent(this.message);
}
2. Run Code Generation
Run the following command in the root directory of your project to generate the code.
flutter pub run build_runner build --delete-conflicting-outputs
3. Provide the Model in the Widget Tree
Wrap the widget tree where you want to share the state with the generated MainInheritedModel widget.
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: MainPage(),
);
}
}
class MainPage extends StatelessWidget {
const MainPage({super.key});
@override
Widget build(BuildContext context) {
return MainInheritedModel(
onEvent: (MainModel model, MainModelEvent event) async {
switch (event) {
case MainModelSnackBarEvent():
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(event.message)));
return true;
}
},
child: _MainPage(),
);
}
}
4. Use the State in the UI
In child widgets, you can easily access the model and state using the generated static methods.
MainInheritedModel.model(context): Accesses the model instance (for method calls, etc.).MainInheritedModel.countOf(context): Accesses thecountstate. The widget is rebuilt only when this state changes.
class _MainPage extends StatelessWidget {
const _MainPage();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('InheritedModel Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
// Depends only on the 'count' state using countOf()
// A Builder was used to create a context to change only the Text Widget.
Builder(
builder: (context) {
return Text(
'${MainInheritedModel.countOf(context)}',
style: Theme.of(context).textTheme.headlineMedium,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// Call the model's method
MainInheritedModel.model(context).onIncrement();
MainInheritedModel.model(context).onShowSnapBar('Increment');
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Advanced Usage: Managing Sub-states with @FlutterInheritedState
Sometimes, a model's state can become complex and have multiple responsibilities. Using @FlutterInheritedState, you can encapsulate related state and logic into a separate class to keep the model cleaner.
1. Define a Sub-state Class
Define a class to manage the sub-state using the @FlutterInheritedState annotation.
lib/user_state.dart
import 'package:flutter_inherited_model/flutter_inherited_model.dart';
part 'user_state.g.dart';
// 1. Define the FlutterInheritedState annotation
@FlutterInheritedState(name: 'UserInheritedState')
// 2. Add the generated Mixin with the 'with' keyword.
class UserState with $UserState {
// 3. Create a private constructor and a factory constructor that connects to the generated class.
UserState._();
factory UserState() = _$UserState;
// 4. Specify the state to manage with the @inheritedModelState annotation.
@inheritedModelState
late String name;
@inheritedModelState
late int age;
// Lifecycle callback when UserInheritedState is attached
@override
void onAttachState() {
super.onAttachState();
}
// Lifecycle callback when UserInheritedState is detached
@override
void onDetachState() {
super.onDetachState();
}
}
2. Include the Sub-state in the Main Model
In the main model, declare UserState, which has the @FlutterInheritedState annotation, as a field. After building, you can access the state of UserState from the UI through the generated code (_$MyModel.userStateOf(context)).
lib/main.dart
// ... (existing code)
@FlutterInheritedModel(
name: 'MainInheritedModel',
// ...
)
class MainModel with $MainModel {
// ... (existing code)
// Include UserState as a sub-state
final userState = UserState();
@override
void onInitState() {
super.onInitState();
count = 0;
// Initialize sub-state
userState.name = 'Gemini';
userState.age = 1;
}
@override
void onDispose() {
// The state must be disposed of on exit.
userState.dispose();
super.onDispose();
}
}
// ... (rest of the code)
3. Provide the Model in the Widget Tree
Wrap the widget tree where you want to share the state with the generated UserInheritedState widget.
class _MainPage extends StatelessWidget {
const _MainPage();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('InheritedModel Example')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Builder(
builder: (context) {
return Text(
'${MainInheritedModel.countOf(context)}',
style: Theme.of(context).textTheme.headlineMedium,
);
},
),
/// Add `UserInheritedState`.
/// You can use `UserInheritedState` in `UserProfileWidget`.
UserInheritedState(
state: MainInheritedModel
.model(context)
.userState,
child: UserProfileWidget())
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
MainInheritedModel.model(context).onIncrement();
MainInheritedModel.model(context).onShowSnapBar('Increment');
},
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
4. Use the Sub-state in the UI
Now, in the UI, you can access the name and age states of UserState through UserInheritedState and be rebuilt according to changes.
class UserProfileWidget extends StatelessWidget {
const UserProfileWidget({super.key});
@override
Widget build(BuildContext context) {
// Rebuild only when the name of UserState changes
final userName = UserInheritedState.nameOf(context);
// Rebuild only when the age of UserState changes
final userAge = UserInheritedState.ageOf(context);
return Column(
children: [
Text('User Name: $userName'),
Row(
children: [
IconButton(onPressed: () {}, icon: Icon(Icons.exposure_plus_1)),
Text('User Age: $userAge'),
IconButton(onPressed: () {}, icon: Icon(Icons.exposure_minus_1)),
],
),
ElevatedButton(
onPressed: () {
// Call the sub-state's method through the model
final model = UserInheritedState.model(context);
model.name = 'Bard';
model.age = 2;
},
child: const Text('Update User'),
),
],
);
}
}
Async Processing and Loading State Management: useAsyncWorker
With the useAsyncWorker: true option, you can use an asyncWorker that is managed in sync with the model's lifecycle.
This allows you to execute tasks that require a Future, such as network requests. You can also check the task's status through InheritedModel.asyncWorkingOf(context).
To improve the user experience, you can display the progress of an asynchronous task in the UI by checking the status with InheritedModel.asyncWorkingOf(context).
1. Enable useAsyncWorker
Add useAsyncWorker: true to the @FlutterInheritedModel annotation.
lib/main_model.dart
import 'package:flutter_inherited_model/flutter_inherited_model.dart';
part 'main_model.g.dart';
@FlutterInheritedModel(
name: 'MainInheritedModel',
useAsyncWorker: true, // Enable async worker
)
class MainModel with $MainModel {
MainModel._();
factory MainModel() = _$MainModel;
@inheritedModelState
late String data;
@override
void onInitState() {
super.onInitState();
data = 'Press the button to fetch data.';
}
// Register a default handler for errors that occur when running an async function with asyncWorker.
@override
void Function(Object e, StackTrace stackTrace)? get asyncWorkerDefaultError => (e, stackTrace) {
emitEvent(MainModelSnackBarEvent(e.toString()));
};
// Asynchronous data loading method
void onFetchData() {
asyncWorker(() async {
await Future.delayed(const Duration(seconds: 3));
data = 'Data fetched successfully!';
});
}
// Asynchronous data error method
// If an error handler is not provided, the asyncWorkerDefaultError function is called to handle the error.
void onFetchDataError() {
asyncWorker(() async {
await Future.delayed(const Duration(seconds: 3));
throw Exception('asyncWorker error');
});
}
}
2. Configure the UI based on Loading State
In the UI, subscribe to the MainInheritedModel.asyncWorkingOf(context) state. When it's true, display a loading indicator that covers the entire screen. Using a Stack widget makes it easy to add an overlay on top of the existing UI.
lib/main_page.dart
class _MainPage extends StatelessWidget {
const _MainPage();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Async Worker Loading Example')),
body: Stack(
children: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Builder(builder: (context) {
return Text(
MainInheritedModel.dataOf(context),
style: Theme.of(context).textTheme.headlineMedium,
);
}),
ElevatedButton(onPressed: () {
MainInheritedModel.model(context).onFetchDataError();
}, child: Text('Fetch error')),
],
),
),
/// Use a Builder to show the loading UI only when asyncWorking.
Builder(
builder: (context) {
final asyncWorking = MainInheritedModel.asyncWorkingOf(context);
if (!asyncWorking) {
return const SizedBox();
}
return Container(
width: double.infinity,
height: double.infinity,
color: Colors.transparent,
alignment: AlignmentDirectional.center,
child: CircularProgressIndicator(),
);
},
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
MainInheritedModel.model(context).onFetchData();
},
tooltip: 'Fetch Data',
child: const Icon(Icons.cloud_download),
),
);
}
}
License
This project is licensed under the MIT License. See the LICENSE file for details.