Skip to content

Software Development at Program Tom LTD

Place for coding, programming, development and software in general.

Menu
  • Blog
  • PDF Booklets
  • Dev Utils & Content
  • Java Spring Boot Or Web Apps
  • English
    • български
    • English
    • Español
    • Português
    • हिन्दी
    • Русский
    • Deutsch
    • Français
    • Italiano
    • العربية
  • About Us
Menu
Progressively Extracting Flutter Bloc Logic for testable code

Progressively Extracting Flutter Bloc Logic for testable code

Posted on October 21, 2025 by Toma Velev

To make Flutter Bloc logic testable, extract business logic into service methods or classes, isolating it from state management. This guide provides a step-by-step approach with examples.

1. Understand the Goal

Why extract logic? Blocs often mix business logic (e.g., API calls, data processing) with state management, making them hard to test. Extracting logic into services:

  • Improves testability by isolating dependencies.
  • Enhances maintainability and reusability.
  • Keeps Blocs focused on coordinating events and states.

Key principle: Blocs handle what to do (state transitions), while services handle how to do it (business logic).

2. Identify Logic to Extract

Analyze your Bloc for:

  • API calls or external data operations (e.g., HTTP requests, database queries).
  • Complex computations or transformations (e.g., filtering, mapping data).
  • Domain-specific rules (e.g., validation, business logic).
  • Side effects (e.g., writing to storage, logging).

Move these to service classes, leaving the Bloc to emit states.

3. Create Service Classes

What are services? Plain Dart classes encapsulating specific functionality.

How to structure them?

  • Define interfaces (abstract classes) for services to enable dependency injection and mocking.
  • Implement concrete service classes for the logic.
  • Inject services into the Bloc via its constructor.

Example: Refactoring a TodoBloc

Before Extraction (Mixed Logic in Bloc)


class TodoBloc extends Bloc<TodoEvent, TodoState> {
  TodoBloc() : super(TodoInitial()) {
    on<FetchTodos>((event, emit) async {
      emit(TodoLoading());
      try {
        final response = await http.get(Uri.parse('https://api.example.com/todos'));
        final todos = jsonDecode(response.body) as List;
        final filteredTodos = todos.where((todo) => todo['completed'] == false).toList();
        emit(TodoLoaded(filteredTodos));
      } catch (e) {
        emit(TodoError('Failed to fetch todos'));
      }
    });
  }
}
  

This Bloc mixes API calls and filtering logic with state management.

Step 1: Define a Service Interface


abstract class TodoService {
  Future<List<dynamic>> fetchTodos();
  List<dynamic> filterIncompleteTodos(List<dynamic> todos);
}
  

Step 2: Implement the Service


class TodoApiService implements TodoService {
  final http.Client client;

  TodoApiService(this.client);

  @override
  Future<List<dynamic>> fetchTodos() async {
    final response = await client.get(Uri.parse('https://api.example.com/todos'));
    if (response.statusCode == 200) {
      return jsonDecode(response.body) as List;
    } else {
      throw Exception('Failed to fetch todos');
    }
  }

  @override
  List<dynamic> filterIncompleteTodos(List<dynamic> todos) {
    return todos.where((todo) => todo['completed'] == false).toList();
  }
}
  

3: Inject the Service into the Bloc


class TodoBloc extends Bloc<TodoEvent, TodoState> {
  final TodoService todoService;

  TodoBloc(this.todoService) : super(TodoInitial()) {
    on<FetchTodos>((event, emit) async {
      emit(TodoLoading());
      try {
        final todos = await todoService.fetchTodos();
        final filteredTodos = todoService.filterIncompleteTodos(todos);
        emit(TodoLoaded(filteredTodos));
      } catch (e) {
        emit(TodoError('Failed to fetch todos'));
      }
    });
  }
}
  

The Bloc now delegates logic to the service, focusing on state emission.

4. Set Up Dependency Injection

Use a dependency injection package like get_it or provider, or pass services manually.

Example with get_it


final getIt = GetIt.instance;

void setup() {
  getIt.registerSingleton<TodoService>(TodoApiService(http.Client()));
}

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

In your app:


BlocProvider(
  create: (context) => TodoBloc(getIt<TodoService>()),
  child: MyHomePage(),
)
  

5. Write Tests for Services

Services are plain Dart classes, making them easy to test with mockito or mocktail.

Example Test for TodoApiService


import 'package:http/http.dart' as http;
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

class MockHttpClient extends Mock implements http.Client {}

void main() {
  late TodoApiService todoService;
  late MockHttpClient mockHttpClient;

  setUp(() {
    mockHttpClient = MockHttpClient();
    todoService = TodoApiService(mockHttpClient);
  });

  test('fetchTodos returns list of todos on success', () async {
    when(mockHttpClient.get(any)).thenAnswer(
      (_) async => http.Response('[{"id": 1, "completed": false}]', 200),
    );

    final todos = await todoService.fetchTodos();

    expect(todos, isA<List>());
    expect(todos.length, 1);
    expect(todos[0]['id'], 1);
  });

  test('fetchTodos throws exception on failure', () async {
    when(mockHttpClient.get(any)).thenAnswer(
      (_) async => http.Response('Error', 500),
    );

    expect(() => todoService.fetchTodos(), throwsException);
  });

  test('filterIncompleteTodos filters out completed todos', () {
    final todos = [
      {'id': 1, 'completed': false},
      {'id': 2, 'completed': true},
    ];

    final filtered = todoService.filterIncompleteTodos(todos);

    expect(filtered.length, 1);
    expect(filtered[0]['id'], 1);
  });
}
  

Testing the Bloc

Mock the service to test the Bloc’s state transitions.


class MockTodoService extends Mock implements TodoService {}

void main() {
  late TodoBloc todoBloc;
  late MockTodoService mockTodoService;

  setUp(() {
    mockTodoService = MockTodoService();
    todoBloc = TodoBloc(mockTodoService);
  });

  test('emits [TodoLoading, TodoLoaded] when fetchTodos succeeds', () async {
    when(mockTodoService.fetchTodos())
        .thenAnswer((_) async => [{'id': 1, 'completed': false}]);
    when(mockTodoService.filterIncompleteTodos(any))
        .thenReturn([{'id': 1, 'completed': false}]);

    todoBloc.add(FetchTodos());

    await expectLater(
      todoBloc.stream,
      emitsInOrder([
        isA<TodoLoading>(),
        isA<TodoLoaded>(),
      ]),
    );
  });
}
  

6. Progressively Extract More Logic

Create additional services for other logic, e.g.:

  • AuthService for authentication.
  • DataRepository for multiple data sources.
  • ValidatorService for input validation.

Example (Validation Service)


abstract class ValidatorService {
  bool isValidTodoTitle(String title);
}

class TodoValidatorService implements ValidatorService {
  @override
  bool isValidTodoTitle(String title) {
    return title.isNotEmpty && title.length <= 50;
  }
}
  

Update the Bloc:


class TodoBloc extends Bloc<TodoEvent, TodoState> {
  final TodoService todoService;
  final ValidatorService validatorService;

  TodoBloc(this.todoService, this.validatorService) : super(TodoInitial()) {
    on<AddTodo>((event, emit) async {
      if (validatorService.isValidTodoTitle(event.title)) {
        // Proceed with adding todo
      } else {
        emit(TodoError('Invalid title'));
      }
    });
  }
}
  

7. Best Practices for Testable Services

  • Single Responsibility: Each service handles one type of logic.
  • Immutable Data: Use immutable data or pure functions where possible.
  • Error Handling: Centralize error handling in services.
  • Mocking: Use interfaces for mockable dependencies.
  • Unit Testing: Test each service method in isolation.

8. Example Folder Structure


lib/
├── blocs/
│   └── todo_bloc.dart
├── services/
│   ├── todo_service.dart
│   ├── todo_api_service.dart
│   └── validator_service.dart
├── models/
│   └── todo.dart
├── screens/
│   └── home_page.dart
└── main.dart
  

9. Common Pitfalls to Avoid

  • Overcomplicating Services: Avoid turning services into god objects.
  • Leaking UI Logic: Keep services UI-agnostic.
  • Not Mocking Dependencies: Always mock external dependencies.
  • Ignoring Error States: Ensure services propagate errors clearly.

10. Testing the Entire Flow

Use integration tests for the app’s behavior and widget tests for UI updates.

Example Integration Test


testWidgets('Todo screen displays todos', (tester) async {
  await tester.pumpWidget(
    BlocProvider(
      create: (_) => TodoBloc(TodoApiService(http.Client())),
      child: MaterialApp(home: TodoScreen()),
    ),
  );

  await tester.pumpAndSettle();
  expect(find.text('Todo 1'), findsOneWidget);
});
  

Summary

  • Extract business logic into service classes with interfaces.
  • Inject services into Blocs using dependency injection.
  • Write unit tests for services using mocks.
  • Keep Blocs focused on state management.
  • Refactor progressively by isolating logic.
  • Feature Flags – Enable Functionality from the BackEnd
  • Integrating xAI Grok API with Spring Boot
  • How to Progresively Integrate AI
  • What is an AI Agent
  • Flutter image scaling

Categories

  • Apps (22)
  • ChatGPT (23)
  • Choosing a Framework (38)
  • Flutter (256)
  • Graphical User Interface (14)
  • Marketing (116)
  • Software Development (281)
  • Spring (44)
  • StartUp (22)
  • Uncategorized (14)
  • Uncategorized (4)
  • Vaadin (14)

Tags

Algorithms (9) crypto (29) flutterdev (39) General (86) Java (7) QR & Bar Codes (3) Software Dev Choices (33) Spring Boot (1) standards (1) Theme (3) User Authentication & Authorization (9) User Experience (10) Utilities (19) WordPress (11)

Product categories

  • All Technologies (84)
    • Flutter Apps (24)
    • GPT (4)
    • Java (38)
    • Native Android (3)
    • PHP (9)
    • Spring (Boot) / Quarkus (35)
    • Utils (15)
    • Vaadin 24+ (27)
    • Vaadin 8 (1)
  • Apps (18)
    • Employees DB (1)
    • Notes (6)
    • Personal Budget (1)
    • Recipes Book (1)
    • Stuff Organizer (1)
    • To-Do (2)
  • PDF Books (3)
  • Source Code Generators (8)

Recent Posts

  • Feature Flags – Enable Functionality from the BackEnd
  • Integrating xAI Grok API with Spring Boot
  • How to Progresively Integrate AI
  • What is an AI Agent
  • Flutter image scaling

Post Categories

  • Apps (22)
  • ChatGPT (23)
  • Choosing a Framework (38)
  • Flutter (256)
  • Graphical User Interface (14)
  • Marketing (116)
  • Software Development (281)
  • Spring (44)
  • StartUp (22)
  • Uncategorized (14)
  • Uncategorized (4)
  • Vaadin (14)