Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flutter Test (Felipe Gonçalves dos Reis) #6

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
47eaed4
First commit
devfelipereis Sep 7, 2024
94298e8
refactor: add onTapRestaurant callback to HomeScreen and update tests
devfelipereis Sep 7, 2024
b5f3bb3
remove a comment
devfelipereis Sep 7, 2024
bf0438b
refactor: remove api key from code
devfelipereis Sep 7, 2024
8f13496
refactor: remove some code used for testing in the RestaurantsRepository
devfelipereis Sep 7, 2024
d508d6a
refactor: getRestaurants can return null
devfelipereis Sep 7, 2024
676c133
refactor: improve they way of using mocked data
devfelipereis Sep 7, 2024
f729290
refactor: better approach to use the mocked api
devfelipereis Sep 7, 2024
b0153b7
docs: add a comment to RestaurantsRepository about a improved way to …
devfelipereis Sep 7, 2024
5dc49db
docs: add some comments about passing the restaurant in the navigation
devfelipereis Sep 7, 2024
e324b5d
refactor: remove test code
devfelipereis Sep 9, 2024
977350f
refactor: improve how user imageUrl is displayd
devfelipereis Sep 9, 2024
bdd3fba
fix: keep state for AllRestaurantsTab
devfelipereis Sep 11, 2024
f44c3df
fix: mockApi always returning false
devfelipereis Sep 11, 2024
93f0748
refactor: add shouldMockApi
devfelipereis Sep 11, 2024
a2c25a0
refactor: an example of a better way to handle errors from network calls
devfelipereis Sep 12, 2024
2f29f8b
refactor: format code
devfelipereis Sep 12, 2024
6cb2949
refactor: sort imports
devfelipereis Sep 12, 2024
6ba12c9
refactor: add bloc to AllRestaurantsTab and HomeScreen
devfelipereis Sep 12, 2024
3f25bd4
fix: forgot to emit errors in the all_restaurants_tab_bloc.dart
devfelipereis Sep 12, 2024
6bfa507
test: adding missing tests for Blocs
devfelipereis Sep 13, 2024
2e95bfc
test: fixing tests... I wrote a test above the other
devfelipereis Sep 13, 2024
54e37fe
feat: save data offline (restaurants list and favorites); add data so…
devfelipereis Sep 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ app.*.map.json
/android/app/release

# fvm
.fvm/flutter_sdk
.fvm/flutter_sdk

coverage/
3 changes: 2 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ linter:
## The following rules are excluded only to keep compatibility with our previous lint set (pedantic).
## There's room to discuss them individually and raise PRs adjusting our codebase.
avoid_print: false
require_trailing_commas: true
require_trailing_commas: true
always_use_package_imports: true
6 changes: 6 additions & 0 deletions build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
targets:
$default:
builders:
json_serializable:
options:
explicit_to_json: true
34 changes: 34 additions & 0 deletions lib/core/domain/error/data_error.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import 'package:restaurant_tour/core/domain/error/error.dart';

sealed class DataError extends BaseError {}

sealed class NetworkError extends DataError {}

sealed class LocalError extends DataError {}

// network errors
final class NoInternetConnectionError extends NetworkError {}

final class TimeoutError extends NetworkError {}

final class ServerError extends NetworkError {}

final class RateLimitError extends NetworkError {}

// local errors
final class ReadLocalDataError extends LocalError {}

final class SaveDataError extends LocalError {}

// adding override only in this class to help with tests
// just an example as I need to compare the result of an error in a test
final class UnknownError extends NetworkError {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is UnknownError;
}

@override
int get hashCode => runtimeType.hashCode;
}
1 change: 1 addition & 0 deletions lib/core/domain/error/error.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
abstract class BaseError {}
6 changes: 6 additions & 0 deletions lib/core/theme/colors.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'dart:ui';

class AppColors {
static const star = Color(0xFFFFB800);
static const dividerColor = Color(0xFFEEEEEE);
}
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:sembast/sembast.dart';

abstract class BaseDatabaseProvider {
Database get database;

Future<void> init();

Future<void> close();
}
25 changes: 25 additions & 0 deletions lib/data/datasources/local/provider/database_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:restaurant_tour/data/datasources/local/provider/base_database_provider.dart';
import 'package:sembast/sembast.dart';
import 'package:sembast/sembast_io.dart';

class DatabaseProvider extends BaseDatabaseProvider {
late final Database _database;

@override
Database get database => _database;

@override
Future<void> init() async {
final dir = await getApplicationDocumentsDirectory();
await dir.create(recursive: true);
final dbPath = join(dir.path, 'database.db');
_database = await databaseFactoryIo.openDatabase(dbPath);
}

@override
Future<void> close() async {
await _database.close();
}
}
81 changes: 81 additions & 0 deletions lib/data/datasources/local/restaurant_local_data_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import 'package:multiple_result/multiple_result.dart';
import 'package:restaurant_tour/core/domain/error/data_error.dart';
import 'package:restaurant_tour/core/domain/error/error.dart';
import 'package:restaurant_tour/data/datasources/local/provider/base_database_provider.dart';
import 'package:restaurant_tour/data/models/restaurant.dart';
import 'package:restaurant_tour/data/repositories/datasource/restaurant_local_data_source.dart';
import 'package:sembast/sembast.dart';

class RestaurantLocalDataSource extends BaseRestaurantLocalDataSource {
RestaurantLocalDataSource({
required BaseDatabaseProvider databaseProvider,
}) : _databaseProvider = databaseProvider;

final BaseDatabaseProvider _databaseProvider;

Database get _db => _databaseProvider.database;

StoreRef<String, Map<String, Object?>> get _store =>
stringMapStoreFactory.store('restaurants');

StoreRef<String, Map<String, Object?>> get _favoritesStore =>
stringMapStoreFactory.store('favorites_restaurants');

@override
Future<Result<List<Restaurant>, BaseError>> getRestaurants() async {
try {
final data = await _store.find(_db);
final restaurants =
data.map((e) => Restaurant.fromJson(e.value)).toList();
return Success(restaurants);
} catch (e) {
return Error(ReadLocalDataError());
}
}

@override
Future<Result<void, BaseError>> insertRestaurants(
List<Restaurant> restaurants,
) async {
try {
await _store.addAll(_db, restaurants.map((e) => e.toJson()).toList());

await _db.transaction((txn) async {
for (final restaurant in restaurants) {
await _store.record(restaurant.id!).put(txn, restaurant.toJson());
}
});

return Success.unit();
} catch (e) {
return Error(SaveDataError());
}
}

@override
Stream<List<Restaurant>> getFavorites() {
return _favoritesStore.query().onSnapshotsSync(_db).map((snapshot) {
return snapshot.map((e) => Restaurant.fromJson(e.value)).toList();
});
}

@override
Future<Result<void, BaseError>> toggleFavorite(Restaurant restaurant) async {
try {
final recordId = restaurant.id!;
final exists = await _favoritesStore.record(recordId).get(_db);

if (exists != null) {
await _favoritesStore.record(recordId).delete(_db);
return Success.unit();
}

_favoritesStore.record(recordId).put(_db, restaurant.toJson());

return Success.unit();
} catch (e) {
// we could map a more specific error here, but for now, we will just return a generic error
return Error(SaveDataError());
}
}
}
87 changes: 87 additions & 0 deletions lib/data/datasources/remote/restaurant_remote_data_source.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import 'package:dio/dio.dart';
import 'package:multiple_result/multiple_result.dart';
import 'package:restaurant_tour/core/domain/error/data_error.dart';
import 'package:restaurant_tour/core/domain/error/error.dart';
import 'package:restaurant_tour/data/dtos/restaurant_dto.dart';
import 'package:restaurant_tour/data/models/restaurant.dart';
import 'package:restaurant_tour/data/repositories/datasource/restaurant_remote_data_source.dart';

class RestaurantRemoteDataSource extends BaseRestaurantRemoteDataSource {
RestaurantRemoteDataSource({
required Dio httpClient,
}) : _httpClient = httpClient;

final Dio _httpClient;

@override
Future<Result<List<Restaurant>, BaseError>> getRestaurants({
int offset = 0,
}) async {
try {
final response = await _httpClient.post<Map<String, dynamic>>(
'/v3/graphql',
data: _getRestaurantsQuery(offset),
);

final data = RestaurantDto.fromJson(response.data!['data']['search']);

if (data.restaurants != null) {
return Success(data.restaurants!);
}

return Error(UnknownError());
} on DioException catch (e) {
// we could map all network errors here and create something to share the logic
// here a simple example
if (e.response?.statusCode == 400) {
return Error(RateLimitError());
}

return switch (e.type) {
DioExceptionType.badResponse => Error(UnknownError()),
DioExceptionType.connectionTimeout => Error(TimeoutError()),
DioExceptionType.connectionError => Error(NoInternetConnectionError()),
_ => Error(UnknownError()),
};
} catch (e) {
return Error(UnknownError());
}
}
}

String _getRestaurantsQuery(int offset) {
return '''
query getRestaurants {
search(location: "Las Vegas", limit: 20, offset: $offset) {
total
business {
id
name
price
rating
photos
reviews {
id
rating
text
user {
id
image_url
name
}
}
categories {
title
alias
}
hours {
is_open_now
}
location {
formatted_address
}
}
}
}
''';
}
22 changes: 22 additions & 0 deletions lib/data/dtos/restaurant_dto.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import 'package:json_annotation/json_annotation.dart';

import 'package:restaurant_tour/data/models/restaurant.dart';

part 'restaurant_dto.g.dart';

@JsonSerializable()
class RestaurantDto {
final int? total;
@JsonKey(name: 'business')
final List<Restaurant>? restaurants;

const RestaurantDto({
this.total,
this.restaurants,
});

factory RestaurantDto.fromJson(Map<String, dynamic> json) =>
_$RestaurantDtoFromJson(json);

Map<String, dynamic> toJson() => _$RestaurantDtoToJson(this);
}
21 changes: 21 additions & 0 deletions lib/data/dtos/restaurant_dto.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions lib/data/models/category.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:restaurant_tour/domain/models/category.dart'
as category_domain_model;

part 'category.g.dart';

@JsonSerializable()
class Category {
final String? alias;
final String? title;

Category({
this.alias,
this.title,
});

factory Category.fromJson(Map<String, dynamic> json) =>
_$CategoryFromJson(json);

Map<String, dynamic> toJson() => _$CategoryToJson(this);

category_domain_model.Category toDomain() => category_domain_model.Category(
alias: alias,
title: title,
);

factory Category.fromDomain(category_domain_model.Category domain) =>
Category(
alias: domain.alias,
title: domain.title,
);
}
17 changes: 17 additions & 0 deletions lib/data/models/category.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions lib/data/models/hours.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:restaurant_tour/domain/models/hours.dart' as hours_domain_model;

part 'hours.g.dart';

@JsonSerializable()
class Hours {
@JsonKey(name: 'is_open_now')
final bool? isOpenNow;

const Hours({
this.isOpenNow,
});

factory Hours.fromJson(Map<String, dynamic> json) => _$HoursFromJson(json);

Map<String, dynamic> toJson() => _$HoursToJson(this);

hours_domain_model.Hours toDomain() => hours_domain_model.Hours(
isOpenNow: isOpenNow,
);

factory Hours.fromDomain(hours_domain_model.Hours domain) => Hours(
isOpenNow: domain.isOpenNow,
);
}
Loading