mirror of https://github.com/flutter/samples.git
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)';
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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…
Reference in new issue