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 blog image

The Visual Studio App Center’s retiring

But sadly….you’re not. See how to make the switch to Sentry for all your crash reporting needs.

Read more

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 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