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

Leonardo gutierrez RestauranTour test #10

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions .fvm/fvm_config.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
{
"flutterSdkVersion": "3.22.3",
"flavors": {}
"flutterSdkVersion": "3.22.3"
}
4 changes: 4 additions & 0 deletions .fvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"flutter": "3.22.3",
"flavors": {}
}
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 Version Cache
.fvm/
14 changes: 7 additions & 7 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"dart.flutterSdkPath": ".fvm/flutter_sdk",
"search.exclude": {
"**/.fvm": true
},
"files.watcherExclude": {
"**/.fvm": true
}
"dart.flutterSdkPath": ".fvm/versions/3.22.3",
"search.exclude": {
"**/.fvm": true
},
"files.watcherExclude": {
"**/.fvm": true
}
}
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ Welcome to Superformula's Coding challenge, we are excited to see what you can b

This take home test aims to evaluate your skills in building a Flutter application. We are looking for a well-structured and well-tested application that demonstrates your knowledge of Flutter and the Dart language.

We are not looking for pixel perfect designs, but we are looking for a well-structured application that demonstrates your skills and best practices developing a flutter aplication. We know there are many ways to solve a problem, and we are interested in seeing how you approach this one. If you have any questions, please don't hesitate to ask.
We are not looking for pixel perfect designs, but we are looking for a well-structured application that demonstrates your skills and best practices developing a flutter application. We know there are many ways to solve a problem, and we are interested in seeing how you approach this one. If you have any questions, please don't hesitate to ask.

Things we'll be looking on your submission:
- App structure for scallability
- App structure for scalability
- Error and optional (?) handling
- Widget tree optimization
- State management
Expand Down
77 changes: 77 additions & 0 deletions documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# RestauranTour App Documentation

## Overview

The RestaurantTour app connects to the Yelp database to provide a list of restaurants, allowing users to view detailed restaurant information, and favorite restaurants.
The app follows a **Clean Architecture** design pattern, with **Riverpod** for state management, **Dio** for networking, and **Mockito** for API testing.

## Architecture

The project follows a **Clean Architecture** design pattern that organizes code distribution in a specific manner, with three primary outer layers as folders.

### Layers:
1. **Presentation Layer**:
- Manages UI, Widgets, and State Management (using **Riverpod**).

2. **Domain Layer**:
- Contains business logic, use cases, and entities. In this case, the layer is provided as `models`.

3. **Data Layer**:
- Manages repositories, networking (using **Dio**), and data sources. In this case, the layer is provided as `repositories`.

### Additional Folder:

- **Core**: Contains functionality that may not fit well with the definition of the standard layers. In this project, the following subfolders were added:
- **Config**: Contains a `strings` file, allowing string manipulation to be detached from the app's logic.
- **Utils**: Stores the `router manager` for handling navigation.

## State Management

**Riverpod** was selected as the state manager for its ease of use.
In this project, Riverpod handles the state of the restaurant list and the favorites functionality.

### Example:

```dart
final restaurantListProvider = FutureProvider<List<Restaurant>>((ref) async {
final repository = ref.watch(restaurantRepositoryProvider);
return repository.getRestaurants();
});
```
## Routing and Navigation

The app has three pages, and the following routes are used to navigate between them:
```dart
class Routes {
static const String main = '/';
static const String restaurantList = '/restaurantList';
static const String restaurantDetails = '/restaurantDetails';
}
```

## Error Handling

The main source of errors in this app comes from null values in the restaurant list.
We handle these errors at the UI level to prevent crashes. For example:

```dart
Text(restaurant.price ?? '$$')
```

## Testing

### Unit Tests

1. **Presentation Layer**:
- Created tests that check whether the toggle favorite functionality correctly adds, contains, and deletes restaurants from the favorites list.

2. **Domain Layer**:
- Created tests that ensure the fromJson and toJson methods produce the correct object and JSON respectively.

3. **Data Layer**:
- Created tests that check whether the API call returns a valid list of restaurants.

### Widget Tests

The only meaningful widget that changes state based on user interaction is the Favorite icon in the Details Page.
A test was created to check if the icon changes properly every time a tap is performed.
22 changes: 22 additions & 0 deletions lib/core/config/strings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class AppStrings {
// General strings
static const appTitle = 'Restaurant App';
static const loading = 'Loading...';
static const errorMessage = 'Something went wrong. Please try again later.';

// Restaurant List Page strings
// Appbar
static const restaurantListTitle = 'RestauranTour';
static const restaurantAllTab = 'All Restaurants';
static const restaurantFavTab = 'My Favorites';
static const restaurantOpen = 'Open Now';
static const restaurantClosed = 'Closed';

// Restaurant Detail Page strings
static const restaurantDetailTitle = 'Restaurant Details';
static const restaurantDetailEmpty = 'No details available.';
static const restaurantDetailLocation = 'Address';
static const restaurantDetailRating = 'Overall Rating';
static const restaurantDetailReviews = 'reviews';

}
44 changes: 44 additions & 0 deletions lib/core/utils/router_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:restaurant_tour/presentation/pages/restaurant_details/restaurant_details_page.dart';

import '../../main.dart';
import '../../models/restaurant.dart';
import '../../presentation/pages/restaurant_list/restaurant_list_page.dart';
//import '../../presentation/pages/restaurant_details/restaurant_details_page.dart';

// Define all the route names here
class Routes {
static const String main = '/';
static const String restaurantList = '/restaurantList';
static const String restaurantDetails = '/restaurantDetails';
}

class RouterManager {
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case Routes.main:
return MaterialPageRoute(builder: (_) => HomePage());
case Routes.restaurantList:
return MaterialPageRoute(builder: (_) => RestaurantListPage());

case Routes.restaurantDetails:
final restaurant = settings.arguments as Restaurant;
return MaterialPageRoute(
builder: (_) => RestaurantDetailsPage(restaurant: restaurant),
);

default:
return _errorRoute(); // Error Route for unknown routes
}
}

// Error route in case of invalid navigation
static Route<dynamic> _errorRoute() {
return MaterialPageRoute(
builder: (_) => Scaffold(
appBar: AppBar(title: const Text('Error')),
body: const Center(child: Text('Page not found')),
),
);
}
}
29 changes: 13 additions & 16 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import 'package:flutter/material.dart';
import 'package:restaurant_tour/repositories/yelp_repository.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '/core/utils/router_manager.dart';
import '/repositories/yelp_repository.dart';
import 'models/restaurant.dart';

void main() {
runApp(const RestaurantTour());
runApp(const ProviderScope(child: RestaurantTour()));
}

class RestaurantTour extends StatelessWidget {
Expand All @@ -12,7 +16,8 @@ class RestaurantTour extends StatelessWidget {
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Restaurant Tour',
home: HomePage(),
initialRoute: Routes.main,
onGenerateRoute: RouterManager.generateRoute,
);
}
}
Expand All @@ -29,20 +34,12 @@ class HomePage extends StatelessWidget {
children: [
const Text('Restaurant Tour'),
ElevatedButton(
child: const Text('Fetch Restaurants'),
child: const Text('Go To Restaurants List'),
onPressed: () async {
final yelpRepo = YelpRepository();

try {
final result = await yelpRepo.getRestaurants();
if (result != null) {
print('Fetched ${result.restaurants!.length} restaurants');
} else {
print('No restaurants fetched');
}
} catch (e) {
print('Failed to fetch restaurants: $e');
}
Navigator.pushNamed(
context,
Routes.restaurantList,
);
},
),
],
Expand Down
6 changes: 4 additions & 2 deletions lib/models/restaurant.g.dart

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:restaurant_tour/presentation/pages/restaurant_details/widgets/restaurant_details_favorite_button.dart';
import '../../../models/restaurant.dart';
import 'widgets/restaurant_details_data_widget.dart';
import 'widgets/restaurant_details_review_list_widget.dart';

class RestaurantDetailsPage extends StatelessWidget {
final Restaurant restaurant;

const RestaurantDetailsPage({super.key, required this.restaurant});

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
actions: [
RestaurantDetailsFavoriteButton(restaurant: restaurant)
],
centerTitle: true,
title: Text(
restaurant.name ?? 'Unknown Restaurant',
style: const TextStyle(
fontFamily: 'Lora',
),
),
),
body: ListView(
children: [
// Main photo
SizedBox.square(
dimension: MediaQuery.sizeOf(context).width,
child: Hero(
tag: restaurant.id ?? 'heroImage',
child: Image.network(
restaurant.heroImage,
fit: BoxFit.fitHeight,
),
),
),
// Price/Category/Open-closed row
RestaurantDetailsDataWidget(restaurant: restaurant,),
// Reviews
RestaurantDetailsReviewListWidget(reviews: restaurant.reviews ?? [],),
],
),
);
}
}
Loading