application_layers_framework 1.0.0 copy "application_layers_framework: ^1.0.0" to clipboard
application_layers_framework: ^1.0.0 copied to clipboard

A framework to standardize the implementation of a Flutter application by separating the application logic into different layers.

The Application Layers Framework (ALF) is designed to standardize the implementation of Flutter applications by promoting a clean architecture. It achieves this by organizing application logic into distinct layers hence the name.

Features #

You may have noticed that many Flutter tutorials on the popular video streaming platform focus on building mobile applications with visually appealing UIs. These tutorials often feature coding sessions that guide you through widget layout techniques, ensuring a pleasant user experience. Some even recreate the UIs of well-known applications from scratch to demonstrate that Flutter can achieve similar results.

However, while these tutorials showcase impressive UI designs, they typically lack real business logic. In practice, a well-architected application is structured into multiple layers, such as the presentation layer, domain layer, and data layer, to ensure maintainability and scalability.

This is where the Application Layers Framework (ALF) comes in. ALF was developed to standardize the design and implementation of these layers, providing a clean, scalable, extendable, and maintainable architecture. By following this framework, developers can avoid the pitfalls of disorganized, spaghetti code and build robust applications with a solid foundation.

Getting started #

A fundamental architectural design pattern that should be implemented in all user-facing applications is the separation of logic for display, control, and data. This approach is commonly known as the Model-View-Controller (MVC) design pattern, which exists in several variations. However, the core principle remains the same which is separating application logic from presentation and control.

Below is the customized variation of the MVC architecture that is adopted by the Application Layers Framework to guide its design and implementation. This structure maps the complete flow from user input to backend interaction ensuring a clean separation of concerns and promoting scalability across the application.

[]

This framework is intended for developers who are already familiar with software development using architectural patterns and have experience building simple Flutter applications.

Usage #

We will showcase the features of the Application Layers Framework by developing a sample application. Our focus is on demonstrating the framework's technical aspects, with a minimal UI layout to keep things simple and clear. The complete source code can be found in the example section.

Preparation and Design #

To begin developing a Flutter application from scratch, run the following command:

$ flutter create sample_app

This command initializes a basic Flutter project that displays a page with a centered counter and a button positioned at the bottom right. Each time the button is pressed, the counter increments by one.

[]

By visualizing the navigation flow along with the relevant events and states, we gain a deeper understanding of the application's overall structure. In this simple Flutter example, the diagram highlights the MyApp and MyHomePage widgets, along with the incrementCounter event, which can be triggered to update the counter state.

[]

Next, let us transform above implementation by integrating the Application Layers Framework, along with enhancements that showcase its key features. In the updated navigation structure, additional events have been introduced, and the counter is now stored in a repository to persist throughout the entire application. Page-specific fields, such as backgroundColor and delta from the Counter page in our sample app, are defined within the page state. These elements are illustrated in the diagram below.

[]

In the original implementation, MyApp is a stateless widget that serves as a non-visible entry point. We will rename it to Root, as it will become the landing page of our sample application. Similarly, MyHomePage will be renamed to Home. While the Home page still displays the counter, incrementing its value now requires navigating to a separate page called Counter, where we demonstrate page-level state management.

The Counter page introduces triggerable events that modify both its local state and the global counter attribute, which is stored in a repository to maintain persistence across the application. This design allows the counter value to remain accessible, even when navigating back to the Home page.

To ensure the Home page reflects updates to the counter whenever it is incremented or decremented in the Counter page, its controller subscribes to repository changes. The backgroundColor field, declared within the page state, is used for demonstration purposes to show how altering a page-specific field can impact UI presentation. Another local field, delta, determines the increment or decrement step for the counter.

Importantly, the Counter page resets to its initial state each time it is launched via navigation from the Home page.

Based on the application requirements outlined above, the following classes and enumerators will be implemented using the Application Layers Framework.

  1. CounterEntity
  2. CounterRepository
  3. RootPage
  4. RootController
  5. HomePage
  6. HomeController
  7. HomeEvent (enum)
  8. CounterPage
  9. CounterController
  10. CounterEvent (enum)
  11. CounterState (enum)

The implementation is fairly straightforward. It involves creating a counter repository class along with its associated entity, and defining the relevant pages with their respective controllers, events, and states based on the navigation structure presented in the diagram above.

As a starting point, make sure the Application Layers Framework package is included in your pubspec.yaml file, since it forms the foundation of the sample application's architecture.

...
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8

  application_layers_framework: any
  ...
dev_dependencies:
...

After saving the file, run the following command to fetch and integrate the framework into your Flutter project:

$ flutter pub get

Now we can proceed with implementing the classes and enums listed above. To maintain focus on the core application logic, all import statements have been omitted from the source code below.

Coding the model and landing page #

First, we need to consider the business or domain objects relevant to the application. Although our sample app is quite simple and only includes a counter, it is still important to follow a proper design approach by incorporating an entity layer. This means we will encapsulate the counter in a dedicated CounterEntity class. The implementation is shown below.

class CounterEntity extends ALFEntity {
  int counter = 0;
}

This entity will be utilized by the CounterRepository as implemented below. The repository offers functionality to increment and decrement the counter value and notifies any subscribed controllers of changes. Since the counter is displayed on the Home page, its controller will subscribe to the repository during the page's initialization to reflect updates in real time.

class CounterRepository extends ALFRepository {
  CounterEntity counterEntity = CounterEntity();

  void increment(int delta) {
    counterEntity.counter = counterEntity.counter + delta;
    notify();
  }

  void decrement(int delta) {
    counterEntity.counter = counterEntity.counter - delta;
    notify();
  }
}

A Flutter application typically begins with a landing page. Although this page will not be visually rendered, it still adheres to the principles of the Application Layers Framework, namely, it is linked to a controller that can handle events and manage state to determine navigation and behavior.

In our sample application, the landing page is named Root. While it does not currently manage state or handle events, we still implement a controller for it. This ensures consistency across all pages and prepares the app for future enhancements, should new requirements arise. For example, some mobile applications display introductory screens on first launch, and subsequently show the home page on future visits.

class RootController extends ALFWidgetController {}

The implementation of the page is shown below.

class RootPage extends ALFWidgetView {
  RootPage({super.key})
    : super(
        controller: RootController(),
        repositories: [ALFRepositoryProvider<CounterRepository>(create: (context) => CounterRepository())],
      );

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true),
      home: HomePage(title: "Flutter Demo Home Page"),
    );
  }
}

There is minimal logic at this stage, primarily the initialization of CounterRepository to make it accessible across the entire application, and rendering the home page via HomePage. You can disregard the syntax error for now, as HomePage will be implemented below.

Coding the Home page #

Next, we move on to the HomePage page, which is responsible for displaying the initial counter value. Since this page relies on its associated controller, we will first examine the controller implementation.

enum HomeEvent { refresh, gotoCounter }

class HomeController extends ALFWidgetController {
  HomeController() {
    register(event: HomeEvent.refresh, trigger: refreshView);
    register(event: HomeEvent.gotoCounter, trigger: gotoCounter);
  }

  void gotoCounter({dynamic params}) {
    Container page = Container();
    Navigator.push(context, MaterialPageRoute(builder: (context) => page));
  }

  @override
  void init() {
    subscribeRepository<CounterRepository>(event: HomeEvent.refresh);
  }
}

There are two events to handle, as illustrated earlier in the navigation diagram. The first involves a button on the page that navigates to the Counter page, where users can increment or decrement the counter. This is triggered by HomeEvent.gotoCounter, which uses Flutter’s Navigator to move to the next page. Since the Counter page has not been implemented yet, we will use an empty container as a placeholder.

The second event handles refreshing the Home page whenever the counter is updated in the repository. This is possible because the init() method subscribes to repository changes, triggering HomeEvent.refresh. For this event, we will use ALF’s built-in refreshView() method, which streamlines UI updates.

Compared to the original implementation, HomePage now extends ALFWidgetView, with its UI logic encapsulated in the build() method. The counter is retrieved from the repository, and pressing the navigation button triggers the event to proceed to the next page.

class HomePage extends ALFWidgetView {
  final String title;

  HomePage({required this.title, super.key}) : super(controller: HomeController());

  @override
  Widget build(BuildContext context) {
    CounterRepository counterRepository = repo<CounterRepository>();
    return Scaffold(
      appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text("The counter is currently at:"),
            Text('${counterRepository.counterEntity.counter}', style: Theme.of(context).textTheme.headlineMedium),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => trigger(event: HomeEvent.gotoCounter),
        tooltip: "Go to counter",
        child: const Icon(Icons.arrow_circle_right),
      ),
    );
  }
}

At this stage, you can already run the application to display the initial counter value, along with a button to navigate to the next page, which will be the final and more complex part of the implementation. To enable this, modify the main() function to use RootPage as the application's entry point.

void main() {
  runApp(RootPage());
}

[]

Coding the Counter page #

As usual, we begin by implementing the controller along with its associated states and events. This page displays three fields: counter, backgroundColor, and delta. The counter is already defined in the repository, while the remaining two fields are initialized within the controller’s constructor using a dictionary. Each field is keyed by an entry from the state enumerator, allowing compile-time validation and reducing the risk of typos during development.

According to the navigation diagram, five distinct events need to be handled, each represented by its own event method in the implementation below. The event responsible for changing the background color accepts an additional parameter that specifies the target color. Handling this event involves updating the corresponding value in the page state and refreshing the page to reflect the change.

The events for incrementing and decrementing the counter invoke the relevant application functions from the repository, followed by refreshing the page. Adjusting the delta value works similarly to the background color change: the field is incremented or decremented by one, then the view is refreshed to display the updated state.

enum CounterEvent { changeBackgroundColor, incrementDelta, decrementDelta, incrementCounter, decrementCounter }

enum CounterState { backgroundColor, delta }

class CounterController extends ALFWidgetController {
  CounterController() : super(state: {CounterState.backgroundColor: Colors.white, CounterState.delta: 1}) {
    register(event: CounterEvent.changeBackgroundColor, trigger: changeBackgroundColor);
    register(event: CounterEvent.incrementDelta, trigger: incrementDelta);
    register(event: CounterEvent.decrementDelta, trigger: decrementDelta);
    register(event: CounterEvent.incrementCounter, trigger: incrementCounter);
    register(event: CounterEvent.decrementCounter, trigger: decrementCounter);
  }

  void changeBackgroundColor({dynamic params}) {
    update(state: {CounterState.backgroundColor: params});
  }

  void incrementDelta({dynamic params}) {
    update(state: {CounterState.delta: state[CounterState.delta] + 1});
  }

  void decrementDelta({dynamic params}) {
    update(state: {CounterState.delta: state[CounterState.delta] - 1});
  }

  void incrementCounter({dynamic params}) {
    CounterRepository counterRepository = repo<CounterRepository>();
    counterRepository.increment(state[CounterState.delta]);
    refreshView();
  }

  void decrementCounter({dynamic params}) {
    CounterRepository counterRepository = repo<CounterRepository>();
    counterRepository.decrement(state[CounterState.delta]);
    refreshView();
  }
}

As previously mentioned, the controller’s update() method modifies the page state and automatically refreshes the view unless the refresh parameter is explicitly set to false. Since the counter’s increment and decrement actions do not utilize the update() method, they must explicitly invoke refreshView() to reflect changes in the UI.

With event handling now in place, the page implementation can render its fields by accessing the state managed within the controller and retrieving the counter value from CounterRepository through the widget tree context. User interactions will trigger events that are dispatched to and processed by the controller.

class CounterPage extends ALFWidgetView {
  CounterPage({super.key}) : super(controller: CounterController());

  @override
  Widget build(BuildContext context) {
    CounterRepository counterRepository = repo<CounterRepository>();
    return Scaffold(
      backgroundColor: state[CounterState.backgroundColor],
      appBar: AppBar(backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text("Flutter Demo Counter Page")),
      body: Center(
        child: SizedBox(
          width: 400,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Column(
                mainAxisSize: MainAxisSize.min,
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  ListTile(
                    title: Text("White color"),
                    leading: Radio<Color>(
                      value: Colors.white,
                      groupValue: state[CounterState.backgroundColor],
                      onChanged: (Color? value) => trigger(event: CounterEvent.changeBackgroundColor, params: value!),
                    ),
                  ),
                  ListTile(
                    title: Text("Blue color"),
                    leading: Radio<Color>(
                      value: Colors.blue,
                      groupValue: state[CounterState.backgroundColor],
                      onChanged: (Color? value) => trigger(event: CounterEvent.changeBackgroundColor, params: value!),
                    ),
                  ),
                  ListTile(
                    title: Text("Green color"),
                    leading: Radio<Color>(
                      value: Colors.green,
                      groupValue: state[CounterState.backgroundColor],
                      onChanged: (Color? value) => trigger(event: CounterEvent.changeBackgroundColor, params: value!),
                    ),
                  ),
                ],
              ),
              Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: () => trigger(event: CounterEvent.decrementCounter),
                    child: const Icon(Icons.remove),
                  ),
                  const SizedBox(width: 10),
                  Text("Counter value: ${counterRepository.counterEntity.counter}"),
                  const SizedBox(width: 10),
                  ElevatedButton(
                    onPressed: () => trigger(event: CounterEvent.incrementCounter),
                    child: const Icon(Icons.add),
                  ),
                ],
              ),
              const SizedBox(height: 10),
              Row(
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  ElevatedButton(
                    onPressed: state[CounterState.delta] <= 1 ? null : () => trigger(event: CounterEvent.decrementDelta),
                    child: const Icon(Icons.remove),
                  ),
                  const SizedBox(width: 10),
                  Text("Delta value: ${state[CounterState.delta]}"),
                  const SizedBox(width: 10),
                  ElevatedButton(
                    onPressed: () => trigger(event: CounterEvent.incrementDelta),
                    child: const Icon(Icons.add),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}

After implementing CounterPage, be sure to update HomeController to use this new page implementation.

class HomeController extends ALFWidgetController {
  ...
  void gotoCounter({dynamic params}) {
    CounterPage page = CounterPage();
    Navigator.push(context, MaterialPageRoute(builder: (context) => page));
  }
  ...
}

Running the application #

Once the application is launched and navigated to the Counter page, the following screen will be displayed. While it may not be visually refined, it effectively fulfills its intended purpose.

[]

For example, selecting a color option will instantly update the page’s background color.

[]

When the counter value is increased and the user returns to the Home page, the updated value is immediately reflected thanks to the page's subscription to changes from CounterRepository.

[][]

When navigating back to the Counter page, you will notice that the backgroundColor and delta attributes have been reset to their initial values. To preserve these values across page transitions, they would need to be stored in the repository rather than within the page state. This behavior helps illustrate the distinction between repository-managed data, which persists throughout the application or a module, and page state, which is scoped to the lifecycle of a single view.

Additional information #

The Application Layers Framework is offered as a free resource to the Flutter community. To support its ongoing development and refinement of the framework, a more in-depth look at how the Application Layers Framework was designed and developed can be found from my Ko-fi page. It also provides detailed documentations along with insights into the following topics and how they integrate seamlessly with ALF. Even a small cup of coffee can be a great source of motivation!

  1. Building The Framework
  2. Internationalization
  3. Common Application Classes
  4. Centralized Application Configurations
  5. Consolidated Package Import
  6. Structuring Your Flutter Project
  7. Navigation And Routing
  8. Field And Page Validation
  9. Notification And Messaging

Credits #

Special thanks to the developers and contributors behind the following Flutter packages, which the Application Layers Framework (ALF) relies on for its functionality and structure.

0
likes
0
points
32
downloads

Publisher

unverified uploader

Weekly Downloads

A framework to standardize the implementation of a Flutter application by separating the application logic into different layers.

Homepage

License

unknown (license)

Dependencies

beamer, flutter, formz, nested, provider

More

Packages that depend on application_layers_framework