created viewmodel and multiple tests

pull/2342/head
Miguel Beltran 4 months ago
parent ce60385ac0
commit 92af5b6551

@ -1,17 +0,0 @@
sealed class Response<T> {
const Response();
factory Response.ok(T value) => Ok(value);
factory Response.error(Exception error) => Error(error);
Ok<T> get asOk => this as Ok<T>;
Error get asError => this as Error<T>;
}
final class Ok<T> extends Response<T> {
const Ok(this.value);
final T value;
}
final class Error<T> extends Response<T> {
const Error(this.error);
final Exception error;
}

@ -0,0 +1,23 @@
sealed class Result<T> {
const Result();
factory Result.ok(T value) => Ok(value);
factory Result.error(Exception error) => Error(error);
Ok<T> get asOk => this as Ok<T>;
Error get asError => this as Error<T>;
}
final class Ok<T> extends Result<T> {
const Ok(this.value);
final T value;
@override
String toString() => 'Result<$T>.ok($value)';
}
final class Error<T> extends Result<T> {
const Error(this.error);
final Exception error;
@override
String toString() => 'Result<$T>.error($error)';
}

@ -29,4 +29,9 @@ class Destination {
/// e.g. 'https://storage.googleapis.com/tripedia-images/destinations/alaska.jpg'
final String imageUrl;
@override
String toString() {
return 'Destination{ref: $ref, name: $name, country: $country, continent: $continent, knownFor: $knownFor, tags: $tags, imageUrl: $imageUrl}';
}
}

@ -1,22 +1,25 @@
import 'package:compass_app/common/utils/response.dart';
import 'package:compass_app/common/utils/result.dart';
import 'package:compass_app/features/results/business/model/destination.dart';
import 'package:compass_app/features/results/data/destination_repository.dart';
/// Perform search over possible destinations
class SearchDestinationUsecase {
SearchDestinationUsecase({required this.repository});
SearchDestinationUsecase({
required DestinationRepository repository,
}) : _repository = repository;
final DestinationRepository repository;
final DestinationRepository _repository;
Future<Response<List<Destination>>> search({ String? continent }) async {
Future<Result<List<Destination>>> search({String? continent}) async {
bool filter(Destination destination) {
return (continent == null || destination.continent == continent);
}
final response = await repository.getDestinations();
return switch (response) {
Ok() => Response.ok(response.value.where(filter).toList()),
Error() => response,
final result = await _repository.getDestinations();
print('Result: $result');
return switch (result) {
Ok() => Result.ok(result.value.where(filter).toList()),
Error() => result,
};
}
}
}

@ -1,12 +1,12 @@
import 'package:compass_app/common/utils/response.dart';
import 'package:compass_app/common/utils/result.dart';
import 'package:compass_app/features/results/business/model/destination.dart';
/// Data source with all possible destinations
class DestinationRepository {
/// Get complete list of destinations
Future<Response<List<Destination>>> getDestinations() {
Future<Result<List<Destination>>> getDestinations() {
// TODO: Load some data
return Future.value(Response.ok([]));
return Future.value(Result.ok([]));
}
}

@ -0,0 +1,26 @@
import 'package:compass_app/features/results/presentation/results_viewmodel.dart';
import 'package:flutter/material.dart';
class ResultsScreen extends StatelessWidget {
const ResultsScreen({
super.key,
required this.resultsViewModel,
});
final ResultsViewModel resultsViewModel;
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListenableBuilder(
listenable: resultsViewModel,
builder: (BuildContext context, Widget? child) {
if (resultsViewModel.loading) {
return const CircularProgressIndicator();
}
return const Placeholder();
},
),
);
}
}

@ -0,0 +1,48 @@
import 'package:compass_app/common/utils/result.dart';
import 'package:compass_app/features/results/business/model/destination.dart';
import 'package:compass_app/features/results/business/usecases/search_destination_usecase.dart';
import 'package:flutter/cupertino.dart';
/// Based on https://docs.flutter.dev/get-started/fwe/state-management#using-mvvm-for-your-applications-architecture
class ResultsViewModel extends ChangeNotifier {
ResultsViewModel({
required SearchDestinationUsecase searchDestinationUsecase,
}) : _searchDestinationUsecase = searchDestinationUsecase;
final SearchDestinationUsecase _searchDestinationUsecase;
// Expose values in ViewModel using getters, hide setters
List<Destination> _destinations = [];
bool _loading = false;
/// List of destinations, may be empty but never null
List<Destination> get destinations => _destinations;
/// Loading state
bool get loading => _loading;
/// Perform search
Future<void> search({String? continent}) async {
// Set loading state and notify the view
_loading = true;
notifyListeners();
// Call the search usecase and request data
final result = await _searchDestinationUsecase.search(continent: continent);
// Set loading state to false
_loading = false;
switch (result) {
case Ok(): {
// If the result is Ok, update the list of destinations
_destinations = result.value;
}
case Error(): {
// TODO: Handle error
print(result.error);
}
}
// After finish loading results, notify the view
notifyListeners();
}
}

@ -1,4 +1,4 @@
import 'package:compass_app/common/utils/response.dart';
import 'package:compass_app/common/utils/result.dart';
import 'package:compass_app/features/results/business/model/destination.dart';
import 'package:compass_app/features/results/business/usecases/search_destination_usecase.dart';
import 'package:compass_app/features/results/data/destination_repository.dart';
@ -24,13 +24,24 @@ void main() {
expect(result.asOk.value.first.name, 'name1');
});
});
group('SearchDestinationUsecase errors', () {
final errorRepository = _ErrorRepository();
final usecase = SearchDestinationUsecase(repository: errorRepository);
test('should return error', () async {
final result = await usecase.search();
expect(result, isA<Error>());
expect(result.asError.error, isNotNull);
});
});
}
class _FakeRepository implements DestinationRepository {
@override
Future<Response<List<Destination>>> getDestinations() {
Future<Result<List<Destination>>> getDestinations() {
return Future.value(
Response.ok(
Result.ok(
[
Destination(
ref: 'ref1',
@ -55,3 +66,10 @@ class _FakeRepository implements DestinationRepository {
);
}
}
class _ErrorRepository implements DestinationRepository {
@override
Future<Result<List<Destination>>> getDestinations() {
return Future.value(Result<List<Destination>>.error(Exception('Invalid')));
}
}

@ -0,0 +1,38 @@
import 'package:compass_app/common/utils/result.dart';
import 'package:compass_app/features/results/business/model/destination.dart';
import 'package:compass_app/features/results/business/usecases/search_destination_usecase.dart';
import 'package:compass_app/features/results/presentation/results_viewmodel.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('ResultsViewModel tests', () {
final fakeUsecase = _FakeUsecase();
final viewModel = ResultsViewModel(searchDestinationUsecase: fakeUsecase);
// perform a simple test
// verifies that the list of items is properly loaded
// TODO: Verify loading state and calls to search method
test('should load items', () async {
expect(viewModel.destinations.length, 0);
await viewModel.search();
expect(viewModel.destinations.length, 1);
});
});
}
class _FakeUsecase implements SearchDestinationUsecase {
@override
Future<Result<List<Destination>>> search({String? continent}) async {
return Result.ok([
Destination(
ref: 'ref1',
name: 'name1',
country: 'country1',
continent: 'continent1',
knownFor: 'knownFor1',
tags: ['tags1'],
imageUrl: 'imageUrl1',
),
]);
}
}
Loading…
Cancel
Save