Flutter Clean Architecture with Riverpod & Supabase: Ultimate Guide

Using Flutter clean architecture with Riverpod might sound like a mouthful, but it’s a game-changer for building scalable, maintainable apps. 

Think of clean architecture as your blueprint for success, organizing your app’s structure so it’s easy to manage as it grows. And Riverpod? It’s the trusty sidekick for state management, ensuring your app runs smoothly. 

We’re also throwing Supabase into the mix as our backend powerhouse. This guide is your roadmap to mastering these tools, making app development not just manageable but a whole lot of fun. Let’s get started!

Flutter App Architecture: A Primer

Flutter has revolutionized the mobile development scene, offering a single framework to craft beautiful, natively compiled applications for mobile, web, and desktop from a single codebase. But even with its intuitive design and efficient performance, the architecture of your app can make or break its success in the long run. That’s where clean architecture and Riverpod come into play, setting the stage for scalable and manageable app development.

Why Focus on Architecture?


Imagine building a house without a blueprint. You might get the walls up, but chances are it won’t be the most stable or functional space. The same goes for app development. Without a solid architectural foundation, your app might work initially but can become a nightmare to maintain and scale. Clean architecture offers that blueprint, organizing code in a way that separates concerns, making your app easier to test, maintain, and scale. There are 2 main type of architecture, thats experts recommend to build scaleable and maintainable apps these architectures are clean architecture and MVVM.

Enter Riverpod, the Opinionated Hero


Riverpod steps in as a flexible and powerful state management solution, designed to overcome the limitations of its predecessor, Provider. What sets Riverpod apart is its opinionated nature, offering developers the freedom to manage app state in a way that best fits their project, without enforcing a one-size-fits-all approach. It seamlessly integrates with Flutter’s reactive model, ensuring your UI updates efficiently and correctly.

A Match Made in Flutter Heaven


When you use Flutter’s clean architecture with Riverpod, you get an app that’s not just a joy to use but also a breeze to develop and maintain. This combination allows for a clear separation of concerns, where your app’s logic, data, and presentation layers work together harmoniously, yet remain independently manageable. This setup paves the way for efficient development workflows, easier debugging, and ultimately, a more robust and scalable application.

Flutter Clean Architecture with Riverpod: The Layers

When we talk about clean architecture in the context of Flutter development, we’re focusing on a layered approach that promotes separation of concerns—a principle that ensures each part of the app handles its own responsibilities without being overly entangled with others. This separation not only simplifies development and maintenance but also enhances the app’s scalability and testability. Integrating Riverpod into this architecture elevates state management to new levels of efficiency and simplicity.

The Data Layer

The data layer is the foundation of your app’s architecture, responsible for managing all data-related operations such as fetching data from the internet, accessing local databases, and persisting user preferences. In a clean architecture, this layer is usually implemented using repositories and data sources, abstracting the complexity of data access from the rest of the app.

Riverpod’s Role: Riverpod can manage the state of the data layer, providing a reactive interface to data changes. For example, you can use Riverpod providers to fetch data from a repository and automatically update the UI when new data is available, without manual intervention.

The Domain Layer

The domain layer acts as the intermediary between the data layer and the presentation layer. It contains the business logic of your app, defining how data is retrieved, processed, and passed to the presentation layer. This layer is typically made up of use cases or interactors, which encapsulate specific business rules or operations.

Riverpod’s Role: Although Riverpod is primarily used for state management in the presentation layer, it can also facilitate the communication between the domain and presentation layers by providing a clean, reactive way to execute use cases and reflect their outcomes on the UI.

The Presentation Layer

The presentation layer is where your app’s UI lives. It’s responsible for presenting data to the user and handling user interactions. This layer is built using widgets in Flutter, which are organized in a way that reflects the app’s design and user experience guidelines.

Riverpod’s Role: This is where Riverpod shines the most. It manages the state of UI components, ensuring they reactively update in response to state changes. With Riverpod, you can easily manage the state of various UI components, trigger actions based on user input, and reflect changes in the app’s state seamlessly.

The Application Layer

While not always explicitly mentioned in discussions about clean architecture, the application layer can be considered the glue that binds the domain and presentation layers. It’s responsible for initializing the application, setting up dependencies, and sometimes orchestrating how different parts of the app interact.

Riverpod’s Role: Riverpod can simplify the application layer by managing dependencies and providing a unified interface for accessing shared resources, like theme data or authentication information, making it easy to maintain a coherent app state across different parts of your application.

By understanding and implementing these layers in your Flutter app, you can leverage the full potential of clean architecture and Riverpod. This structure not only makes your app more maintainable and scalable but also ensures it can adapt to changing requirements with minimal effort. 

Functional Programming in Flutter

Integrating functional programming principles into Flutter development, especially when using clean architecture and Riverpod, can significantly enhance the quality and maintainability of your code. Functional programming (FP) focuses on pure functions, immutability, and higher-order functions, concepts that align well with the reactive and componentized nature of Flutter.

The Essence of Functional Programming

Functional programming treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. It’s a paradigm that promotes a declarative approach to coding, where you express the logic of computation without describing its control flow. This approach leads to code that’s easier to understand, test, and debug.

Why FP Matters in Flutter?

  • FP’s emphasis on immutability meshes well with Flutter’s reactive framework. By avoiding mutable state, you reduce the risk of unexpected side effects, making state management more predictable.
  • These functions, which return the same result every time they’re called with the same arguments, are ideal for UI rendering and state transformation in Flutter. They ensure consistency and reliability in your app’s behavior.
  • Flutter’s use of widgets and callbacks can be seen as a form of higher-order functions, where functions take functions as arguments or return them. This is central to creating reusable and composable components in Flutter.

Integrating FP with Clean Architecture and Riverpod

  • Riverpod, inherently functional in its design, complements FP by treating app state as immutable and ensuring updates are predictable and efficient. By adopting FP principles, you can use Riverpod to manage app state in a way that’s both scalable and easy to reason about.
  • The use of pure functions and immutability in the domain layer (business logic) simplifies the testing and validation of app logic, ensuring it behaves as expected under various conditions.
  • Leveraging FP’s emphasis on function composition and higher-order functions, you can create highly modular and reusable UI components. This approach simplifies the construction of complex UIs and enhances the overall readability and maintainability of your presentation layer.

Example: Applying FP in Flutter with Riverpod

Consider a simple task list app where tasks can be added, completed, or removed. Using Riverpod for state management and FP principles, you can create a task management system that is easy to understand and modify.

  • Define a task model as an immutable data class.
  • Use Riverpod’s StateNotifierProvider to manage the state of tasks, ensuring state changes are handled in a pure, predictable manner.
  • Implement business logic as pure functions that take the current state and an action to produce a new state, facilitating easy testing and modification.

This integration of FP with clean architecture and Riverpod not only makes your Flutter apps more robust and maintainable but also aligns with the core principles of Flutter development—creating efficient, reactive apps with minimal boilerplate.

Project Structure and Directory Organization

Organizing a Flutter project effectively is crucial for maintaining clean architecture and leveraging the benefits of Riverpod and functional programming. A well-structured directory and clear project organization facilitate easier navigation, development, and testing, especially as the project grows. 

Here’s how you can organize your Flutter project to align with clean architecture principles, while incorporating Riverpod for state management.

Best Practices for Organizing a Flutter Project

  • Divide your project into layers (Data, Domain, Presentation) to isolate responsibilities. This separation makes it easier to manage dependencies and testing.
  • Break down features into modules or packages, especially for large projects. This approach supports reusability and scalability.
  • Use clear and consistent naming for files and directories to reflect their purpose and content, making them easy to locate and understand.

Detailed Directory Structure

Here’s a suggested structure that follows clean architecture principles, adapted for Flutter and Riverpod:

/lib
  /src
    /core
      /errors           # Exception handling and error messages
      /theme            # Global styling and theme data
      /utils            # Utility classes and functions
    /features          # Feature modules (e.g., login, dashboard)
      /example_feature
        /data           # Data layer - repositories, data models, data sources
          /models
          /datasources
          /repositories
        /domain         # Domain layer - entities, use cases, repositories interfaces
          /entities
          /repositories
          /usecases
        /presentation   # Presentation layer - screens, widgets, state management
          /pages
          /widgets
          /state         # Riverpod state management files
    /services          # Global services like API clients, database
  main.dart             # Entry point

Integrating Riverpod

With the project structured around clean architecture, integrating Riverpod for state management becomes straightforward:

  • State Management in Presentation Layer: Use Riverpod providers within the presentation/state directory to manage UI state, making it reactive to changes in the data or domain layers.
  • Dependency Injection: Utilize Riverpod for dependency injection, easily accessing services and data repositories across your app without tight coupling.
  • Global Access: Riverpod allows for globally accessible state and services, simplifying how you manage and access core functionalities like user authentication or theme switching.

Example: Feature Module Organization

Let’s consider an example_feature module within your app, such as a user profile:

  • Data Layer: Includes models like UserProfile, data sources for API calls, and repositories that abstract away the source of the user data.
  • Domain Layer: Contains entities representing core business logic, such as User, and use cases like GetUserProfile or UpdateUserProfile, interfacing with repositories.
  • Presentation Layer: Comprises screens like UserProfilePage, widgets specific to displaying user information, and state management files where Riverpod providers handle the state of user data, facilitating interactions between UI and business logic.

Learn more about APIs Integration in Flutter clean architechture.

By adhering to this structure and utilizing Riverpod, your Flutter projects will not only be well-organized but also maintain a clean separation of concerns, ensuring that your codebase remains manageable and scalable as your application evolves.

Also, read Implementing clean architecture with Getx for building scalable and maintaianable applications.

Implementing Clean Architecture with Riverpod and Supabase

Implementing clean architecture in a Flutter project with Riverpod for state management and Supabase as the backend requires a thoughtful approach to ensure each layer communicates effectively while maintaining their responsibilities. Supabase adds a robust, scalable backend solution, offering features like authentication, real-time databases, and storage. This section outlines the steps to integrate these technologies into a cohesive system.

Setting Up Supabase

  • Create a Supabase Project: Start by setting up a new project on Supabase.io. This will be your backend, providing services like authentication, database, and storage.
  • Initialize Supabase Client: In your Flutter project, initialize the Supabase client with your project’s URL and public anon key, typically in a service or utility class for global access.

Integrating Supabase with the Data Layer

  • Data Sources: Implement data sources in the data layer that interact directly with Supabase for CRUD operations. For example, a UserDataSource class can handle fetching and updating user profiles.
  • Repositories: Repositories interface with the data sources, abstracting the specifics of data operations from the domain layer. They ensure the domain layer isn’t coupled with the data source, allowing for easier testing and changes to the data source.

Domain Layer: Business Logic and Use Cases

  • Entities and Models: Define entities that represent the core business concepts of your app, independent of the database or UI.
  • Use Cases: Implement use cases that encapsulate specific business rules, calling upon repositories to interact with data. Each use case should perform a single task, such as GetUserProfileUseCase.

Presentation Layer: UI and State Management with Riverpod

  • State Management: Utilize Riverpod providers to manage state within the presentation layer. Define state notifiers for each feature, such as UserProfileNotifier, to handle state changes and communicate with use cases.
  • Building UI: Construct UI components that listen to Riverpod providers. Use Consumer widgets or the useProvider hook in Flutter hooks to rebuild parts of the UI in response to state changes.

Example: User Authentication Flow

  • Supabase Authentication: Implement user authentication using Supabase’s auth services. Create a AuthService that utilizes Supabase for signing in, signing out, and managing user sessions.
  • Auth State Management: Use a Riverpod state notifier, such as AuthNotifier, to track the authentication state across the app. It interacts with the AuthService to perform authentication operations and triggers UI updates accordingly.
  • UI Implementation: Develop authentication screens that respond to the auth state. For instance, a login screen could use a form to capture user credentials, calling upon the AuthNotifier to sign in the user and navigate based on the auth state changes observed through Riverpod providers.

Testing and Validation

  • Write unit tests for your use cases, repositories, and state notifiers. Mock Supabase services and repositories to test business logic and state management independently of external services.
  • Test the integration between layers, ensuring that data flows correctly from Supabase through your repositories to the domain and presentation layers, and that state changes are accurately reflected in the UI.

Testing Your Clean Architecture Flutter App

Testing is an integral part of developing a robust, maintainable Flutter app, especially when employing clean architecture, Riverpod for state management, and external services like Supabase. It ensures that each component functions correctly in isolation and in collaboration with other parts of the app. 

Unit Testing: Ensuring Solid Foundations

  • Domain Layer: Test use cases and entities by mocking repository interfaces. Verify business logic executes as expected under various conditions.
  • Data Layer: Mock external services (like Supabase APIs) to test repositories and data sources, ensuring data is correctly fetched, transformed, and stored.
  • Presentation Layer: Focus on testing Riverpod state notifiers. Mock use cases to verify state changes occur as intended in response to events.

Widget Testing: Verifying the UI

  • Test individual widgets in isolation to ensure they render correctly with given states.
  • Use ProviderScope to override Riverpod providers with mock values or notifiers, simulating different UI states without requiring actual data from the backend or domain logic.
  • Validate user interactions, like tapping buttons or entering text, to ensure they trigger the expected state changes and navigations.

Integration Testing: Connecting the Dots

  • Simulate real user flows from the UI down to the data layer, verifying the entire stack works together seamlessly.
  • Use a combination of mocked providers and a test Supabase instance (if feasible) to test authentication flows, data fetching, and persistence across app restarts.
  • Check error handling and loading states across different parts of the app to ensure a smooth user experience under all conditions.

End-to-End Testing: The Final Check

  • Although more challenging with external dependencies like Supabase, consider end-to-end tests for critical user journeys using a dedicated test project in Supabase.
  • Automate user interactions with the app and verify outcomes, such as successfully creating a new user account, fetching data, and updating records.

Best Practices for Testing

  • Automate Where Possible: Use CI/CD pipelines to run your tests automatically on various devices and configurations.
  • Mock Wisely: Proper mocking isolates tests, ensuring they don’t fail due to external changes. However, keep mocks up to date with actual service behavior.
  • Incremental Testing: Start testing early in the development process and add tests incrementally as features are developed to catch issues early.
  • Coverage Goals: Aim for high test coverage but focus on testing logic and scenarios that are critical to the app’s functionality and user experience.

Conclusion

Mastering Flutter Clean Architecture with Riverpod and Supabase offers a robust framework for building scalable, maintainable Flutter apps. This guide has walked you through the foundational principles of clean architecture, the dynamic capabilities of Riverpod for state management, and the powerful backend support of Supabase. By embracing these concepts, developers can create applications that are not only efficient and responsive but also easy to evolve and maintain, setting a new standard for excellence in Flutter app development.

FAQ’s

How do you manage different configurations (dev, staging, production) in Flutter with Supabase?

Managing different environments in Flutter with Supabase involves creating separate Supabase projects for each environment. You can then configure environment-specific variables (like Supabase URL and anon key) in Flutter using different files (e.g., env_dev.dart, env_prod.dart) and load the appropriate configuration based on the build variant. Utilize Flutter’s ability to define custom build configurations and Dart’s conditional imports to switch between these settings dynamically.

What strategies are best for complex state management with Riverpod in large Flutter apps?

For complex state management in large Flutter apps, consider structuring your Riverpod providers hierarchically and leveraging Flutter’s context to access them efficiently. Use StateNotifierProvider for complex state logic and ChangeNotifierProvider for UI-centric state changes. Additionally, split state management logic into smaller, manageable pieces and utilize Riverpod’s family modifiers to create more dynamic and reusable state management solutions.

How to test and mock Supabase services in Flutter for integration tests?

To test and mock Supabase services in Flutter, use the mockito package to create mock classes for your Supabase client and services. Override the Supabase client with these mocks during testing to simulate various responses and scenarios. This approach allows you to test your application logic without making actual calls to Supabase, ensuring your tests are fast and reliable.

Best practices for optimizing Riverpod performance in Flutter apps?

Optimizing performance in Flutter apps with Riverpod involves minimizing the number of rebuilds and ensuring efficient state updates. Use providers selectively and avoid unnecessary rebuilds by splitting your state into multiple providers. Leverage the const keyword with widgets when possible, and use Riverpod’s autoDispose modifiers to automatically dispose of state when no longer needed. Also, consider using family and futureProvider or streamProvider for asynchronous data fetching and caching.

Implementing feature toggling or A/B testing in Flutter with clean architecture?

Implementing feature toggling or A/B testing in a Flutter app with clean architecture involves creating a flexible system to manage feature flags or experiment variations. Use a combination of remote config services (like Firebase Remote Config) and local state management (with Riverpod) to dynamically control feature availability. Integrate the feature flag checks into your app’s decision-making points within the domain or presentation layers, ensuring that features can be toggled without requiring a full app deployment.

Leave a Comment

Your email address will not be published. Required fields are marked *

Muhammad Ayaz

Muhammad Ayaz

Muhammad Ayaz is an SEO expert and tech content writer who turns complex tech topics into engaging content. While not a coder, his work is rigorously checked by Adnan Khan, Etechviral's senior developer, to ensure accuracy before it goes live.

Related Blogs

Converting JSON to Dart: The Ultimate Guide

In Flutter app development, seamlessly integrating JSON data into Dart code is crucial for building efficient, high-performing applications. This guide provides a comprehensive look at

Scroll to Top

Apply for Sales & Marketing

Company Description

eTechViral is a leading tech company with a team of skilled professionals specializing in developing customized software solutions from design to development. We prioritize client relationships, quality deliverables, and a compassionate exchange of energy.


Role & Responsibilities

This is a full-time remote role for a Sales and Marketing Specialist. The Sales and Marketing Specialist will be responsible for developing and implementing sales and marketing strategies, maintaining customer relationships, providing training to sales team members, and managing sales operations.

Role & Responsibilities

  • Excellent communication and customer service skills
  • Demonstrated ability to drive sales and meet quotas
  • Experience in sales management and operation
  • Ability to provide training to sales team members
  • Bachelor’s degree in Marketing, Business Administration or related field
  • Knowledge of virtual sales and marketing tools is a plus
  • Ability to work independently and remotely
eTechviral-logo

Welcome to eTechviral