Series 1 of 2 — Part 5 of 5
Building a Flutter Shopping App — Part 5: Cleaning Up DI with SharedPreferencesGetter
Series: Building a Flutter Shopping App — From Zero to Architecture
After the MVVM refactor in Part 4, the architecture was solid. But there was one remaining issue with how I was passing SharedPreferences instances into repositories — it was repetitive and easy to get wrong.
The fix was a small pattern called SharedPreferencesGetter, and it taught me something important about dependency injection in Flutter.
The problem
SharedPreferences is asynchronous to initialise. You call SharedPreferences.getInstance() and get a Future. This means you cannot create a SharedPreferences instance in a constructor the way you can with most dependencies.
My original approach was an init method on each repository that needed SharedPreferences, called from the ViewModel. Every repository had an init method, every ViewModel called it, and the order of initialisation mattered. It was fragile.
// ❌ Fragile: requires callers to remember to invoke init()
class CartRepository {
late SharedPreferences _prefs;
Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
}
The solution: SharedPreferencesGetter
Instead of each repository managing its own SharedPreferences lifecycle, I created a typedef for a function that returns Future<SharedPreferences>:
typedef SharedPreferencesGetter = Future<SharedPreferences> Function();
Repositories now take a SharedPreferencesGetter in their constructor:
class CartRepository {
final SharedPreferencesGetter _getPrefs;
CartRepository({required SharedPreferencesGetter getPrefs}) : _getPrefs = getPrefs;
Future<void> saveCart(List<CartItem> items) async {
final prefs = await _getPrefs();
// use prefs...
}
}
In the Riverpod provider, pass SharedPreferences.getInstance as the getter:
final cartRepositoryProvider = Provider((ref) => CartRepository(
getPrefs: SharedPreferences.getInstance,
));
In tests, pass a mock getter that returns a pre-configured instance:
final mockCartRepo = CartRepository(
getPrefs: () async => mockSharedPreferencesInstance,
);
No init methods, no fragile initialisation order.
The broader principle
This is a small pattern but it illustrates something that applies across the whole codebase: dependencies should be injected, not created. A class that creates its own dependencies is harder to test and harder to reuse. A class that receives them can be tested with mocks and reused in different contexts.
Wrapping up Series 1
Over five posts, this shopping app went from a blank Flutter project to a full e-commerce app with Firebase Auth, Google Sign-In with account merging, Firestore, push notifications, MVVM architecture, and clean dependency injection.
The most valuable thing I built here was not the app — it was the understanding of why each architectural layer exists. I built it wrong first, felt the pain, and then fixed it. That sequence is something no tutorial can give you.
Now see what it looks like when you apply all of this from the very first commit in Series 2!
Top comments (0)