MWWM

Build Status Coverage Status Pub Version Pub Likes Pub popularity Flutter Platform

This package is part of the SurfGear toolkit made by Surf.

MWWM Cover

About

MVVM-inspired lightweight architectural framework for Flutter apps made with respect to Clean Architecture.

Currently supported features

  • Complete separation of the application's codebase into independent layers: UI, presentation and business logic;
  • Keeps widget tree clear: the main building block is just an extended version of StatefulWidget;
  • Built-in mechanisms for handling asynchronous operations;
  • The ability to easily implement the default error handling strategy;
  • An event-like mechanism that helps keep the business logic well structured and testable.

Overview

MWWM is the perfect mix of the Flutter framework architectural concept, the simple MVVM pattern, and the Clean Architecture principles.

MWWM's superpowers

  • Simple - no crazy complex abstractions that change the structure of your project beyond recognition;

  • Flexible - you can apply MWWM components to a certain project area (screen or widget). You still can use StatefulWidget and StatelessWidget wherever you would like to keep things simple;

  • Scalable - the scale of your project is not a problem. You can use additional MWWM features as your project grows more complex (such as Model);

  • No limitations - this architecture package doesn't dictate what DI, navigation, or any other approaches you should use in your project. You can even implement communication between layers the way you want. Or take our advice and do so with another package. Everything is up to you!

Key components

MWWM Architecture Scheme

Widget

This is where your declarative layouts live. Components of this layer contain UI-related code only and act like typical StatefulWidget. The Widget contains the link to its widget model.

Presented by CoreMwwmWidget class.

WidgetModel

WidgetModel is like the mechanism behind your widget. Its concept is very similar to State but there is one big difference. State knows nothing about the business logic of your app and can't refer to any business logic components, while WidgetModel can.

  • It tells the current state of the widget, which can be meaningful for business logic scenarios;
  • It knows which user input (or any other UI event) triggers a certain business logic scenario and acts as a dispatcher between them.

Presented by the WidgetModel class.

Model

Unlike other components, the Model is optional.

Model is a great way to simplify the business logic layer of your app by breaking it into two sets of abstractions:

  • Change is an intent to do something without any concrete implementation details, only input parameters;
  • Performer is the reaction to the Change that is associated with it. The Performer contains the implementation of a certain operation that is triggered by the Change.

Presented by the Model, Change, and Performer classes.

Usage

Basic use case (without Model)

MWWM Architecture Mini Scheme

Create WidgetModel

Create a WidgetModel for the widget by extending the WidgetModel class.

If you want certain part of your code to run as soon as the widget is initialized, you can override the onLoad() function and place this code right after the super-function invocation.

class LoginWm extends WidgetModel {

  LoginWm(
    WidgetModelDependencies baseDependencies,
  ) : super(baseDependencies);

  @override
  void onLoad() {
    super.onLoad();
    ...
  }
}

Implement WidgetModel builder

You need to create an instance of the WidgetModel. We recommend using top-level functions for this.

Don't forget to pass WidgetModelDependencies as the first argument. Use it as a bundle that contains a set of dependencies required for all WidgetModels in your app (e.g. error handlers, loggers).

WidgetModel buildLoginWM(BuildContext context) => 
  LoginWm(
    WidgetModelDependencies(),
  );
}

Create Widget

Create a Widget by extending the CoreMwwmWidget class.

class LoginScreen extends CoreMwwmWidget {

  LoginScreen({
    WidgetModelBuilder widgetModelBuilder,
  })  : assert(widgetModelBuilder != null),
        super(widgetModelBuilder: widgetModelBuilder);

  @override
  State<StatefulWidget> createState() => _LoginScreenState();
}

Create WidgetState

CoreMwwmWidget is an extended version of StatefulWidget. We need to attach a State for it by extending WidgetState class.

Collect the widget tree and return it from a build() function the way you usually do it with regular StatefulWidget.

Every WidgetState refers to its WidgetModel. That's why you need to specify the concrete WidgetModel type as a generic type of the WidgetState.

class _LoginScreenState extends WidgetState<LoginWm> {

  @override
  Widget build(BuildContext context) => 
    Scaffold(
      body: Container(), 
    );
}

Create entry point

You will most likely start your screen with a Route. In this case, the Route becomes the place where you put everything together.

  class LoginScreenRoute extends MaterialPageRoute {
    LoginScreenRoute()
        : super(
            builder: (context) => LoginScreen(
              widgetModelBuilder: buildLoginWM(),
            ),
          );
  }

That's it! Now you can implement your first MWWM-powered screen.

Advanced use case (with Model)

In case your app is too complicated, you can simplify it by breaking down the business logic layer into a set of smaller components (Performers), each responsible for performing a single business logic operation.

Create Change

Change is the intention to perform an action. Each operation is associated with a Change.

Create a Change by extending FutureChange or StreamChange classes.

The difference between them is the type of the returned operation result. FutureChange will wrap the result into a Future, and StreamChange into a Stream.

A Change can also serve to contain passing input parameters if they are needed to perform an operation. If not, leave the class body empty.

class LoginUser extends FutureChange<UserProfile> {
  final String login;
  final String password;
}

Create Performer

Performer - is a functional part of the contract. Performer should be responsible for no more than one operation. Each Performer is associated with Change one-to-one.

While declaring Performer you should specify two types of parameters: the first is the operation result type (the same as you already set up in Change), the second is the associated Change type.

You can insert any object into the Performer to launch any necessary operations.

Create a Performer by extending the FuturePerformer or StreamPerformer classes.

class LoginUserPerformer extends FuturePerformer<UserProfile, LoginUser> {

  final AuthService authService;

  @override
  Future<UserProfile> perform(LoginUser change) {
    // api call retrieving user profile instance
    return userProfile;
  }
}

Pass configured Model to WidgetModel

You probably already know the basic MWWM use case. If not, see this section.

Declare Model instance as an argument for your WidgetModel constructor and pass it to its super-constructor.

class LoginWm extends WidgetModel {

  LoginWm(
    WidgetModelDependencies baseDependencies,
    Model model,
    ) : super(baseDependencies, model: model);
}

Go back to the top-level function that creates WidgetModel. Pass the newly created Model instance to the WidgetModel's constructor.

Remember to pass an array containing all the Performers that can be accessed through this WidgetModel to the Model's constructor.

WidgetModel buildLoginWM(BuildContext context) => 
  LoginWm(
    WidgetModelDependencies(),
    Model([
      LoginUserPerformer(),
    ]),
  );
}

Perform the operation

Finally, you can request your Model to perform any operation by passing the right Change through the Model.perform() function. You just have to correctly process the result of the operation.

class LoginWm extends WidgetModel {

  void performUserLogin() async {
    final userProfile = await model.perform(LoginUser(login, password);,
    ...
  }
}

That's all folks! You are now familiar with the advanced technique of using MWWM-architecture.

Asynchronous operations handling

MWWM package provides built-in capabilities for asynchronous operations. In addition, you can take advantage of the centralized error handling function.

Function name Supported data type Is default error handling mechanism enabled
subscribe() Stream -
subscribeHandleError() Stream +
doFuture() Future -
doFutureHandleError() Future +

When using doFuture() and subscribe(), select Future or Stream as the first parameter. The onValue function handles the result of the asynchronous operation as the second.

The onError parameter is optional and can be used to handle errors manually.

doFuture<bool>(
  isAuthenticated(),
  (isAuth) {
    if (isAuth)
      navigator.push(MainScreenRoute());
  },
  onError: (error) {
    print(error);
  },
);

What about doFutureHandleError() and subscribeHandleError()? You can use them the same exact way as their "no handle error" companions, with all the benefits of the default error handling mechanism.

You can still use the onError function to pass but it will now act as an addition to default error handling.

doFutureHandleError<bool>(
  login(),
  (isLogin) {
    if (isLogin)
      navigator.push(MainScreenRoute());
  },
  onError: (error) {
    print(error);
  },
);

Default error handling customization

Error handling is easily customizable.

First, extend the ErrorHandler class and override the handleError() function. This function will be used whenever an error occurs. You can implement your error handling right there.

class CustomErrorHandler extends ErrorHandler {
  @override
  void handleError(Object e) {
    debugPrint('Custom error handler regretfully informs that $e has occurred.');
  }
}

Follow the top-level function that creates WidgetModel. Pass your custom error handler instance as errorHandler argument value. That's it.

WidgetModel buildLoginWM(BuildContext context) => 
  LoginWm(
    WidgetModelDependencies(
      errorHandler: LoginScreenErrorHandler(),
    ),    
  );
}

Now all errors that occur during asynchronous operations launched by the doFutureHandleError() and subscribeHandleError() functions will fall into a custom handler.

FAQ

Where should my UI layout be placed?

Directly in the WidgetState.build() function. For widgets that are simple use StatelessWidget or StatefulWidget from SDK.

How can I access the widget model?

Every WidgetState reference its WidgetModel through wm getter. You can access it immediately after running initState().

Where should I place the navigation?

We don't limit you in the choice of navigation approaches, but we strongly recommend implementing navigation in the WidgetModel.

You decide how your project will be arranged. However, we recommend you organize your code this way:

  • /lib
    • model/
      • feature1/
        • services
        • changes.dart
        • performer.dart
      • feature2/
        • services
        • changes.dart
        • performer.dart
    • ui/
      • screen1/
        • wm.dart
        • widget.dart
        • route.dart
      • screen2/
        • wm.dart
        • widget.dart
        • route.dart

Installation

Add mwwm to your pubspec.yaml file:

dependencies:
  mwwm: ^1.0.0

You can use both stable and dev versions of the package listed above in the badges bar.

Changelog

All notable changes to this project will be documented in this file.

Issues

For issues, file directly in the main SurfGear repo.

Contribute

If you would like to contribute to the package (e.g. by improving the documentation, solving a bug or adding a cool new feature), please review our contribution guide first and send us your pull request.

Your PRs are always welcome.

How to reach us

Please feel free to ask any questions about this package. Join our community chat on Telegram. We speak English and Russian.

Telegram

License

Apache License, Version 2.0

Libraries

mwwm