DEV Community

loading...
Cover image for Building scalable Flutter apps (Architecture, Styling, Conventions, State management)

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

Nour El-Din Shobier
Software Engineer | Flutter Developer
Updated on ・6 min read

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

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

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.

PMVVM

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
      @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();
      }
    }
Enter fullscreen mode Exit fullscreen mode

You can also access the context inside the ViewModel directly

    class MyViewModel extends ViewModel {
      @override
      void init() {
        var height = MediaQuery.of(context).size.height;
      }
    }
Enter fullscreen mode Exit fullscreen mode

2. Declare MVVM inside your widget.

    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(),
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

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

      @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')),
          ],
        );
      }
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
Enter fullscreen mode Exit fullscreen mode

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';
    }
Enter fullscreen mode Exit fullscreen mode

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,
      };
    }
Enter fullscreen mode Exit fullscreen mode

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;

  @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: ....,
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

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

Discussion (1)

Collapse
lohanidamodar profile image
Damodar Lohani

Nice. I like the idea. This will add a little more flavor to my already existing feature based folder structure.

Also, I like your PMVVM way and package. Looks intuitive and clean.