diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..39fb081a4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/README.md b/README.md
index 951ef0e2c..af004ead4 100644
--- a/README.md
+++ b/README.md
@@ -1,47 +1,13 @@
-# Wave Software Development Challenge
-Applicants for the [Mobile engineer](https://wave.bamboohr.co.uk/jobs/view.php?id=6) role at Wave must complete the following challenge, and submit a solution prior to the onsite interview.
+## Highlights
-The purpose of this exercise is to create something that we can work on together during the onsite. We do this so that you get a chance to collaborate with Wavers during the interview in a situation where you know something better than us (it's your code, after all!)
+This solution makes use of the repository pattern to decouple the data access layer from the user interface. The repository acts as a bridge between these two layers.
-There isn't a hard deadline for this exercise; take as long as you need to complete it. However, in terms of total time spent actively working on the challenge, we ask that you not spend more than a few hours, as we value your time and are happy to leave things open to discussion in the onsite interview.
+Product objects are wrapped by LiveData which observes the lifecycle of the main activity, minimising the chance of data leaks. The main activity observes this LiveData object to ensure that the UI matches the actual data.
-You can write your app using your favorite language, tools, platform, etc. Whether that means something native or something hybrid is completely up to you.
+A ViewModel is used to transport data from the repository to the app's RecyclerView. This ViewModel manages data retention through configuration changes.
-Send your submission to [dev.careers@waveapps.com](dev.careers@waveapps.com). Feel free to email [dev.careers@waveapps.com](dev.careers@waveapps.com) if you have any questions.
+## Setup
-## Submission Instructions
-1. Fork this project on github. You will need to create an account if you don't already have one.
-1. Complete the project as described below within your fork.
-1. Push all of your changes to your fork on github and submit a pull request.
-1. You should also email [dev.careers@waveapps.com](dev.careers@waveapps.com) and your recruiter to let them know you have submitted a solution. Make sure to include your github username in your email (so we can match applicants with pull requests.)
-
-## Alternate Submission Instructions (if you don't want to publicize completing the challenge)
-1. Clone the repository.
-1. Complete your project as described below within your local repository.
-1. Email a patch file to [dev.careers@waveapps.com](dev.careers@waveapps.com).
-
-## Project Description
-In this project, we're going to be creating a simple app that shows a Wave user the products that they can charge for on their invoices.
-
-You'll be using the public Wave API in this challenge. You can find the documentation [here](http://docs.waveapps.io/). You will specifically be interested in [the products endpoint](http://docs.waveapps.io/endpoints/products.html#get--businesses-business_id-products-), and [using an access token with the API](http://docs.waveapps.io/oauth/index.html#use-the-access-token-to-access-the-api).
-
-Your Wave contact will supply you with a business ID and a Wave API token before you begin.
-
-### What your application must do:
-
-1. Your app must retrieve the list of products for the specific business ID sent to you by your Wave contact
-1. The list of products should be fetched and shown to the user in a list view when the app is launched.
-1. Each item in the list view should show the product name and price (formatted as a dollar amount.)
-
-You are not required to add any interactivity to the app -- i.e. you do not need to send the user to a detail view when they touch one of the list items.
-
-Your app is allowed to render nothing if there is no internet connection when it loads.
-
-Once you're done, please submit a paragraph or two in your `README` about what you are particularly proud of in your implementation, and why.
-
-## Evaluation
-Evaluation of your submission will be based on the following criteria.
-
-1. Did your application fulfill the basic requirements?
-1. Did you document the method for setting up and running your application?
-1. Did you follow the instructions for submission?
+1. Download the .apk file is located at the root of this repository.
+1. Open a terminal and navigate to the directory of the downloaded .apk
+1. Use ADB to install adb install "app-debug.apk"
diff --git a/app-debug.apk b/app-debug.apk
new file mode 100644
index 000000000..33bffad73
Binary files /dev/null and b/app-debug.apk differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..16c2fdcb5
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/xyz/cdeegan/wavechallenge/data/entity/Product.java b/app/src/main/java/xyz/cdeegan/wavechallenge/data/entity/Product.java
new file mode 100644
index 000000000..80628068f
--- /dev/null
+++ b/app/src/main/java/xyz/cdeegan/wavechallenge/data/entity/Product.java
@@ -0,0 +1,17 @@
+package xyz.cdeegan.wavechallenge.data.entity;
+
+public class Product
+{
+ private double price;
+ private String name;
+
+ public double getPrice()
+ {
+ return price;
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+}
diff --git a/app/src/main/java/xyz/cdeegan/wavechallenge/data/service/ProductAPIResponse.java b/app/src/main/java/xyz/cdeegan/wavechallenge/data/service/ProductAPIResponse.java
new file mode 100644
index 000000000..98920a7d8
--- /dev/null
+++ b/app/src/main/java/xyz/cdeegan/wavechallenge/data/service/ProductAPIResponse.java
@@ -0,0 +1,33 @@
+package xyz.cdeegan.wavechallenge.data.service;
+
+import java.util.List;
+
+import xyz.cdeegan.wavechallenge.data.entity.Product;
+
+public class ProductAPIResponse
+{
+ private List products;
+ private Throwable error;
+
+ public ProductAPIResponse(List products)
+ {
+ this.products = products;
+ this.error = null;
+ }
+
+ public ProductAPIResponse(Throwable error)
+ {
+ this.products = null;
+ this.error = error;
+ }
+
+ public List getProducts()
+ {
+ return products;
+ }
+
+ public Throwable getError()
+ {
+ return error;
+ }
+}
diff --git a/app/src/main/java/xyz/cdeegan/wavechallenge/data/service/ProductAPIService.java b/app/src/main/java/xyz/cdeegan/wavechallenge/data/service/ProductAPIService.java
new file mode 100644
index 000000000..19ef259b0
--- /dev/null
+++ b/app/src/main/java/xyz/cdeegan/wavechallenge/data/service/ProductAPIService.java
@@ -0,0 +1,15 @@
+package xyz.cdeegan.wavechallenge.data.service;
+
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Header;
+import retrofit2.http.Path;
+import xyz.cdeegan.wavechallenge.data.entity.Product;
+
+public interface ProductAPIService
+{
+ @GET("/businesses/{business_id}/products/")
+ Call> getProductList(@Header("Authorization") String authHeader, @Path("business_id") String businessID);
+}
diff --git a/app/src/main/java/xyz/cdeegan/wavechallenge/repository/ProductRepository.java b/app/src/main/java/xyz/cdeegan/wavechallenge/repository/ProductRepository.java
new file mode 100644
index 000000000..f37005165
--- /dev/null
+++ b/app/src/main/java/xyz/cdeegan/wavechallenge/repository/ProductRepository.java
@@ -0,0 +1,54 @@
+package xyz.cdeegan.wavechallenge.repository;
+
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.MutableLiveData;
+
+import java.util.List;
+
+import retrofit2.Call;
+import retrofit2.Callback;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.converter.gson.GsonConverterFactory;
+import xyz.cdeegan.wavechallenge.data.entity.Product;
+import xyz.cdeegan.wavechallenge.data.service.ProductAPIResponse;
+import xyz.cdeegan.wavechallenge.data.service.ProductAPIService;
+
+public class ProductRepository implements ProductRepositoryInterface
+{
+ private static final String WAVE_BASE_URL = "https://api.waveapps.com";
+ private static final String WAVE_API_TOKEN = "6W9hcvwRvyyZgPu9Odq7ko8DSY8Nfm";
+ private ProductAPIService productAPIService;
+
+ public ProductRepository()
+ {
+ Retrofit retrofit = new Retrofit.Builder()
+ .baseUrl(WAVE_BASE_URL)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build();
+
+ this.productAPIService = retrofit.create(ProductAPIService.class);
+ }
+
+ @Override
+ public LiveData getProducts(String businessID)
+ {
+ final MutableLiveData liveData = new MutableLiveData<>();
+ Call> call = productAPIService.getProductList("Bearer " + WAVE_API_TOKEN, businessID);
+ call.enqueue(new Callback>()
+ {
+ @Override
+ public void onResponse(Call> call, Response> response)
+ {
+ liveData.setValue(new ProductAPIResponse(response.body()));
+ }
+
+ @Override
+ public void onFailure(Call> call, Throwable t)
+ {
+ liveData.setValue(new ProductAPIResponse(t));
+ }
+ });
+ return liveData;
+ }
+}
diff --git a/app/src/main/java/xyz/cdeegan/wavechallenge/repository/ProductRepositoryInterface.java b/app/src/main/java/xyz/cdeegan/wavechallenge/repository/ProductRepositoryInterface.java
new file mode 100644
index 000000000..90098bfec
--- /dev/null
+++ b/app/src/main/java/xyz/cdeegan/wavechallenge/repository/ProductRepositoryInterface.java
@@ -0,0 +1,10 @@
+package xyz.cdeegan.wavechallenge.repository;
+
+import android.arch.lifecycle.LiveData;
+
+import xyz.cdeegan.wavechallenge.data.service.ProductAPIResponse;
+
+public interface ProductRepositoryInterface
+{
+ LiveData getProducts(String businessID);
+}
diff --git a/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductAdapter.java b/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductAdapter.java
new file mode 100644
index 000000000..4c464fe40
--- /dev/null
+++ b/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductAdapter.java
@@ -0,0 +1,51 @@
+package xyz.cdeegan.wavechallenge.ui;
+
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.List;
+
+import xyz.cdeegan.wavechallenge.R;
+import xyz.cdeegan.wavechallenge.data.entity.Product;
+
+public class ProductAdapter extends RecyclerView.Adapter
+{
+ private List products;
+
+ ProductAdapter(List products)
+ {
+ this.products = products;
+ }
+
+ @NonNull
+ @Override
+ public ProductListViewholder onCreateViewHolder(@NonNull ViewGroup parent, int viewType)
+ {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.list_item_product, parent, false);
+ return new ProductListViewholder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ProductListViewholder holder, int position)
+ {
+ Product product = products.get(position);
+ holder.productNameTextView.setText(product.getName());
+ holder.productPriceTextView.setText(String.format("$%.2f", product.getPrice()));
+ }
+
+ @Override
+ public int getItemCount()
+ {
+ return products.size();
+ }
+
+ public void setItems(List products)
+ {
+ this.products = products;
+ notifyDataSetChanged();
+ }
+}
diff --git a/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductListActivity.java b/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductListActivity.java
new file mode 100644
index 000000000..e9b0a7dfa
--- /dev/null
+++ b/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductListActivity.java
@@ -0,0 +1,58 @@
+package xyz.cdeegan.wavechallenge.ui;
+
+import android.arch.lifecycle.ViewModelProviders;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.DividerItemDecoration;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+
+import xyz.cdeegan.wavechallenge.R;
+
+public class ProductListActivity extends AppCompatActivity
+{
+ private RecyclerView productListView;
+ private ProductAdapter productAdapter;
+ private ProductListViewModel productListViewModel;
+ private static final String BUSINESS_ID = "89746d57-c25f-4cec-9c63-34d7780b044b";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_product_list);
+
+ productListView = findViewById(R.id.product_list_view);
+
+ //set recyclerview adapter
+ productAdapter = new ProductAdapter(new ArrayList<>());
+ productListView.setAdapter(productAdapter);
+
+ //set layout manager
+ LinearLayoutManager layoutManager = new LinearLayoutManager(getApplicationContext(), LinearLayoutManager.VERTICAL, false);
+ productListView.setLayoutManager(layoutManager);
+
+ //set divider decoration
+ final DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(productListView
+ .getContext(), layoutManager.getOrientation());
+ productListView.addItemDecoration(dividerItemDecoration);
+
+ //attach viewmodel to recyclerview
+ productListViewModel = ViewModelProviders.of(this).get(ProductListViewModel.class);
+ productListViewModel.loadProducts(BUSINESS_ID).observe(this, products ->
+ {
+ if(products.getError() != null)
+ {
+ Toast.makeText(getApplicationContext(), R.string.product_list_error, Toast.LENGTH_LONG)
+ .show();
+ }
+ else
+ {
+ productAdapter.setItems(products.getProducts());
+ }
+ });
+ }
+}
diff --git a/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductListViewModel.java b/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductListViewModel.java
new file mode 100644
index 000000000..4713d85bb
--- /dev/null
+++ b/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductListViewModel.java
@@ -0,0 +1,28 @@
+package xyz.cdeegan.wavechallenge.ui;
+
+import android.arch.lifecycle.LiveData;
+import android.arch.lifecycle.MediatorLiveData;
+import android.arch.lifecycle.ViewModel;
+
+import xyz.cdeegan.wavechallenge.data.service.ProductAPIResponse;
+import xyz.cdeegan.wavechallenge.repository.ProductRepository;
+import xyz.cdeegan.wavechallenge.repository.ProductRepositoryInterface;
+
+public class ProductListViewModel extends ViewModel
+{
+ private MediatorLiveData productAPIResponse;
+ private ProductRepositoryInterface productRepository;
+
+ public ProductListViewModel()
+ {
+ productAPIResponse = new MediatorLiveData<>();
+ productRepository = new ProductRepository();
+ }
+
+ public LiveData loadProducts(String businessID)
+ {
+ productAPIResponse.addSource(productRepository.getProducts(businessID),
+ response -> productAPIResponse.setValue(response));
+ return productAPIResponse;
+ }
+}
diff --git a/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductListViewholder.java b/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductListViewholder.java
new file mode 100644
index 000000000..15c0e609b
--- /dev/null
+++ b/app/src/main/java/xyz/cdeegan/wavechallenge/ui/ProductListViewholder.java
@@ -0,0 +1,20 @@
+package xyz.cdeegan.wavechallenge.ui;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.widget.TextView;
+
+import xyz.cdeegan.wavechallenge.R;
+
+public class ProductListViewholder extends RecyclerView.ViewHolder
+{
+ TextView productNameTextView;
+ TextView productPriceTextView;
+
+ public ProductListViewholder(View itemView)
+ {
+ super(itemView);
+ productNameTextView = itemView.findViewById(R.id.textview_product_name);
+ productPriceTextView = itemView.findViewById(R.id.textview_product_price);
+ }
+}
diff --git a/app/src/main/res/layout/activity_product_list.xml b/app/src/main/res/layout/activity_product_list.xml
new file mode 100644
index 000000000..70f40e945
--- /dev/null
+++ b/app/src/main/res/layout/activity_product_list.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/list_item_product.xml b/app/src/main/res/layout/list_item_product.xml
new file mode 100644
index 000000000..d50e174db
--- /dev/null
+++ b/app/src/main/res/layout/list_item_product.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
\ No newline at end of file