Skip to main content
← Blog
Desarrollo

The Single Responsibility Principle and Unit Tests in Flutter

3 min read
The Single Responsibility Principle and Unit Tests in Flutter

As I mentioned in the post about SOLID principles, we’re starting this series with the Single Responsibility Principle (SRP) — for me, one of the most important principles in software development.

This principle states that a class should have only one reason to change — that is, a single responsibility.

When you combine SRP with unit tests, you get a synergy that significantly improves code quality and makes it easier to evolve over time. In this article we’ll explore how these two concepts complement each other and how to apply them effectively in Flutter projects.

Why SRP is crucial for unit tests

  • Isolated tests: Classes with a single responsibility are much easier to isolate and test independently. You can simulate their environment with test data and verify behavior without worrying about side effects elsewhere in the system.
  • Higher code coverage: SRP lets you decompose code into smaller, more focused units — each of which is straightforward to write a test for. More tests, more coverage, more confidence.
  • Early error detection: Unit tests help you catch errors early in the development cycle. With SRP, errors tend to surface in isolated unit tests rather than hiding in complex classes, making them easier to locate and fix.
  • Safe refactoring: Unit tests act as a safety net when you make changes. If you refactor a class that follows SRP, you can run its tests to confirm nothing broke.

Practical example: A counter widget

This widget mixes presentation logic, business logic, and state management:

class MyWidget extends StatelessWidget {
  final String title;
  final int count;

  MyWidget({required this.title, required this.count});

  @override
  Widget build(BuildContext context) {
    if (count == 0) {
      return Text('No elements');
    } else {
      return Column(
        children: [],
      );
    }
  }
}

This widget violates SRP because it:

  • Presents information — shows the title, count, and a button
  • Manages business logic — increments the count on button press
  • Manages state — uses setState to update the count

Refactoring to follow SRP

Here’s the same functionality split into proper responsibilities:

class MyWidget extends StatelessWidget {
  final String title;
  final int count;
  final Function onIncrement;

  MyWidget({
    required this.title,
    required this.count,
    required this.onIncrement,
  });

  @override
  Widget build(BuildContext context) {
    if (count == 0) {
      return Text('No elements');
    } else {
      return Column(
        children: [],
      );
    }
  }
}

class MyStatefulWidget extends StatefulWidget {
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  int count = 0;

  void incrementCount() {
    setState(() {
      count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MyWidget(
      title: 'My title',
      count: count,
      onIncrement: incrementCount,
    );
  }
}

In this refactoring:

  • MyWidget handles presentation — it receives the count and the increment function as parameters
  • MyStatefulWidget handles state management and business logic

Each widget now has a single responsibility, making the code easier to understand, test, and maintain.

Next steps

Always try to decompose your classes — whether they’re widgets or business classes — into the smallest units possible. Don’t worry if you end up with 100 different classes. They’re free. That decomposition gives you more control over your code and makes your tests smaller, faster, and easier to maintain.

See you in the next article.


More in Desarrollo