Skip to content
This repository has been archived by the owner on Aug 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1 from Superformula/scaffolding
Browse files Browse the repository at this point in the history
  • Loading branch information
brianb-sf authored Feb 12, 2021
2 parents c36a2b7 + b728cc8 commit 02f3041
Show file tree
Hide file tree
Showing 8 changed files with 504 additions and 57 deletions.
116 changes: 106 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,118 @@
# Superformula Mobile Developer Coding Test
# RestauranTour

Make sure you read **all** of this document carefully, and follow the guidelines in it.
Be sure to read **all** of this document carefully, and follow the guidelines within.

## Requirements

There is only one test here currently, please review and get back to us.
### App Structure

## What We Are Looking For
#### Restaurant List Page

Use any libraries that you would normally use if this were a real production App. Please note: we're interested in your code & the way you solve the problem, not how well you can use a particular library or feature.
- Tab Bar
- List of favorites (stored client side)
- List of businesses
- Hero image
- Name
- Price
- Category
- Rating (rounded to the nearest value)
- Open/Closed
- Pagination via `Load More` button

_We're interested in your method and how you approach the problem just as much as we're interested in the end result._
#### Restaurant Detail View

Here's what you should strive for:
- Ability to favorite a business
- Name
- Hero image
- Price and category
- Address
- Rating
- Total reviews
- List of reviews
- User name
- Rating
- User image
- Review Text (These are just snippets of the full review, usually like 3-4 lines long)

- Good use of structure, security, and performance best practices.
- Solid testing approach.
- Extensible code.
#### Misc.

- Clear doumentation on the structure and architecture of your application.
- Clear and logical commit messages.

## Test Coverage

To demonstrate your experience writing different types of tests in Flutter please do the following:

- Choose ONE class and write a unit test.
- Choose ONE widget and write a widget test.

Feel free to add more tests as you see fit but the above is the minimum requirement.

## Design

- See this [Figma File](https://www.figma.com/file/UOQDbU02GG2yaJMfrO9q9d/Flutter-Test?node-id=0%3A1) for design specifics like
fonts, themes, colors, etc.

![List View](screenshots/listview.png)
![Detail View](screenshots/detailview.png)

## API

The [Yelp GraphQL API](https://www.yelp.com/developers/graphql/guides/intro) is used as the API for this Application. We have provided the boilerplate of the API requests and backing data models to save you some time. To successfully make a request to the Yelp GraphQL API, please follow these steps:

1. Please go to https://www.yelp.com/signup and sign up for a developer account.
1. Once signed up, navigate to https://www.yelp.com/developers/v3/manage_app.
1. Create a new app by filling out the required information.
1. Once your app is created, scroll down and join the `Developer Beta`. This allows you to use the GraphQL API.
1. Copy your API Key from your app page and paste it on `line 5` [yelp_repository.dart](app/lib/yelp_repository.dart) replacing the `<PUT YOUR API KEY HERE>` with your key.
1. Run the app and tap the `Fetch Restaurants` button. If you see a log like `Fetched x restaurants` you are all set!

## Technical Requirements

### State Management

Please restrict your usage of state or dependency injection to the following options:

1. [provider](https://pub.dev/packages/provider)
2. [Riverpod](https://pub.dev/packages/riverpod)
3. [bloc](https://pub.dev/packages/bloc)
4. [get_it](https://pub.dev/packages/get_it)/[get_it_mixins](https://pub.dev/packages/get_it_mixin)
5. [Mobx](https://pub.dev/packages/mobx)

We ask this because this challenge values consistency and efficiency over ingenuity. Using commonly used libraries ensures that we can review your code in a timely manner and allows us to provide better feedback.

## Coding Values

At **Superformula** we strive to build applications that have

- Consistent architecture
- Extensible, clean code
- Solid testing
- Good security & performance best practices

### Clear, consistent architecture

Approach your submission as if it were a real world app. This includes Use any libraries that you would normally choose.

_Please note: we're interested in your code & the way you solve the problem, not how well you can use a particular library or feature._

### Easy to understand

Writing boring code that is easy to follow is essential at **Superformula**.

We're interested in your method and how you approach the problem just as much as we're interested in the end result.

### Solid testing approach

While the purpose of this challenge is not to guage whether you can achieve 100% test coverage, we do seek to evaluate whether you know how & what to test.

## Bonus Requirements

If you are feeling up to it and want to add some more functionality to the application try some of these bonus items

- Implement persistance storage for favorited restaurants
- Implement the *Bonus Detail Screen*
- Implement a basic CI job that runs your tests and builds the app

## Q&A

Expand Down
21 changes: 20 additions & 1 deletion app/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:restaurantour/respositories/yelp_repository.dart';

void main() {
runApp(RestauranTour());
Expand All @@ -23,7 +24,25 @@ class HomePage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('RestauranTour'),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('RestauranTour'),
RaisedButton(
child: const Text('Fetch Restaurants'),
onPressed: () async {
final yelpRepo = YelpRepository();

try {
final result = await yelpRepo.getRestaurants();
print('Fetched ${result.restaurants.length} restaurants');
} catch (e) {
print('Failed to fetch restaurants: $e');
}
},
)
],
),
),
);
}
Expand Down
144 changes: 144 additions & 0 deletions app/lib/models/restaurant.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import 'package:json_annotation/json_annotation.dart';

part 'restaurant.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);
}

@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);
}

@JsonSerializable()
class User {
final String id;
@JsonKey(name: 'image_url')
final String imageUrl;
final String name;

const User({
this.id,
this.imageUrl,
this.name,
});

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

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

@JsonSerializable()
class Review {
final String id;
final int rating;
final User user;

const Review({
this.id,
this.rating,
this.user,
});

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

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

@JsonSerializable()
class Location {
@JsonKey(name: 'formatted_address')
final String formattedAddress;

Location({
this.formattedAddress,
});

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

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

@JsonSerializable()
class Restaurant {
final String id;
final String name;
final String price;
final double rating;
@JsonKey(name: 'review_count')
final int reviewCount;
final List<String> photos;
final List<Category> categories;
final List<Hours> hours;
final List<Review> reviews;
final Location location;

const Restaurant({
this.id,
this.name,
this.price,
this.rating,
this.reviewCount,
this.photos,
this.categories,
this.hours,
this.reviews,
this.location,
});

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

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

/// Use the first category for the category shown to the user
String get displayCategory =>
categories?.isNotEmpty ?? false ? categories.first.title : '';

/// Use the first image as the image shown to the user
String get heroImage => photos?.isNotEmpty ?? false ? photos.first : null;

/// This logic is probably not correct in all cases but it is ok
/// for this application
bool get isOpen => hours?.isNotEmpty ?? false ? hours.first.isOpenNow : null;
}

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

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

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

Map<String, dynamic> toJson() => _$RestaurantQueryResultToJson(this);
}
Loading

0 comments on commit 02f3041

Please sign in to comment.