Flutter Clean Architecture Example

repository·main·Indexed 21 days ago

https://github.com/guilherme-v/flutter-clean-architecture-example

A demonstration project showcasing the implementation of Clean Architecture in Flutter. It features a three-layer pattern (Presentation, Domain, and Data) to ensure testability and separation of concerns, and compares various state management solutions including Provider, Riverpod, Bloc, Cubit, GetIt, and MobX.

Tokens
5.1K
Snippets
15
Records
21
Agent score
72%

What's inside flutter-clean-architecture-example

  1. Overview of Clean Architecture in this project

    main

    This project implements a three-layer Clean Architecture pattern to ensure code is testable, extensible, and maintains a clear separation of concerns. The architecture follows the 'Dependency Rule' (modeled like an onion), where inner layers (Domain) are independent and cannot be accessed by outer layers (Data/Presentation).

    Key goals include:

    • State Management Transparency: Treating state management as an implementation detail so it can be swapped easily.
    • Testability: Allowing all layers to be independently unit tested.
    • Extensibility: Making the codebase easy to adapt to new requirements.
  2. Data mapping: DTOs, Entities, and States

    main

    To prevent one layer's requirements from leaking into another, this project uses specific data models for each layer:

    LayerData ModelPurpose
    DataDTO (Data Transfer Object)Handles JSON serialization/deserialization for network/database communication.
    DomainEntityRepresents core business concepts with plain data and domain-specific methods.
    PresentationStateRepresents the specific way data is displayed and interacted with in the UI.

    Note: Use Interface Adapters (mappers) to convert data between these formats as it moves across layer boundaries.

  3. How the three layers work together

    main

    The project is divided into three distinct layers. To maintain separation of concerns, data is mapped between layers using specialized objects (DTOs, Entities, and States).

    1. Presentation Layer (UI)

    Contains the Flutter framework code, Widgets, and state management logic. It is responsible for:

    • Managing application state.
    • Handling UI aspects (navigation, internationalization, updates).
    • Using States classes to represent the UI data.

    2. Domain Layer

    The core business logic layer. It is a pure Dart module with no external dependencies. It includes:

    • Entities: Core business objects (e.g., Character) with properties and methods.
    • Use Cases: Classes that encapsulate specific business logic (e.g., GetAllCharacters) and coordinate data flow.
    • Repository Interfaces: Abstractions that define how data should be accessed without knowing the implementation details.

    3. Data Layer

    Acts as the boundary to the external world. It includes:

    • Repository Implementations: Concrete logic for the interfaces defined in the Domain layer.
    • Data Sources: Network or database calls.
    • DTOs (Data Transfer Objects): Simple containers used for JSON serialization/deserialization.
    • Caching & Coordination: Managing data flow between local and remote sources.
  4. Implement infinite scrolling in CharacterView content

    main

    The _Content widget (a private StatefulWidget) manages the character list and implements infinite scrolling logic using a ScrollController.

    Key Behaviors:

    1. Data Selection: It selects both the characters list and the hasReachedEnd boolean from the CharacterPageBloc state.
    2. Pagination: It listens to scroll events via _onScroll. When the user scrolls to within 90% of the bottom of the list (_isBottom), it dispatches a FetchNextPageEvent to the CharacterPageBloc to load more data.
    3. List Rendering:
      • It uses a ListView.builder.
      • If hasReachedEnd is false, it adds an extra item to the list to show a CharacterListItemLoading indicator at the bottom.
      • The first item (index 0) is rendered with a CharacterListItemHeader.
    4. Navigation: Tapping a CharacterListItem triggers _goToDetails, which uses CharacterDetailsPage.route(character: character) to navigate to the details screen.
    // Inside __ContentState
    void _onScroll() {
        if (_isBottom) {
          pageBloc.add(const FetchNextPageEvent());
        }
    }
    
    bool get _isBottom {
        if (!_scrollController.hasClients) return false;
        final maxScroll = _scrollController.position.maxScrollExtent;
        final currentScroll = _scrollController.offset;
        return currentScroll >= (maxScroll * 0.9);
    }
  5. Initialize and run the application

    main

    The application entry point performs asynchronous initialization of essential services before launching the UI. It ensures Flutter bindings are ready, initializes SharedPreferences, sets up the dependency injection container via initializeGetIt(), and configures flutter_animate for hot reload support. The application is wrapped in a ProviderScope to enable Riverpod state management.

    void main() async {
      WidgetsFlutterBinding.ensureInitialized();
      sharedPref = await SharedPreferences.getInstance();
      await initializeGetIt();
      Animate.restartOnHotReload = true;
    
      runApp(const ProviderScope(child: AppRoot()));
    }
  6. State management implementations explored

    main

    The project demonstrates how Clean Architecture can be applied across various state management libraries. The following libraries have been implemented and tested:

    State ManagerAppliedUnit testsWidget tests
    ProviderYesYesYes
    RiverpodYesYesIn Progress
    BlocYesYesYes
    CubitYesYesYes
    GetItYesYesYes
    MobXYesYesIn Progress
  7. Implement a character list page using CharacterPageCubit

    main

    To display a list of characters, use the CharacterPage widget. It follows a standard Bloc pattern where the CharacterPageCubit is provided via BlocProvider and initialized by calling fetchNextPage().

    Key components of this implementation:

    • Initialization: The CharacterPage injects GetAllCharacters (a domain use case) into the CharacterPageCubit.
    • State Management: The UI uses context.select to react to changes in CharacterPageCubit.state.status, state.characters, and state.hasReachedEnd.
    • Pagination: Infinite scrolling is implemented by attaching a ScrollController to a ListView.builder. When the user scrolls past 90% of the maxScrollExtent, pageCubit.fetchNextPage() is triggered.
    • Navigation: Tapping a character item uses CharacterDetailsPage.route(character: character) to navigate to the details view.
    // To use the character list page in your widget tree:
    const CharacterPage()
  8. Implement a character list page using MobX

    main

    To implement a character list page following the MobX pattern in this project, you should separate the page into three distinct parts: a StatelessWidget for dependency injection, a StatefulWidget (View) for lifecycle management, and a private StatefulWidget (Content) for UI rendering and scroll handling.

    1. Dependency Injection: Use a StatelessWidget to instantiate the CharacterPageStore, passing in required domain use cases (e.g., GetAllCharacters) retrieved via context.read().
    2. Lifecycle Management: Use a StatefulWidget to trigger initial data fetching in initState using WidgetsBinding.instance.addPostFrameCallback to ensure the store's fetchNextPage() is called after the first frame.
    3. Reactive UI: Wrap the UI in an Observer widget from flutter_mobx. Use the store's contentStatus to toggle between a loading indicator and the main content.
    4. Pagination: Implement infinite scrolling by attaching a ScrollController to a ListView.builder. When the user scrolls near the bottom (e.g., 90% of the way), call store.fetchNextPage().

    Note: The store's charactersList and hasReachedEnd properties should drive the list length and the visibility of loading indicators at the end of the list.

    // 1. The Entry Point (Dependency Injection)
    class CharacterPage extends StatelessWidget {
      const CharacterPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return CharacterView(
          store: CharacterPageStore(
            getAllCharacters: context.read<GetAllCharacters>(),
          ),
        );
      }
    }
    
    // 2. The View (Lifecycle)
    class CharacterView extends StatefulWidget {
      const CharacterView({super.key, required this.store});
      final CharacterPageStore store;
    
      @override
      State<CharacterView> createState() => _CharacterViewState();
    }
    
    class _CharacterViewState extends State<CharacterView> {
      @override
      void initState() {
        super.initState();
        WidgetsBinding.instance.addPostFrameCallback((_) {
          widget.store.fetchNextPage();
        });
      }
    
      @override
      Widget build(BuildContext context) {
        return Observer(
          builder: (_) => widget.store.contentStatus == CharacterPageStatus.loading
              ? const Center(child: CircularProgressIndicator())
              : _Content(store: widget.store),
        );
      }
    }
  9. CharacterPageCubit state properties

    main

    The CharacterPageCubit manages the state for the character list. When building the UI, you should select the following properties from the state:

    • status: An enum (e.g., CharacterPageStatus.initial) used to determine if a loading indicator should be shown.
    • characters: A list of Character entities to be rendered in the ListView.
    • hasReachedEnd: A boolean indicating if the pagination has reached the end of the available data.
  10. Navigate to the Character Details page

    main

    To navigate to the character details view, use the static route method on the CharacterDetailsPage class. This method handles the creation of the CharacterDetailsPageStore using the provided Character entity and returns a MaterialPageRoute ready for use with Flutter's Navigator.

    Navigator.of(context).push(
      CharacterDetailsPage.route(character: character),
    );
  11. Initialize the MobX implementation with AppUsingMobX

    main

    To use the MobX-based presentation layer, instantiate AppUsingMobX. This widget requires a GetAllCharacters usecase to be passed into its constructor.

    Because MobX does not provide a built-in dependency injection widget, this implementation uses RepositoryProvider from the flutter_bloc package to inject the getAllCharacters usecase into the widget tree. This allows the MobX stores used within the UI to access the domain layer via the provided usecase.

    // Example of how to instantiate the MobX entry point
    AppUsingMobX(
      getAllCharacters: getAllCharactersUseCaseInstance,
    );