Skip to content
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
58 changes: 12 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
## Setup
Download the apk from the root folder and install in your android device, by running the following command <code>adb install "waveapp-debug.apk"</code>
Alternatively, The apk can be built from source using Android Studio (Requires Java 8, gradle)

## Highlights
1. Seperation of Concern with modular design (Decoupled with MVVM)
2. Simple MVVM Pattern with Data Binding developed using TDD
3. Android Lifecycle components
4. Unit Test Infrastructure with unit tests for ViewModel and Adapter
5. Retrofit library for network calls
6. ViewModel implemented with Contract
7. RecyclerView for scalable UI performance

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!)

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.

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.

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.

## 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 change: 1 addition & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
47 changes: 47 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
plugins {
id 'com.android.application'
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
buildFeatures {
dataBinding true
}
defaultConfig {
applicationId "com.kannan.sample.waveapp"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

}

dependencies {
implementation 'android.arch.lifecycle:extensions:1.1.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.android.volley:volley:1.1.0'
implementation 'com.squareup.retrofit2:retrofit:2.0.0'
implementation 'com.squareup.retrofit2:converter-gson:2.2.0'
testImplementation 'junit:junit:4.+'
testImplementation "org.robolectric:robolectric:3.1"
testImplementation "org.mockito:mockito-core:1.10.19"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
21 changes: 21 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.kannan.sample.waveapp;

import android.content.Context;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.kannan.sample.waveapp", appContext.getPackageName());
}
}
24 changes: 24 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.kannan.sample.waveapp">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WaveApp">
<activity android:name=".view.activity.HomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
23 changes: 23 additions & 0 deletions app/src/main/java/com/kannan/sample/waveapp/api/ProductsAPI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.kannan.sample.waveapp.api;

import com.kannan.sample.waveapp.model.Product;

import java.util.List;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.Path;


public interface ProductsAPI {
String ACCESS_TOKEN = "6W9hcvwRvyyZgPu9Odq7ko8DSY8Nfm";

/**
* Gets the list of products for the provided businessID
* @param businessID The business ID for which products need to be fetched
*/
@Headers("Authorization: Bearer " + ACCESS_TOKEN)
@GET("businesses/{business_id}/products/")
Call<List<Product>> getProducts(@Path("business_id") String businessID);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.kannan.sample.waveapp.contract;

import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;

import com.kannan.sample.waveapp.model.Product;

import java.util.List;

public interface ViewModelContract {
void observe(LifecycleOwner lifecycleOwner, Observer<List<Product>> observer);
void getProducts(String BUSINESS_ID);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.kannan.sample.waveapp.facade;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.kannan.sample.waveapp.api.ProductsAPI;
import com.kannan.sample.waveapp.model.Product;
import java.util.List;
import retrofit2.Callback;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class ProductsApiFacade {
private static final String BASE_URL = "https://api.waveapps.com/";
private Retrofit retrofit;

public ProductsApiFacade() {
Gson gson = new GsonBuilder()
.setLenient()
.create();
retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
}

public void getProducts(String businessID, Callback<List<Product>> callback) {
retrofit.create(ProductsAPI.class)
.getProducts(businessID)
.enqueue(callback);


}
}
28 changes: 28 additions & 0 deletions app/src/main/java/com/kannan/sample/waveapp/model/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.kannan.sample.waveapp.model;

import androidx.databinding.BaseObservable;
import androidx.databinding.Bindable;

import java.text.NumberFormat;
import java.util.Locale;

public class Product extends BaseObservable {
private String name;
private int price;

public Product(String name, int price) {
this.name = name;
this.price = price;
}

@Bindable
public String getName() {
return name;
}

@Bindable
public String getPrice() {
return NumberFormat.getCurrencyInstance(Locale.getDefault()).format(price);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.kannan.sample.waveapp.util;

import android.content.Context;
import android.net.ConnectivityManager;

public class ConnectionUtil {
public static boolean isConnectivityAvailable (Context context) {
ConnectivityManager connectivityManager = ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
return connectivityManager != null && connectivityManager.getActiveNetworkInfo() != null && connectivityManager.getActiveNetworkInfo().isConnected();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.kannan.sample.waveapp.view.activity;

import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import android.os.Bundle;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.kannan.sample.waveapp.R;
import com.kannan.sample.waveapp.contract.ViewModelContract;
import com.kannan.sample.waveapp.model.Product;
import com.kannan.sample.waveapp.util.ConnectionUtil;
import com.kannan.sample.waveapp.view.adapter.ProductsListAdapter;
import com.kannan.sample.waveapp.viewmodel.ProductsViewModel;

import java.util.List;

public class HomeActivity extends AppCompatActivity {
private RecyclerView productsRecyclerView;
private ProgressBar progressBar;
private TextView errorTextView;
private ViewModelContract viewModel;
private static final String BUSINESS_ID = "89746d57-c25f-4cec-9c63-34d7780b044b";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setupView();
initialize();
}

private void setupView (){
setContentView(R.layout.activity_home);
errorTextView = findViewById(R.id.tv_error);
productsRecyclerView = findViewById(R.id.rv_products_list);
progressBar = findViewById(R.id.pb_spinner);
productsRecyclerView.setAdapter(new ProductsListAdapter(this));
productsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
}

private void initialize() {
viewModel = ViewModelProviders.of(this).get(ProductsViewModel.class);
viewModel.observe(this, products -> { updateView(products); });

if (ConnectionUtil.isConnectivityAvailable(this)) {
progressBar.setVisibility(View.VISIBLE);
viewModel.getProducts(BUSINESS_ID);
} else {
errorTextView.setText(R.string.no_connection);
errorTextView.setVisibility(View.VISIBLE);
}
}

private void updateView(List<Product> products) {
progressBar.setVisibility(View.GONE);
if (products == null) {
errorTextView.setText(R.string.api_failure);
errorTextView.setVisibility(View.VISIBLE);
} else {
errorTextView.setVisibility(View.GONE);
((ProductsListAdapter) productsRecyclerView.getAdapter()).setItems(products);
}
}

@Override
protected void onDestroy() {
productsRecyclerView = null;
progressBar = null;
viewModel = null;
super.onDestroy();
}
}
Loading