Separation of Concerns (SoC) is a fundamental design principle that transforms complex software development into a manageable, scalable process. By breaking down your application into distinct, focused modules, you achieve:
- Modularity: Each component has a single, well-defined responsibility
- Maintainability: Changes in one module minimally impact others
- Testability: Individual components can be tested in isolation
- Scalability: New features can be added with minimal friction
The ViewModel acts as a pure business logic coordinator that:
- Transforms data for UI presentation
- Manages UI state
- Delegates I/O operations to specialized providers
- Communicates only through interfaces
Key Principles:
- All dependencies are singletons within its own scope
- You can have multiple scopes that will be disposed when the widget tree is disposed, along with all dependencies that belongs to the current scope (that's why your dependencies can implement
IDisposable
) - Never performs direct I/O operations
- Depends on abstractions, not concrete implementations
- Maintains a clean separation between business logic and data sources
What goes in the ViewModel?
- You should be able to create and test the ViewModel in Dart alone, without any Flutter classes, widgets or contexts.
- It has a
BuildContext
, some Flutter class, such as Material, Widget, Cupertino, etc.? It goes in the View. - You can build a class with business logic that is used by multiple view models, if you have such shared logic (think of
signOut
for example: it can be used by multiple view models). Let's call those classesService
. In this case, the golden rule is: Services haveconst
constructors (so they cannot have any state in it). ViewModels are always non-const (because they inheritChangeNotifier
).
Dependencies are external services your application requires:
- Authentication services
- Database connections
- File system access
- Network APIs
- Platform-specific plugins
Our dependency injection framework allows you to:
- Inject dependencies at runtime
- Swap implementations easily
- Create modular, testable code
// Providers are like operating system drivers: they implement
// something, like API access, authentication (through, for
// instance, FirebaseAuth), storage, etc.
//
// Any ViewModel or dependency that implements `IInitializable`
// will be initialized when the dependencies are built.
//
// The ViewModel only have a contract, the interface to communicate
// with those providers:
abstract interface class IAuthenticationProvider
implements IInitializable {
Future<bool> authenticate(String username, String password);
}
// The view model is the business logic that uses your providers
// It is made for one view
//
// If you have common logic that you want to share between view
// models, such as `signOut`, you can create a class that is
// responsible to hold the business logic and binding the
// providers with the view model.
final class AuthViewModel extends ChangeNotifier {
AuthViewModel(this._authProvider);
final AuthenticationProvider _authProvider;
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
Future<void> login(String username, String password) async {
_isAuthenticated = await _authProvider.authenticate(username, password);
notifyListeners();
}
}
// A widget that creates and manages the ViewModel. It contains
// some nice methods, such as initState or dispose.
final class AuthenticationView extends ViewWidget<AuthViewModel> {
const AuthenticationView({super.key});
@override
AuthViewModel viewModelFactory(Dependencies scope) {
return AuthViewModel(scope.get<IAuthenticationProvider>());
}
@override
Widget build(BuildContext context, AuthViewModel viewModel) {
return Text(
viewModel.isAuthenticated
? "Is Authenticated"
: "Is Not Authenticated"
);
}
}
// Your root widget should be a Dependencies widget:
runApp(
// Builds the dependency injection scope from here on,
// initializing all `IInitializable` dependencies.
//
// The order of dependencies is NOT important, since the
// package will automatically sort them by dependency
DependenciesBuilder(
dependencies: [
// Whenever someone needs an IAuthenticationProvider,
// a FirebaseAuthenticationProvider will be provided.
Dependency<IAuthenticationProvider>(
(scope) => FirebaseAuthenticationProvider(),
),
Dependency<ISomeOtherProvider>(
(scope) => SomeOtherProvider(
authenticationProvider:
// This is how you get a dependency from the scope.
scope.get<IAuthenticationProvider>(),
),
// This is important when one dependency depends on
// another to ensure the correct order of instantiation
// and initialization of the dependencies.
dependsOn: [IAuthenticationProvider],
),
],
builder: (context, scope) => const MainApp(),
);
);
- Clean Architecture: Enforces separation of concerns
- Flexible: Easy dependency management
- Testable: Mock dependencies effortlessly
- Runtime Configuration: Change dependencies dynamically
- Minimal Boilerplate: Simple, intuitive API
Read the source code to discover the full power of this package!
Separation of Concerns (SoC) - GeeksforGeeks https://www.geeksforgeeks.org/separation-of-concerns-soc/
how do you handle Business Logic? : r/FlutterDev - Reddit https://www.reddit.com/r/FlutterDev/comments/drwruf/people_who_use_provider_for_state_management_how/
A quick intro to Dependency Injection: what it is, and when to use it https://www.freecodecamp.org/news/a-quick-intro-to-dependency-injection-what-it-is-and-when-to-use-it-7578c84fa88f/
Separation of concerns - Wikipedia https://en.wikipedia.org/wiki/Separation_of_concerns
Guide to app architecture - Flutter Documentation https://docs.flutter.dev/app-architecture/guide
Dependency injection - Wikipedia https://en.wikipedia.org/wiki/Dependency_injection