Flutter App Architecture - Data Layer

Flutter App Architecture - Data Layer

This article is a complete guide to the data layer, covering data sources, providers, repositories, error handling, caching, testing and more. For the best grasp of the topic, read the sections sequentially, as each builds upon the previous one.

Table of Contents

  1. Understanding Data Sources
    1. Importance of interfaces
    2. Data Transfer Object
    3. Error handling
    4. Improve network requests
  2. Repository
    1. Business Models
    2. Expose only needed data
    3. Caching
  3. Testing
    1. Testing Network Data Sources
    2. Testing Local Data Sources
  4. Architecture Recommendations

Understanding Data Sources

Data sources directly interact with raw data providers, which are generally of two types:

  1. Local: These store data on the device, like databases (SQLite, SharedPreferences), caches, and file I/O.
  2. Remote: These keep data offsite, such as in the cloud, on company servers, or through services like Firebase and Supabase.

Data sources process responses and errors from data providers, managing communication as an intermediary with a simplified, efficient API.

Data sources typically support some or all CRUD (create, read, update, delete) functions. For instance, adding a user to a local database is a create operation, while modifying user properties is an update operation.

Each data source features an interface, serving as an agreement between the data source and a higher-level module. Below are two examples of such interfaces: ArticlesDataSource and CartDataSource:

abstract class ArticlesDataSource {
  // Operation to read articles (R)
  Future<List<ArticleDto>> fetchArticles();
}

abstract class CartDataSource {
  // Operation to read cart items (R)
  Future<List<CartItemDto>> fetchCartItems();

  // Operation to create a cart item (C)
  Future<void> addCartItem(CartItemDto cartItem);

  // Operation to update a cart item (U)
  Future<void> updateCartItem(CartItemDto cartItem);

  // Operation to delete a cart item (D)
  Future<void> deleteCartItem(CartItemDto cartItem);
}

ArticlesDataSource enables articles reading, while CartDataSource provides full CRUD operations for cart management. Despite their distinct functions, they share similarities: each method returns a Future, and read operations yield DTOs.

💡
DTO, or Data Transfer Object, is a simple class with fields designed solely for transporting data.

Importance of Interfaces

Companies typically start with PaaS or SaaS, enjoying early success. As their monthly active users (MAU) and workload grow, so do costs, prompting a shift to custom backends and self-implemented services.

Established interfaces are crucial in these scenarios. They enable seamless migration to new implementations without causing any changes in other layers. Key rules must be followed:

  1. Data sources must always return a model (DTO) that is constructed from raw data obtained from data provider.
  2. Data source must handle the exceptions of an underlying data provider and convert them to system-recognized exceptions.
  3. Data source methods should return futures. There should be no synchronous APIs, even if some library (e.g. SharedPreferences) gives you this ability.

Further in the article, we delve into these rules in detail. For now, let's focus on implementing the ArticlesDataSource interface.

Read more