tezvyn:

Repository Pattern: Hide Your Data Sources

AI-drafted, machine-checkedintermediate

A repository is a contract that hides where your data lives. Your Flutter app should not care if a user profile comes from Firebase, SQLite, or a mock. Engineers often leak HTTP clients or database models into UI instead of returning domain models.

WHY IT EXISTS: Flutter apps rarely pull from a single source. A typical project fetches JSON from a REST API, caches it in SQLite or Hive, and falls back to SharedPreferences for settings. Without a dedicated boundary, your widgets and state managers end up juggling network clients, serialization logic, and error handling. The repository pattern exists to seal that chaos behind a stable contract so the rest of your codebase can treat data access as a solved detail rather than an ongoing concern.

THE MENTAL MODEL: Think of the repository as your app's personal librarian. The business logic walks up and asks for a specific book. The librarian knows which warehouse to check, whether to pull from the local shelf or order from a remote supplier, and how to handle a missing title. The requester never sees the supply chain, only the book. In code terms, that means your BLoC or Riverpod provider asks for a User object and has no idea if it came from Firestore, a local cache, or a hardcoded mock.

HOW IT WORKS: You start with an abstract Dart class that declares only the operations your domain needs, such as a method that asynchronously fetches a User by ID or streams a list of Orders. Behind that interface you build concrete implementations. One might wrap a Dio client for remote calls, another might wrap Drift or Hive for local storage. The repository implementation coordinates them. For example, it can return cached data immediately while triggering a background refresh, or it can merge remote and local streams. Because the rest of the app depends on the abstract class, you can swap the entire data layer during tests by injecting a fake that returns frozen values.

WHEN TO USE IT: Reach for this pattern when you have multiple data sources that need orchestration, when you want to unit-test business logic without hitting real networks, or when your team needs to keep UI code decoupled from third-party SDKs. It pays off the moment you have more than one origin for the same entity or when you anticipate switching providers, such as moving from a REST API to GraphQL or from Firebase to a self-hosted backend.

WHEN NOT TO USE IT: Avoid it for a throwaway prototype or a screen that performs a single unauthenticated GET request with no caching. The overhead of defining an interface, remote source, local source, and mapper for one endpoint creates boilerplate without buying clarity. If your data layer is trivial, let the state manager call the HTTP client directly and refactor later when the surface area grows.

ONE CANONICAL EXAMPLE: Imagine a fitness app that tracks daily water intake. The HydrationRepository interface exposes methods to fetch today intake and log a new glass. The production implementation writes to a local Drift database for instant offline persistence and syncs to a remote Node API in the background. The UI layer and the Cubit that manages state only import HydrationRepository. During integration tests, you inject a fake repository that prepopulates known values, so your widget tests run in milliseconds without configuring a database or a mock server.

Get five bites like this every day.

Tezvyn delivers a daily feed of 60-second tech bites with quizzes to lock in what you learn.