├── .idea
├── .name
├── emacs.xml
├── vcs.xml
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── misc.xml
├── runConfigurations.xml
├── gradle.xml
└── jarRepositories.xml
├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── styles.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── menu
│ │ │ │ └── menu_product_list.xml
│ │ │ ├── drawable
│ │ │ │ ├── ic_filter_24dp.xml
│ │ │ │ ├── ic_cart_24.xml
│ │ │ │ ├── ic_remove_cart_24.xml
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── navigation
│ │ │ │ └── nav_main.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── fragment_product_list.xml
│ │ │ │ ├── item_basic_product.xml
│ │ │ │ └── item_tiled_product.xml
│ │ │ └── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── lucasmontano
│ │ │ │ └── shopping
│ │ │ │ ├── ui
│ │ │ │ ├── models
│ │ │ │ │ ├── CartUiModel.kt
│ │ │ │ │ └── ProductUiModel.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── adapters
│ │ │ │ │ ├── AdapterDelegate.kt
│ │ │ │ │ ├── AdapterDelegatesManager.kt
│ │ │ │ │ ├── DelegationAdapter.kt
│ │ │ │ │ ├── ProductAdapterDelegate.kt
│ │ │ │ │ └── TileProductAdapterDelegate.kt
│ │ │ │ └── ProductListFragment.kt
│ │ │ │ ├── ShoppingApplication.kt
│ │ │ │ ├── utilities
│ │ │ │ └── Constants.kt
│ │ │ │ ├── data
│ │ │ │ ├── entities
│ │ │ │ │ ├── ProductCartRelation.kt
│ │ │ │ │ ├── ProductEntity.kt
│ │ │ │ │ └── ProductShoppingEntity.kt
│ │ │ │ ├── domain
│ │ │ │ │ ├── ProductDomainModel.kt
│ │ │ │ │ ├── CouchDomainModel.kt
│ │ │ │ │ └── ChairDomainModel.kt
│ │ │ │ ├── mappers
│ │ │ │ │ └── ModelMappers.kt
│ │ │ │ ├── dao
│ │ │ │ │ ├── ProductDao.kt
│ │ │ │ │ └── CartDao.kt
│ │ │ │ ├── Converters.kt
│ │ │ │ ├── repositories
│ │ │ │ │ ├── ProductRepository.kt
│ │ │ │ │ └── CartRepository.kt
│ │ │ │ └── AppDatabase.kt
│ │ │ │ ├── binding
│ │ │ │ └── GlideAdapter.kt
│ │ │ │ ├── viewmodels
│ │ │ │ ├── CartViewModel.kt
│ │ │ │ └── ProductListViewModel.kt
│ │ │ │ ├── di
│ │ │ │ └── DatabaseModule.kt
│ │ │ │ └── workers
│ │ │ │ └── InitDatabaseWorker.kt
│ │ └── AndroidManifest.xml
│ ├── androidTest
│ │ └── java
│ │ │ └── com
│ │ │ └── lucasmontano
│ │ │ └── shopping
│ │ │ ├── data
│ │ │ ├── InitDatabaseWorkerTest.kt
│ │ │ └── ProductDaoTest.kt
│ │ │ ├── MainTestRunner.kt
│ │ │ ├── utilities
│ │ │ └── LiveDataTestUtil.kt
│ │ │ └── repositories
│ │ │ └── CartRepositoriesTest.kt
│ └── test
│ │ └── java
│ │ └── com
│ │ └── lucasmontano
│ │ └── shopping
│ │ ├── TestUtilities.kt
│ │ └── viewmodels
│ │ └── ProductListViewModelTest.kt
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── README.md
├── .gitignore
├── gradle.properties
├── gradlew.bat
└── gradlew
/.idea/.name:
--------------------------------------------------------------------------------
1 | Shopping
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "Shopping"
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lucasmontano/shopping/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ui/models/CartUiModel.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.ui.models
2 |
3 | data class CartUiModel(val total: String)
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Intro
2 | A shopping app inspired by [Sunflower](https://github.com/android/sunflower) to illustrate Android development best practices with Android Jetpack.
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Shopping
3 | Filter by type
4 |
--------------------------------------------------------------------------------
/.idea/emacs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 200dp
4 | 16dp
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ShoppingApplication.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class ShoppingApplication : Application()
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Oct 10 10:47:42 CEST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/utilities/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.utilities
2 |
3 | // Data
4 | const val DATABASE_NAME = "shopping-db"
5 | const val DATA_FILENAME = "products.json"
6 | const val DATABASE_TABLE_PRODUCTS = "products"
7 | const val DATABASE_TABLE_CART = "cart"
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ui/models/ProductUiModel.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.ui.models
2 |
3 | interface ProductUiModel {
4 | val id: String
5 | }
6 |
7 | data class TiledUiModelProduct(
8 | override val id: String,
9 | val name: String,
10 | val imageUrl: String
11 | ) : ProductUiModel
12 |
13 | data class BasicProductUiModel(override val id: String, val name: String) : ProductUiModel
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_product_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_filter_24dp.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/entities/ProductCartRelation.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.entities
2 |
3 | import androidx.room.Embedded
4 | import androidx.room.Relation
5 |
6 | // TODO check if we need to have redudance of products in the cart
7 | data class ProductCartRelation(
8 | @Embedded
9 | val product: ProductEntity,
10 |
11 | @Relation(parentColumn = "id", entityColumn = "product_id")
12 | val productShoppingEntities: List = emptyList()
13 | )
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.ui
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import com.lucasmontano.shopping.R
6 | import dagger.hilt.android.AndroidEntryPoint
7 |
8 | @AndroidEntryPoint
9 | class MainActivity : AppCompatActivity() {
10 |
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | setContentView(R.layout.activity_main)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/binding/GlideAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.binding
2 |
3 | import android.widget.ImageView
4 | import androidx.databinding.BindingAdapter
5 | import com.bumptech.glide.Glide
6 | import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
7 |
8 | @BindingAdapter("imageFromUrl")
9 | fun bindImageFromUrl(view: ImageView, imageUrl: String?) {
10 | if (!imageUrl.isNullOrEmpty()) {
11 | Glide.with(view.context)
12 | .load(imageUrl)
13 | .transition(DrawableTransitionOptions.withCrossFade())
14 | .into(view)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/entities/ProductEntity.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.entities
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import com.google.gson.annotations.SerializedName
7 | import com.lucasmontano.shopping.utilities.DATABASE_TABLE_PRODUCTS
8 |
9 | @Entity(tableName = DATABASE_TABLE_PRODUCTS)
10 | data class ProductEntity(
11 | @PrimaryKey
12 | @ColumnInfo(name = "id")
13 | @SerializedName("id")
14 | val productId: String,
15 | val name: String,
16 | val type: String,
17 | val imageUrl: String = "",
18 | val price: Price
19 | ) {
20 |
21 | override fun toString() = name
22 |
23 | data class Price(val value: Double, val currency: String)
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ui/adapters/AdapterDelegate.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.ui.adapters
2 |
3 | import android.view.ViewGroup
4 | import androidx.recyclerview.widget.RecyclerView
5 |
6 | abstract class AdapterDelegate {
7 |
8 | abstract fun getViewType(): Int
9 |
10 | abstract fun isForViewType(items: List, position: Int): Boolean
11 |
12 | abstract fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
13 |
14 | abstract fun onBindViewHolder(
15 | item: T,
16 | holder: RecyclerView.ViewHolder,
17 | position: Int
18 | )
19 |
20 | abstract fun onBindViewHolder(
21 | item: T,
22 | holder: RecyclerView.ViewHolder,
23 | position: Int,
24 | payloads: MutableList
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_cart_24.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_remove_cart_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/domain/ProductDomainModel.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.domain
2 |
3 | data class ProductDomainModel(
4 | override val id: String,
5 | override val name: String,
6 | override val price: Double,
7 | override val currency: String,
8 | override val color: String,
9 | override val imageUrl: String
10 | ) : HasBasicProductAttr
11 |
12 | interface HasBasicProductAttr {
13 | val id: String
14 | val name: String
15 | val price: Double
16 | val currency: String
17 | val color: String
18 | val imageUrl: String
19 | }
20 |
21 | interface HasSeats {
22 | val numberOfSeats: Int
23 | }
24 |
25 | interface HasMaterial {
26 | val material: String
27 | }
28 |
29 | data class ProductWithSeats(override val numberOfSeats: Int) : HasSeats
30 |
31 | data class ProductWithMaterial(override val material: String) : HasMaterial
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/entities/ProductShoppingEntity.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.entities
2 |
3 | import androidx.room.*
4 | import com.lucasmontano.shopping.utilities.DATABASE_TABLE_CART
5 | import java.util.*
6 |
7 | @Entity(
8 | tableName = DATABASE_TABLE_CART,
9 | foreignKeys = [
10 | ForeignKey(
11 | entity = ProductEntity::class,
12 | parentColumns = ["id"],
13 | childColumns = ["product_id"]
14 | )
15 | ],
16 | indices = [Index("product_id")]
17 | )
18 | data class ProductShoppingEntity(
19 | @ColumnInfo(name = "product_id") val productId: String,
20 | @ColumnInfo(name = "quantity") val quantity: Int,
21 | @ColumnInfo(name = "added_cart_date") val addedToCartDate: Calendar = Calendar.getInstance()
22 | ) {
23 | @PrimaryKey(autoGenerate = true)
24 | @ColumnInfo(name = "id")
25 | var productShoppingId: Long = 0
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/mappers/ModelMappers.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.mappers
2 |
3 | import com.lucasmontano.shopping.data.domain.*
4 | import com.lucasmontano.shopping.data.entities.ProductEntity
5 |
6 | fun ProductEntity.toDomainModel(): HasBasicProductAttr? {
7 | val productAttr = ProductDomainModel(
8 | id = productId,
9 | name = name,
10 | imageUrl = imageUrl,
11 | price = 0.0,
12 | currency = "",
13 | color = ""
14 | )
15 | return when (type) {
16 | "chair" -> {
17 | ChairDomainModel(
18 | productAttr = productAttr,
19 | materialAttr = ProductWithMaterial("")
20 | )
21 | }
22 | "couch" -> {
23 | CouchDomainModel(
24 | productAttr = productAttr,
25 | seatsAttr = ProductWithSeats(2)
26 | )
27 | }
28 | else -> null
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/domain/CouchDomainModel.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.domain
2 |
3 | data class CouchDomainModel(
4 | private val productAttr: HasBasicProductAttr,
5 | private val seatsAttr: HasSeats,
6 | ) : HasBasicProductAttr by productAttr, HasSeats by seatsAttr {
7 |
8 | override val id = productAttr.id
9 | override val name = productAttr.name
10 |
11 | override fun equals(other: Any?): Boolean {
12 | if (this === other) return true
13 | if (javaClass != other?.javaClass) return false
14 |
15 | other as HasBasicProductAttr
16 |
17 | if (id != other.id) return false
18 | if (name != other.name) return false
19 |
20 | return true
21 | }
22 |
23 | // I need to Read Effective Java to understand that better :)
24 | override fun hashCode(): Int {
25 | var result = id.hashCode()
26 | result = 31 * result + name.hashCode()
27 | return result
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/domain/ChairDomainModel.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.domain
2 |
3 | data class ChairDomainModel(
4 | private val productAttr: HasBasicProductAttr,
5 | private val materialAttr: HasMaterial
6 | ) : HasBasicProductAttr by productAttr, HasMaterial by materialAttr {
7 |
8 | override val id = productAttr.id
9 | override val name = productAttr.name
10 |
11 | override fun equals(other: Any?): Boolean {
12 | if (this === other) return true
13 | if (javaClass != other?.javaClass) return false
14 |
15 | other as HasBasicProductAttr
16 |
17 | if (id != other.id) return false
18 | if (name != other.name) return false
19 |
20 | return true
21 | }
22 |
23 | // I need to Read Effective Java to understand that better :)
24 | override fun hashCode(): Int {
25 | var result = id.hashCode()
26 | result = 31 * result + name.hashCode()
27 | return result
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_product_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
13 |
14 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/dao/ProductDao.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.dao
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.Dao
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 | import androidx.room.Query
8 | import com.lucasmontano.shopping.data.entities.ProductEntity
9 | import com.lucasmontano.shopping.utilities.DATABASE_TABLE_PRODUCTS
10 |
11 | @Dao
12 | interface ProductDao {
13 | @Query("SELECT * FROM $DATABASE_TABLE_PRODUCTS ORDER BY name")
14 | fun getAllProducts(): LiveData>
15 |
16 | @Query("SELECT * FROM $DATABASE_TABLE_PRODUCTS WHERE type = :type ORDER BY name")
17 | fun getAllProductsByType(type: String): LiveData>
18 |
19 | @Query("SELECT * FROM $DATABASE_TABLE_PRODUCTS WHERE id = :productId")
20 | fun getProduct(productId: String): LiveData
21 |
22 | @Insert(onConflict = OnConflictStrategy.REPLACE)
23 | suspend fun insertAll(productList: List)
24 | }
25 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/Converters.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data
2 |
3 | import androidx.room.TypeConverter
4 | import com.lucasmontano.shopping.data.entities.ProductEntity
5 | import org.json.JSONObject
6 | import java.util.*
7 |
8 | /**
9 | * Type converters to allow Room to reference complex data types.
10 | */
11 | class Converters {
12 | @TypeConverter
13 | fun calendarToDatestamp(calendar: Calendar): Long = calendar.timeInMillis
14 |
15 | @TypeConverter
16 | fun datestampToCalendar(value: Long): Calendar =
17 | Calendar.getInstance().apply { timeInMillis = value }
18 |
19 | @TypeConverter
20 | fun priceToJsonStringify(value: ProductEntity.Price): String = JSONObject().apply {
21 | put("value", value.value)
22 | put("currency", value.currency)
23 | }.toString()
24 |
25 | @TypeConverter
26 | fun jsonStringifyToPrice(value: String): ProductEntity.Price {
27 | val priceJson = JSONObject(value)
28 | return ProductEntity.Price(
29 | value = priceJson.getDouble("value"),
30 | currency = priceJson.getString("currency")
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/repositories/ProductRepository.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.repositories
2 |
3 | import androidx.lifecycle.Transformations
4 | import com.lucasmontano.shopping.data.dao.ProductDao
5 | import com.lucasmontano.shopping.data.domain.HasBasicProductAttr
6 | import com.lucasmontano.shopping.data.entities.ProductEntity
7 | import com.lucasmontano.shopping.data.mappers.toDomainModel
8 | import javax.inject.Inject
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | class ProductRepository @Inject constructor(private val productDao: ProductDao) {
13 |
14 | fun getAllProducts(type: String? = null) = type?.run {
15 | Transformations.map(productDao.getAllProductsByType(this)) { products ->
16 | products.mapToDomainModel()
17 | }
18 | } ?: Transformations.map(productDao.getAllProducts()) { products ->
19 | products.mapToDomainModel()
20 | }
21 |
22 | fun getProduct(productId: String) = productDao.getProduct(productId)
23 |
24 | private fun List.mapToDomainModel(): List {
25 | return this.mapNotNull {
26 | it.toDomainModel()
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/lucasmontano/shopping/data/InitDatabaseWorkerTest.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data
2 |
3 | import android.content.Context
4 | import androidx.test.core.app.ApplicationProvider
5 | import androidx.work.ListenableWorker.Result
6 | import androidx.work.WorkManager
7 | import androidx.work.testing.TestListenableWorkerBuilder
8 | import com.lucasmontano.shopping.workers.InitDatabaseWorker
9 | import org.hamcrest.CoreMatchers.`is`
10 | import org.junit.Assert.assertThat
11 | import org.junit.Before
12 | import org.junit.Test
13 | import org.junit.runner.RunWith
14 | import org.junit.runners.JUnit4
15 |
16 | @RunWith(JUnit4::class)
17 | class InitDatabaseWorkerTest {
18 | private lateinit var context: Context
19 | private lateinit var workManager: WorkManager
20 |
21 | @Before
22 | fun setup() {
23 | context = ApplicationProvider.getApplicationContext()
24 | workManager = WorkManager.getInstance(context)
25 | }
26 |
27 | @Test
28 | fun testRefreshMainDataWork() {
29 | val worker = TestListenableWorkerBuilder(context).build()
30 | val result = worker.startWork().get()
31 | assertThat(result, `is`(Result.success()))
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/lucasmontano/shopping/MainTestRunner.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.lucasmontano.shopping
18 |
19 | import android.app.Application
20 | import android.content.Context
21 | import androidx.test.runner.AndroidJUnitRunner
22 | import dagger.hilt.android.testing.HiltTestApplication
23 |
24 | // A custom runner to set up the instrumented application class for tests.
25 | class MainTestRunner : AndroidJUnitRunner() {
26 |
27 | override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
28 | return super.newApplication(cl, HiltTestApplication::class.java.name, context)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/viewmodels/CartViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.viewmodels
2 |
3 | import androidx.hilt.lifecycle.ViewModelInject
4 | import androidx.lifecycle.*
5 | import com.lucasmontano.shopping.data.entities.ProductCartRelation
6 | import com.lucasmontano.shopping.data.repositories.CartRepository
7 | import kotlinx.coroutines.launch
8 |
9 | class CartViewModel @ViewModelInject internal constructor(
10 | private val cartRepository: CartRepository
11 | ) : ViewModel() {
12 |
13 | private val cart: LiveData> = cartRepository.getAllProducts()
14 |
15 | val uiState: LiveData = Transformations.switchMap(cart) {
16 | // TODO map to domain model
17 | var total = 0.0
18 | val currency = cart.value?.firstOrNull()?.product?.price?.currency ?: "EUR"
19 | cart.value?.forEach {
20 | total += it.product.price.value
21 | }
22 | MutableLiveData(ViewState("$currency $$total"))
23 | }
24 |
25 | fun addToCart(productId: String) = viewModelScope.launch {
26 | cartRepository.addProduct(productId)
27 | }
28 |
29 | fun removeFromCart(productId: String) = viewModelScope.launch {
30 | cartRepository.removeProduct(productId)
31 | }
32 |
33 | data class ViewState(val total: String)
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/dao/CartDao.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.dao
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.room.*
5 | import com.lucasmontano.shopping.data.entities.ProductCartRelation
6 | import com.lucasmontano.shopping.data.entities.ProductShoppingEntity
7 | import com.lucasmontano.shopping.utilities.DATABASE_TABLE_CART
8 | import com.lucasmontano.shopping.utilities.DATABASE_TABLE_PRODUCTS
9 |
10 | @Dao
11 | interface CartDao {
12 |
13 | @Query("SELECT * FROM $DATABASE_TABLE_CART")
14 | fun getProductsShopping(): LiveData>
15 |
16 | @Query("SELECT * FROM $DATABASE_TABLE_CART WHERE product_id = :productId LIMIT 1")
17 | fun getProductShopping(productId: String): ProductShoppingEntity?
18 |
19 | @Query("SELECT EXISTS(SELECT 1 FROM $DATABASE_TABLE_CART WHERE product_id = :productId LIMIT 1)")
20 | fun isAddedToCart(productId: String): LiveData
21 |
22 | @Transaction
23 | @Query("SELECT * FROM $DATABASE_TABLE_PRODUCTS WHERE id IN (SELECT DISTINCT(product_id) FROM $DATABASE_TABLE_CART)")
24 | fun getAllProducts(): LiveData>
25 |
26 | @Insert
27 | suspend fun addProduct(productShoppingEntity: ProductShoppingEntity): Long
28 |
29 | @Delete
30 | suspend fun removeProduct(productShoppingEntity: ProductShoppingEntity)
31 |
32 | @Update
33 | suspend fun updateProduct(productShoppingEntity: ProductShoppingEntity)
34 | }
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ui/adapters/AdapterDelegatesManager.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.ui.adapters
2 |
3 | import android.view.ViewGroup
4 | import androidx.recyclerview.widget.RecyclerView
5 |
6 | class AdapterDelegatesManager {
7 |
8 | private val delegates: HashMap> = HashMap()
9 |
10 | fun addDelegate(delegate: AdapterDelegate) {
11 | delegates[delegate.getViewType()] = delegate
12 | }
13 |
14 | fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
15 | return delegateForViewType(viewType)?.onCreateViewHolder(parent, viewType)
16 | ?: throw Exception("No Delegate Found for ViewType $viewType")
17 | }
18 |
19 | fun onBindViewHolder(item: T, holder: RecyclerView.ViewHolder, position: Int) {
20 | delegateForViewType(holder.itemViewType)?.onBindViewHolder(item, holder, position)
21 | ?: throw Exception("No Delegate Found for ViewType ${holder.itemViewType}")
22 | }
23 |
24 | fun onBindViewHolder(
25 | item: T,
26 | holder: RecyclerView.ViewHolder,
27 | position: Int,
28 | payload: MutableList
29 | ) {
30 | delegateForViewType(holder.itemViewType)?.onBindViewHolder(item, holder, position, payload)
31 | ?: throw Exception("No Delegate Found for ViewType ${holder.itemViewType}")
32 | }
33 |
34 | private fun delegateForViewType(viewType: Int): AdapterDelegate? = delegates.get(viewType)
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/repositories/CartRepository.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data.repositories
2 |
3 | import com.lucasmontano.shopping.data.dao.CartDao
4 | import com.lucasmontano.shopping.data.entities.ProductShoppingEntity
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | @Singleton
11 | class CartRepository @Inject constructor(
12 | private val cartDao: CartDao
13 | ) {
14 |
15 | suspend fun addProduct(productId: String, quantity: Int = 1) {
16 | withContext(Dispatchers.IO) {
17 | cartDao.getProductShopping(productId)?.let { shopping ->
18 | cartDao.updateProduct(shopping.copy(quantity = shopping.quantity - 2))
19 | } ?: cartDao.addProduct(ProductShoppingEntity(productId, quantity))
20 | }
21 | }
22 |
23 | suspend fun removeProduct(productId: String) {
24 | withContext(Dispatchers.IO) {
25 | cartDao.getProductShopping(productId)?.let { shopping ->
26 | if (shopping.quantity == 1) {
27 | cartDao.removeProduct(shopping)
28 | } else {
29 | cartDao.updateProduct(shopping.copy(quantity = shopping.quantity - 1))
30 | }
31 | }
32 | }
33 | }
34 |
35 | fun isOnCart(productId: String) =
36 | cartDao.isAddedToCart(productId)
37 |
38 | fun getAllProducts() = cartDao.getAllProducts()
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/test/java/com/lucasmontano/shopping/TestUtilities.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.lucasmontano.shopping
18 |
19 | import androidx.lifecycle.LiveData
20 | import java.util.concurrent.CountDownLatch
21 | import java.util.concurrent.TimeUnit
22 |
23 | /**
24 | * Helper method for testing LiveData objects, from
25 | * https://github.com/googlesamples/android-architecture-components.
26 | *
27 | * Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds.
28 | * Once we got a notification via onChanged, we stop observing.
29 | */
30 | @Throws(InterruptedException::class)
31 | fun getValueUnitTest(liveData: LiveData): T {
32 | val data = arrayOfNulls(1)
33 | val latch = CountDownLatch(1)
34 | liveData.observeForever { o ->
35 | data[0] = o
36 | latch.countDown()
37 | }
38 | latch.await(2, TimeUnit.SECONDS)
39 |
40 | @Suppress("UNCHECKED_CAST")
41 | return data[0] as T
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/lucasmontano/shopping/utilities/LiveDataTestUtil.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.lucasmontano.shopping.utilities
18 |
19 | import androidx.lifecycle.LiveData
20 | import java.util.concurrent.CountDownLatch
21 | import java.util.concurrent.TimeUnit
22 |
23 | /**
24 | * Helper method for testing LiveData objects, from
25 | * https://github.com/googlesamples/android-architecture-components.
26 | *
27 | * Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds.
28 | * Once we got a notification via onChanged, we stop observing.
29 | */
30 | @Throws(InterruptedException::class)
31 | fun getValue(liveData: LiveData): T {
32 | val data = arrayOfNulls(1)
33 | val latch = CountDownLatch(1)
34 | liveData.observeForever { o ->
35 | data[0] = o
36 | latch.countDown()
37 | }
38 | latch.await(2, TimeUnit.SECONDS)
39 |
40 | @Suppress("UNCHECKED_CAST")
41 | return data[0] as T
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/di/DatabaseModule.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.lucasmontano.shopping.di
18 |
19 | import android.content.Context
20 | import com.lucasmontano.shopping.data.AppDatabase
21 | import com.lucasmontano.shopping.data.dao.CartDao
22 | import com.lucasmontano.shopping.data.dao.ProductDao
23 | import dagger.Module
24 | import dagger.Provides
25 | import dagger.hilt.InstallIn
26 | import dagger.hilt.android.components.ApplicationComponent
27 | import dagger.hilt.android.qualifiers.ApplicationContext
28 | import javax.inject.Singleton
29 |
30 | @InstallIn(ApplicationComponent::class)
31 | @Module
32 | class DatabaseModule {
33 |
34 | @Singleton
35 | @Provides
36 | fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase {
37 | return AppDatabase.getInstance(context)
38 | }
39 |
40 | @Provides
41 | fun provideProductDao(appDatabase: AppDatabase): ProductDao {
42 | return appDatabase.productDao()
43 | }
44 |
45 | @Provides
46 | fun provideCartDao(appDatabase: AppDatabase): CartDao {
47 | return appDatabase.cartDao()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/workers/InitDatabaseWorker.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.workers
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.work.CoroutineWorker
6 | import androidx.work.WorkerParameters
7 | import com.google.gson.Gson
8 | import com.google.gson.reflect.TypeToken
9 | import com.google.gson.stream.JsonReader
10 | import com.lucasmontano.shopping.data.AppDatabase
11 | import com.lucasmontano.shopping.data.entities.ProductEntity
12 | import com.lucasmontano.shopping.utilities.DATA_FILENAME
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.coroutineScope
15 | import kotlinx.coroutines.withContext
16 |
17 | class InitDatabaseWorker(
18 | context: Context,
19 | workerParams: WorkerParameters
20 | ) : CoroutineWorker(context, workerParams) {
21 |
22 | override suspend fun doWork(): Result = coroutineScope {
23 | try {
24 | val productsWrapper: ProductsWrapper = withContext(Dispatchers.IO) {
25 | applicationContext.assets.open(DATA_FILENAME).use { inputStream ->
26 | JsonReader(inputStream.reader()).use { jsonReader ->
27 | val productType = object : TypeToken() {}.type
28 | Gson().fromJson(jsonReader, productType)
29 | }
30 | }
31 | }
32 |
33 | val database = AppDatabase.getInstance(applicationContext)
34 | database.productDao().insertAll(productsWrapper.products)
35 |
36 | Result.success()
37 | } catch (ex: Exception) {
38 | Log.e(TAG, "Error initializing database", ex)
39 | Result.failure()
40 | }
41 | }
42 |
43 | data class ProductsWrapper(val products: List)
44 |
45 | companion object {
46 | private val TAG = this::class.simpleName
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/data/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data
2 |
3 | import android.content.Context
4 | import androidx.room.Database
5 | import androidx.room.Room
6 | import androidx.room.RoomDatabase
7 | import androidx.room.TypeConverters
8 | import androidx.sqlite.db.SupportSQLiteDatabase
9 | import androidx.work.OneTimeWorkRequestBuilder
10 | import androidx.work.WorkManager
11 | import com.lucasmontano.shopping.data.dao.CartDao
12 | import com.lucasmontano.shopping.data.dao.ProductDao
13 | import com.lucasmontano.shopping.data.entities.ProductEntity
14 | import com.lucasmontano.shopping.data.entities.ProductShoppingEntity
15 | import com.lucasmontano.shopping.utilities.DATABASE_NAME
16 | import com.lucasmontano.shopping.workers.InitDatabaseWorker
17 |
18 | @Database(
19 | entities = [ProductShoppingEntity::class, ProductEntity::class],
20 | version = 1,
21 | exportSchema = false
22 | )
23 | @TypeConverters(Converters::class)
24 | abstract class AppDatabase : RoomDatabase() {
25 |
26 | abstract fun productDao(): ProductDao
27 | abstract fun cartDao(): CartDao
28 |
29 | companion object {
30 |
31 | @Volatile
32 | private var instance: AppDatabase? = null
33 |
34 | fun getInstance(context: Context): AppDatabase {
35 | return instance ?: synchronized(this) {
36 | instance ?: buildDatabase(context).also { instance = it }
37 | }
38 | }
39 |
40 | private fun buildDatabase(context: Context): AppDatabase {
41 | return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
42 | .addCallback(
43 | object : RoomDatabase.Callback() {
44 | override fun onCreate(db: SupportSQLiteDatabase) {
45 | super.onCreate(db)
46 | val request = OneTimeWorkRequestBuilder().build()
47 | WorkManager.getInstance(context).enqueue(request)
48 | }
49 | }
50 | )
51 | .build()
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_basic_product.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
27 |
28 |
35 |
36 |
45 |
46 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ui/adapters/DelegationAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.ui.adapters
2 |
3 | import android.view.ViewGroup
4 | import androidx.recyclerview.widget.DiffUtil
5 | import androidx.recyclerview.widget.ListAdapter
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.lucasmontano.shopping.ui.models.BasicProductUiModel
8 | import com.lucasmontano.shopping.ui.models.TiledUiModelProduct
9 |
10 | class DelegationAdapter(diffCallback: DiffUtil.ItemCallback) :
11 | ListAdapter(diffCallback), BindableAdapter> {
12 |
13 | private val adapterDelegatesManager: AdapterDelegatesManager = AdapterDelegatesManager()
14 |
15 | constructor(
16 | diffCallback: DiffUtil.ItemCallback,
17 | vararg delegate: AdapterDelegate
18 | ) : this(diffCallback) {
19 | delegate.forEach {
20 | adapterDelegatesManager.addDelegate(it)
21 | }
22 | }
23 |
24 | fun addDelegates(vararg delegate: AdapterDelegate) {
25 | delegate.forEach {
26 | adapterDelegatesManager.addDelegate(it)
27 | }
28 | }
29 |
30 | override fun getItemViewType(position: Int): Int {
31 | return when (this.getItem(position)) {
32 | is TiledUiModelProduct -> TiledUiModelProduct::class.hashCode()
33 | is BasicProductUiModel -> BasicProductUiModel::class.hashCode()
34 | else -> super.getItemViewType(position)
35 | }
36 | }
37 |
38 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
39 | return adapterDelegatesManager.onCreateViewHolder(parent, viewType)
40 | }
41 |
42 | override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
43 | adapterDelegatesManager.onBindViewHolder(getItem(position), holder, position)
44 | }
45 |
46 | override fun onBindViewHolder(
47 | holder: RecyclerView.ViewHolder,
48 | position: Int,
49 | payloads: MutableList
50 | ) {
51 | adapterDelegatesManager.onBindViewHolder(getItem(position), holder, position, payloads)
52 | }
53 |
54 | override fun setData(data: List?) {
55 | submitList(data)
56 | }
57 | }
58 |
59 | interface BindableAdapter {
60 | fun setData(data: T?)
61 | }
62 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/viewmodels/ProductListViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.viewmodels
2 |
3 | import androidx.hilt.lifecycle.ViewModelInject
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.Transformations
7 | import androidx.lifecycle.ViewModel
8 | import com.lucasmontano.shopping.data.domain.HasBasicProductAttr
9 | import com.lucasmontano.shopping.data.repositories.CartRepository
10 | import com.lucasmontano.shopping.data.repositories.ProductRepository
11 | import com.lucasmontano.shopping.ui.models.BasicProductUiModel
12 | import com.lucasmontano.shopping.ui.models.ProductUiModel
13 | import com.lucasmontano.shopping.ui.models.TiledUiModelProduct
14 |
15 | class ProductListViewModel @ViewModelInject internal constructor(
16 | productRepository: ProductRepository
17 | ) : ViewModel() {
18 |
19 | private val filter = MutableLiveData()
20 |
21 | private val products: LiveData> = Transformations.switchMap(filter) {
22 | when (filter.value) {
23 | is FilterState.Type -> {
24 | it as FilterState.Type
25 | Transformations.map(productRepository.getAllProducts(it.type)) { products ->
26 | products.toUiModel()
27 | }
28 | }
29 | else -> Transformations.map(productRepository.getAllProducts()) { products ->
30 | products.toUiModel()
31 | }
32 | }
33 | }
34 |
35 | val uiState: LiveData = Transformations.switchMap(products) {
36 | MutableLiveData(ViewState(it))
37 | }
38 |
39 | init {
40 | filter.postValue(FilterState.Empty)
41 | }
42 |
43 | fun clearFilter() = filter.postValue(FilterState.Empty)
44 |
45 | fun filterByType(type: String) = filter.postValue(FilterState.Type(type))
46 |
47 | private fun List.toUiModel(): List {
48 | return this.mapIndexed { index, productEntity ->
49 | when (index) {
50 | 0 -> TiledUiModelProduct(
51 | productEntity.id,
52 | productEntity.name,
53 | productEntity.imageUrl
54 | )
55 | else -> BasicProductUiModel(productEntity.id, productEntity.name)
56 | }
57 | }
58 | }
59 |
60 | data class ViewState(val products: List)
61 |
62 | private sealed class FilterState {
63 | object Empty : FilterState()
64 | data class Type(val type: String) : FilterState()
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_tiled_product.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
14 |
15 |
18 |
19 |
20 |
27 |
28 |
34 |
35 |
42 |
43 |
52 |
53 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ui/adapters/ProductAdapterDelegate.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.ui.adapters
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.databinding.DataBindingUtil
6 | import androidx.lifecycle.LifecycleOwner
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.lucasmontano.shopping.R
9 | import com.lucasmontano.shopping.databinding.ItemBasicProductBinding
10 | import com.lucasmontano.shopping.ui.models.BasicProductUiModel
11 | import com.lucasmontano.shopping.ui.models.ProductUiModel
12 |
13 | internal class ProductAdapterDelegate(
14 | private val viewLifecycleOwner: LifecycleOwner,
15 | val addToCartListener: (value: String) -> Unit,
16 | val removeFromCartListener: (value: String) -> Unit
17 | ) : AdapterDelegate() {
18 |
19 | override fun getViewType() = BasicProductUiModel::class.hashCode()
20 |
21 | override fun isForViewType(items: List, position: Int): Boolean {
22 | return items[position] is BasicProductUiModel
23 | }
24 |
25 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
26 | val binding: ItemBasicProductBinding = DataBindingUtil.inflate(
27 | LayoutInflater.from(parent.context),
28 | R.layout.item_basic_product,
29 | parent,
30 | false
31 | )
32 | return ViewHolder(binding)
33 | }
34 |
35 | inner class ViewHolder(private val binding: ItemBasicProductBinding) :
36 | RecyclerView.ViewHolder(binding.root) {
37 |
38 | init {
39 | binding.setAddProductListener {
40 | binding.item?.let { product ->
41 | addToCartListener.invoke(product.id)
42 | }
43 | }
44 | binding.setRemoveProductListener {
45 | binding.item?.let { product ->
46 | removeFromCartListener.invoke(product.id)
47 | }
48 | }
49 | }
50 |
51 | fun bind(product: BasicProductUiModel) {
52 | binding.apply {
53 | item = product
54 | executePendingBindings()
55 | }
56 | }
57 | }
58 |
59 | override fun onBindViewHolder(
60 | item: ProductUiModel,
61 | holder: RecyclerView.ViewHolder,
62 | position: Int,
63 | payloads: MutableList
64 | ) {
65 | (holder as ViewHolder).bind(item as BasicProductUiModel)
66 | }
67 |
68 | override fun onBindViewHolder(
69 | item: ProductUiModel,
70 | holder: RecyclerView.ViewHolder,
71 | position: Int
72 | ) {
73 | (holder as ViewHolder).bind(item as BasicProductUiModel)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ui/adapters/TileProductAdapterDelegate.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.ui.adapters
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.databinding.DataBindingUtil
6 | import androidx.lifecycle.LifecycleOwner
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.lucasmontano.shopping.R
9 | import com.lucasmontano.shopping.databinding.ItemTiledProductBinding
10 | import com.lucasmontano.shopping.ui.models.ProductUiModel
11 | import com.lucasmontano.shopping.ui.models.TiledUiModelProduct
12 |
13 | internal class TileProductAdapterDelegate(
14 | private val viewLifecycleOwner: LifecycleOwner,
15 | val addToCartListener: (value: String) -> Unit,
16 | val removeFromCartListener: (value: String) -> Unit
17 | ) : AdapterDelegate() {
18 |
19 | override fun getViewType() = TiledUiModelProduct::class.hashCode()
20 |
21 | override fun isForViewType(items: List, position: Int): Boolean {
22 | return items[position] is TiledUiModelProduct
23 | }
24 |
25 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
26 | val binding: ItemTiledProductBinding = DataBindingUtil.inflate(
27 | LayoutInflater.from(parent.context),
28 | R.layout.item_tiled_product,
29 | parent,
30 | false
31 | )
32 | return ViewHolder(binding)
33 | }
34 |
35 | inner class ViewHolder(private val binding: ItemTiledProductBinding) :
36 | RecyclerView.ViewHolder(binding.root) {
37 |
38 | init {
39 | binding.setAddProductListener {
40 | binding.item?.let { product ->
41 | addToCartListener.invoke(product.id)
42 | }
43 | }
44 | binding.setRemoveProductListener {
45 | binding.item?.let { product ->
46 | removeFromCartListener.invoke(product.id)
47 | }
48 | }
49 | }
50 |
51 | fun bind(product: TiledUiModelProduct) {
52 | binding.apply {
53 | item = product
54 | executePendingBindings()
55 | }
56 | }
57 | }
58 |
59 | override fun onBindViewHolder(
60 | item: ProductUiModel,
61 | holder: RecyclerView.ViewHolder,
62 | position: Int,
63 | payloads: MutableList
64 | ) {
65 | (holder as ViewHolder).bind(item as TiledUiModelProduct)
66 | }
67 |
68 | override fun onBindViewHolder(
69 | item: ProductUiModel,
70 | holder: RecyclerView.ViewHolder,
71 | position: Int
72 | ) {
73 | (holder as ViewHolder).bind(item as TiledUiModelProduct)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/lucasmontano/shopping/data/ProductDaoTest.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.data
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.room.Room
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import androidx.test.platform.app.InstrumentationRegistry
7 | import com.lucasmontano.shopping.data.dao.ProductDao
8 | import com.lucasmontano.shopping.data.entities.ProductEntity
9 | import com.lucasmontano.shopping.utilities.getValue
10 | import kotlinx.coroutines.runBlocking
11 | import org.hamcrest.Matchers.equalTo
12 | import org.junit.After
13 | import org.junit.Assert.assertThat
14 | import org.junit.Before
15 | import org.junit.Rule
16 | import org.junit.Test
17 | import org.junit.runner.RunWith
18 |
19 | @RunWith(AndroidJUnit4::class)
20 | class ProductDaoTest {
21 | private lateinit var database: AppDatabase
22 | private lateinit var dao: ProductDao
23 |
24 | private val productA = ProductEntity(
25 | "1",
26 | "Name A",
27 | "Type 1",
28 | "URL",
29 | ProductEntity.Price(10.0, "")
30 | )
31 |
32 | private val productB = ProductEntity(
33 | "2",
34 | "Name B",
35 | "Type 2",
36 | "URL",
37 | ProductEntity.Price(10.0, "")
38 | )
39 |
40 | private val productC = ProductEntity(
41 | "3",
42 | "Name C",
43 | "Type 1",
44 | "URL",
45 | ProductEntity.Price(10.0, "")
46 | )
47 |
48 | @get:Rule
49 | var instantTaskExecutorRule = InstantTaskExecutorRule()
50 |
51 | @Before
52 | fun createDb() = runBlocking {
53 | val context = InstrumentationRegistry.getInstrumentation().targetContext
54 | database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
55 | dao = database.productDao()
56 | dao.insertAll(listOf(productB, productC, productA))
57 | }
58 |
59 | @After
60 | fun closeDb() {
61 | database.close()
62 | }
63 |
64 | @Test
65 | fun testGetAllProducts() {
66 | val productList = getValue(dao.getAllProducts())
67 | assertThat(productList.size, equalTo(3))
68 | assertThat(productList[0], equalTo(productA))
69 | assertThat(productList[1], equalTo(productB))
70 | assertThat(productList[2], equalTo(productC))
71 | }
72 |
73 | @Test
74 | fun testGetProductsByType() {
75 | val productList = getValue(dao.getAllProductsByType("Type 1"))
76 | assertThat(productList.size, equalTo(2))
77 | assertThat(productList[0], equalTo(productA))
78 | assertThat(productList[1], equalTo(productC))
79 | }
80 |
81 | @Test
82 | fun testGetProduct() {
83 | assertThat(getValue(dao.getProduct(productA.productId)), equalTo(productA))
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/lucasmontano/shopping/repositories/CartRepositoriesTest.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.repositories
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.room.Room
5 | import androidx.test.ext.junit.runners.AndroidJUnit4
6 | import androidx.test.platform.app.InstrumentationRegistry
7 | import com.lucasmontano.shopping.data.AppDatabase
8 | import com.lucasmontano.shopping.data.dao.CartDao
9 | import com.lucasmontano.shopping.data.dao.ProductDao
10 | import com.lucasmontano.shopping.data.entities.ProductEntity
11 | import com.lucasmontano.shopping.data.repositories.CartRepository
12 | import com.lucasmontano.shopping.utilities.getValue
13 | import kotlinx.coroutines.runBlocking
14 | import org.hamcrest.Matchers.equalTo
15 | import org.junit.After
16 | import org.junit.Assert.assertThat
17 | import org.junit.Assert.assertTrue
18 | import org.junit.Before
19 | import org.junit.Rule
20 | import org.junit.Test
21 | import org.junit.runner.RunWith
22 |
23 | @RunWith(AndroidJUnit4::class)
24 | class CartRepositoriesTest {
25 | private lateinit var database: AppDatabase
26 | private lateinit var productDao: ProductDao
27 | private lateinit var dao: CartDao
28 | private lateinit var repository: CartRepository
29 |
30 | private val productA = ProductEntity(
31 | "1",
32 | "Name A",
33 | "Type 1",
34 | "URL",
35 | ProductEntity.Price(10.0, "")
36 | )
37 |
38 | private val productB = ProductEntity(
39 | "2",
40 | "Name B",
41 | "Type 2",
42 | "URL",
43 | ProductEntity.Price(10.0, "")
44 | )
45 |
46 | private val productC = ProductEntity(
47 | "3",
48 | "Name C",
49 | "Type 1",
50 | "URL",
51 | ProductEntity.Price(10.0, "")
52 | )
53 |
54 | @get:Rule
55 | var instantTaskExecutorRule = InstantTaskExecutorRule()
56 |
57 | @Before
58 | fun createDb() = runBlocking {
59 | val context = InstrumentationRegistry.getInstrumentation().targetContext
60 | database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
61 | productDao = database.productDao()
62 | productDao.insertAll(listOf(productB, productC, productA))
63 |
64 | dao = database.cartDao()
65 | repository = CartRepository(dao)
66 | }
67 |
68 | @After
69 | fun closeDb() {
70 | database.close()
71 | }
72 |
73 | @Test
74 | fun testAddProductToCart() = runBlocking {
75 | repository.addProduct(productA.productId, 1)
76 | val productList = getValue(dao.getAllProducts())
77 | assertThat(productList.size, equalTo(1))
78 | assertThat(productList[0].product, equalTo(productA))
79 | assertTrue(getValue(dao.isAddedToCart(productA.productId)))
80 | }
81 |
82 | @Test
83 | fun testGetAllCartProducts() = runBlocking {
84 | repository.addProduct(productA.productId, 1)
85 | repository.addProduct(productB.productId, 1)
86 | assertThat(getValue(repository.getAllProducts()).size, equalTo(2))
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/app/src/test/java/com/lucasmontano/shopping/viewmodels/ProductListViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.viewmodels
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import androidx.lifecycle.MutableLiveData
5 | import com.lucasmontano.shopping.data.domain.*
6 | import com.lucasmontano.shopping.data.repositories.ProductRepository
7 | import com.lucasmontano.shopping.getValueUnitTest
8 | import io.mockk.MockKAnnotations
9 | import io.mockk.every
10 | import io.mockk.impl.annotations.MockK
11 | import org.junit.Assert.assertTrue
12 | import org.junit.Before
13 | import org.junit.Rule
14 | import org.junit.Test
15 |
16 | class ProductListViewModelTest {
17 |
18 | @Rule
19 | @JvmField
20 | val instantExecutor = InstantTaskExecutorRule()
21 |
22 | @MockK(relaxed = true)
23 | lateinit var repository: ProductRepository
24 |
25 | private lateinit var viewModel: ProductListViewModel
26 |
27 | private val productA = ChairDomainModel(
28 | ProductDomainModel(
29 | id = "1",
30 | name = "Name A",
31 | price = 10.00,
32 | currency = "EUR",
33 | color = "blue",
34 | imageUrl = ""
35 | ),
36 | ProductWithMaterial(material = "Wood")
37 | )
38 | private val productB = CouchDomainModel(
39 | ProductDomainModel(
40 | id = "2",
41 | name = "Name B",
42 | price = 10.00,
43 | currency = "EUR",
44 | color = "blue",
45 | imageUrl = ""
46 | ),
47 | ProductWithSeats(numberOfSeats = 2)
48 | )
49 | private val productC = ChairDomainModel(
50 | ProductDomainModel(
51 | id = "3",
52 | name = "Name B",
53 | price = 10.00,
54 | currency = "EUR",
55 | color = "blue",
56 | imageUrl = ""
57 | ),
58 | ProductWithMaterial(material = "Wood")
59 | )
60 |
61 | @Before
62 | fun setUp() {
63 | MockKAnnotations.init(this)
64 |
65 | viewModel = ProductListViewModel(repository)
66 |
67 | every {
68 | repository.getAllProducts(null)
69 | } returns MutableLiveData(
70 | listOf(
71 | productA, productB, productC
72 | )
73 | )
74 |
75 | every {
76 | repository.getAllProducts("chair")
77 | } returns MutableLiveData(
78 | listOf(
79 | productA, productC
80 | )
81 | )
82 | }
83 |
84 | @Test
85 | @Throws(InterruptedException::class)
86 | fun `when initializing the viewmodel, default products are listed`() {
87 | assertTrue(getValueUnitTest(viewModel.uiState).products.isNotEmpty())
88 | }
89 |
90 | @Test
91 | @Throws(InterruptedException::class)
92 | fun `when filtering by type, repository returns the right products`() {
93 | viewModel.filterByType("chair")
94 | assertTrue(getValueUnitTest(viewModel.uiState).products.size == 2)
95 | }
96 |
97 | @Test
98 | @Throws(InterruptedException::class)
99 | fun `when clearing filter, all products are available`() {
100 | viewModel.filterByType("chair")
101 | viewModel.clearFilter()
102 | assertTrue(getValueUnitTest(viewModel.uiState).products.size == 3)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 | apply plugin: 'kotlin-kapt'
5 | apply plugin: 'dagger.hilt.android.plugin'
6 |
7 | android {
8 | compileSdkVersion 29
9 | buildToolsVersion "30.0.2"
10 |
11 | buildFeatures {
12 | dataBinding true
13 | }
14 |
15 | compileOptions {
16 | sourceCompatibility JavaVersion.VERSION_1_8
17 | targetCompatibility JavaVersion.VERSION_1_8
18 | }
19 |
20 | kotlinOptions {
21 | jvmTarget = JavaVersion.VERSION_1_8.toString()
22 | }
23 |
24 | defaultConfig {
25 | applicationId "com.lucasmontano.shopping"
26 | minSdkVersion 28
27 | targetSdkVersion 29
28 | versionCode 1
29 | versionName "1.0"
30 |
31 | // TODO change to MainTestRunner when implementing ui tests
32 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
33 | }
34 |
35 | buildTypes {
36 | release {
37 | minifyEnabled false
38 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
39 | }
40 | }
41 | }
42 |
43 | dependencies {
44 | def room_version = "2.2.5"
45 | def work_version = "2.4.0"
46 | def assistedInject_version = "0.5.2"
47 | def hilt_version = "2.28.3-alpha"
48 | def hiltViewModel_version = "1.0.0-alpha02"
49 | def nav_version = "2.3.0"
50 |
51 | implementation fileTree(dir: "libs", include: ["*.jar"])
52 |
53 | // Core
54 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
55 | implementation 'androidx.core:core-ktx:1.3.2'
56 | implementation 'androidx.appcompat:appcompat:1.2.0'
57 | implementation 'androidx.constraintlayout:constraintlayout:2.0.2'
58 |
59 | // Navigation
60 | implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
61 | implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
62 |
63 | // Tasks, Workers
64 | implementation "androidx.work:work-runtime-ktx:$work_version"
65 |
66 | // Data
67 | implementation "androidx.room:room-runtime:$room_version"
68 | kapt "androidx.room:room-compiler:$room_version"
69 | implementation "androidx.room:room-ktx:$room_version"
70 | implementation 'com.google.code.gson:gson:2.8.6'
71 |
72 | // DI
73 | compileOnly "com.squareup.inject:assisted-inject-annotations-dagger2:$assistedInject_version"
74 | kapt "com.squareup.inject:assisted-inject-processor-dagger2:$assistedInject_version"
75 | implementation "com.google.dagger:hilt-android:$hilt_version"
76 | kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
77 | implementation "androidx.hilt:hilt-lifecycle-viewmodel:$hiltViewModel_version"
78 | kapt "androidx.hilt:hilt-compiler:$hiltViewModel_version"
79 |
80 | // Others
81 | implementation 'com.github.bumptech.glide:glide:4.11.0'
82 | annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
83 |
84 | // Test helpers
85 | testImplementation "androidx.room:room-testing:$room_version"
86 | testImplementation 'junit:junit:4.13'
87 | testImplementation "io.mockk:mockk:1.10.2"
88 | testImplementation 'androidx.arch.core:core-testing:2.1.0'
89 |
90 | androidTestImplementation 'androidx.arch.core:core-testing:2.1.0'
91 | androidTestImplementation "androidx.work:work-testing:$work_version"
92 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
93 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
94 |
95 | testImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
96 | kaptTest "com.google.dagger:hilt-android-compiler:$hilt_version"
97 |
98 | androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"
99 | androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
100 | kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
101 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lucasmontano/shopping/ui/ProductListFragment.kt:
--------------------------------------------------------------------------------
1 | package com.lucasmontano.shopping.ui
2 |
3 | import android.os.Bundle
4 | import android.view.*
5 | import android.widget.TextView
6 | import androidx.fragment.app.Fragment
7 | import androidx.fragment.app.viewModels
8 | import androidx.recyclerview.widget.DiffUtil
9 | import androidx.recyclerview.widget.LinearLayoutManager
10 | import com.lucasmontano.shopping.R
11 | import com.lucasmontano.shopping.databinding.FragmentProductListBinding
12 | import com.lucasmontano.shopping.ui.adapters.DelegationAdapter
13 | import com.lucasmontano.shopping.ui.adapters.ProductAdapterDelegate
14 | import com.lucasmontano.shopping.ui.adapters.TileProductAdapterDelegate
15 | import com.lucasmontano.shopping.ui.models.ProductUiModel
16 | import com.lucasmontano.shopping.viewmodels.CartViewModel
17 | import com.lucasmontano.shopping.viewmodels.ProductListViewModel
18 | import dagger.hilt.android.AndroidEntryPoint
19 |
20 | @AndroidEntryPoint
21 | class ProductListFragment : Fragment() {
22 |
23 | private val viewModel: ProductListViewModel by viewModels()
24 |
25 | // TODO move this to a new ui component
26 | private val cartViewModel: CartViewModel by viewModels()
27 |
28 | private lateinit var binding: FragmentProductListBinding
29 |
30 | private val itemsDiff = object : DiffUtil.ItemCallback() {
31 | override fun areItemsTheSame(
32 | oldItem: ProductUiModel,
33 | newItem: ProductUiModel
34 | ): Boolean {
35 | return oldItem.id == newItem.id
36 | }
37 |
38 | override fun areContentsTheSame(
39 | oldItem: ProductUiModel,
40 | newItem: ProductUiModel
41 | ): Boolean {
42 | return oldItem.equals(newItem)
43 | }
44 | }
45 |
46 | override fun onCreateView(
47 | inflater: LayoutInflater,
48 | container: ViewGroup?,
49 | savedInstanceState: Bundle?
50 | ): View? {
51 | binding = FragmentProductListBinding.inflate(inflater, container, false)
52 | context ?: return binding.root
53 |
54 | binding.lifecycleOwner = viewLifecycleOwner
55 |
56 | val delegationAdapter = DelegationAdapter(
57 | itemsDiff,
58 | TileProductAdapterDelegate(
59 | viewLifecycleOwner,
60 | addProductToCart(),
61 | removeProductToCart()
62 | ),
63 | ProductAdapterDelegate(
64 | viewLifecycleOwner,
65 | addProductToCart(),
66 | removeProductToCart()
67 | )
68 | )
69 | binding.productsRecyclerView.adapter = delegationAdapter
70 | binding.productsRecyclerView.layoutManager =
71 | LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
72 |
73 | subscribeUiToProducts(delegationAdapter)
74 | subscribeUiToCart(binding.cartTotalTextView)
75 |
76 | setHasOptionsMenu(true)
77 | return binding.root
78 | }
79 |
80 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
81 | inflater.inflate(R.menu.menu_product_list, menu)
82 | }
83 |
84 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
85 | return when (item.itemId) {
86 | R.id.filter_type -> {
87 | // TODO filter by type
88 | true
89 | }
90 | else -> super.onOptionsItemSelected(item)
91 | }
92 | }
93 |
94 | private fun addProductToCart(): (value: String) -> Unit {
95 | return { productId ->
96 | cartViewModel.addToCart(productId)
97 | }
98 | }
99 |
100 | private fun removeProductToCart(): (value: String) -> Unit {
101 | return { productId ->
102 | cartViewModel.removeFromCart(productId)
103 | }
104 | }
105 |
106 | private fun subscribeUiToCart(cartTotalTextView: TextView) {
107 | cartViewModel.uiState.observe(viewLifecycleOwner) { uiState ->
108 | cartTotalTextView.text = uiState.total
109 | }
110 | }
111 |
112 | private fun subscribeUiToProducts(adapter: DelegationAdapter) {
113 | viewModel.uiState.observe(viewLifecycleOwner) { uiState ->
114 | adapter.setData(uiState.products)
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | xmlns:android
33 |
34 | ^$
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | xmlns:.*
44 |
45 | ^$
46 |
47 |
48 | BY_NAME
49 |
50 |
51 |
52 |
53 |
54 |
55 | .*:id
56 |
57 | http://schemas.android.com/apk/res/android
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | .*:name
67 |
68 | http://schemas.android.com/apk/res/android
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | name
78 |
79 | ^$
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | style
89 |
90 | ^$
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | .*
100 |
101 | ^$
102 |
103 |
104 | BY_NAME
105 |
106 |
107 |
108 |
109 |
110 |
111 | .*
112 |
113 | http://schemas.android.com/apk/res/android
114 |
115 |
116 | ANDROID_ATTRIBUTE_ORDER
117 |
118 |
119 |
120 |
121 |
122 |
123 | .*
124 |
125 | .*
126 |
127 |
128 | BY_NAME
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------