This repository demonstrates the implementation of the Model-View-ViewModel (MVVM) design pattern in Flutter. It uses the provider package for state management, with a focus on using shared ViewModels across multiple screens. This allows for efficient state management where the ViewModel's state can be shared between different UI components while still following the MVVM pattern.
The MVVM pattern helps separate the concerns of the app by dividing it into three key components:
- Model: Represents the data structure.
- View: Represents the UI and layout.
- ViewModel: Handles business logic and state, and provides data to the view.
In this example, we also demonstrate the use of Shared ViewModels. This allows different parts of the application to share the same ViewModel instance, ensuring that changes in one part of the app reflect in other parts.
The ViewModel holds the app's business logic and state. It communicates with the model layer to fetch or store data and exposes that data to the view. The ViewModel can be shared across different screens.
initState: Initialize the ViewModel.init: Set up any necessary dependencies or services.onBuild: Called when the view is built.onMount: Called when the view is mounted.onUnmount: Called when the view is unmounted.onResume,onPause,onInactive,onDetach: Lifecycle hooks to manage app states.
The MVVM widget binds the ViewModel with the UI. It handles lifecycle management and ensures that the ViewModel is provided to the view.
MVVM<MyViewModel>(
viewModel: MyViewModel(),
view: () => MyView(),
);StatelessView is a subclass of StatelessWidget that builds the UI based on the ViewModel data. It can optionally listen for updates based on the reactive flag.
class MyView extends StatelessView<MyViewModel> {
@override
Widget render(BuildContext context, MyViewModel vm) {
return Scaffold(
body: Center(
child: Text(vm.data),
),
);
}
}The SharedVM widget enables the sharing of a ViewModel across different parts of the app. It ensures that data is maintained consistently across different views or screens. This is particularly useful when you want to share state between multiple views that need to react to changes in the same ViewModel instance.
SharedVM<MyViewModel>(
builder: (context, vm) => MyView(),
);
void setupLocator() {
locator.registerFactory<FirstScreenVm>(() => FirstScreenVm());
locator.registerFactory<SecondScreenVm>(() => SecondScreenVm());
}
class FirstScreen extends StatelessWidget {
const FirstScreen({super.key});
@override
Widget build(BuildContext context) {
return MVVM(
view: () => Builder(builder: (context) {
return const FirstScreenView();
}),
viewModel: locator<FirstScreenVm>());
}
}
class FirstScreenView extends StatelessView<FirstScreenVm> {
const FirstScreenView({super.key});
@override
Widget render(BuildContext context, FirstScreenVm vm) {
return Column(
children: [
ElevatedButton(
onPressed: () {
vm.increment();
},
child: const Text('Increment'),
),
Text(vm.counter.toString()),
ElevatedButton(
onPressed: () {
vm.decrement();
},
child: const Text('Decrement'),
),
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SecondScreen()),
);
},
child: const Text('Go to Next Screen'),
),
],
);
}
}
class SecondScreen extends StatelessWidget {
const SecondScreen({super.key});
@override
Widget build(BuildContext context) {
return MVVM<SecondScreenVm>(
viewModel: locator<SecondScreenVm>(),
view: () => SecondScreenView());
}
}
/// Example One
class SecondScreenView extends StatelessView<SecondScreenVm> {
const SecondScreenView({super.key});
@override
Widget render(BuildContext context, SecondScreenVm vm) {
return SharedVM<FirstScreenVm>(
builder: (context, sharedVm) {
return Scaffold(
body: Column(
children: [
Text("Shared ViewModel"),
Row(
children: [
ElevatedButton(
onPressed: () {
sharedVm.increment();
},
child: const Text('Increment Shared'),
),
Text(sharedVm.counter.toString()),
ElevatedButton(
onPressed: () {
sharedVm.decrement();
},
child: const Text('Decrement Shared'),
),
],
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Back'),
),
Text("Current ViewModel"),
Row(
children: [
ElevatedButton(
onPressed: () {
vm.increment();
},
child: const Text('Increment Current'),
),
Text(vm.counter.toString()),
ElevatedButton(
onPressed: () {
vm.decrement();
},
child: const Text('Decrement Current'),
),
],
),
],
),
);
},
);
}
}
/// Example two for easy usage
class SecondScreenView
extends StatelessViewWithSharedVmCurrentVM<FirstScreenVm, SecondScreenVm> {
const SecondScreenView({super.key, super.reactiveShared = false});
@override
Widget render(
BuildContext context, FirstScreenVm sharedVm, SecondScreenVm vm) {
return Scaffold(
body: Column(
children: [
Text("Shared vm"),
Row(
children: [
ElevatedButton(
onPressed: () {
sharedVm.increment();
},
child: const Text('Increment'),
),
Text(sharedVm.counter.toString()),
ElevatedButton(
onPressed: () {
sharedVm.decrement();
},
child: const Text('decrement'),
),
],
),
Spacer(),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('back'),
),
Spacer(),
Text("Current vm"),
Row(
children: [
ElevatedButton(
onPressed: () {
vm.increment();
},
child: const Text('Increment'),
),
Text(vm.counter.toString()),
ElevatedButton(
onPressed: () {
vm.decrement();
},
child: const Text('decrement'),
),
],
),
],
));
}
@override
FirstScreenVm provideSharedViewModel() {
return locator<FirstScreenVm>();
}
}