Building scalable Flutter apps (Architecture, Styling, Conventions, State management)
Tags: flutter
Published: 2021-06-24 01:45:20

After two years of Fluttering, I would like to share in this article the best practices that I’ve learned of how to build a scalable application.

I will not say what you ***should*** do, but what you ***could*** do. This guideline will make sure you, and anyone else maintaining the application, can find anything you’re looking for easily and intuitively. 

That said, let’s discuss how you can achieve that.

## 1) Architecture: Feature-based

Feature is an essential concept to understand any software design. It’s also used to describe user requirements for software development. Therefore, if we structure our projects by features, it will be easier to manage the project when it grows as we construct the system with bigger units.


### Organize project by features

In complex apps, it’s hard to understand how different modules collaborate. A feature-oriented architecture is helpful for this because we’ve grouped related logic `(widgets|utils|pages|stores|models|..etc)` into features. We don’t need to think about how the small parts work together but how features work together to construct the app. By analyzing dependencies between features the app could auto-generate understandable diagrams for developers to learn or review the project.

### Features types

To keep any feature from getting polluted, it’s important to decouple the **business logic** of that feature from its **presentation**. That’s why we should split the app into two different layers:


- **Infrastructure features:** contains all the features that are responsible for implementing the business logic of the application (e.g: auth, http, config, user, articles, events, schools, …etc.)

- **App features:** contains all the features that are responsible for implementing the presentation of the application (e.g: auth, home, settings, user, articles, events, schools, …etc.)

> **Notice** that auth, user, events, articles, …etc. features can be both infrastructure and app features, so what is the difference? that’s what we will discuss in the next section **(Features anatomy)**.

![Feature types](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/luixyjq9gdq1nt26v921.png)

### Features anatomy
- **Infrastructure features:** maintains services, repositories, models, dtos, utils, interceptors, validators, interfaces, …etc
- **App features:** maintains pages, widgets, styles, fonts, colors, …etc.


> **Note:** An app feature may consume multiple Infrastructure features

![Features anatomy](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ar79wq3cjsyv1lirdk4q.png)

## 2) Naming conventions: Naming files
### Snake case (snake_case)

snake_case is a naming style where **all letters in the name are lowercase** **and** **it uses** ***underscores*** **to separate words** in a name. In addition, in **Angular**, a dot is used to separate the name, type, and extension for file names. `file_name.type.dart`

Including the type in the file names make it easy to find a specific file type using a text editor or an IDE.

Most common files types are: .widget, .style, .service, .model, .util, .store


> Create additional type names if you must but take care not to create too many.

**Examples**
- `file_name.widget.dart`
- `file_name.style.dart`
- `file_name.model.dart`
- `file_name.util.dart`

## 3) State management: Provider + MVVM

State management is a complex topic in Flutter. Each State Management approach has its characteristics and each person has different preferences. For me, [Provider](https://pub.dev/packages/provider) was the best choice because it is easy to understand and it doesn’t use much code.

That said, Provider itself isn’t enough to build scalable apps, so I ended up building my package for state management that combines both Provider and MVVM features and called it [PMVVM](https://pub.dev/packages/pmvvm).


### P.MVVM

In PMVVM we have 3 major pieces are needed, everything else is up to you. These pieces are:


- **View**: It represents the UI of the application devoid of any Application Logic. The `ViewModel` sends notifications to the `view` to update the UI whenever state changes.
- **ViewModel**: It acts as a bridge between the `Model` and the `View`. It’s responsible for transforming the data from the `Model`, it also holds the events of the `View`
- **Model**: Holds app data and the business logic. It consists of the business logic - local and remote data source, model classes, repository. They’re usually simple classes.

![PMVVM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dpa5jtr1aingqhpae1vm.png)

**Advantages ✔️** 


- Your code is even more easily testable.
- Your code is further decoupled (the biggest advantage.)
- The package structure is even easier to navigate.
- The project is even easier to maintain.
- Your team can add new features even more quickly.

**When to use it 👌** 

To keep it simple, use the `MVVM` whenever your widget has its own events that can mutate the state directly e.g: pages, posts, ..etc.

**Some Notes**


- `View` can't access the `Model` directly
- `View` is devoid of any application logic
- `ViewModel` can have more than one `View`.

### Usage

**1.** Build your `ViewModel`.

```dart
    class MyViewModel extends ViewModel {
      int counter = 0;
    
      // Optional
      @override
      void init() {
        // It's called after the ViewModel is constructed
      }
    
      // Optional
      @override
      void onBuild() {
        // It's called everytime the view is rebuilt
      }
    
      void increase() {
        counter++;
        notifyListeners();
      }
    }
```
You can also access the `context` inside the `ViewModel` directly

```dart
    class MyViewModel extends ViewModel {
      @override
      void init() {
        var height = MediaQuery.of(context).size.height;
      }
    }
```

**2.** Declare `MVVM` inside your widget.

```dart
    class MyWidget extends StatelessWidget {
      const MyWidget({Key key}) : super(key: key);
    
      @override
      Widget build(BuildContext context) {
        return MVVM<MyViewModel>(
          view: (context, vmodel) => _MyView(),
          viewModel: MyViewModel(),
        );
      }
    }
```
**3.** Build your `View`.

```dart
    class _MyView extends StatelessView<MyViewModel> {
      /// Set [reactive] to [false] if you don't want the view to listen to the ViewModel.
      /// It's [true] by default.
      const _MyView({Key key}) : super(key: key, reactive: true); 
    
      @override
      Widget render(context, vmodel) {
        return Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text(vmodel.counter.toString()),
            SizedBox(height: 24),
            RaisedButton(onPressed: vmodel.increase, child: Text('Increase')),
          ],
        );
      }
```

> For more details, head to the package documentation

### P.MVVM for Web, Mobile, and Desktop together

`pmvvm` works perfectly especially if your app runs on multiple platforms. All you need is to create a single view model that controls all these views:

![PMVVM for crossplatforms](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/99swc9c60c1z6cxtblrs.png)

## 4) Styling

In Flutter, we often make files for colors, strings, text styles, themes. This way all of these values are kept in one, easy to find a place that should make life easier for the person who gets stuck with maintaining the app.

### Styling as a feature

We should group `app-wide` colors, fonts, themes, and animations as an app feature called `styles`. This approach will make all the widgets in the application consume the styles from a single source.

**Example:**

`colors.style.dart`
```dart
    abstract class CColors {
      static const white0 = Color(0xffffffff);
      static const black100 = Color(0xff000000);
      static const blue10 = Color(0xffedf5ff);
      static const blue20 = Color(0xffd0e2ff);
      static const blue30 = Color(0xffa6c8ff);
    }
```
`text.style.dart`
```dart
    abstract class CFonts {
      static const primaryRegular = 'IBMPlexSans-Regular';
      static const primaryLight = 'IBMPlexSans-Light';
      static const primaryMedium = 'IBMPlexSans-Medium';
      static const primarySemibold = 'IBMPlexSans-SemiBold';
      static const primaryBold = 'IBMPlexSans-Bold';
    }
```

> More examples can be found [**Here**](https://github.com/NourEldinShobier/carbon-flutter/tree/master/lib/shared/styles)

### Widgets styling

If your widget is complex and has some reactive behavior based on specific actions (e.g: background color changes when a button is tapped), then you probably need to separate your widget colors and layout variables from the widget code.

**Example:**

`tile.style.dart`
```dart
    abstract class TileStyle {
      static const Map<String, dynamic> layouts = {
        'tile-padding': const EdgeInsets.all(16),
      };
      static const Map<String, Color> colors = {
        'tile-enabled-background-color': CColors.gray90,
        'tile-enabled-label-color': CColors.gray30,
        'tile-enabled-title-color': CColors.gray10,
        'tile-enabled-description-color': CColors.gray30,
        //
        'tile-disabled-background-color': CColors.gray90,
        'tile-disabled-label-color': CColors.gray70,
        'tile-disabled-title-color': CColors.gray70,
        'tile-disabled-description-color': CColors.gray70,
      };
    }
```
`tile.widget.dart`
```dart
 class CTile extends StatelessWidget {
  const CTile({
    Key? key,
    this.enable = true,
    ...
  }) : super(key: key);

  final bool enable;

  final _colors = CTileStyle.colors;
  final _layouts = CTileStyle.layouts;

  @override
  Widget build(BuildContext context) {
    /// styles helpers
    String cwidget = 'tile';
    String state = enable ? 'enabled' : 'disabled';

    return IgnorePointer(
      ignoring: !enable,
      child: Container(
        color: _colors['$cwidget-$state-background-color'],
        padding: _layouts['$cwidget-padding'],
        child: ....,
      ),
    );
  }
}

```

> More examples can be found [**Here**](https://github.com/NourEldinShobier/carbon-flutter/tree/master/lib/features)


## Conclusion

In this article, we covered the 4 main things you need in large application development.

Here they are in summary:


- Construct your application as a set of features working together.
- Define the type of each dart file using file_name.type.dart.
- Using MVVM to manage your state is easier than other alternatives such as BLoC.
- Separate your widgets styles from the presentation code.

## Source code

A full example for this article can be found [**here**](https://github.com/NourEldinShobier/scalable_flutter_app)