DEV Community

Kelvin Wieth
Kelvin Wieth

Posted on

Rethinking interfaces in Flutter projects

Let's say we have this code:

import 'package:flutter/foundation.dart';

abstract interface class NameRepository {
  Future<List<String>> getAll();
}

class NameRepositoryImpl implements NameRepository {
  NameRepositoryImpl({required dynamic httpClient}) : _httpClient = httpClient;

  final dynamic _httpClient;

  @override
  Future<List<String>> getAll() async => _httpClient
      .get('v1/names')
      .body()
      .maybeAs<List<String>>()
      .orElse(() => []);
}

class NameController extends ChangeNotifier {
  NameController({required NameRepository repository})
      : _repository = repository;

  final NameRepository _repository;

  bool _isLoading = false;
  bool get isLoading => _isLoading;

  List<String> _names = [];
  List<String> get names => _names;

  Future<void> init() async {
    _isLoading = true;
    notifyListeners();

    _names = await _repository.getAll();
    _isLoading = false;
    notifyListeners();
  }
}
Enter fullscreen mode Exit fullscreen mode

Pretty common, hum? At first, we all think interfaces are good to write tests, like this:

import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';

class MockNameRepository extends Mock implements NameRepository {}

void main() {
  late MockNameRepository mockNameRepository;

  setUp(() {
    mockNameRepository = MockNameRepository();
  });

  test('init calls repository and sets correct properties', () async {
    // Arrange
    final sut = NameController(repository: mockNameRepository);
    when(() => mockNameRepository.getAll()).thenAnswer((_) async => ['foo']);

    // Act
    await sut.init();

    // Assert
    verify(() => mockNameRepository.getAll()).called(1);
    expect(sut.names, equals(['foo']));
    expect(sut.isLoading, isFalse);
  });
}
Enter fullscreen mode Exit fullscreen mode

But the question is: since Dart allows us to implement and extend regular classes, not only abstract ones, why do we keep creating interfaces for simple cases like this? Instead, we could simply do:

class NameRepository {
  NameRepository({required dynamic httpClient}) : _httpClient = httpClient;

  final dynamic _httpClient;

  Future<List<String>> getAll() async => _httpClient
      .get('v1/names')
      .body()
      .maybeAs<List<String>>()
      .orElse(() => []);
}
Enter fullscreen mode Exit fullscreen mode

And incredibly we don't have to touch our tests to fix.

Of course there are still cases where interfaces are good and the best choice, specially when you build native plugins or something that needs more than one implementation - for instance, switching from online to a local database depending on device connection.

However, since most of the time we only use interfaces for test purposes, this approach has at least 2 benefits:

  • Eliminates boilerplate code;
  • Makes the debug proccess less painful, as we don't have to find for implementations of our interface; pressing F12 will lead us directly to the code.

This idea came from this thread on Bloc Discord server.

Sentry mobile image

Is your mobile app slow? Improve performance with these key strategies.

Improve performance with key strategies like TTID/TTFD & app start analysis.

Read the blog post

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay