Building scalable Flutter apps (Architecture, Styling, Conventions, State management)

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).

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

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 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.

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.

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.

class MyViewModel extends ViewModel {
      int counter = 0;

      // Optional
      
      void init() {
        // It's called after the ViewModel is constructed
      }

      // Optional
      
      void onBuild() {
        // It's called everytime the view is rebuilt
      }

      void increase() {
        counter++;
        notifyListeners();
      }
    }

You can also access the context inside the ViewModel directly

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

2. Declare MVVM inside your widget.

class MyWidget extends StatelessWidget {
      const MyWidget({Key key}) : super(key: key);

      
      Widget build(BuildContext context) {
        return MVVM<MyViewModel>(
          view: (context, vmodel) => _MyView(),
          viewModel: MyViewModel(),
        );
      }
    }

3. Build your View.

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); 

      
      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:

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

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

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

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

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

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;

  
  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

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

33