DEV Community

Cover image for Keep the Code Simple in Flutter
Abdeldjalil Chougui
Abdeldjalil Chougui

Posted on

Keep the Code Simple in Flutter

Simple code is easier to understand, modify, and maintain, which is crucial for the long-term success of any project. In this article, we will discuss some tips and tricks to keep the code simple in Flutter.

1- Follow the Single Responsibility Principle (SRP)
SRP is a fundamental principle of object-oriented programming (OOP). It suggests that a class should have only one responsibility and should not be responsible for more than one task. Following the SRP makes the code modular, easy to understand, and maintainable. In Flutter, widgets are the fundamental building blocks of the user interface, and they should follow the SRP. If a widget has too many responsibilities, it becomes difficult to reuse, and any changes made to it can have unintended consequences.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My App'),
      ),
      body: ListView(
        children: [
          ProductTile(name: 'Product 1', price: 10.0),
          ProductTile(name: 'Product 2', price: 20.0),
          ProductTile(name: 'Product 3', price: 30.0),
        ],
      ),
    );
  }
}

class ProductTile extends StatelessWidget {
  final String name;
  final double price;

  const ProductTile({Key? key, required this.name, required this.price})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(name),
      subtitle: Text('\$${price.toString()}'),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

2- Use Composition Pattern In Flutter
In Flutter, composition is a powerful concept that allows developers to build complex user interfaces by combining smaller widgets together to create larger ones. Instead of inheriting from a base widget class, as is the case in object-oriented programming, Flutter widgets are built by composing multiple smaller widgets together.

Composition in Flutter works through the use of composition patterns, such as Container, Row, Column, Stack, and more. These widgets can be nested within each other to create complex layouts.

For example, here's a simple Flutter widget that uses composition to display a centered message:

import 'package:flutter/material.dart';

class CenteredMessage extends StatelessWidget {
  final String message;

  const CenteredMessage({Key? key, required this.message}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.grey[300],
      child: Center(
        child: Text(
          message,
          style: TextStyle(fontSize: 20),
        ),
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

In this example, we define a CenteredMessage widget that takes a message parameter and displays it in the center of a grey Container. We use the Center widget to center the Text widget within the Container.

By using composition in Flutter, we can build reusable and composable widgets that are easy to understand and maintain. This allows us to build complex user interfaces quickly and efficiently, without sacrificing readability or maintainability.

3- Keep it Short and Sweet
Code that is too long and complex is difficult to read, understand, and maintain. It's best to keep your code short and sweet. Break it down into smaller functions, classes, or widgets that perform specific tasks. This approach makes it easy to test, debug, and modify the code as needed.

4- Don't Repeat Yourself (DRY)
DRY is another fundamental principle of software development. It suggests that you should avoid duplicating code as much as possible. Duplicated code is hard to maintain because any changes made to one instance of the code may not reflect in the other instance. In Flutter, you can avoid duplication by creating reusable widgets and functions that perform specific tasks.

class Product {
  final String name;
  final double price;

  const Product({required this.name, required this.price});
}

class ProductTile extends StatelessWidget {
  final Product product;

  const ProductTile({Key? key, required this.product}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListTile(
      title: Text(product.name),
      subtitle: Text('\$${product.price.toString()}'),
    );
  }
}

class HomePage extends StatelessWidget {
  final List<Product> products = [
    Product(name: 'Product 1', price: 10.0),
    Product(name: 'Product 2', price: 20.0),
    Product(name: 'Product 3', price: 30.0),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('My App'),
      ),
      body: ListView(
        children: products.map((product) => ProductTile(product: product)).toList(),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In the example above, the Product class is defined with a name and price property, and the ProductTile widget takes a Product object as input and renders it. The HomePage widget defines a list of Product objects and maps each one to a ProductTile widget using the map() function.

By following the DRY principle, we avoid repeating the same code in multiple places and keep our code more maintainable.

5- Use State Management
Flutter is a reactive framework, which means that the user interface updates automatically when there is a change in the state of the application. However, managing state in Flutter can be challenging, especially in large applications. Therefore, it's essential to use state management libraries like Provider, Bloc, Redux, or MobX to simplify the process. These libraries provide a structured way of managing the application state and make it easy to understand and modify.

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MaterialApp(
        title: 'Flutter Demo',
        home: MyHomePage(),
      ),
    );
  }
}

class Counter with ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = Provider.of<Counter>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Demo'),
      ),
      body: Center(
        child: Text(
          '${counter.count}',
          style: Theme.of(context).textTheme.headline4,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counter.increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

In this example, we define a Counter class that extends ChangeNotifier from the provider package. The Counter class has a private _count field and a public count getter. The increment() method updates the _count field and notifies any listeners of the change.

In the MyHomePage widget, we use the Provider.of(context) method to get an instance of the Counter class, which we can use to display the current count and increment the count when the user taps the floating action button.

By using the provider package, we can easily manage state in our Flutter apps and keep our code simple and easy to understand.

6- Test Your Code
Testing is an integral part of software development. It helps you identify bugs and ensures that your code is functioning correctly. In Flutter, you can use the built-in testing framework or third-party libraries like Flutter Test or Mockito to write tests for your code. Writing tests ensures that your code is robust, easy to maintain, and performs as expected.

import 'package:flutter_test/flutter_test.dart';

import 'package:my_app/counter.dart';

void main() {
  group('Counter', () {
    test('starts with zero', () {
      final counter = Counter();
      expect(counter.count, 0);
    });

    test('increments count', () {
      final counter = Counter();
      counter.increment();
      expect(counter.count, 1);
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

In this example, we define a group of tests for the Counter class. The first test checks that a new Counter object starts with a count of zero. The second test checks that calling the increment() method on a Counter object increases the count by one.

We use the expect() function to check the values of the count property on the Counter object.

By writing unit tests for our code, we can ensure that it works as expected and catch any regressions that might occur when making changes to our code. This helps us to maintain the quality of our code and avoid introducing bugs.

In conclusion, keeping your code simple in Flutter is essential for the long-term success of your project. By following these best practices, you can create high-quality code that is easy to read, understand, and maintain.

Top comments (0)