Patterns, Data, and Input

Design Patterns for Structuring Applications


Learning Objectives

  • You know of the Model-View-Controller design pattern.
  • You know of the Model-View-ViewModel design pattern.
  • You know how to structure a Flutter application into a model (or a service), a controller, and a view using GetX.

Model-View-Controller

When building applications, it is meaningful to separate concerns to make the code easier to manage and maintain. Separating concerns means dividing the application into different parts, each responsible for a specific aspect of the application. This makes the code easier to understand, test, and modify, as each part of the application is responsible for a specific task.

There exists a handful of design patterns — such as the Model-View-Controller (MVC) — that can be used to separate concerns in an application.

Model-View-Controller (MVC) is a design pattern that separates an application into three main components: the user interface (the view), the data (the model), and the application logic (the controller). The view is responsible for displaying data to the user and for taking input from the user, the model is responsible for managing the data, and the controller is responsible for linking the model and the view together, processing input, and updating the view based on the model. The gist of the MVC pattern is shown in Figure 1.

Fig 1. — Model-View-Controller design pattern separates concerns of an application into the user interface (the view), the data (the model), and the application logic linking the model and the view (the controller).
Loading Exercise...

Model-View-ViewModel

The Model-View-ViewModel (MVVM) design pattern is somewhat similar to the MVC pattern. In the MVVM pattern, like in MVC, the view is responsible for displaying data to the user and for taking input from the user. Similarly, the model is responsible for managing the data.

The difference between the two patterns is in the controller and the view model. In MVVM, there is no explicit controller: the pattern uses data binding to communicate the changes from the view model to the view and vice versa. The data binding is typically reactive, meaning that the view is automatically updated when the model changes.

The gist of the MVVM pattern is shown in Figure 2.

Fig 2. — Model-View-ViewModel design pattern separates concerns of an application into the user interface (the view), the data (the model), and a view model. Communication between the view and the view model is done through a data binding.

Layered architecture

When thinking of how to concretely structure an application, one way is the layered architecture. In a layered architecture, the application is divided into layers, where each layer is responsible for a specific aspect of the application. The layers are typically organized in a hierarchical manner, with each layer depending on the layer below it.

The layers typically include a presentation layer that is the user interface, a business logic layer that implements the application logic, and a data access layer that interacts with the data. The presentation layer is responsible for displaying data to the user and for taking input from the user, the business logic layer is responsible for implementing the application logic, such as processing input and updating the data, and the data access layer is responsible for interacting with the data, such as reading and writing data to a database.

The business logic layer is often divided into two parts, a controller layer that handles input from the presentation layer and a service layer that implements the application logic.

The idea of the layered architecture is shown in Figure 3.

Fig 3. — Layered architecture separates concerns of an application into a presentation layer, a controller layer, a service layer, and a data access layer.

Patterns and Flutter

In our Flutter applications so far, both MVC and MVVM patterns have been present. The MVVM pattern is present in reactive programming with GetX that is used to manage state and to automatically update the view when the model changes. The MVC pattern, on the other hand, is visible e.g. in parts of the application that are not explicitly reactive, such as in navigation between screens.

However, even there, the frameworks and libraries take care of some of the interaction, as the framework for example is responsible for explicitly changing the view upon navigation.

We have not, however, explicitly considered a layered architecture, as often all of the functionality has been implemented in a single class. In the following, we will consider how to structure a Flutter application into two parts, where we have a controller that encapsulates the data and allows interacting with it, and a view that is responsible for displaying the data to the user and for taking input from the user.

In the following example, the service and the data access layers are not present, and their functionality have been combined into the controller. This is a simplification — in larger applications, having a separate service and data access layer could become beneficial.

First, we create a controller that encapsulates the data and the state. The “state” would be the count variable, which is an observable variable, while the increment method would allow incrementing the count.

// imports

class CountController {
  var count = 0.obs;

  void increment() {
    count++;
  }
}

The view class could be implemented as a stateless widget. Here, the controller is created when the view is created, and added as a property for the class.

import 'package:flutter/material.dart';
import 'package:get/get.dart';

class ClickCounterView extends StatelessWidget {
  final controller = CountController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Obx(
          () => Text('Likes: ${controller.count}'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => controller.increment(),
        child: const Icon(Icons.favorite),
      ),
    );
  }
}

As a whole, the application would be as follows.

Run the program to see the output

If we would, for example, want to add conditional behavior to the application that decides what is shown based on the count, we could compare the value of the count to some threshold. As an example, in the following, the application changes the text to “You are awesome” when the count is 10 or greater.

// ...
    return Scaffold(
      body: Center(
        child: Obx(
          () => controller.count.value >= 10
              ? Text('You are awesome!')
              : Text('${controller.count}'),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => controller.increment(),
        child: const Icon(Icons.favorite),
      ),
    );
// ...

Loading Exercise...

Decisions and ternary operator

Dart has the basic functionality for making decisions. In the above example, we used a ternary operator to decide what text to show based on the value of the count. The ternary operator is a shorthand for an if-else statement, and it is used to evaluate an expression and return a value based on the result of the evaluation. The ternary operator has the following syntax:

condition ? expression1 : expression2

The above syntax is equivalent to the following if-else statement:

if (condition) {
  return expression1;
} else {
  return expression2;
}

The ternary operator is useful when the decision is simple and can be expressed in a single line. If the decision is more complex, it is often better to use an if-else statement or a switch statement.

The syntax for if-else if-else statement is as follows:

if (condition1) {
  // code to execute if condition1 is true
} else if (condition2) {
  // code to execute if condition2 is true
} else {
  // code to execute if neither condition1 nor condition2 is true
}

Similarly, the syntax for a switch statement is as follows:

switch (expression) {
  case value1:
    // code to execute if expression is equal to value1
    break;
  case value2:
    // code to execute if expression is equal to value2
    break;
  default:
    // code to execute if expression is not equal to any of the values
}
Loading Exercise...