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.

Image of AssemblyAI

Automatic Speech Recognition with AssemblyAI

Experience near-human accuracy, low-latency performance, and advanced Speech AI capabilities with AssemblyAI's Speech-to-Text API. Sign up today and get $50 in API credit. No credit card required.

Try the API

Top comments (0)

👋 Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay