Journey to the clean architecture for my flutter app
Tags: flutter, dart, architecture, cleancode
Published: 2022-10-31 19:15:31

---
title: Journey to the clean architecture for my flutter app
published: true
description: 
tags: #flutter #dart #architecture #cleancode
---

## Motivation

When I started my flutter project two years ago, Android Jetpack architecture inspired me a lot. DataBinding between ViewModel and View using LiveData that comes from the data layer passing through the repository and so on, as I mentioned in another [posting](https://dev.to/leehack/journey-to-the-riverpod-for-my-flutter-app-2hk0)

![MVVM with data layer](https://i.imgur.com/e1sCpLq.png)

(I cannot find this diagram anymore from the official google document. They added the domain layer as an optional recommendation: [link](https://developer.android.com/topic/architecture))

My project folder structure was something like this:
```
- lib
  - repositories
    - a_repository.dart
    - b_repository.dart
  - models
    - a_model.dart
    - b_model.dart
  - services
    - a_service.dart
    - b_service.dart
  - ui
    - pages
      - pageA
        - a_page.dart
        - a_viewmodel.dart
      - pageB
        - b_page.dart
        - b_viewmodel.dart
    - widgets
      - a_widget.dart
      - b_widget.dart
  - main.dart
  - DI.dart
```

`DI.dart` wasn't truly doing the dependency injection. Still, I was initializing all global dependencies as singletons to inject them into the page-scoped ViewModels when initializing the page.

The problem with this approach was that the ViewModel held too much business logic. If I needed to use similar logic for another page, I had to reimplement the logic in the ViewModel. Eventually, I started adding ViewModels in a shared folder and reusing them directly from the widgets, creating another instance of the ViewModel. It reduced the code duplication, but it made it look messy. Whenever I tried to add a new feature, it was confusing where I should put the new codes.

## Clean architecture
![From cleancoder blog](https://i.imgur.com/RPFDkAy.png)

After some research, I decided to apply the clean architecture I've used for a golang backend project. The clean architecture was quite famous for the flutter project, too, since it's a recommended architecture by [BLoC](https://bloclibrary.dev/), a popular state management library in dart.

### Layer-First structure
I started to restructure the directory based on the layer. (At this moment, [I also decided to use riverpod instead of provider](https://dev.to/leehack/journey-to-the-riverpod-for-my-flutter-app-2hk0))
```
- lib
  - data
    - remote
      - repositories
        - a_repository_impl.dart
        - b_repository_impl.dart
      - models
        - a_model.dart
        - b_model.dart
      - services
        - a_service.dart
        - b_service.dart
      - data_provider.dart
  - domain
    - entities
      - a_entity.dart
      - b_entity.dart
    - repositories
      - a_repository.dart
      - b_repository.dart
    - usecases
      - featureA
        - get.dart
        - get_many.dart
      - featureB
        - get.dart
        - get_by_filter.dart
    - providers
      - feature_a_usecase_provider.dart
      - feature_b_usecase_provider.dart
  - presentation
    - states
      - a_state_provider.dart
      - b_state_provider.dart
    - pages
      - pageA
        - a_page.dart
      - pageB
        - b_page.dart
    - widgets
      - a_widget.dart
      - b_widget.dart
  - main.dart
```

The repositories in the **domain** layer are **abstract** classes that interface between the **domain** and the **data** layer. The **data_provider.dart** initialize the service and repository providers. The providers in the domain layer are initializing the usecases using repository providers defined in **data_provider.dart**. The **states** in the **presentation** layer only access **entities** and **usecase** providers in the **domain** layer.

**The model is a service-specific model that holds entity getter(mapper)**
```dart
// a_model.dart
class ModelA {
    ModelA({this.name});
    final name;

    EntityA get entity => Entity(name: name);
}
```

**The service can locally or remotely access the raw data source**
```dart
// a_service.dart
abstract class ServiceA {
    ModelA get(String a);
}
class ServiceAImpl implements ServiceA {
    ModelA get(String a) {
        return ModelA(name: "test");
    }
}
```

**The implementation of the repository that accesses the service**
```dart
// a_repository_impl.dart
class RepositoryAImpl implements RepositoryA {
    RepositoryAImpl(this.service);
    final ServiceA service;
    EntityA get(String id) {
        return service.get(id).entity;
    }
}
```

**Initialize services and repositories(Services are private)**
```dart
// data_provider.dart
final _serviceAProvider = Provider<ServiceA>((_) => ServiceAImpl());
final repositoryAProvider = Provider<RepositoryA>(
    (ref) => RepositoryAImpl(ref.watch(_serviceAProvider)),
);
```

**Domain entity that application uses**
```dart
// a_entity.dart
class EntityA {
    EntityA({this.name});
    final name;
}
```

**Interfacing between domain and data layer**
```dart
// a_repository.dart
abstract class RepositoryA {
    EntityA get(String id);
}
```

**The usecase can access the repository**
```dart
// featureA/get.dart
abstract class GetA {
    EntityA execute(String id);
}
class GetAImpl implements GetA {
    GetAImpl(this.repo)
    final RepositoryA repo;

    EntityA execute(String id) {
        return repo.get(id)
    }
}
```

**The states in the presentation layer access the usecase through the provider**
```dart
// feature_a_usecase_provider.dart
final getAProvider = Provider<GetA>((ref) => 
    GetImpl(ref.watch(repositoryAProvider)),
);
```

**States are globally shared and accessible from widgets**
```dart
// a_state_provider.dart
final stateAProvider = StateNotifierProvider<EntityA>((ref){
    return StateANotifier(EntityA(), ref);
});
class StateANotifier extends StateNotifier<EntityA> {
    StateANotifier(super.state, this.ref)
    final Ref ref;

    void fetch(String id) {
        state = ref.watch(getAProvider).execute(id);
    }
}
```

**Widget accesses the states**
```dart
// a_widget.dart
class WidgetA extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final stateA = ref.watch(stateAProvider);
    return InkWell(
      onTap: () => ref.read(stateAProvider.notifier).fetch(),
      child: Text('${stateA.name}'),
    );
}
```

After the massive refactoring of the architecture (Even though it looks like there are a lot of boilerplate codes), I was pretty satisfied. I don't need to think twice about where to put which logic. Every business logic goes to the usecase and entity. If the view needs to update the state, I create the **StateNotifierProvider**. The Widgets only focus on the viewing part. The separation gives better testability and lets the codes keep the single responsibility rule.


### Feature-First structure

After the refactoring, the only problem I had was that it was not easy to navigate the source code. When implementing a feature, I wished all related codes were grouped. Since the codes are split based on the layer, It was pretty easy to lose my focus.
So, I decided to do another refactoring for a feature-first structure.

```
- lib
  - features
    - featureA
      - domain
        - entity
          - a_entity.dart
        - repository
          - a_repository.dart
        - usecases
          - get.dart
          - get_many.dart
      - data
        - repositories
          - a_repository_impl.dart
        - models
          - a_model.dart
        - services
          - a_service.dart
        - data_provider.dart
      - providers
        - feature_a_usecase_provider.dart
    - featureB
      - domain
        - entity
          - b_entity.dart
        - repository
          - b_repository.dart
        - usecases
          - get.dart
          - get_many.dart
      - data
        - repositories
          - b_repository_impl.dart
        - models
          - b_model.dart
        - services
          - b_service.dart
        - data_provider.dart
      - providers
        - feature_b_usecase_provider.dart
  - presentation
    - states
      - a_state_provider.dart
      - b_state_provider.dart
    - pages
      - pageA
        - a_page.dart
      - pageB
        - b_page.dart
    - widgets
      - a_widget.dart
      - b_widget.dart
  - main.dart
```

For the code-wise, nothing changed. Some might wonder why I didn't put the presentation in the feature directory. I still don't have a conclusion on this, but I thought the UI should use different domains, and I didn't want to cross-reference between features. I didn't specify above, but I put the shared codes in the **core** feature. It's the only exception that other features can reference.


## Conclusion

I don't have any good recommendations for a small project or team if the clean architecture is a good choice or not since it requires some boilerplate codes. But It gives us a good separation between layers and domains, making the code more testable. It can take time to write the pattern every time adding a new feature, but the time can be easily compensated by focusing on implementing a single domain feature. There's no perfect architecture or solution. One day, we need to refactor our code base. A library we've been using probably doesn't fit our needs anymore or introduces security vulnerabilities. The well-structured architecture allows quickly switching a library or refactoring a part of your code.

## References
- https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
- https://codewithandrea.com/articles/flutter-project-structure/
- https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/