diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..a12cdf47
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,33 @@
+## Stores user specific settings ##
+.gradle
+.idea/
+.settings/
+.idea
+*.iml
+
+## could be located in different module directories ##
+*.iml
+
+## generated content ##
+bin/
+out/
+gen/
+build/
+
+## local machine properties ##
+local.properties
+
+## folder that contains all test reports ##
+test-reports/
+
+## Test server created by calabash ##
+test_servers/
+
+## Misc ##
+AndroidManifest.out.xml
+com_crashlytics_export_strings.xml
+crashlytics-build.properties
+screenshot_*
+.DS_Store
+Thumbs.db
+desktop.ini
\ No newline at end of file
diff --git a/README.md b/README.md
index e9e90833..ed736499 100755
--- a/README.md
+++ b/README.md
@@ -1,116 +1,40 @@
-## Introduction
+## Heartstone Android by Niels Masdorp
-Hsiao here at Splendo is a very enthusiastic casual Hearthstone player. He is also a user of the KLM houses apps ([iOS](https://itunes.apple.com/nl/app/klm-houses/id371664245?l=en&mt=8) / [Android](https://play.google.com/store/apps/details?id=com.klm.mobile.houses&hl=en))
+### Architecture
+This app is separated in 2 modules:
-He wants you to build a web app that has similar UI/UX. Similar way to go from the grid view to detail view, and also being able to scroll through the detail views like a carousel (hint: download the Houses app and have a look at how it works) but he wants the app to show Hearthstone card images.
+- app: Android module that is responsible for showing the actual app, it uses the MVP architecture for the presentation layer.
+This module also contains the data providers
+- domain: This is where all the business logic is located, it is a pure Java module with no Android related dependencies
-We have supplied you with a json file (`cards.json`) containing all the Heartstone cards currently available.
+In order to create a clear separation of concerns and to separate low level components (UI) from the high level components
+(Entities and Use Cases) I have used the [Clean Architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html)
+proposed by Uncle Bob.
-Hsiao is especially interested in the app showing `Legendary` cards with the `Deathrattle Mechanic`, below are examples of such a cards :
+### Third party libraries
-```json
-{
- "cardId": "FP1_014",
- "name": "Stalagg",
- "cardSet": "Naxxramas",
- "type": "Minion",
- "rarity": "Legendary",
- "cost": 5,
- "attack": 7,
- "health": 4,
- "text": "Deathrattle: If Feugen also died this game, summon Thaddius.",
- "flavor": "Stalagg want to write own flavor text. \"STALAGG AWESOME!\"",
- "artist": "Dany Orizio",
- "collectible": true,
- "elite": true,
- "playerClass": "Neutral",
- "howToGet": "Unlocked in The Construct Quarter, in the Naxxramas adventure.",
- "howToGetGold": "Crafting unlocked in The Construct Quarter, in the Naxxramas adventure.",
- "img": "http://wow.zamimg.com/images/hearthstone/cards/enus/original/FP1_014.png",
- "imgGold": "http://wow.zamimg.com/images/hearthstone/cards/enus/animated/FP1_014_premium.gif",
- "locale": "enUS",
- "mechanics": [
- {
- "name": "Deathrattle"
- }
- ]
-}
-```
+In order to have proper dependency injection to tie all these components together I have chosen to use the
+well known and community supported Dagger 2 library. For image loading I have chosen to use Glide, which is just a preference
+over something like Picasso.
-and
+For logging I have used Jake Wharton's Timber. And lastly to create asynchronous streams I have used RxJava 2. For this project
+I could have gone without since there is not a lot of data flowing through the application, but I chose to include it
+to show that I am comfortable using it.
-```json
-{
- "cardId": "CFM_902",
- "name": "Aya Blackpaw",
- "cardSet": "Mean Streets of Gadgetzan",
- "type": "Minion",
- "rarity": "Legendary",
- "cost": 6,
- "attack": 5,
- "health": 3,
- "text": " Battlecry and Deathrattle: Summon a Jade Golem.",
- "flavor": "Though young, Aya took over as the leader of Jade Lotus through her charisma and strategic acumen when her predecessor was accidentally crushed by a jade golem.",
- "artist": "Glenn Rane",
- "collectible": true,
- "elite": true,
- "playerClass": "Neutral",
- "multiClassGroup": "Jade Lotus",
- "classes": [
- "Druid",
- "Rogue",
- "Shaman"
- ],
- "img": "http://media.services.zam.com/v1/media/byName/hs/cards/enus/CFM_902.png",
- "imgGold": "http://media.services.zam.com/v1/media/byName/hs/cards/enus/animated/CFM_902_premium.gif",
- "locale": "enUS",
- "mechanics": [
- {
- "name": "Jade Golem"
- },
- {
- "name": "Battlecry"
- },
- {
- "name": "Deathrattle"
- }
- ]
-}
-```
+### Data
-## Assignment
+Cards: I have not chosen to host any data on a backend. I stored the JSON file in /assets and loaded it into memory.
-You are free to choose the patterns and architectures to create this web app, the requirements are :
+Favorites: Are stored in Shared Preferences
-### Backend
+Both data storage solutions are easily interchangeable with other solutions due to the architecture (e.g. implement storage interface
+and bind new implementation in Dagger module)
-* Create an API using a Java (plain java or Groovy/Cotlin) backend allowing you to get card information for at least legendary deathrattle cards
-* The API should also support filtering based on relevant request parameters. Ideally, the API should enable the following, listed from easy to hard:
- * filter by least the following fields: `type`, `rarity`, `classes`, and `mechanics`
- * return sorted results (for example, alphabetically sorted), supporting both ascending and descending
- * (optional) return the results by pages (based on a page size request parameter), iterating over the pages are maintained by a cursor which is included in the response, this cursor is used in the subsequent request
+### Filtering
-### Web Application
+Data repository accepts a request model with criteria for cards and is being used to query legendary cards with a certain
+mechanic, this can of course be extended to support more criteria as well.
-* Create the web app using JavaScript. You can use either plain JavaScript or a Framework of your choice
-* Show the card images in a grid like the houses app
-* when user click on a grid item , navigate to the card detail view where you can display more information regarding the card ( what you would like to show and how is up to you ), when in detail view the navigation to the next and previous card should be the same as the Houses App
-* The user should be able to set a card as favourite and this info should be persisted when the app closes, how to show cards that are tagged as favourites and how to persist that information is up to you
+### Sorting
-
-## What we would like to see
-
-* Proper handling of asynchonous calls
-* Clean code
-* Relevant design patterns
-* Javascript best practices
-* UI should remain responsive during content loading
-* Should you use 3rd party libraries and frameworks please motivate your choice
-* Unit tests
-* Writing the backend using Google AppEngine is a plus, but feel free to use Amazon AWS, Tomcat or anything you prefer for handling your API calls
-
-## Finally
-
-To submit your result, fork this repository. When you are satisfied with your result, create a Pull Request. Make sure your backend is up and running somewhere for the duration of the review and tell us in the comments where to find it.
-
-Good Luck!
+The request model also supports a sorting strategy (asc and desc), currently, this sorts the list by the name of the card
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 00000000..9754b17c
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,108 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
+
+ext {
+ VERSION_NAME = "1.0.0"
+ VERSION_CODE = 1
+}
+
+androidExtensions {
+ experimental = true
+}
+
+android {
+ compileSdkVersion 27
+ defaultConfig {
+ applicationId "com.nielsmasdorp.heartstone"
+ minSdkVersion 21
+ targetSdkVersion 27
+ versionCode VERSION_CODE
+ versionName VERSION_NAME
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ debug {
+ minifyEnabled false
+
+ versionNameSuffix "-debug"
+ applicationIdSuffix ".debug"
+ }
+ acceptance {
+ signingConfig signingConfigs.debug
+ initWith buildTypes.debug
+
+ versionNameSuffix "-acceptance"
+ applicationIdSuffix ".acceptance"
+
+ minifyEnabled true
+ useProguard true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+
+ matchingFallbacks = ['release']
+ }
+ release {
+ minifyEnabled true
+ useProguard true
+
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+
+ packagingOptions {
+ exclude 'META-INF/DEPENDENCIES'
+ exclude 'META-INF/NOTICE'
+ exclude 'META-INF/LICENSE'
+ exclude 'META-INF/LICENSE.txt'
+ exclude 'META-INF/NOTICE.txt'
+ exclude 'META-INF/rxjava.properties'
+ exclude 'META-INF/rxkotlin.properties'
+ exclude 'META-INF/rxkotlin_main.kotlin_module'
+ }
+}
+
+dependencies {
+ implementation project(':domain')
+
+ implementation "com.android.support:appcompat-v7:$android_support_version"
+ implementation "com.android.support:recyclerview-v7:$android_support_version"
+ implementation "com.android.support:design:$android_support_version"
+ implementation "com.android.support:cardview-v7:$android_support_version"
+ api "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+
+ // Gson
+ implementation 'com.google.code.gson:gson:2.8.4'
+
+ //Dagger
+ api "com.google.dagger:dagger:$dagger_version"
+ kapt "com.google.dagger:dagger-compiler:$dagger_version"
+ kapt "com.google.dagger:dagger-android-processor:$dagger_version"
+ api("com.google.dagger:dagger-android-support:$dagger_version", {
+ exclude group: 'com.google.code.findbugs'
+ })
+
+ // Jake
+ api 'com.jakewharton.timber:timber:4.5.1'
+
+ // Glide
+ implementation 'com.github.bumptech.glide:glide:4.7.1'
+ kapt 'com.github.bumptech.glide:compiler:4.7.1'
+
+ // ReactiveX
+ api 'io.reactivex.rxjava2:rxandroid:2.0.1'
+ api "io.reactivex.rxjava2:rxjava:$reactive_x_version"
+ api "io.reactivex.rxjava2:rxkotlin:$reactive_x_version"
+ api 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
+ testApi 'io.reactivex.rxjava2:rxandroid:2.0.1'
+ testApi "io.reactivex.rxjava2:rxjava:$reactive_x_version"
+ testApi "io.reactivex.rxjava2:rxkotlin:$reactive_x_version"
+
+ testApi 'junit:junit:4.12'
+ testApi 'org.mockito:mockito-core:2.11.0'
+ testApi 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
+
+ androidTestApi('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -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
diff --git a/app/src/androidTest/java/com/nielsmasdorp/heartstone/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/nielsmasdorp/heartstone/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..edfcdd7b
--- /dev/null
+++ b/app/src/androidTest/java/com/nielsmasdorp/heartstone/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.nielsmasdorp.heartstone
+
+import android.support.test.InstrumentationRegistry
+import android.support.test.runner.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getTargetContext()
+ assertEquals("com.nielsmasdorp.flightapp", appContext.packageName)
+ }
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..3831d4eb
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cards.json b/app/src/main/assets/cards.json
similarity index 100%
rename from cards.json
rename to app/src/main/assets/cards.json
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/HeartstoneApp.kt b/app/src/main/java/com/nielsmasdorp/heartstone/HeartstoneApp.kt
new file mode 100644
index 00000000..17defbb8
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/HeartstoneApp.kt
@@ -0,0 +1,25 @@
+package com.nielsmasdorp.heartstone
+
+import dagger.android.AndroidInjector
+import dagger.android.DaggerApplication
+import timber.log.Timber
+
+class HeartstoneApp : DaggerApplication() {
+
+ override fun onCreate() {
+ super.onCreate()
+ setupTimber()
+ }
+
+ override fun applicationInjector(): AndroidInjector {
+ return DaggerHeartstoneAppComponent.builder()
+ .heartstoneAppModule(HeartstoneAppModule())
+ .create(this)
+ }
+
+ private fun setupTimber() {
+ if (BuildConfig.DEBUG) {
+ Timber.plant(Timber.DebugTree())
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/HeartstoneAppComponent.kt b/app/src/main/java/com/nielsmasdorp/heartstone/HeartstoneAppComponent.kt
new file mode 100644
index 00000000..b12a8867
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/HeartstoneAppComponent.kt
@@ -0,0 +1,21 @@
+package com.nielsmasdorp.heartstone
+
+import com.nielsmasdorp.heartstone.presentation.card.CardsActivityModule
+import dagger.Component
+import dagger.android.AndroidInjector
+import dagger.android.support.AndroidSupportInjectionModule
+import javax.inject.Singleton
+
+@Singleton
+@Component(modules = [
+ AndroidSupportInjectionModule::class,
+ HeartstoneAppModule::class,
+ CardsActivityModule::class])
+interface HeartstoneAppComponent : AndroidInjector {
+
+ @Component.Builder
+ abstract class Builder : AndroidInjector.Builder() {
+
+ abstract fun heartstoneAppModule(heartstoneAppModule: HeartstoneAppModule): Builder
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/HeartstoneAppModule.kt b/app/src/main/java/com/nielsmasdorp/heartstone/HeartstoneAppModule.kt
new file mode 100644
index 00000000..69299882
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/HeartstoneAppModule.kt
@@ -0,0 +1,18 @@
+package com.nielsmasdorp.heartstone
+
+import android.app.Application
+import android.content.Context
+import com.nielsmasdorp.heartstone.generic.dagger.AppContext
+import dagger.Module
+import dagger.Provides
+
+@Module
+class HeartstoneAppModule {
+
+ @Provides
+ @AppContext
+ fun provideApplicationContext(application: HeartstoneApp): Context = application
+
+ @Provides
+ fun provideApplication(application: HeartstoneApp): Application = application
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/data/card/JSONCardRepository.kt b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/JSONCardRepository.kt
new file mode 100644
index 00000000..bee2b983
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/JSONCardRepository.kt
@@ -0,0 +1,83 @@
+package com.nielsmasdorp.heartstone.data.card
+
+import android.app.Application
+import com.google.gson.Gson
+import com.nielsmasdorp.domain.card.data.CardRepository
+import com.nielsmasdorp.domain.card.entity.Card
+import com.nielsmasdorp.domain.card.entity.CardRequest
+import com.nielsmasdorp.heartstone.data.card.model.JSONCard
+import com.nielsmasdorp.heartstone.data.card.model.JSONCardSets
+import io.reactivex.Single
+import javax.inject.Inject
+
+/**
+ * [CardRepository] implementation that fetches cards from a JSON file in /assets
+ */
+class JSONCardRepository @Inject constructor(private val application: Application) : CardRepository {
+
+ override fun getCards(cardRequest: CardRequest): Single> {
+ val json = application.assets.open(CARDS_JSON_FILE_NAME).bufferedReader().use {
+ it.readText()
+ }
+ val cardSets = Gson().fromJson(json, JSONCardSets::class.java)
+ return Single.just(getAllCards(cardSets)
+ .map { mapToCard(it) }
+ .filter { card ->
+ cardRequest.requestedType?.let { card.type == it } ?: true &&
+ cardRequest.requestedRarity?.let { card.rarity == it } ?: true &&
+ cardRequest.requestedClass?.let { card.classes.contains(it) } ?: true &&
+ cardRequest.requestedMechanic?.let { card.mechanics.contains(it) } ?: true
+ }
+ .let { cards ->
+ when (cardRequest.sortingStrategy) {
+ CardRequest.Sort.ASCENDING -> cards.sortedBy { it.name }
+ CardRequest.Sort.DESCENDING -> cards.sortedByDescending { it.name }
+ CardRequest.Sort.NONE -> cards
+ }
+ }
+ )
+ }
+
+ private fun getAllCards(cardSets: JSONCardSets): List {
+ return listOf(
+ cardSets.basicCards,
+ cardSets.blackRockMountainCards,
+ cardSets.classicCards,
+ cardSets.creditsCards,
+ cardSets.gadgetzanCards,
+ cardSets.goblinsVsGnomesCards,
+ cardSets.grandTournamentCards,
+ cardSets.hallOfFameCards,
+ cardSets.heroSkinsCards,
+ cardSets.journeyUnGoroCards,
+ cardSets.karazhanCards,
+ cardSets.leagueOfExplorersCards
+ ).flatten()
+ }
+
+ private fun mapToCard(jsonCard: JSONCard): Card {
+ return Card(
+ id = jsonCard.cardId,
+ name = jsonCard.name,
+ imgUrl = jsonCard.img,
+ cardSet = jsonCard.cardSet,
+ type = jsonCard.type,
+ rarity = jsonCard.rarity,
+ cost = jsonCard.cost,
+ attack = jsonCard.attack,
+ health = jsonCard.health,
+ text = jsonCard.htmlText,
+ flavor = jsonCard.flavour,
+ classes = jsonCard.classes,
+ mechanics = jsonCard.mechanics?.map { it.name }
+ )
+ }
+
+ private fun List?.contains(value: String): Boolean {
+ return this != null && this.contains(value)
+ }
+
+ companion object {
+ private const val CARDS_JSON_FILE_NAME = "cards.json"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/data/card/SharedPrefsFavoritesCardRepository.kt b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/SharedPrefsFavoritesCardRepository.kt
new file mode 100644
index 00000000..044f8396
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/SharedPrefsFavoritesCardRepository.kt
@@ -0,0 +1,41 @@
+package com.nielsmasdorp.heartstone.data.card
+
+import android.content.Context
+import android.content.SharedPreferences
+import com.nielsmasdorp.domain.card.data.FavoriteCardsRepository
+import com.nielsmasdorp.heartstone.generic.dagger.AppContext
+import javax.inject.Inject
+
+/**
+ * [FavoriteCardsRepository] implementation that stores the favorite ids in Shared Preferences
+ */
+class SharedPrefsFavoritesCardRepository @Inject constructor(@AppContext context: Context) : FavoriteCardsRepository {
+
+ private val sharedPrefs: SharedPreferences by lazy {
+ context.getSharedPreferences(CARD_FAVORITES_STORAGE, Context.MODE_PRIVATE)
+ }
+
+ override fun addToFavorites(cardId: String) {
+ sharedPrefs.getStringSet(CARD_FAVORITES_KEY, mutableSetOf()).apply {
+ add(cardId)
+ sharedPrefs.edit().putStringSet(CARD_FAVORITES_KEY, this).apply()
+ }
+ }
+
+ override fun removeFromFavorites(cardId: String) {
+ sharedPrefs.getStringSet(CARD_FAVORITES_KEY, mutableSetOf()).apply {
+ remove(cardId)
+ sharedPrefs.edit().putStringSet(CARD_FAVORITES_KEY, this).apply()
+ }
+ }
+
+ override fun isAddedToFavorites(cardId: String): Boolean {
+ return sharedPrefs.getStringSet(CARD_FAVORITES_KEY, emptySet()).contains(cardId)
+ }
+
+ private companion object {
+
+ private const val CARD_FAVORITES_STORAGE = "CARD_FAVORITES_STORAGE"
+ private const val CARD_FAVORITES_KEY = "CARD_FAVORITES"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/data/card/model/JSONCard.kt b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/model/JSONCard.kt
new file mode 100644
index 00000000..39f77007
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/model/JSONCard.kt
@@ -0,0 +1,27 @@
+package com.nielsmasdorp.heartstone.data.card.model
+
+/**
+ * model for a card in the JSON in /assets
+ */
+data class JSONCard(
+ val cardId: String,
+ val name: String?,
+ val cardSet: String?,
+ val type: String?,
+ val rarity: String?,
+ val cost: Int?,
+ val attack: Int?,
+ val health: Int?,
+ val htmlText: String?,
+ val flavour: String?,
+ val artist: String?,
+ val collectible: String?,
+ val elite: Boolean?,
+ val playerClass: String?,
+ val multiClassGroup: String?,
+ val classes: List?,
+ val img: String?,
+ val imgGold: String?,
+ val locale: String?,
+ val mechanics: List?
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/data/card/model/JSONCardMechanic.kt b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/model/JSONCardMechanic.kt
new file mode 100644
index 00000000..90273610
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/model/JSONCardMechanic.kt
@@ -0,0 +1,8 @@
+package com.nielsmasdorp.heartstone.data.card.model
+
+/**
+ * Model for the mechanic value of a [JSONCard]
+ */
+data class JSONCardMechanic(
+ val name: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/data/card/model/JSONCardSets.kt b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/model/JSONCardSets.kt
new file mode 100644
index 00000000..ce67c447
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/data/card/model/JSONCardSets.kt
@@ -0,0 +1,57 @@
+package com.nielsmasdorp.heartstone.data.card.model
+
+import com.google.gson.annotations.SerializedName
+
+/**
+ * Model for all sets of [JSONCard]s in the JSON file in /assets
+ */
+data class JSONCardSets(
+
+ @SerializedName("Basic")
+ val basicCards: List,
+
+ @SerializedName("Classic")
+ val classicCards: List,
+
+ @SerializedName("Hall of Fame")
+ val hallOfFameCards: List,
+
+ @SerializedName("Naxxramas")
+ val naxxramasCards: List,
+
+ @SerializedName("Goblins vs Gnomes")
+ val goblinsVsGnomesCards: List,
+
+ @SerializedName("Blackrock Mountain")
+ val blackRockMountainCards: List,
+
+ @SerializedName("The Grand Tournament")
+ val grandTournamentCards: List,
+
+ @SerializedName("The League of Explorers")
+ val leagueOfExplorersCards: List,
+
+ @SerializedName("Whispers of the Old Gods")
+ val whispersOfTheOldGodsCards: List,
+
+ @SerializedName("One Night in Karazhan")
+ val karazhanCards: List,
+
+ @SerializedName("Mean Streets of Gadgetzan")
+ val gadgetzanCards: List,
+
+ @SerializedName("Journey to Un'Goro")
+ val journeyUnGoroCards: List,
+
+ @SerializedName("Tavern Brawl")
+ val tavernBrawlCards: List,
+
+ @SerializedName("Hero Skins")
+ val heroSkinsCards: List,
+
+ @SerializedName("Missions")
+ val missionsCards: List,
+
+ @SerializedName("Credits")
+ val creditsCards: List
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/generic/dagger/AppContext.kt b/app/src/main/java/com/nielsmasdorp/heartstone/generic/dagger/AppContext.kt
new file mode 100644
index 00000000..bf816395
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/generic/dagger/AppContext.kt
@@ -0,0 +1,11 @@
+package com.nielsmasdorp.heartstone.generic.dagger
+
+import android.content.Context
+import javax.inject.Qualifier
+
+/**
+ * Annotation used to mark Application [Context] instances
+ */
+@Qualifier
+@Retention(AnnotationRetention.RUNTIME)
+annotation class AppContext
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/CardViewModel.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/CardViewModel.kt
new file mode 100644
index 00000000..053fd031
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/CardViewModel.kt
@@ -0,0 +1,19 @@
+package com.nielsmasdorp.heartstone.presentation.card
+
+import android.annotation.SuppressLint
+import android.os.Parcelable
+import kotlinx.android.parcel.Parcelize
+
+/**
+ * View model for a single card in Heartstone
+ */
+@SuppressLint("ParcelCreator")
+@Parcelize
+data class CardViewModel(
+ val id: String,
+ val name: String,
+ val imgUrl: String,
+ val cardSet: String,
+ val type: String,
+ val rarity: String,
+ val text: String) : Parcelable
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/CardsActivity.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/CardsActivity.kt
new file mode 100644
index 00000000..5d1000a5
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/CardsActivity.kt
@@ -0,0 +1,52 @@
+package com.nielsmasdorp.heartstone.presentation.card
+
+import android.os.Bundle
+import android.support.v4.app.Fragment
+import android.support.v7.app.AppCompatActivity
+import com.nielsmasdorp.heartstone.R
+import com.nielsmasdorp.heartstone.presentation.card.grid.CardGridFragment
+import dagger.android.AndroidInjection
+import dagger.android.AndroidInjector
+import dagger.android.DispatchingAndroidInjector
+import dagger.android.support.HasSupportFragmentInjector
+import javax.inject.Inject
+
+/**
+ * Main activity for the cards feature in the Heartstone app
+ */
+class CardsActivity : AppCompatActivity(), HasSupportFragmentInjector {
+
+ @Inject
+ lateinit var fragmentDispatchingAndroidInjector: DispatchingAndroidInjector
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ AndroidInjection.inject(this)
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_cards)
+
+ if (savedInstanceState != null) {
+ CURRENT_POSITION = savedInstanceState.getInt(KEY_CURRENT_POSITION, 0)
+ return
+ }
+ supportFragmentManager
+ .beginTransaction()
+ .add(R.id.fragmentContainer, CardGridFragment(), CardGridFragment::class.java.simpleName)
+ .commit()
+ }
+
+ @Override
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ outState.putInt(KEY_CURRENT_POSITION, CURRENT_POSITION)
+ }
+
+ override fun supportFragmentInjector(): AndroidInjector = fragmentDispatchingAndroidInjector
+
+ companion object {
+
+ private const val KEY_CURRENT_POSITION = "KEY_CURRENT_POSITION"
+
+ /** Current clicked card position in the chosen set of cards */
+ var CURRENT_POSITION = 0
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/CardsActivityModule.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/CardsActivityModule.kt
new file mode 100644
index 00000000..1867c5d0
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/CardsActivityModule.kt
@@ -0,0 +1,17 @@
+package com.nielsmasdorp.heartstone.presentation.card
+
+import com.nielsmasdorp.heartstone.presentation.card.carousel.dagger.CardCarouselFragmentModule
+import com.nielsmasdorp.heartstone.presentation.card.detail.dagger.CardDetailFragmentModule
+import com.nielsmasdorp.heartstone.presentation.card.grid.dagger.CardGridFragmentModule
+import dagger.Module
+import dagger.android.ContributesAndroidInjector
+
+@Module
+abstract class CardsActivityModule {
+
+ @ContributesAndroidInjector(modules = [
+ CardGridFragmentModule::class,
+ CardCarouselFragmentModule::class,
+ CardDetailFragmentModule::class])
+ abstract fun cardsActivity(): CardsActivity
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarousel.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarousel.kt
new file mode 100644
index 00000000..3b2e4a15
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarousel.kt
@@ -0,0 +1,24 @@
+package com.nielsmasdorp.heartstone.presentation.card.carousel
+
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+
+/**
+ * Contract for the card carousel feature
+ */
+interface CardCarousel {
+
+ interface View {
+
+ var cards: List
+ }
+
+ interface Presenter {
+
+ fun startPresenting()
+ }
+
+ interface CardsProvider {
+
+ val cards: List?
+ }
+}
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselFragment.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselFragment.kt
new file mode 100644
index 00000000..fab8b91d
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselFragment.kt
@@ -0,0 +1,108 @@
+package com.nielsmasdorp.heartstone.presentation.card.carousel
+
+import android.content.Context
+import android.os.Bundle
+import android.support.v4.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.nielsmasdorp.heartstone.R
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import dagger.android.support.AndroidSupportInjection
+import javax.inject.Inject
+import android.support.transition.TransitionInflater
+import android.support.v4.app.SharedElementCallback
+import android.support.v4.view.ViewPager
+import com.nielsmasdorp.heartstone.presentation.card.CardsActivity
+import kotlinx.android.synthetic.main.fragment_card_detail.view.*
+import kotlinx.android.synthetic.main.fragment_cards_carousel.*
+
+class CardCarouselFragment : Fragment(), CardCarousel.View {
+
+ @Inject
+ lateinit var presenter: CardCarousel.Presenter
+
+ override var cards: List = emptyList()
+ set(value) {
+ field = value
+ carouselAdapter.cards = value
+ cardsCarouselViewPager.currentItem = CardsActivity.CURRENT_POSITION
+ }
+
+ private val carouselAdapter: CardCarouselPagerAdapter by lazy {
+ CardCarouselPagerAdapter(this)
+ }
+
+ override fun onAttach(context: Context) {
+ AndroidSupportInjection.inject(this)
+ super.onAttach(context)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_cards_carousel, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ initPager()
+
+ prepareSharedElementTransition()
+
+ if (savedInstanceState == null) {
+ postponeEnterTransition()
+ }
+ presenter.startPresenting()
+ }
+
+ private fun prepareSharedElementTransition() {
+ sharedElementEnterTransition = TransitionInflater.from(context)
+ .inflateTransition(R.transition.image_shared_element_transition).apply {
+ duration = resources.getInteger(R.integer.default_animation_length_ms).toLong()
+ }
+ setEnterSharedElementCallback(
+ object : SharedElementCallback() {
+ override fun onMapSharedElements(names: List, sharedElements: MutableMap) {
+ val currentFragment = cardsCarouselViewPager.adapter?.instantiateItem(cardsCarouselViewPager, CardsActivity.CURRENT_POSITION) as Fragment
+ val view = currentFragment.view ?: return
+
+ sharedElements.put(names[0], view.cardDetailImage)
+ }
+ })
+ }
+
+ private fun initPager() {
+ cardsCarouselViewPager.apply {
+ addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
+ override fun onPageScrollStateChanged(state: Int) {
+ // no op
+ }
+
+ override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
+ // no op
+ }
+
+ override fun onPageSelected(position: Int) {
+ CardsActivity.CURRENT_POSITION = position
+ }
+ })
+ adapter = carouselAdapter
+ }
+ }
+
+ companion object {
+
+ const val CARDS_KEY = "CARDS_KEY"
+
+ /**
+ * Create a new instance of this fragment
+ * @param cards [ArrayList] of [CardViewModel]s that need to be shown in the carousel
+ */
+ fun newInstance(cards: ArrayList): CardCarouselFragment {
+ return CardCarouselFragment().apply {
+ arguments = Bundle().apply {
+ putParcelableArrayList(CARDS_KEY, cards)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselPagerAdapter.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselPagerAdapter.kt
new file mode 100644
index 00000000..d5bf12ae
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselPagerAdapter.kt
@@ -0,0 +1,24 @@
+package com.nielsmasdorp.heartstone.presentation.card.carousel
+
+import android.support.v4.app.Fragment
+import android.support.v4.app.FragmentStatePagerAdapter
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import com.nielsmasdorp.heartstone.presentation.card.detail.CardDetailFragment
+
+/**
+ * Adapter class for the card carousel
+ */
+class CardCarouselPagerAdapter(fragment: Fragment) : FragmentStatePagerAdapter(fragment.childFragmentManager) {
+
+ var cards: List = emptyList()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ override fun getCount(): Int = cards.count()
+
+ override fun getItem(position: Int): Fragment {
+ return CardDetailFragment.newInstance(cards[position])
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselPresenter.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselPresenter.kt
new file mode 100644
index 00000000..e3417857
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselPresenter.kt
@@ -0,0 +1,14 @@
+package com.nielsmasdorp.heartstone.presentation.card.carousel
+
+import javax.inject.Inject
+
+/**
+ * Presenter that presents data for the card carousel view
+ */
+class CardCarouselPresenter @Inject constructor(private val view: CardCarousel.View,
+ private val cardsProvider: CardCarousel.CardsProvider) : CardCarousel.Presenter {
+
+ override fun startPresenting() {
+ view.cards = cardsProvider.cards ?: emptyList()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardsProvider.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardsProvider.kt
new file mode 100644
index 00000000..f20038c7
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardsProvider.kt
@@ -0,0 +1,14 @@
+package com.nielsmasdorp.heartstone.presentation.card.carousel
+
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import com.nielsmasdorp.heartstone.presentation.card.carousel.CardCarouselFragment.Companion.CARDS_KEY
+import javax.inject.Inject
+
+/**
+ * [CardCarousel.CardsProvider] implementation that provides the cards through fragment arguments
+ */
+class CardsProvider @Inject constructor(private val cardCarouselFragment: CardCarouselFragment) : CardCarousel.CardsProvider {
+
+ override val cards: List?
+ get() = cardCarouselFragment.arguments?.getParcelableArrayList(CARDS_KEY)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/dagger/CardCarouselFragmentModule.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/dagger/CardCarouselFragmentModule.kt
new file mode 100644
index 00000000..9e4d7267
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/dagger/CardCarouselFragmentModule.kt
@@ -0,0 +1,30 @@
+package com.nielsmasdorp.heartstone.presentation.card.carousel.dagger
+
+import android.support.v4.app.Fragment
+import com.nielsmasdorp.heartstone.presentation.card.carousel.CardCarousel
+import com.nielsmasdorp.heartstone.presentation.card.carousel.CardCarouselFragment
+import com.nielsmasdorp.heartstone.presentation.card.carousel.CardCarouselPresenter
+import com.nielsmasdorp.heartstone.presentation.card.carousel.CardsProvider
+import dagger.Binds
+import dagger.Module
+import dagger.android.AndroidInjector
+import dagger.android.support.FragmentKey
+import dagger.multibindings.IntoMap
+
+@Module(subcomponents = [CardCarouselSubcomponent::class])
+abstract class CardCarouselFragmentModule {
+
+ @Binds
+ @IntoMap
+ @FragmentKey(CardCarouselFragment::class)
+ internal abstract fun bindCardCarouselFragmentInjectorFactory(builder: CardCarouselSubcomponent.Builder): AndroidInjector.Factory
+
+ @Binds
+ abstract fun bindCardCarouselView(cardCarouselFragment: CardCarouselFragment): CardCarousel.View
+
+ @Binds
+ abstract fun bindCardCarouselPresenter(cardCarouselPresenter: CardCarouselPresenter): CardCarousel.Presenter
+
+ @Binds
+ abstract fun bindCardsProvider(cardsProvider: CardsProvider): CardCarousel.CardsProvider
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/dagger/CardCarouselSubcomponent.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/dagger/CardCarouselSubcomponent.kt
new file mode 100644
index 00000000..c6992718
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/carousel/dagger/CardCarouselSubcomponent.kt
@@ -0,0 +1,12 @@
+package com.nielsmasdorp.heartstone.presentation.card.carousel.dagger
+
+import com.nielsmasdorp.heartstone.presentation.card.carousel.CardCarouselFragment
+import dagger.Subcomponent
+import dagger.android.AndroidInjector
+
+@Subcomponent
+interface CardCarouselSubcomponent : AndroidInjector {
+
+ @Subcomponent.Builder
+ abstract class Builder : AndroidInjector.Builder()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardDetail.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardDetail.kt
new file mode 100644
index 00000000..fc2aedc5
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardDetail.kt
@@ -0,0 +1,27 @@
+package com.nielsmasdorp.heartstone.presentation.card.detail
+
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+
+/**
+ * Contract for the card details feature
+ */
+interface CardDetail {
+
+ interface View {
+
+ var card: CardViewModel?
+ fun showCardAsFavorite(notifyUser: Boolean = true)
+ fun showCardNotFavorite()
+ }
+
+ interface Presenter {
+
+ fun startPresenting()
+ fun onAddToFavoritesClicked()
+ }
+
+ interface CardProvider {
+
+ val card: CardViewModel?
+ }
+}
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardDetailFragment.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardDetailFragment.kt
new file mode 100644
index 00000000..899d568f
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardDetailFragment.kt
@@ -0,0 +1,112 @@
+package com.nielsmasdorp.heartstone.presentation.card.detail
+
+import android.content.Context
+import android.os.Bundle
+import android.support.v4.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.nielsmasdorp.heartstone.R
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import dagger.android.support.AndroidSupportInjection
+import kotlinx.android.synthetic.main.fragment_card_detail.*
+import javax.inject.Inject
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.text.Html
+import android.text.Spanned
+import android.widget.ArrayAdapter
+import android.widget.Toast
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.request.RequestOptions
+import com.bumptech.glide.request.target.Target
+
+class CardDetailFragment : Fragment(), CardDetail.View {
+
+ @Inject
+ lateinit var presenter: CardDetail.Presenter
+
+ override var card: CardViewModel? = null
+ set(value) {
+ field = value
+ value?.let {
+ showCard(it)
+ }
+ }
+
+ override fun onAttach(context: Context) {
+ AndroidSupportInjection.inject(this)
+ super.onAttach(context)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.fragment_card_detail, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ cardDetailFavoritesIcon.setOnClickListener { presenter.onAddToFavoritesClicked() }
+ presenter.startPresenting()
+ }
+
+ override fun showCardAsFavorite(notifyUser: Boolean) {
+ cardDetailFavoritesIcon.setImageResource(R.drawable.ic_favorites_filled_24dp)
+ if (notifyUser) Toast.makeText(context, getString(R.string.card_details_added_favorites_message), Toast.LENGTH_SHORT).show()
+ }
+
+ override fun showCardNotFavorite() {
+ cardDetailFavoritesIcon.setImageResource(R.drawable.ic_favorites_empty_24dp)
+ Toast.makeText(context, getString(R.string.card_details_removed_favorites_message), Toast.LENGTH_SHORT).show()
+ }
+
+ private fun showCard(card: CardViewModel) {
+ cardDetailImage.transitionName = card.name
+ Glide.with(this)
+ .load(card.imgUrl)
+ .listener(object : RequestListener {
+ override fun onLoadFailed(exception: GlideException?, model: Any, target: Target, isFirstResource: Boolean): Boolean {
+ parentFragment?.startPostponedEnterTransition()
+ return false
+ }
+
+ override fun onResourceReady(resource: Drawable, model: Any, target: Target, dataSource: DataSource, isFirstResource: Boolean): Boolean {
+ parentFragment?.startPostponedEnterTransition()
+ return false
+ }
+ })
+ .into(cardDetailImage)
+ cardDetailName.text = card.name
+ cardDetailSet.text = card.cardSet
+ cardDetailType.text = card.type
+ cardDetailRarity.text = card.rarity
+ cardDetailDescription.text = getTextAsHtml(card.text)
+ }
+
+ private fun getTextAsHtml(html: String): Spanned {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY)
+ } else {
+ Html.fromHtml(html)
+ }
+ }
+
+ companion object {
+
+ const val CARD_KEY = "CARD_KEY"
+
+ /**
+ * Create a new instance of this fragment
+ * @param card [CardViewModel] that needs to be shown in this fragment
+ */
+ fun newInstance(card: CardViewModel): CardDetailFragment {
+ return CardDetailFragment().apply {
+ arguments = Bundle().apply {
+ putParcelable(CARD_KEY, card)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardDetailPresenter.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardDetailPresenter.kt
new file mode 100644
index 00000000..0f8a18cb
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardDetailPresenter.kt
@@ -0,0 +1,35 @@
+package com.nielsmasdorp.heartstone.presentation.card.detail
+
+import com.nielsmasdorp.domain.card.AddCardToFavorites
+import com.nielsmasdorp.domain.card.IsCardAddedToFavorites
+import com.nielsmasdorp.domain.card.RemoveCardFromFavorites
+import javax.inject.Inject
+
+/**
+ * Presenter that presents data to the view in the card details
+ */
+class CardDetailPresenter @Inject constructor(private val view: CardDetail.View,
+ private val cardProvider: CardDetail.CardProvider,
+ private val isCardAddedToFavorites: IsCardAddedToFavorites,
+ private val removeCardFromFavorites: RemoveCardFromFavorites,
+ private val addCardToFavorites: AddCardToFavorites) : CardDetail.Presenter {
+
+ override fun startPresenting() {
+ cardProvider.card?.let {
+ view.card = it
+ if (isCardAddedToFavorites.execute(it.id)) view.showCardAsFavorite(notifyUser = false)
+ }
+ }
+
+ override fun onAddToFavoritesClicked() {
+ view.card?.let {
+ if (isCardAddedToFavorites.execute(it.id)) {
+ removeCardFromFavorites.execute(it.id)
+ view.showCardNotFavorite()
+ } else {
+ addCardToFavorites.execute(it.id)
+ view.showCardAsFavorite()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardProvider.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardProvider.kt
new file mode 100644
index 00000000..c31dd649
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/CardProvider.kt
@@ -0,0 +1,13 @@
+package com.nielsmasdorp.heartstone.presentation.card.detail
+
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import javax.inject.Inject
+
+/**
+ * [CardDetail.CardProvider] implementation that provides a single card through fragment arguments
+ */
+class CardProvider @Inject constructor(private val cardDetailFragment: CardDetailFragment) : CardDetail.CardProvider {
+
+ override val card: CardViewModel?
+ get() = cardDetailFragment.arguments?.getParcelable(CardDetailFragment.CARD_KEY)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/dagger/CardDetailFragmentModule.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/dagger/CardDetailFragmentModule.kt
new file mode 100644
index 00000000..353dd905
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/dagger/CardDetailFragmentModule.kt
@@ -0,0 +1,35 @@
+package com.nielsmasdorp.heartstone.presentation.card.detail.dagger
+
+import android.support.v4.app.Fragment
+import com.nielsmasdorp.domain.card.data.FavoriteCardsRepository
+import com.nielsmasdorp.heartstone.data.card.SharedPrefsFavoritesCardRepository
+import com.nielsmasdorp.heartstone.presentation.card.detail.CardDetail
+import com.nielsmasdorp.heartstone.presentation.card.detail.CardDetailFragment
+import com.nielsmasdorp.heartstone.presentation.card.detail.CardDetailPresenter
+import com.nielsmasdorp.heartstone.presentation.card.detail.CardProvider
+import dagger.Binds
+import dagger.Module
+import dagger.android.AndroidInjector
+import dagger.android.support.FragmentKey
+import dagger.multibindings.IntoMap
+
+@Module(subcomponents = [CardDetailSubcomponent::class])
+abstract class CardDetailFragmentModule {
+
+ @Binds
+ @IntoMap
+ @FragmentKey(CardDetailFragment::class)
+ internal abstract fun bindCardDetailFragmentInjectorFactory(builder: CardDetailSubcomponent.Builder): AndroidInjector.Factory
+
+ @Binds
+ abstract fun bindCardDetailView(cardDetailFragment: CardDetailFragment): CardDetail.View
+
+ @Binds
+ abstract fun bindCardDetailPresenter(cardDetailPresenter: CardDetailPresenter): CardDetail.Presenter
+
+ @Binds
+ abstract fun bindCardProvider(cardProvider: CardProvider): CardDetail.CardProvider
+
+ @Binds
+ abstract fun bindFavoritesRepository(sharedPrefsFavoritesCardRepository: SharedPrefsFavoritesCardRepository): FavoriteCardsRepository
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/dagger/CardDetailSubcomponent.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/dagger/CardDetailSubcomponent.kt
new file mode 100644
index 00000000..0aa01c9d
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/detail/dagger/CardDetailSubcomponent.kt
@@ -0,0 +1,12 @@
+package com.nielsmasdorp.heartstone.presentation.card.detail.dagger
+
+import com.nielsmasdorp.heartstone.presentation.card.detail.CardDetailFragment
+import dagger.Subcomponent
+import dagger.android.AndroidInjector
+
+@Subcomponent
+interface CardDetailSubcomponent : AndroidInjector {
+
+ @Subcomponent.Builder
+ abstract class Builder : AndroidInjector.Builder()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGrid.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGrid.kt
new file mode 100644
index 00000000..e744dc1f
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGrid.kt
@@ -0,0 +1,31 @@
+package com.nielsmasdorp.heartstone.presentation.card.grid
+
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+
+/**
+ * Contract for the card grid feature
+ */
+interface CardGrid {
+
+ interface View {
+
+ var cards: List
+ }
+
+ interface Presenter {
+
+ fun startPresenting()
+ fun stopPresenting()
+ fun openCardDetails(cardViewModel: CardViewModel)
+ }
+
+ interface Navigator {
+
+ fun openCardDetails(cardViewModel: CardViewModel)
+ }
+
+ interface StringProvider {
+
+ fun getUnknownAttributeString(): String
+ }
+}
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridAdapter.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridAdapter.kt
new file mode 100644
index 00000000..69dd8b6c
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridAdapter.kt
@@ -0,0 +1,81 @@
+package com.nielsmasdorp.heartstone.presentation.card.grid
+
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.nielsmasdorp.heartstone.R
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import android.graphics.drawable.Drawable
+import android.support.v4.app.Fragment
+import android.widget.ImageView
+import com.bumptech.glide.Glide
+import com.bumptech.glide.RequestManager
+import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.target.Target
+import com.nielsmasdorp.heartstone.presentation.card.CardsActivity
+import kotlinx.android.synthetic.main.grid_item_card.view.*
+
+/**
+ * Adapter that shows the list of cards used in the card grid
+ */
+class CardGridAdapter(fragment: Fragment) : RecyclerView.Adapter() {
+
+ private var requestManager: RequestManager = Glide.with(fragment)
+
+ var onCardClickedListener: ((CardViewModel) -> Unit)? = null
+
+ var onClickedImageLoaded: (() -> Unit)? = null
+
+ var cards: List = emptyList()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CardViewHolder {
+ return CardViewHolder(LayoutInflater.from(parent.context)
+ .inflate(R.layout.grid_item_card, parent, false),
+ requestManager
+ )
+ }
+
+ override fun onBindViewHolder(holder: CardViewHolder, position: Int) = holder.bind()
+
+ override fun getItemCount(): Int = cards.size
+
+ fun getPositionForViewModel(clickedViewModel: CardViewModel): Int {
+ return cards.indexOf(clickedViewModel)
+ }
+
+ inner class CardViewHolder(itemView: View, private val requestManager: RequestManager) : RecyclerView.ViewHolder(itemView) {
+
+ fun bind() {
+ itemView.apply {
+ cardGridImage.transitionName = cards[adapterPosition].name
+ requestManager
+ .load(cards[adapterPosition].imgUrl)
+ .listener(object : RequestListener {
+ override fun onLoadFailed(e: GlideException?, model: Any,
+ target: Target, isFirstResource: Boolean): Boolean {
+ if (CardsActivity.CURRENT_POSITION == adapterPosition) {
+ onClickedImageLoaded?.invoke()
+ }
+ return false
+ }
+
+ override fun onResourceReady(resource: Drawable, model: Any, target: Target, dataSource: DataSource, isFirstResource: Boolean): Boolean {
+ if (CardsActivity.CURRENT_POSITION == adapterPosition) {
+ onClickedImageLoaded?.invoke()
+ }
+ return false
+ }
+ })
+ .into(cardGridImage)
+ setOnClickListener { onCardClickedListener?.invoke(cards[adapterPosition]) }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridFragment.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridFragment.kt
new file mode 100644
index 00000000..ddc3fd64
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridFragment.kt
@@ -0,0 +1,127 @@
+package com.nielsmasdorp.heartstone.presentation.card.grid
+
+import android.content.Context
+import android.os.Bundle
+import android.support.transition.TransitionInflater
+import android.support.transition.TransitionSet
+import android.support.v4.app.Fragment
+import android.support.v4.app.SharedElementCallback
+import android.support.v4.content.ContextCompat
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.nielsmasdorp.heartstone.R
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import dagger.android.support.AndroidSupportInjection
+import javax.inject.Inject
+import android.view.View.OnLayoutChangeListener
+import com.nielsmasdorp.heartstone.presentation.card.CardsActivity
+import com.nielsmasdorp.heartstone.presentation.card.carousel.CardCarouselFragment
+import kotlinx.android.synthetic.main.grid_item_card.view.*
+
+/**
+ * Fragment used to display a grid of cards
+ */
+class CardGridFragment : Fragment(), CardGrid.View, CardGrid.Navigator, CardGrid.StringProvider {
+
+ @Inject
+ lateinit var presenter: CardGrid.Presenter
+
+ override var cards: List = emptyList()
+ set(value) {
+ field = value
+ cardGridAdapter.cards = value
+ scrollToPosition()
+ }
+
+ private lateinit var cardGridAdapter: CardGridAdapter
+
+ lateinit var recyclerView: RecyclerView
+
+ override fun onAttach(context: Context) {
+ AndroidSupportInjection.inject(this)
+ super.onAttach(context)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ recyclerView = inflater.inflate(R.layout.fragment_cards_grid, container, false) as RecyclerView
+ cardGridAdapter = CardGridAdapter(this).apply {
+ onCardClickedListener = ::onCardClicked
+ onClickedImageLoaded = { startPostponedEnterTransition() }
+ }
+ recyclerView.adapter = cardGridAdapter
+
+ prepareTransitions()
+ postponeEnterTransition()
+
+ return recyclerView
+ }
+
+ override fun onStart() {
+ super.onStart()
+ presenter.startPresenting()
+ }
+
+ override fun onStop() {
+ presenter.stopPresenting()
+ super.onStop()
+ }
+
+ override fun openCardDetails(cardViewModel: CardViewModel) {
+ val clickedPosition = cardGridAdapter.getPositionForViewModel(cardViewModel)
+ CardsActivity.CURRENT_POSITION = clickedPosition
+ val sharedTransitionView = recyclerView.layoutManager.findViewByPosition(clickedPosition)
+ (exitTransition as TransitionSet).excludeTarget(sharedTransitionView, true)
+ fragmentManager?.apply {
+ beginTransaction()
+ .setReorderingAllowed(true)
+ .addSharedElement(sharedTransitionView.cardGridImage, sharedTransitionView.cardGridImage.transitionName)
+ .replace(R.id.fragmentContainer, CardCarouselFragment.newInstance(cards as ArrayList), CardCarouselFragment::class.java.simpleName)
+ .addToBackStack(null)
+ .commit()
+ }
+ }
+
+ override fun getUnknownAttributeString(): String = resources.getString(R.string.unknown)
+
+ private fun onCardClicked(clickedViewModel: CardViewModel) {
+ presenter.openCardDetails(clickedViewModel)
+ }
+
+ private fun scrollToPosition() {
+ recyclerView.addOnLayoutChangeListener(object : OnLayoutChangeListener {
+ override fun onLayoutChange(v: View,
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int) {
+ recyclerView.removeOnLayoutChangeListener(this)
+ val layoutManager = recyclerView.layoutManager
+ val viewAtPosition = layoutManager.findViewByPosition(CardsActivity.CURRENT_POSITION)
+ if (viewAtPosition == null || layoutManager
+ .isViewPartiallyVisible(viewAtPosition, false, true)) {
+ recyclerView.post({ layoutManager.scrollToPosition(CardsActivity.CURRENT_POSITION) })
+ }
+ }
+ })
+ }
+
+ private fun prepareTransitions() {
+ exitTransition = TransitionInflater.from(context).inflateTransition(R.transition.grid_exit_transition).apply {
+ duration = resources.getInteger(R.integer.default_animation_length_ms).toLong()
+ }
+ setExitSharedElementCallback(
+ object : SharedElementCallback() {
+ override fun onMapSharedElements(names: List, sharedElements: MutableMap) {
+ recyclerView.findViewHolderForAdapterPosition(CardsActivity.CURRENT_POSITION)?.let {
+ sharedElements.put(names[0], it.itemView.cardGridImage)
+ }
+ }
+ })
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridPresenter.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridPresenter.kt
new file mode 100644
index 00000000..c74744da
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridPresenter.kt
@@ -0,0 +1,70 @@
+package com.nielsmasdorp.heartstone.presentation.card.grid
+
+import com.nielsmasdorp.domain.card.GetCards
+import com.nielsmasdorp.domain.card.entity.Card
+import com.nielsmasdorp.domain.card.entity.CardRequest
+import com.nielsmasdorp.domain.card.entity.CardRequest.Companion.DEATHRATTLE_MECHANIC
+import com.nielsmasdorp.domain.card.entity.CardRequest.Companion.LEGENDARY_RARITY
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.Disposable
+import io.reactivex.schedulers.Schedulers
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * Presenter that presents data for the card grid view
+ */
+class CardGridPresenter @Inject constructor(private val view: CardGrid.View,
+ private val navigator: CardGrid.Navigator,
+ private val stringProvider: CardGrid.StringProvider,
+ private val getCards: GetCards) : CardGrid.Presenter {
+
+ private var disposable: Disposable? = null
+
+ override fun startPresenting() {
+ fetchCards()
+ }
+
+ override fun stopPresenting() {
+ disposable?.dispose()
+ }
+
+ override fun openCardDetails(cardViewModel: CardViewModel) {
+ navigator.openCardDetails(cardViewModel)
+ }
+
+ private fun fetchCards() {
+ val request = CardRequest(
+ requestedRarity = LEGENDARY_RARITY,
+ requestedMechanic = DEATHRATTLE_MECHANIC,
+ sortingStrategy = CardRequest.Sort.DESCENDING
+ )
+ disposable = getCards.execute(request)
+ .map { it.map { mapToViewModel(it) } }
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(::onCards, ::onCardsError)
+ }
+
+ private fun mapToViewModel(card: Card): CardViewModel {
+ val unknown: String = stringProvider.getUnknownAttributeString()
+ return CardViewModel(
+ id = card.id,
+ name = card.name ?: unknown,
+ imgUrl = card.imgUrl ?: "",
+ cardSet = card.cardSet ?: unknown,
+ type = card.type ?: unknown,
+ rarity = card.rarity ?: unknown,
+ text = card.text ?: unknown
+ )
+ }
+
+ private fun onCards(cards: List) {
+ view.cards = cards
+ }
+
+ private fun onCardsError(error: Throwable) {
+ Timber.e("error fetching cards", error)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/dagger/CardGridFragmentModule.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/dagger/CardGridFragmentModule.kt
new file mode 100644
index 00000000..835e3009
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/dagger/CardGridFragmentModule.kt
@@ -0,0 +1,37 @@
+package com.nielsmasdorp.heartstone.presentation.card.grid.dagger
+
+import android.support.v4.app.Fragment
+import com.nielsmasdorp.domain.card.data.CardRepository
+import com.nielsmasdorp.heartstone.data.card.JSONCardRepository
+import com.nielsmasdorp.heartstone.presentation.card.grid.CardGrid
+import com.nielsmasdorp.heartstone.presentation.card.grid.CardGridFragment
+import com.nielsmasdorp.heartstone.presentation.card.grid.CardGridPresenter
+import dagger.Binds
+import dagger.Module
+import dagger.android.AndroidInjector
+import dagger.android.support.FragmentKey
+import dagger.multibindings.IntoMap
+
+@Module(subcomponents = [CardGridSubcomponent::class])
+abstract class CardGridFragmentModule {
+
+ @Binds
+ @IntoMap
+ @FragmentKey(CardGridFragment::class)
+ internal abstract fun bindCardGridFragmentInjectorFactory(builder: CardGridSubcomponent.Builder): AndroidInjector.Factory
+
+ @Binds
+ abstract fun bindCardGridView(cardGridFragment: CardGridFragment): CardGrid.View
+
+ @Binds
+ abstract fun bindCardGridPresenter(cardGridPresenter: CardGridPresenter): CardGrid.Presenter
+
+ @Binds
+ abstract fun bindCardGridNavigator(cardGridFragment: CardGridFragment): CardGrid.Navigator
+
+ @Binds
+ abstract fun bindCardGridStringProvider(cardGridFragment: CardGridFragment): CardGrid.StringProvider
+
+ @Binds
+ abstract fun bindCardRepository(memoryCardRepository: JSONCardRepository): CardRepository
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/dagger/CardGridSubcomponent.kt b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/dagger/CardGridSubcomponent.kt
new file mode 100644
index 00000000..56c08b17
--- /dev/null
+++ b/app/src/main/java/com/nielsmasdorp/heartstone/presentation/card/grid/dagger/CardGridSubcomponent.kt
@@ -0,0 +1,12 @@
+package com.nielsmasdorp.heartstone.presentation.card.grid.dagger
+
+import com.nielsmasdorp.heartstone.presentation.card.grid.CardGridFragment
+import dagger.Subcomponent
+import dagger.android.AndroidInjector
+
+@Subcomponent
+interface CardGridSubcomponent : AndroidInjector {
+
+ @Subcomponent.Builder
+ abstract class Builder : AndroidInjector.Builder()
+}
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 00000000..c7bd21db
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_favorites_empty_24dp.xml b/app/src/main/res/drawable/ic_favorites_empty_24dp.xml
new file mode 100644
index 00000000..329d8519
--- /dev/null
+++ b/app/src/main/res/drawable/ic_favorites_empty_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_favorites_filled_24dp.xml b/app/src/main/res/drawable/ic_favorites_filled_24dp.xml
new file mode 100644
index 00000000..0b805375
--- /dev/null
+++ b/app/src/main/res/drawable/ic_favorites_filled_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..d5fccc53
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout-land/fragment_card_detail.xml b/app/src/main/res/layout-land/fragment_card_detail.xml
new file mode 100644
index 00000000..ead15530
--- /dev/null
+++ b/app/src/main/res/layout-land/fragment_card_detail.xml
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-land/fragment_cards_grid.xml b/app/src/main/res/layout-land/fragment_cards_grid.xml
new file mode 100644
index 00000000..6e9cff28
--- /dev/null
+++ b/app/src/main/res/layout-land/fragment_cards_grid.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_cards.xml b/app/src/main/res/layout/activity_cards.xml
new file mode 100644
index 00000000..fbf2e688
--- /dev/null
+++ b/app/src/main/res/layout/activity_cards.xml
@@ -0,0 +1,5 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_card_detail.xml b/app/src/main/res/layout/fragment_card_detail.xml
new file mode 100644
index 00000000..655d2343
--- /dev/null
+++ b/app/src/main/res/layout/fragment_card_detail.xml
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_cards_carousel.xml b/app/src/main/res/layout/fragment_cards_carousel.xml
new file mode 100644
index 00000000..26b4b2d8
--- /dev/null
+++ b/app/src/main/res/layout/fragment_cards_carousel.xml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_cards_grid.xml b/app/src/main/res/layout/fragment_cards_grid.xml
new file mode 100644
index 00000000..dfcd39ba
--- /dev/null
+++ b/app/src/main/res/layout/fragment_cards_grid.xml
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/grid_item_card.xml b/app/src/main/res/layout/grid_item_card.xml
new file mode 100644
index 00000000..c84fbcfe
--- /dev/null
+++ b/app/src/main/res/layout/grid_item_card.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..eca70cfe
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..a2f59082
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 00000000..1b523998
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..ff10afd6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 00000000..115a4c76
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..dcd3cd80
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..459ca609
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..8ca12fe0
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..8e19b410
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..b824ebdd
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 00000000..4c19a13c
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/transition/grid_exit_transition.xml b/app/src/main/res/transition/grid_exit_transition.xml
new file mode 100644
index 00000000..8575c21e
--- /dev/null
+++ b/app/src/main/res/transition/grid_exit_transition.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/transition/image_shared_element_transition.xml b/app/src/main/res/transition/image_shared_element_transition.xml
new file mode 100644
index 00000000..f5fd2d6f
--- /dev/null
+++ b/app/src/main/res/transition/image_shared_element_transition.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..3ab3e9cb
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..487cd2dd
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,12 @@
+
+
+ 16dp
+ 14sp
+ 8dp
+ 125dp
+ 150dp
+ 2dp
+ 250dp
+ 300dp
+ 16sp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml
new file mode 100644
index 00000000..0e76df47
--- /dev/null
+++ b/app/src/main/res/values/integers.xml
@@ -0,0 +1,4 @@
+
+
+ 375
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..d1a13d08
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,14 @@
+
+ Heartstone
+ Heartstone Cards
+ Card added to favorites!
+ Card removed from favorites!
+ Card name:
+ Card set:
+ Card type:
+ Card rarity:
+ Card description:
+ Classes:
+ Mechanics:
+ Unknown
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..173741b2
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/nielsmasdorp/heartstone/presentation/RxMockitoJUnitRunner.kt b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/RxMockitoJUnitRunner.kt
new file mode 100644
index 00000000..aae9e4ab
--- /dev/null
+++ b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/RxMockitoJUnitRunner.kt
@@ -0,0 +1,20 @@
+package com.nielsmasdorp.heartstone.presentation
+
+import io.reactivex.android.plugins.RxAndroidPlugins
+import io.reactivex.plugins.RxJavaPlugins
+import io.reactivex.schedulers.Schedulers
+import org.mockito.junit.MockitoJUnitRunner
+
+/**
+ * Custom test runner that remaps Rx schedulers to trampoline, for testing purposes.
+ */
+class RxMockitoJUnitRunner(classUnderTest: Class<*>) : MockitoJUnitRunner(classUnderTest) {
+
+ init {
+ RxJavaPlugins.reset()
+ RxAndroidPlugins.reset()
+ RxJavaPlugins.setIoSchedulerHandler { _ -> Schedulers.trampoline() }
+ RxAndroidPlugins.setInitMainThreadSchedulerHandler { _ -> Schedulers.trampoline() }
+ RxJavaPlugins.setComputationSchedulerHandler { _ -> Schedulers.trampoline() }
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/CardTestUtil.kt b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/CardTestUtil.kt
new file mode 100644
index 00000000..4d021430
--- /dev/null
+++ b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/CardTestUtil.kt
@@ -0,0 +1,38 @@
+package com.nielsmasdorp.heartstone.presentation.card
+
+import com.nielsmasdorp.domain.card.entity.Card
+
+object CardTestUtil {
+
+ /**
+ * Create test [Card] with optional set values
+ */
+ fun createTestCard(id: String = "1",
+ name: String = "name",
+ imgUrl: String = "img",
+ cardSet: String = "set",
+ type: String = "type",
+ rarity: String = "rarity",
+ cost: Int = 10,
+ attack: Int = 4,
+ health: Int = 6,
+ text: String = "text",
+ flavor: String = "flavor",
+ classes: List = emptyList(),
+ mechanics: List = emptyList()): Card {
+ return Card(id, name, imgUrl, cardSet, type, rarity, cost, attack, health, text, flavor, classes, mechanics)
+ }
+
+ /**
+ * Create test [CardViewModel] with optional set values
+ */
+ fun createTestCardViewModel(id: String = "1",
+ name: String = "name",
+ imgUrl: String = "img",
+ cardSet: String = "set",
+ type: String = "type",
+ rarity: String = "rarity",
+ text: String = "text"): CardViewModel {
+ return CardViewModel(id, name, imgUrl, cardSet, type, rarity, text)
+ }
+}
diff --git a/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselPresenterTest.kt b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselPresenterTest.kt
new file mode 100644
index 00000000..9b322bfb
--- /dev/null
+++ b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/carousel/CardCarouselPresenterTest.kt
@@ -0,0 +1,42 @@
+@file:Suppress("IllegalIdentifier")
+
+package com.nielsmasdorp.heartstone.presentation.card.carousel
+
+import com.nhaarman.mockito_kotlin.given
+import com.nhaarman.mockito_kotlin.verify
+import com.nielsmasdorp.heartstone.presentation.RxMockitoJUnitRunner
+import com.nielsmasdorp.heartstone.presentation.card.CardTestUtil
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.InjectMocks
+import org.mockito.Mock
+
+/**
+ * Test for the [CardCarouselPresenter]
+ */
+@RunWith(RxMockitoJUnitRunner::class)
+class CardCarouselPresenterTest {
+
+ @Mock
+ lateinit var view: CardCarousel.View
+
+ @Mock
+ lateinit var cardsProvided: CardCarousel.CardsProvider
+
+ @InjectMocks
+ lateinit var presenter: CardCarouselPresenter
+
+ @Test
+ fun `Show cards when presenter starts`() {
+ //when
+ val viewModels: List = listOf(CardTestUtil.createTestCardViewModel(id = "1"))
+ given(cardsProvided.cards).willReturn(viewModels)
+
+ //when
+ presenter.startPresenting()
+
+ //then
+ verify(view).cards = viewModels
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/details/CardDetailsPresenterTest.kt b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/details/CardDetailsPresenterTest.kt
new file mode 100644
index 00000000..a33be3e0
--- /dev/null
+++ b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/details/CardDetailsPresenterTest.kt
@@ -0,0 +1,95 @@
+@file:Suppress("IllegalIdentifier")
+
+package com.nielsmasdorp.heartstone.presentation.card.details
+
+import com.nhaarman.mockito_kotlin.given
+import com.nhaarman.mockito_kotlin.verify
+import com.nielsmasdorp.domain.card.AddCardToFavorites
+import com.nielsmasdorp.domain.card.IsCardAddedToFavorites
+import com.nielsmasdorp.domain.card.RemoveCardFromFavorites
+import com.nielsmasdorp.heartstone.presentation.RxMockitoJUnitRunner
+import com.nielsmasdorp.heartstone.presentation.card.CardTestUtil
+import com.nielsmasdorp.heartstone.presentation.card.detail.CardDetail
+import com.nielsmasdorp.heartstone.presentation.card.detail.CardDetailPresenter
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.InjectMocks
+import org.mockito.Mock
+
+/**
+ * Test for the [CardDetailsPresenter]
+ */
+@RunWith(RxMockitoJUnitRunner::class)
+class CardDetailsPresenterTest {
+
+ @Mock
+ lateinit var view: CardDetail.View
+
+ @Mock
+ lateinit var cardProvider: CardDetail.CardProvider
+
+ @Mock
+ lateinit var isCardAddedToFavorites: IsCardAddedToFavorites
+
+ @Mock
+ lateinit var addCardToFavorites: AddCardToFavorites
+
+ @Mock
+ lateinit var removeCardFromFavorites: RemoveCardFromFavorites
+
+ @InjectMocks
+ lateinit var presenter: CardDetailPresenter
+
+ @Test
+ fun `Show provided card when presenter starts`() {
+ //when
+ val viewModel = CardTestUtil.createTestCardViewModel(id = "1")
+ given(cardProvider.card).willReturn(viewModel)
+
+ //when
+ presenter.startPresenting()
+
+ //then
+ verify(view).card = viewModel
+ }
+
+ @Test
+ fun `Show favorite card when presenter starts`() {
+ //when
+ val viewModel = CardTestUtil.createTestCardViewModel(id = "1")
+ given(cardProvider.card).willReturn(viewModel)
+ given(isCardAddedToFavorites.execute("1")).willReturn(true)
+
+ //when
+ presenter.startPresenting()
+
+ //then
+ verify(view).showCardAsFavorite(notifyUser = false)
+ }
+
+ @Test
+ fun `Add correct card to favorites when card is clicked and it was not added to the favorites`() {
+ //given
+ given(view.card).willReturn(CardTestUtil.createTestCardViewModel(id = "1"))
+ given(isCardAddedToFavorites.execute("1")).willReturn(false)
+
+ //when
+ presenter.onAddToFavoritesClicked()
+
+ //then
+ verify(addCardToFavorites).execute("1")
+ }
+
+ @Test
+ fun `Remove correct card from favorites when card is clicked and it was already added to the favorites`() {
+ //given
+ given(view.card).willReturn(CardTestUtil.createTestCardViewModel(id = "1"))
+ given(isCardAddedToFavorites.execute("1")).willReturn(true)
+
+ //when
+ presenter.onAddToFavoritesClicked()
+
+ //then
+ verify(removeCardFromFavorites).execute("1")
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridPresenterTest.kt b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridPresenterTest.kt
new file mode 100644
index 00000000..6b73f259
--- /dev/null
+++ b/app/src/test/java/com/nielsmasdorp/heartstone/presentation/card/grid/CardGridPresenterTest.kt
@@ -0,0 +1,68 @@
+@file:Suppress("IllegalIdentifier")
+
+package com.nielsmasdorp.heartstone.presentation.card.grid
+
+import com.nhaarman.mockito_kotlin.given
+import com.nhaarman.mockito_kotlin.verify
+import com.nielsmasdorp.domain.card.GetCards
+import com.nielsmasdorp.domain.card.entity.CardRequest
+import com.nielsmasdorp.heartstone.presentation.RxMockitoJUnitRunner
+import com.nielsmasdorp.heartstone.presentation.card.CardTestUtil
+import com.nielsmasdorp.heartstone.presentation.card.CardViewModel
+import io.reactivex.Single
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.InjectMocks
+import org.mockito.Mock
+
+/**
+ * Test for the [CardGridPresenter]
+ */
+@RunWith(RxMockitoJUnitRunner::class)
+class CardGridPresenterTest {
+
+ @Mock
+ lateinit var view: CardGrid.View
+
+ @Mock
+ lateinit var navigator: CardGrid.Navigator
+
+ @Mock
+ lateinit var getCards: GetCards
+
+ @Mock
+ lateinit var stringProvider: CardGrid.StringProvider
+
+ @InjectMocks
+ lateinit var presenter: CardGridPresenter
+
+ @Test
+ fun `Show legendary cards with deathrattle mechanic when presenter starts`() {
+ //given
+ val request = CardRequest(
+ requestedRarity = CardRequest.LEGENDARY_RARITY,
+ requestedMechanic = CardRequest.DEATHRATTLE_MECHANIC,
+ sortingStrategy = CardRequest.Sort.DESCENDING
+ )
+ given(getCards.execute(request))
+ .willReturn(Single.just(listOf(CardTestUtil.createTestCard(id = "1"))))
+
+ //when
+ presenter.startPresenting()
+
+ //then
+ verify(view).cards = listOf(CardTestUtil.createTestCardViewModel(id = "1"))
+ }
+
+ @Test
+ fun `Navigate to card details when card is clicked`() {
+ //given
+ val viewModel = CardTestUtil.createTestCardViewModel("1")
+
+ //when
+ presenter.openCardDetails(viewModel)
+
+ //then
+ verify(navigator).openCardDetails(viewModel)
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000..ca6ee9ce
--- /dev/null
+++ b/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..3cc93777
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,29 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ ext {
+ kotlin_version = '1.2.30'
+ dagger_version = '2.12'
+ android_support_version = '27.1.1'
+ reactive_x_version = '2.1.0'
+ }
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.0.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/domain/.gitignore b/domain/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/domain/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/domain/build.gradle b/domain/build.gradle
new file mode 100644
index 00000000..c9f25538
--- /dev/null
+++ b/domain/build.gradle
@@ -0,0 +1,25 @@
+apply plugin: 'kotlin'
+apply plugin: 'kotlin-kapt'
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+
+ //Dagger
+ implementation "com.google.dagger:dagger:$dagger_version"
+ kapt "com.google.dagger:dagger-compiler:$dagger_version"
+ kapt "com.google.dagger:dagger-android-processor:$dagger_version"
+ implementation("com.google.dagger:dagger-android-support:$dagger_version", {
+ exclude group: 'com.google.code.findbugs'
+ })
+
+ // ReactiveX
+ implementation "io.reactivex.rxjava2:rxjava:$reactive_x_version"
+ implementation "io.reactivex.rxjava2:rxkotlin:$reactive_x_version"
+
+ testImplementation 'junit:junit:4.12'
+ testImplementation "com.nhaarman:mockito-kotlin-kt1.1:1.5.0"
+ testImplementation 'org.mockito:mockito-core:2.10.0'
+}
+
+sourceCompatibility = "1.7"
+targetCompatibility = "1.7"
\ No newline at end of file
diff --git a/domain/src/main/java/com/nielsmasdorp/domain/card/AddCardToFavorites.kt b/domain/src/main/java/com/nielsmasdorp/domain/card/AddCardToFavorites.kt
new file mode 100644
index 00000000..e0683d89
--- /dev/null
+++ b/domain/src/main/java/com/nielsmasdorp/domain/card/AddCardToFavorites.kt
@@ -0,0 +1,15 @@
+package com.nielsmasdorp.domain.card
+
+import com.nielsmasdorp.domain.card.data.FavoriteCardsRepository
+import javax.inject.Inject
+
+/**
+ * Use case that adds a card id to the favorites
+ */
+class AddCardToFavorites @Inject constructor(private val favoriteCardsRepository: FavoriteCardsRepository) {
+
+ /**
+ * @param cardId id that should be added to the favorites
+ */
+ fun execute(cardId: String) = favoriteCardsRepository.addToFavorites(cardId)
+}
diff --git a/domain/src/main/java/com/nielsmasdorp/domain/card/GetCards.kt b/domain/src/main/java/com/nielsmasdorp/domain/card/GetCards.kt
new file mode 100644
index 00000000..a996a644
--- /dev/null
+++ b/domain/src/main/java/com/nielsmasdorp/domain/card/GetCards.kt
@@ -0,0 +1,19 @@
+package com.nielsmasdorp.domain.card
+
+import com.nielsmasdorp.domain.card.data.CardRepository
+import com.nielsmasdorp.domain.card.entity.Card
+import com.nielsmasdorp.domain.card.entity.CardRequest
+import io.reactivex.Single
+import javax.inject.Inject
+
+/**
+ * Use case that retrieves cards
+ */
+class GetCards @Inject constructor(private val repository: CardRepository) {
+
+ /**
+ * @return a list of cards based on the criteria provided in
+ * @param cardRequest
+ */
+ fun execute(cardRequest: CardRequest): Single> = repository.getCards(cardRequest)
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/nielsmasdorp/domain/card/IsCardAddedToFavorites.kt b/domain/src/main/java/com/nielsmasdorp/domain/card/IsCardAddedToFavorites.kt
new file mode 100644
index 00000000..611c8bf8
--- /dev/null
+++ b/domain/src/main/java/com/nielsmasdorp/domain/card/IsCardAddedToFavorites.kt
@@ -0,0 +1,16 @@
+package com.nielsmasdorp.domain.card
+
+import com.nielsmasdorp.domain.card.data.FavoriteCardsRepository
+import javax.inject.Inject
+
+/**
+ * Use case that checks if a card id was added to favorites
+ */
+class IsCardAddedToFavorites @Inject constructor(private val favoriteCardsRepository: FavoriteCardsRepository) {
+
+ /**
+ * @param cardId id of the card that can be in favorites
+ * @return true when the id exists in favorites
+ */
+ fun execute(cardId: String) = favoriteCardsRepository.isAddedToFavorites(cardId)
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/nielsmasdorp/domain/card/RemoveCardFromFavorites.kt b/domain/src/main/java/com/nielsmasdorp/domain/card/RemoveCardFromFavorites.kt
new file mode 100644
index 00000000..ba67e02e
--- /dev/null
+++ b/domain/src/main/java/com/nielsmasdorp/domain/card/RemoveCardFromFavorites.kt
@@ -0,0 +1,15 @@
+package com.nielsmasdorp.domain.card
+
+import com.nielsmasdorp.domain.card.data.FavoriteCardsRepository
+import javax.inject.Inject
+
+/**
+ * Use case that adds a card id to the favorites
+ */
+class RemoveCardFromFavorites @Inject constructor(private val favoriteCardsRepository: FavoriteCardsRepository) {
+
+ /**
+ * @param cardId id that should be added to the favorites
+ */
+ fun execute(cardId: String) = favoriteCardsRepository.removeFromFavorites(cardId)
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/nielsmasdorp/domain/card/data/CardRepository.kt b/domain/src/main/java/com/nielsmasdorp/domain/card/data/CardRepository.kt
new file mode 100644
index 00000000..e996d19a
--- /dev/null
+++ b/domain/src/main/java/com/nielsmasdorp/domain/card/data/CardRepository.kt
@@ -0,0 +1,17 @@
+package com.nielsmasdorp.domain.card.data
+
+import com.nielsmasdorp.domain.card.entity.Card
+import com.nielsmasdorp.domain.card.entity.CardRequest
+import io.reactivex.Single
+
+/**
+ * Interface for an repository that can provide a list of [Card]s
+ */
+interface CardRepository {
+
+ /**
+ * @return a list of cards based on the criteria provided in
+ * @param cardRequest
+ */
+ fun getCards(cardRequest: CardRequest): Single>
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/nielsmasdorp/domain/card/data/FavoriteCardsRepository.kt b/domain/src/main/java/com/nielsmasdorp/domain/card/data/FavoriteCardsRepository.kt
new file mode 100644
index 00000000..9681b878
--- /dev/null
+++ b/domain/src/main/java/com/nielsmasdorp/domain/card/data/FavoriteCardsRepository.kt
@@ -0,0 +1,23 @@
+package com.nielsmasdorp.domain.card.data
+
+/**
+ * Interface for a repository that deals with favorite cards
+ */
+interface FavoriteCardsRepository {
+
+ /**
+ * @param cardId id of the card that should be added to the favorites
+ */
+ fun addToFavorites(cardId: String)
+
+ /**
+ * @param cardId id of the card that should be removed from the favorites
+ */
+ fun removeFromFavorites(cardId: String)
+
+ /**
+ * @param cardId id of the card
+ * @return true when the card was added to the favorites, false otherwise
+ */
+ fun isAddedToFavorites(cardId: String): Boolean
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/nielsmasdorp/domain/card/entity/Card.kt b/domain/src/main/java/com/nielsmasdorp/domain/card/entity/Card.kt
new file mode 100644
index 00000000..19ab2c99
--- /dev/null
+++ b/domain/src/main/java/com/nielsmasdorp/domain/card/entity/Card.kt
@@ -0,0 +1,19 @@
+package com.nielsmasdorp.domain.card.entity
+
+/**
+ * Domain entity for a card used in Heartstone
+ */
+data class Card(
+ val id: String,
+ val name: String?,
+ val imgUrl: String?,
+ val cardSet: String?,
+ val type: String?,
+ val rarity: String?,
+ val cost: Int?,
+ val attack: Int?,
+ val health: Int?,
+ val text: String?,
+ val flavor: String?,
+ val classes: List?,
+ val mechanics: List?)
\ No newline at end of file
diff --git a/domain/src/main/java/com/nielsmasdorp/domain/card/entity/CardRequest.kt b/domain/src/main/java/com/nielsmasdorp/domain/card/entity/CardRequest.kt
new file mode 100644
index 00000000..41f5e09b
--- /dev/null
+++ b/domain/src/main/java/com/nielsmasdorp/domain/card/entity/CardRequest.kt
@@ -0,0 +1,25 @@
+package com.nielsmasdorp.domain.card.entity
+
+/**
+ * Card request used to query specific cards
+ */
+data class CardRequest(val requestedType: String? = null,
+ val requestedRarity: String? = null,
+ val requestedClass: String? = null,
+ val requestedMechanic: String? = null,
+ val sortingStrategy: Sort = Sort.NONE
+) {
+
+ enum class Sort {
+ NONE,
+ ASCENDING,
+ DESCENDING
+ }
+
+ companion object {
+
+ /** Constants used in the current version of the app */
+ const val LEGENDARY_RARITY = "Legendary"
+ const val DEATHRATTLE_MECHANIC = "Deathrattle"
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/nielsmasdorp/domain/card/AddCardToFavoritesTest.kt b/domain/src/test/java/com/nielsmasdorp/domain/card/AddCardToFavoritesTest.kt
new file mode 100644
index 00000000..9094a954
--- /dev/null
+++ b/domain/src/test/java/com/nielsmasdorp/domain/card/AddCardToFavoritesTest.kt
@@ -0,0 +1,31 @@
+package com.nielsmasdorp.domain.card
+
+import com.nhaarman.mockito_kotlin.verify
+import com.nielsmasdorp.domain.card.data.FavoriteCardsRepository
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.InjectMocks
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+
+/**
+ * Tests for [AddCardToFavorites] use case
+ */
+@RunWith(MockitoJUnitRunner::class)
+class AddCardToFavoritesTest {
+
+ @Mock
+ lateinit var favoritesRepository: FavoriteCardsRepository
+
+ @InjectMocks
+ lateinit var addCardToFavorites: AddCardToFavorites
+
+ @Test
+ fun `Save card to favorites when executed`() {
+ // when
+ addCardToFavorites.execute("1")
+
+ //verify
+ verify(favoritesRepository).addToFavorites("1")
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/nielsmasdorp/domain/card/CardTestUtil.kt b/domain/src/test/java/com/nielsmasdorp/domain/card/CardTestUtil.kt
new file mode 100644
index 00000000..73fedab8
--- /dev/null
+++ b/domain/src/test/java/com/nielsmasdorp/domain/card/CardTestUtil.kt
@@ -0,0 +1,25 @@
+package com.nielsmasdorp.domain.card
+
+import com.nielsmasdorp.domain.card.entity.Card
+
+object CardTestUtil {
+
+ /**
+ * Create test [Card] with optional set values
+ */
+ fun createTestCard(id: String = "1",
+ name: String = "test",
+ imgUrl: String = "testImg",
+ cardSet: String = "testCardSet",
+ type: String = "testType",
+ rarity: String = "testRarity",
+ cost: Int = 10,
+ attack: Int = 4,
+ health: Int = 6,
+ text: String = "testText",
+ flavor: String = "testFlavor",
+ classes: List = emptyList(),
+ mechanics: List = emptyList()): Card {
+ return Card(id, name, imgUrl, cardSet, type, rarity, cost, attack, health, text, flavor, classes, mechanics)
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/nielsmasdorp/domain/card/GetCardsTest.kt b/domain/src/test/java/com/nielsmasdorp/domain/card/GetCardsTest.kt
new file mode 100644
index 00000000..822bead3
--- /dev/null
+++ b/domain/src/test/java/com/nielsmasdorp/domain/card/GetCardsTest.kt
@@ -0,0 +1,38 @@
+package com.nielsmasdorp.domain.card
+
+import com.nhaarman.mockito_kotlin.given
+import com.nielsmasdorp.domain.card.data.CardRepository
+import com.nielsmasdorp.domain.card.entity.CardRequest
+import io.reactivex.Single
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.InjectMocks
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+
+/**
+ * Tests for [GetCards] use case
+ */
+@RunWith(MockitoJUnitRunner::class)
+class GetCardsTest {
+
+ @Mock
+ lateinit var cardRepository: CardRepository
+
+ @InjectMocks
+ lateinit var getCards: GetCards
+
+ @Test
+ fun `Get cards return unmodified cards`() {
+ // Given
+ val card = CardTestUtil.createTestCard(id = "1")
+ val request = CardRequest()
+ given(cardRepository.getCards(request)).willReturn(Single.just(listOf(card)))
+
+ //when
+ val testDisposable = getCards.execute(request).test()
+
+ //verify
+ testDisposable.assertValue(listOf(card))
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/nielsmasdorp/domain/card/IsCardAddedToFavoritesTest.kt b/domain/src/test/java/com/nielsmasdorp/domain/card/IsCardAddedToFavoritesTest.kt
new file mode 100644
index 00000000..81234f55
--- /dev/null
+++ b/domain/src/test/java/com/nielsmasdorp/domain/card/IsCardAddedToFavoritesTest.kt
@@ -0,0 +1,44 @@
+package com.nielsmasdorp.domain.card
+
+import com.nhaarman.mockito_kotlin.given
+import com.nielsmasdorp.domain.card.data.CardRepository
+import com.nielsmasdorp.domain.card.data.FavoriteCardsRepository
+import com.nielsmasdorp.domain.card.entity.CardRequest
+import io.reactivex.Single
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.InjectMocks
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+
+/**
+ * Tests for [isCardAddedToFavorites] use case
+ */
+@RunWith(MockitoJUnitRunner::class)
+class IsCardAddedToFavoritesTest {
+
+ @Mock
+ lateinit var favoritesRepository: FavoriteCardsRepository
+
+ @InjectMocks
+ lateinit var isCardAddedToFavorites: IsCardAddedToFavorites
+
+ @Test
+ fun `Get true when card was added to favorites`() {
+ // Given
+ given(favoritesRepository.isAddedToFavorites("1")).willReturn(true)
+
+ //verify
+ Assert.assertEquals(true, isCardAddedToFavorites.execute("1"))
+ }
+
+ @Test
+ fun `Get false when card was not added to favorites`() {
+ // Given
+ given(favoritesRepository.isAddedToFavorites("1")).willReturn(false)
+
+ //verify
+ Assert.assertEquals(false, isCardAddedToFavorites.execute("1"))
+ }
+}
\ No newline at end of file
diff --git a/domain/src/test/java/com/nielsmasdorp/domain/card/RemoveCardFromFavoritesTest.kt b/domain/src/test/java/com/nielsmasdorp/domain/card/RemoveCardFromFavoritesTest.kt
new file mode 100644
index 00000000..8f9c818f
--- /dev/null
+++ b/domain/src/test/java/com/nielsmasdorp/domain/card/RemoveCardFromFavoritesTest.kt
@@ -0,0 +1,31 @@
+package com.nielsmasdorp.domain.card
+
+import com.nhaarman.mockito_kotlin.verify
+import com.nielsmasdorp.domain.card.data.FavoriteCardsRepository
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.InjectMocks
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+
+/**
+ * Tests for [RemoveCardFromFavorites] use case
+ */
+@RunWith(MockitoJUnitRunner::class)
+class RemoveCardFromFavoritesTest {
+
+ @Mock
+ lateinit var favoritesRepository: FavoriteCardsRepository
+
+ @InjectMocks
+ lateinit var removeCardFromFavorites: RemoveCardFromFavorites
+
+ @Test
+ fun `Remove card from favorites when executed`() {
+ // when
+ removeCardFromFavorites.execute("1")
+
+ //verify
+ verify(favoritesRepository).removeFromFavorites("1")
+ }
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..aac7c9b4
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,17 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..13372aef
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..8027ac2d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon May 07 09:03:26 CEST 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..9d82f789
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..aec99730
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..b33b4976
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app', ':domain'