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();
}
}
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);
});
}
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(() => []);
}
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.
Top comments (0)