Refactoring Duplicated View Logic into Mixins in Flutter

Published on March 23, 2026 3 min read

When building Flutter apps with state management solutions like ChangeNotifierProvider or StateNotifierProvider, it’s very common to end up duplicating logic across multiple view models.

At first, everything feels fine. Then a few weeks later:

  • You copy-paste a method from one ViewModel to another
  • You tweak it slightly
  • Bugs start diverging
  • Maintenance becomes painful

This is usually the point where developers ask:

“Should I extract this into a use case?” or “Can I reuse this logic without over-engineering?”

Problem: Duplicated View Logic

Let’s say you have two ViewModels:

class ProfileViewModel extends ChangeNotifier {
bool isLoading = false;

Future<void> followUser(String userId) async {
isLoading = true;
notifyListeners();

try {
await api.follow(userId);
} finally {
isLoading = false;
notifyListeners();
}
}
}

class PostViewModel extends StateNotifier<PostState> {
PostViewModel() : super(PostState());

Future<void> followUser(String userId) async {
state = state.copyWith(isLoading: true);

try {
await api.follow(userId);
} finally {
state = state.copyWith(isLoading: false);
}
}
}

Same logic. Different state systems. This duplication leads to:

  • Inconsistent behavior
  • Harder testing
  • Increased maintenance cost

Solution: Extract into a Mixin

Instead of duplicating logic, we can move shared behavior into a mixin.

mixin FollowUserMixin { 
Future<void> followUser(String userId) async {
setLoading(true);
try {
await performFollow(userId);
} finally {
setLoading(false);
}
}

Future<void> performFollow(String userId);

void setLoading(bool value);
}

Use it in ViewModels

class ProfileViewModel extends ChangeNotifier with FollowUserMixin {
bool isLoading = false;
@override Future<void> performFollow(String userId) {
return api.follow(userId);
}
@override void setLoading(bool value) {
isLoading = value;
notifyListeners();
}
}
class PostViewModel extends StateNotifier<PostState> with FollowUserMixin {
PostViewModel() : super(PostState());
@override Future<void> performFollow(String userId) {
return api.follow(userId);
}
@override void setLoading(bool value) {
state = state.copyWith(isLoading : value);
}
}

Mixins give you:

1. Reusable behavior without inheritance problems

You don’t need a base class that both ViewModels extend.

2. Separation of concerns (lightweight)

  • Mixin → how something works
  • ViewModel → how state is updated

3. Flexible integration

Each ViewModel adapts the mixin to its own state system.

Mixins vs Use Case Layer

Many developers immediately jump to “clean architecture” and create a use case. But not all duplication deserves a use case.

When to Use a Mixin

Use a mixin if:

  • Logic is UI-related
  • Depends on ViewModel state
  • Needs access to notifyListeners or state
  • Slightly varies per ViewModel

Examples:

  • Loading handling
  • Pagination logic
  • Retry logic
  • Debouncing user actions

These are presentation concerns, not domain logic.

When to Use a Use Case

Use a use case if:

  • Logic is business/domain logic
  • Should be shared across multiple layers
  • Independent of UI
  • Needs to be testable in isolation

class FollowUserUseCase {
final UserRepository repository;

FollowUserUseCase(this.repository);

Future<void> call(String userId) {
return repository.follow(userId);
}
}

Then your ViewModel becomes:

await followUserUseCase(userId);

Thanks for reading.

Refactoring Duplicated View Logic into Mixins in Flutter

Category: clean-architectureTags: clean-architecture, flutter, mobile-app-development, computer-science