diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7f5f55408..4155a3b7b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -72,8 +72,10 @@ dependencies { // Mockk testImplementation("io.mockk:mockk:1.13.5") androidTestImplementation("io.mockk:mockk-android:1.13.5") + // DI Library + implementation(project(":bunadi")) // Reflection - implementation("org.jetbrains.kotlin", "kotlin-reflect", "1.8.21") + implementation(kotlin("reflect")) } kapt { diff --git a/app/src/main/java/woowacourse/shopping/ShoppingApplication.kt b/app/src/main/java/woowacourse/shopping/ShoppingApplication.kt index 9a50dc565..ed7faf4fd 100644 --- a/app/src/main/java/woowacourse/shopping/ShoppingApplication.kt +++ b/app/src/main/java/woowacourse/shopping/ShoppingApplication.kt @@ -1,19 +1,14 @@ package woowacourse.shopping import android.app.Application -import woowacourse.shopping.data.repository.DefaultCartRepository -import woowacourse.shopping.data.repository.DefaultProductRepository -import woowacourse.shopping.di.injector.modules -import woowacourse.shopping.repository.CartRepository -import woowacourse.shopping.repository.ProductRepository +import com.woowacourse.bunadi.dsl.modules +import woowacourse.shopping.ui.common.di.module.DaoModule class ShoppingApplication : Application() { override fun onCreate() { super.onCreate() - modules { - inject(DefaultProductRepository()) - inject(DefaultCartRepository()) + module(DaoModule(this@ShoppingApplication)) } } } diff --git a/app/src/main/java/woowacourse/shopping/data/ShoppingDatabase.kt b/app/src/main/java/woowacourse/shopping/data/ShoppingDatabase.kt index e898bbc43..4b154e9e7 100644 --- a/app/src/main/java/woowacourse/shopping/data/ShoppingDatabase.kt +++ b/app/src/main/java/woowacourse/shopping/data/ShoppingDatabase.kt @@ -6,4 +6,8 @@ import androidx.room.RoomDatabase @Database(entities = [CartProductEntity::class], version = 1, exportSchema = false) abstract class ShoppingDatabase : RoomDatabase() { abstract fun cartProductDao(): CartProductDao + + companion object { + const val DATABASE_NAME = "cart-database" + } } diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/CartMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/CartMapper.kt new file mode 100644 index 000000000..2ea07ef1a --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/mapper/CartMapper.kt @@ -0,0 +1,23 @@ +package woowacourse.shopping.data.mapper + +import woowacourse.shopping.data.CartProductEntity +import woowacourse.shopping.model.CartProduct +import woowacourse.shopping.model.Product + +fun Product.toEntity(): CartProductEntity = CartProductEntity( + name = name, + price = price, + imageUrl = imageUrl, +) + +fun List.toDomain(): List = map { cartProductEntity -> + CartProduct( + product = Product( + name = cartProductEntity.name, + price = cartProductEntity.price, + imageUrl = cartProductEntity.imageUrl, + ), + id = cartProductEntity.id, + createdAt = cartProductEntity.createdAt, + ) +} diff --git a/app/src/main/java/woowacourse/shopping/data/mapper/ProductMapper.kt b/app/src/main/java/woowacourse/shopping/data/mapper/ProductMapper.kt deleted file mode 100644 index 03855de9e..000000000 --- a/app/src/main/java/woowacourse/shopping/data/mapper/ProductMapper.kt +++ /dev/null @@ -1,12 +0,0 @@ -package woowacourse.shopping.data.mapper - -import woowacourse.shopping.data.CartProductEntity -import woowacourse.shopping.model.Product - -fun Product.toEntity(): CartProductEntity { - return CartProductEntity( - name = name, - price = price, - imageUrl = imageUrl, - ) -} diff --git a/app/src/main/java/woowacourse/shopping/data/repository/DatabaseCartRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/DatabaseCartRepository.kt new file mode 100644 index 000000000..07875bb14 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/repository/DatabaseCartRepository.kt @@ -0,0 +1,26 @@ +package woowacourse.shopping.data.repository + +import com.woowacourse.bunadi.annotation.Singleton +import woowacourse.shopping.data.CartProductDao +import woowacourse.shopping.data.mapper.toDomain +import woowacourse.shopping.data.mapper.toEntity +import woowacourse.shopping.model.CartProduct +import woowacourse.shopping.model.Product +import woowacourse.shopping.repository.CartRepository + +@Singleton +class DatabaseCartRepository( + private val dao: CartProductDao, +) : CartRepository { + override suspend fun addCartProduct(product: Product) { + dao.insert(product.toEntity()) + } + + override suspend fun getAllCartProducts(): List { + return dao.getAll().toDomain() + } + + override suspend fun deleteCartProduct(id: Long) { + dao.delete(id) + } +} diff --git a/app/src/main/java/woowacourse/shopping/data/repository/DefaultCartRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/DefaultCartRepository.kt deleted file mode 100644 index 4b5e25027..000000000 --- a/app/src/main/java/woowacourse/shopping/data/repository/DefaultCartRepository.kt +++ /dev/null @@ -1,18 +0,0 @@ -package woowacourse.shopping.data.repository - -import woowacourse.shopping.model.Product -import woowacourse.shopping.repository.CartRepository - -class DefaultCartRepository : CartRepository { - private val cartProducts: MutableList = mutableListOf() - - override fun addCartProduct(product: Product) { - cartProducts.add(product) - } - - override fun getAllCartProducts(): List = cartProducts.toList() - - override fun deleteCartProduct(id: Int) { - cartProducts.removeAt(id) - } -} diff --git a/app/src/main/java/woowacourse/shopping/data/repository/DefaultProductRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/DefaultProductRepository.kt index 5fc51701b..1c5a7bdcc 100644 --- a/app/src/main/java/woowacourse/shopping/data/repository/DefaultProductRepository.kt +++ b/app/src/main/java/woowacourse/shopping/data/repository/DefaultProductRepository.kt @@ -1,8 +1,10 @@ package woowacourse.shopping.data.repository +import com.woowacourse.bunadi.annotation.Singleton import woowacourse.shopping.model.Product import woowacourse.shopping.repository.ProductRepository +@Singleton class DefaultProductRepository : ProductRepository { private val products: List = listOf( Product( diff --git a/app/src/main/java/woowacourse/shopping/data/repository/InMemoryCartRepository.kt b/app/src/main/java/woowacourse/shopping/data/repository/InMemoryCartRepository.kt new file mode 100644 index 000000000..2bcd158fd --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/data/repository/InMemoryCartRepository.kt @@ -0,0 +1,27 @@ +package woowacourse.shopping.data.repository + +import com.woowacourse.bunadi.annotation.Singleton +import woowacourse.shopping.data.CartProductEntity +import woowacourse.shopping.data.mapper.toDomain +import woowacourse.shopping.data.mapper.toEntity +import woowacourse.shopping.model.CartProduct +import woowacourse.shopping.model.Product +import woowacourse.shopping.repository.CartRepository + +@Singleton +class InMemoryCartRepository : CartRepository { + private val cartProducts = mutableListOf() + private var lastId: Long = 0 + + override suspend fun addCartProduct(product: Product) { + cartProducts.add(product.toEntity().apply { id = ++lastId }) + } + + override suspend fun getAllCartProducts(): List { + return cartProducts.toDomain() + } + + override suspend fun deleteCartProduct(id: Long) { + cartProducts.removeIf { it.id == id } + } +} diff --git a/app/src/main/java/woowacourse/shopping/di/injector/DependencyInjector.kt b/app/src/main/java/woowacourse/shopping/di/injector/DependencyInjector.kt deleted file mode 100644 index dffc7ae0b..000000000 --- a/app/src/main/java/woowacourse/shopping/di/injector/DependencyInjector.kt +++ /dev/null @@ -1,46 +0,0 @@ -package woowacourse.shopping.di.injector - -import woowacourse.shopping.di.util.validateHasPrimaryConstructor -import kotlin.reflect.KParameter -import kotlin.reflect.javaType - -object ClassInjector { - val dependencies = mutableMapOf, Any>() - - inline fun inject(instance: T) { - dependencies[T::class.java] = instance - } - - inline fun inject(): T { - val primaryConstructor = validateHasPrimaryConstructor() - val parameterValues = getParameterValues(primaryConstructor.parameters) - - return primaryConstructor.call(*parameterValues.toTypedArray()) - } - - @OptIn(ExperimentalStdlibApi::class) - fun getParameterValues(parameters: List): MutableList { - val parameterTypes = parameters.map { it.type } - val parameterValues = mutableListOf() - - parameterTypes.forEach { paramType -> - val parameterType = paramType.javaType - val parameterValue = dependencies[parameterType] - - requireNotNull(parameterValue) { "[ERROR] 주입할 의존성이 존재하지 않습니다." } - parameterValues.add(parameterValue) - } - - return parameterValues - } -} - -class ClassInjectorDsl { - inline fun inject(instance: T) { - ClassInjector.inject(instance) - } -} - -fun modules(block: ClassInjectorDsl.() -> Unit) { - ClassInjectorDsl().apply(block) -} diff --git a/app/src/main/java/woowacourse/shopping/di/util/UtilFunctions.kt b/app/src/main/java/woowacourse/shopping/di/util/UtilFunctions.kt deleted file mode 100644 index 31b89afa4..000000000 --- a/app/src/main/java/woowacourse/shopping/di/util/UtilFunctions.kt +++ /dev/null @@ -1,9 +0,0 @@ -package woowacourse.shopping.di.util - -import kotlin.reflect.KFunction -import kotlin.reflect.full.primaryConstructor - -inline fun validateHasPrimaryConstructor(): KFunction { - val primaryConstructor = T::class.primaryConstructor - return requireNotNull(primaryConstructor) { "[ERROR] 주생성자가 존재하지 않습니다." } -} diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt index 5bf18d004..3b5f992de 100644 --- a/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt +++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt @@ -5,7 +5,7 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import woowacourse.shopping.R import woowacourse.shopping.databinding.ActivityCartBinding -import woowacourse.shopping.di.lazy.viewModel +import woowacourse.shopping.ui.util.viewModel.viewModel class CartActivity : AppCompatActivity() { private val binding by lazy { ActivityCartBinding.inflate(layoutInflater) } diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartProductAdapter.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartProductAdapter.kt index 272acf333..bbf98ae86 100644 --- a/app/src/main/java/woowacourse/shopping/ui/cart/CartProductAdapter.kt +++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartProductAdapter.kt @@ -2,18 +2,18 @@ package woowacourse.shopping.ui.cart import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import woowacourse.shopping.model.Product +import woowacourse.shopping.model.CartProduct class CartProductAdapter( - items: List, - onClickDelete: (position: Int) -> Unit, + items: List, + onClickDelete: (id: Long) -> Unit, private val dateFormatter: DateFormatter, ) : RecyclerView.Adapter() { - private val items: MutableList = items.toMutableList() + private val items: MutableList = items.toMutableList() - private val onClickDelete = { position: Int -> - onClickDelete(position) + private val onClickDelete = { id: Long, position: Int -> + onClickDelete(id) removeItem(position) } diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartProductViewHolder.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartProductViewHolder.kt index aef478fa5..6f04b3d70 100644 --- a/app/src/main/java/woowacourse/shopping/ui/cart/CartProductViewHolder.kt +++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartProductViewHolder.kt @@ -4,31 +4,30 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import woowacourse.shopping.databinding.ItemCartProductBinding -import woowacourse.shopping.model.Product +import woowacourse.shopping.model.CartProduct class CartProductViewHolder( private val binding: ItemCartProductBinding, - private val dateFormatter: DateFormatter, - onClickDelete: (position: Int) -> Unit, + dateFormatter: DateFormatter, + onClickDelete: (id: Long, position: Int) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { init { + binding.dateFormatter = dateFormatter binding.ivCartProductDelete.setOnClickListener { - val position = adapterPosition - onClickDelete(position) + onClickDelete(binding.item!!.id, adapterPosition) } } - fun bind(product: Product) { - binding.item = product - // TODO: Step2 - dateFormatter를 활용하여 상품이 담긴 날짜와 시간을 출력하도록 변경 + fun bind(cartProduct: CartProduct) { + binding.item = cartProduct } companion object { fun from( parent: ViewGroup, dateFormatter: DateFormatter, - onClickDelete: (position: Int) -> Unit, + onClickDelete: (id: Long, position: Int) -> Unit, ): CartProductViewHolder { val binding = ItemCartProductBinding .inflate(LayoutInflater.from(parent.context), parent, false) diff --git a/app/src/main/java/woowacourse/shopping/ui/cart/CartViewModel.kt b/app/src/main/java/woowacourse/shopping/ui/cart/CartViewModel.kt index dc5af010a..31b165323 100644 --- a/app/src/main/java/woowacourse/shopping/ui/cart/CartViewModel.kt +++ b/app/src/main/java/woowacourse/shopping/ui/cart/CartViewModel.kt @@ -3,26 +3,33 @@ package woowacourse.shopping.ui.cart import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import woowacourse.shopping.model.Product +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import woowacourse.shopping.model.CartProduct import woowacourse.shopping.repository.CartRepository +import woowacourse.shopping.ui.common.di.qualifier.DatabaseCartRepositoryQualifier class CartViewModel( - private val cartRepository: CartRepository, + @DatabaseCartRepositoryQualifier val cartRepository: CartRepository, ) : ViewModel() { - private val _cartProducts: MutableLiveData> = + private val _cartProducts: MutableLiveData> = MutableLiveData(emptyList()) - val cartProducts: LiveData> get() = _cartProducts + val cartProducts: LiveData> get() = _cartProducts private val _onCartProductDeleted: MutableLiveData = MutableLiveData(false) val onCartProductDeleted: LiveData get() = _onCartProductDeleted fun fetchAllCartProducts() { - _cartProducts.value = cartRepository.getAllCartProducts() + viewModelScope.launch { + _cartProducts.value = cartRepository.getAllCartProducts() + } } - fun deleteCartProduct(id: Int) { - cartRepository.deleteCartProduct(id) - _onCartProductDeleted.value = true + fun deleteCartProduct(id: Long) { + viewModelScope.launch { + cartRepository.deleteCartProduct(id) + _onCartProductDeleted.value = true + } } } diff --git a/app/src/main/java/woowacourse/shopping/ui/common/di/module/DaoModule.kt b/app/src/main/java/woowacourse/shopping/ui/common/di/module/DaoModule.kt new file mode 100644 index 000000000..be36e560e --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/common/di/module/DaoModule.kt @@ -0,0 +1,15 @@ +package woowacourse.shopping.ui.common.di.module + +import android.content.Context +import androidx.room.Room +import com.woowacourse.bunadi.module.Module +import woowacourse.shopping.data.CartProductDao +import woowacourse.shopping.data.ShoppingDatabase + +class DaoModule(private val context: Context) : Module { + fun provideCartProductDao(): CartProductDao = Room.databaseBuilder( + context, + ShoppingDatabase::class.java, + ShoppingDatabase.DATABASE_NAME, + ).build().cartProductDao() +} diff --git a/app/src/main/java/woowacourse/shopping/ui/common/di/qualifier/Qualifier.kt b/app/src/main/java/woowacourse/shopping/ui/common/di/qualifier/Qualifier.kt new file mode 100644 index 000000000..bb57cae19 --- /dev/null +++ b/app/src/main/java/woowacourse/shopping/ui/common/di/qualifier/Qualifier.kt @@ -0,0 +1,15 @@ +package woowacourse.shopping.ui.common.di.qualifier + +import com.woowacourse.bunadi.annotation.Qualifier +import woowacourse.shopping.data.repository.DatabaseCartRepository +import woowacourse.shopping.data.repository.DefaultProductRepository +import woowacourse.shopping.data.repository.InMemoryCartRepository + +@Qualifier(DefaultProductRepository::class) +annotation class DefaultProductRepositoryQualifier + +@Qualifier(InMemoryCartRepository::class) +annotation class InMemoryCartRepositoryQualifier + +@Qualifier(DatabaseCartRepository::class) +annotation class DatabaseCartRepositoryQualifier diff --git a/app/src/main/java/woowacourse/shopping/ui/main/MainActivity.kt b/app/src/main/java/woowacourse/shopping/ui/main/MainActivity.kt index 20f81891c..f7085c5f3 100644 --- a/app/src/main/java/woowacourse/shopping/ui/main/MainActivity.kt +++ b/app/src/main/java/woowacourse/shopping/ui/main/MainActivity.kt @@ -7,8 +7,8 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import woowacourse.shopping.R import woowacourse.shopping.databinding.ActivityMainBinding -import woowacourse.shopping.di.lazy.viewModel import woowacourse.shopping.ui.cart.CartActivity +import woowacourse.shopping.ui.util.viewModel.viewModel class MainActivity : AppCompatActivity() { private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) } diff --git a/app/src/main/java/woowacourse/shopping/ui/main/MainViewModel.kt b/app/src/main/java/woowacourse/shopping/ui/main/MainViewModel.kt index 6d2ee66f4..8b154ee0a 100644 --- a/app/src/main/java/woowacourse/shopping/ui/main/MainViewModel.kt +++ b/app/src/main/java/woowacourse/shopping/ui/main/MainViewModel.kt @@ -3,13 +3,17 @@ package woowacourse.shopping.ui.main import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import woowacourse.shopping.model.Product import woowacourse.shopping.repository.CartRepository import woowacourse.shopping.repository.ProductRepository +import woowacourse.shopping.ui.common.di.qualifier.DatabaseCartRepositoryQualifier +import woowacourse.shopping.ui.common.di.qualifier.DefaultProductRepositoryQualifier class MainViewModel( - private val productRepository: ProductRepository, - private val cartRepository: CartRepository, + @DefaultProductRepositoryQualifier private val productRepository: ProductRepository, + @DatabaseCartRepositoryQualifier private val cartRepository: CartRepository, ) : ViewModel() { private val _products: MutableLiveData> = MutableLiveData(emptyList()) val products: LiveData> get() = _products @@ -18,8 +22,10 @@ class MainViewModel( val onProductAdded: LiveData get() = _onProductAdded fun addCartProduct(product: Product) { - cartRepository.addCartProduct(product) - _onProductAdded.value = true + viewModelScope.launch { + cartRepository.addCartProduct(product) + _onProductAdded.value = true + } } fun fetchAllProducts() { diff --git a/app/src/main/java/woowacourse/shopping/di/lazy/ActivityViewModelLazy.kt b/app/src/main/java/woowacourse/shopping/ui/util/viewModel/ActivityViewModelLazy.kt similarity index 69% rename from app/src/main/java/woowacourse/shopping/di/lazy/ActivityViewModelLazy.kt rename to app/src/main/java/woowacourse/shopping/ui/util/viewModel/ActivityViewModelLazy.kt index 2c0b64210..39c5c724c 100644 --- a/app/src/main/java/woowacourse/shopping/di/lazy/ActivityViewModelLazy.kt +++ b/app/src/main/java/woowacourse/shopping/ui/util/viewModel/ActivityViewModelLazy.kt @@ -1,10 +1,9 @@ -package woowacourse.shopping.di.lazy +package woowacourse.shopping.ui.util.viewModel import androidx.activity.ComponentActivity import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelLazy -import woowacourse.shopping.di.injector.ClassInjector -import woowacourse.shopping.di.util.viewModelFactory +import com.woowacourse.bunadi.injector.DependencyInjector inline fun ComponentActivity.viewModel(): Lazy { return ViewModelLazy( @@ -15,5 +14,5 @@ inline fun ComponentActivity.viewModel(): Lazy { } inline fun createViewModel(): VM { - return ClassInjector.inject() + return DependencyInjector.inject(VM::class) } diff --git a/app/src/main/java/woowacourse/shopping/di/util/ViewModelFactory.kt b/app/src/main/java/woowacourse/shopping/ui/util/viewModel/ViewModelFactory.kt similarity index 88% rename from app/src/main/java/woowacourse/shopping/di/util/ViewModelFactory.kt rename to app/src/main/java/woowacourse/shopping/ui/util/viewModel/ViewModelFactory.kt index 563a01869..93ef11153 100644 --- a/app/src/main/java/woowacourse/shopping/di/util/ViewModelFactory.kt +++ b/app/src/main/java/woowacourse/shopping/ui/util/viewModel/ViewModelFactory.kt @@ -1,4 +1,4 @@ -package woowacourse.shopping.di.util +package woowacourse.shopping.ui.util.viewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider diff --git a/app/src/main/res/layout/item_cart_product.xml b/app/src/main/res/layout/item_cart_product.xml index d09b4f553..c5715daa8 100644 --- a/app/src/main/res/layout/item_cart_product.xml +++ b/app/src/main/res/layout/item_cart_product.xml @@ -7,7 +7,11 @@ + type="woowacourse.shopping.model.CartProduct" /> + + { + DependencyInjector.inject(InterfacePropertyClass::class) + } + } + + @Test + fun 인터페이스_타입인_필드에_식별자_애노테이션을_추가하지_않으면_주입할_성_예외가_발생한다() { + // given: 인터페이스 타입을 필드로 가진 클래스가 존재한다. + class InterfaceFieldClass { + @Inject + private lateinit var dependency: Dependency + } + + // when: 클래스를 주입한다. + // then: 인터페이스 타입인 필드에 식별자 애노테이션이 없으므로 예외가 발생한다. + assertThrows { + DependencyInjector.inject(InterfaceFieldClass::class) + } + } +} diff --git a/app/src/test/java/woowacourse/fakeClasses/FakeConstructorDependency.kt b/app/src/test/java/woowacourse/fakeClasses/FakeConstructorDependency.kt new file mode 100644 index 000000000..53b41e33b --- /dev/null +++ b/app/src/test/java/woowacourse/fakeClasses/FakeConstructorDependency.kt @@ -0,0 +1,22 @@ +package woowacourse.fakeClasses + +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModel +import com.woowacourse.bunadi.annotation.Qualifier +import woowacourse.shopping.ui.util.viewModel.viewModel + +@Qualifier(AConstructorDependency::class) +annotation class AConstructorDependencyQualifier + +interface ConstructorDependency + +class AConstructorDependency : ConstructorDependency + +class ConstructorTestActivity : AppCompatActivity() { + val viewModel: ConstructorTestViewModel by viewModel() +} + +class ConstructorTestViewModel( + @AConstructorDependencyQualifier + val constructorDependency: ConstructorDependency, +) : ViewModel() diff --git a/app/src/test/java/woowacourse/fakeClasses/FakeFieldDependency.kt b/app/src/test/java/woowacourse/fakeClasses/FakeFieldDependency.kt new file mode 100644 index 000000000..69d24f39c --- /dev/null +++ b/app/src/test/java/woowacourse/fakeClasses/FakeFieldDependency.kt @@ -0,0 +1,31 @@ +package woowacourse.fakeClasses + +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModel +import com.woowacourse.bunadi.annotation.Inject +import com.woowacourse.bunadi.annotation.Qualifier +import woowacourse.shopping.ui.util.viewModel.viewModel + +interface FieldDependency + +class AFieldDependency : FieldDependency +class BFieldDependency : FieldDependency + +@Qualifier(AFieldDependency::class) +annotation class AFieldDependencyQualifier + +@Qualifier(BFieldDependency::class) +annotation class BFieldDependencyQualifier + +class FieldTestActivity : AppCompatActivity() { + val viewModel: FieldTestViewModel by viewModel() +} + +class FieldTestViewModel : ViewModel() { + @Inject + @AFieldDependencyQualifier + lateinit var fieldWithInjectAnnotation: FieldDependency + private lateinit var fieldWithoutInjectAnnotation: FieldDependency + + fun isFieldWithoutInjectAnnotationInitialized() = ::fieldWithoutInjectAnnotation.isInitialized +} diff --git a/app/src/test/java/woowacourse/shopping/MainCoroutineRule.kt b/app/src/test/java/woowacourse/shopping/MainCoroutineRule.kt new file mode 100644 index 000000000..e291316ef --- /dev/null +++ b/app/src/test/java/woowacourse/shopping/MainCoroutineRule.kt @@ -0,0 +1,25 @@ +package woowacourse.shopping + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.rules.TestWatcher +import org.junit.runner.Description + +@OptIn(ExperimentalCoroutinesApi::class) +class MainCoroutineRule constructor( + private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), +) : TestWatcher() { + override fun starting(description: Description) { + super.starting(description) + Dispatchers.setMain(testDispatcher) + } + + override fun finished(description: Description) { + super.finished(description) + Dispatchers.resetMain() + } +} diff --git a/app/src/test/java/woowacourse/shopping/ui/cart/CartViewModelTest.kt b/app/src/test/java/woowacourse/shopping/ui/cart/CartViewModelTest.kt index e7601c26b..85cc05da7 100644 --- a/app/src/test/java/woowacourse/shopping/ui/cart/CartViewModelTest.kt +++ b/app/src/test/java/woowacourse/shopping/ui/cart/CartViewModelTest.kt @@ -2,24 +2,28 @@ package woowacourse.shopping.ui.cart import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.mockk.Runs -import io.mockk.every +import io.mockk.coEvery import io.mockk.just import io.mockk.mockk import org.junit.Before import org.junit.Rule import org.junit.Test +import woowacourse.shopping.MainCoroutineRule import woowacourse.shopping.getOrAwaitValue +import woowacourse.shopping.model.CartProduct import woowacourse.shopping.model.Product import woowacourse.shopping.repository.CartRepository class CartViewModelTest { - private lateinit var viewModel: CartViewModel private lateinit var cartRepository: CartRepository @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() + @get:Rule + val mainDispatcher = MainCoroutineRule() + @Before fun setup() { cartRepository = mockk() @@ -30,11 +34,11 @@ class CartViewModelTest { fun 모든_장바구니_목록을_불러오면_기존_장바구니_목록을_갱신한다() { // given: 불러올 장바구니 목록이 존재한다. val expected = listOf( - Product("우테코 과자", 1000, "snackimage"), - Product("우테코 쥬스", 2000, "juiceimage"), - Product("우테코 아이스크림", 3000, "icecreamimage"), + CartProduct(0, 0L, Product("우테코 과자", 1000, "snack")), + CartProduct(1, 0L, Product("우테코 쥬스", 2000, "juice")), + CartProduct(2, 2L, Product("우테코 아이스크림", 3000, "icecream")), ) - every { cartRepository.getAllCartProducts() } returns expected + coEvery { cartRepository.getAllCartProducts() } returns expected // when: 장바구니 목록을 불러온다. viewModel.fetchAllCartProducts() @@ -47,8 +51,8 @@ class CartViewModelTest { @Test fun 장바구니에서_상품을_제거하면_제거_상태가_참이_된다() { // given: 제거할 상품 ID와 상품 목록이 존재한다. - val productIdForDeleting = 1 - every { cartRepository.deleteCartProduct(productIdForDeleting) } just Runs + val productIdForDeleting = 1L + coEvery { cartRepository.deleteCartProduct(productIdForDeleting) } just Runs // when: 장바구니에서 상품을 제거한다. viewModel.deleteCartProduct(productIdForDeleting) diff --git a/app/src/test/java/woowacourse/shopping/ui/main/MainViewModelTest.kt b/app/src/test/java/woowacourse/shopping/ui/main/MainViewModelTest.kt index afaadd8d4..086e4bb15 100644 --- a/app/src/test/java/woowacourse/shopping/ui/main/MainViewModelTest.kt +++ b/app/src/test/java/woowacourse/shopping/ui/main/MainViewModelTest.kt @@ -2,12 +2,14 @@ package woowacourse.shopping.ui.main import androidx.arch.core.executor.testing.InstantTaskExecutorRule import io.mockk.Runs +import io.mockk.coEvery import io.mockk.every import io.mockk.just import io.mockk.mockk import org.junit.Before import org.junit.Rule import org.junit.Test +import woowacourse.shopping.MainCoroutineRule import woowacourse.shopping.getOrAwaitValue import woowacourse.shopping.model.Product import woowacourse.shopping.repository.CartRepository @@ -22,6 +24,9 @@ class MainViewModelTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() + @get:Rule + val mainDispatcher = MainCoroutineRule() + @Before fun setUp() { productRepository = mockk() @@ -37,7 +42,7 @@ class MainViewModelTest { fun 장바구니_목록에_제품을_추가하면_상품_추가_상태가_참이_된다() { // given: 장바구니에 추가할 상품이 존재한다. val product = mockk() - every { cartRepository.addCartProduct(product) } just Runs + coEvery { cartRepository.addCartProduct(product) } just Runs // when: 장바구니에 상품을 추가한다. viewModel.addCartProduct(product) diff --git a/bunadi/.gitignore b/bunadi/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/bunadi/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/bunadi/build.gradle.kts b/bunadi/build.gradle.kts new file mode 100644 index 000000000..68f3cf5e6 --- /dev/null +++ b/bunadi/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("java-library") + id("org.jetbrains.kotlin.jvm") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_7 + targetCompatibility = JavaVersion.VERSION_1_7 +} + +dependencies { + implementation("org.jetbrains.kotlin", "kotlin-reflect", "1.8.21") +} diff --git a/bunadi/src/main/java/com/woowacourse/bunadi/annotation/Inject.kt b/bunadi/src/main/java/com/woowacourse/bunadi/annotation/Inject.kt new file mode 100644 index 000000000..08a1c9e22 --- /dev/null +++ b/bunadi/src/main/java/com/woowacourse/bunadi/annotation/Inject.kt @@ -0,0 +1,5 @@ +package com.woowacourse.bunadi.annotation + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +annotation class Inject diff --git a/bunadi/src/main/java/com/woowacourse/bunadi/annotation/Qualifier.kt b/bunadi/src/main/java/com/woowacourse/bunadi/annotation/Qualifier.kt new file mode 100644 index 000000000..285225f58 --- /dev/null +++ b/bunadi/src/main/java/com/woowacourse/bunadi/annotation/Qualifier.kt @@ -0,0 +1,5 @@ +package com.woowacourse.bunadi.annotation + +import kotlin.reflect.KClass + +annotation class Qualifier(val clazz: KClass<*>) diff --git a/bunadi/src/main/java/com/woowacourse/bunadi/annotation/Scope.kt b/bunadi/src/main/java/com/woowacourse/bunadi/annotation/Scope.kt new file mode 100644 index 000000000..70f037450 --- /dev/null +++ b/bunadi/src/main/java/com/woowacourse/bunadi/annotation/Scope.kt @@ -0,0 +1,3 @@ +package com.woowacourse.bunadi.annotation + +annotation class Singleton diff --git a/bunadi/src/main/java/com/woowacourse/bunadi/dsl/DependencyModuleDsl.kt b/bunadi/src/main/java/com/woowacourse/bunadi/dsl/DependencyModuleDsl.kt new file mode 100644 index 000000000..8f0cfe04f --- /dev/null +++ b/bunadi/src/main/java/com/woowacourse/bunadi/dsl/DependencyModuleDsl.kt @@ -0,0 +1,14 @@ +package com.woowacourse.bunadi.dsl + +import com.woowacourse.bunadi.injector.DependencyInjector +import com.woowacourse.bunadi.module.Module + +class DependencyModuleDsl { + fun module(module: Module) { + DependencyInjector.module(module) + } +} + +fun modules(block: DependencyModuleDsl.() -> Unit) { + DependencyModuleDsl().block() +} diff --git a/bunadi/src/main/java/com/woowacourse/bunadi/injector/DependencyInjector.kt b/bunadi/src/main/java/com/woowacourse/bunadi/injector/DependencyInjector.kt new file mode 100644 index 000000000..4088629c0 --- /dev/null +++ b/bunadi/src/main/java/com/woowacourse/bunadi/injector/DependencyInjector.kt @@ -0,0 +1,65 @@ +package com.woowacourse.bunadi.injector + +import com.woowacourse.bunadi.annotation.Inject +import com.woowacourse.bunadi.annotation.Singleton +import com.woowacourse.bunadi.module.Module +import com.woowacourse.bunadi.util.core.Cache +import com.woowacourse.bunadi.util.createInstance +import com.woowacourse.bunadi.util.parseFromQualifier +import com.woowacourse.bunadi.util.validateHasPrimaryConstructor +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty +import kotlin.reflect.full.declaredMemberFunctions +import kotlin.reflect.full.hasAnnotation +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.jvm.jvmErasure + +object DependencyInjector { + private val cache = Cache() + + fun inject(clazz: KClass): T { + val dependencyKey = DependencyKey.createDependencyKey(clazz) + val cached = cache[dependencyKey] + if (cached != null) return cached as T + + val primaryConstructor = clazz.validateHasPrimaryConstructor() + val dependency = primaryConstructor.createInstance() + injectMemberProperties(clazz, dependency) + + if (clazz.hasAnnotation()) { + caching(dependencyKey, dependency) + } + return dependency + } + + private fun injectMemberProperties(clazz: KClass, instance: T) { + clazz.memberProperties.forEach { property -> + if (!property.hasAnnotation()) return@forEach + if (property !is KMutableProperty<*>) return@forEach + property.isAccessible = true + + val dependencyKey = DependencyKey.createDependencyKey(property) + val realType = property.parseFromQualifier() + val propertyInstance = inject(realType ?: property.returnType.jvmErasure) + + if (clazz.hasAnnotation()) { + caching(dependencyKey, propertyInstance) + } + property.setter.call(instance, propertyInstance) + } + } + + fun module(module: Module) { + val providers = module::class.declaredMemberFunctions + providers.forEach { provider -> cache.caching(module, provider) } + } + + fun caching(dependencyKey: DependencyKey, dependency: Any? = null) { + cache.caching(dependencyKey, dependency) + } + + fun clear() { + cache.clear() + } +} diff --git a/bunadi/src/main/java/com/woowacourse/bunadi/injector/DependencyKey.kt b/bunadi/src/main/java/com/woowacourse/bunadi/injector/DependencyKey.kt new file mode 100644 index 000000000..0a9168dac --- /dev/null +++ b/bunadi/src/main/java/com/woowacourse/bunadi/injector/DependencyKey.kt @@ -0,0 +1,44 @@ +package com.woowacourse.bunadi.injector + +import com.woowacourse.bunadi.annotation.Inject +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty +import kotlin.reflect.KType +import kotlin.reflect.full.starProjectedType + +data class DependencyKey( + val type: KType, + val annotation: Annotation? = null, +) { + companion object { + fun createDependencyKey(clazz: KClass<*>): DependencyKey { + val returnType = clazz.starProjectedType + val annotation = clazz.annotations.firstOrNull() + + return DependencyKey(returnType, annotation) + } + + fun createDependencyKey(provider: KFunction<*>): DependencyKey { + val returnType = provider.returnType + val annotation = provider.annotations.firstOrNull() + + return DependencyKey(returnType, annotation) + } + + fun createDependencyKey(parameter: KParameter): DependencyKey { + val returnType = parameter.type + val annotation = parameter.annotations.firstOrNull() + + return DependencyKey(returnType, annotation) + } + + fun createDependencyKey(property: KProperty<*>): DependencyKey { + val returnType = property.returnType + val annotation = property.annotations.first { it.annotationClass != Inject::class } + + return DependencyKey(returnType, annotation) + } + } +} diff --git a/bunadi/src/main/java/com/woowacourse/bunadi/module/Module.kt b/bunadi/src/main/java/com/woowacourse/bunadi/module/Module.kt new file mode 100644 index 000000000..3fedd0609 --- /dev/null +++ b/bunadi/src/main/java/com/woowacourse/bunadi/module/Module.kt @@ -0,0 +1,3 @@ +package com.woowacourse.bunadi.module + +interface Module diff --git a/bunadi/src/main/java/com/woowacourse/bunadi/util/ReflectionUtil.kt b/bunadi/src/main/java/com/woowacourse/bunadi/util/ReflectionUtil.kt new file mode 100644 index 000000000..fcefff56a --- /dev/null +++ b/bunadi/src/main/java/com/woowacourse/bunadi/util/ReflectionUtil.kt @@ -0,0 +1,49 @@ +package com.woowacourse.bunadi.util + +import com.woowacourse.bunadi.annotation.Inject +import com.woowacourse.bunadi.annotation.Qualifier +import com.woowacourse.bunadi.injector.DependencyInjector +import com.woowacourse.bunadi.injector.DependencyKey +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.KProperty +import kotlin.reflect.full.primaryConstructor +import kotlin.reflect.jvm.jvmErasure + +fun KClass.validateHasPrimaryConstructor(): KFunction { + return requireNotNull(primaryConstructor) { "[ERROR] 주생성자가 존재하지 않습니다." } +} + +fun KFunction.createInstance(): T { + return call(*parameters.createInstances()) +} + +fun List.createInstances(): Array = map { parameter -> + val realType = parameter.parseFromQualifier() + val paramDependencyKey = DependencyKey.createDependencyKey(parameter) + + val instance = if (realType != null) { + DependencyInjector.inject(realType) + } else { + DependencyInjector.inject(parameter.type.jvmErasure) + } + DependencyInjector.caching(paramDependencyKey, instance) + instance +}.toTypedArray() + +fun KParameter.parseFromQualifier(): KClass<*>? { + val annotation = annotations.firstOrNull() + return annotation?.parseClassInQualifier() +} + +fun KProperty<*>.parseFromQualifier(): KClass<*>? { + val annotation = annotations.find { it !is Inject } + return annotation?.parseClassInQualifier() +} + +private fun Annotation.parseClassInQualifier(): KClass<*>? { + val annotation = annotationClass + val qualifier = annotation.annotations.find { it is Qualifier } as? Qualifier + return qualifier?.clazz +} diff --git a/bunadi/src/main/java/com/woowacourse/bunadi/util/core/Cache.kt b/bunadi/src/main/java/com/woowacourse/bunadi/util/core/Cache.kt new file mode 100644 index 000000000..eecd5c550 --- /dev/null +++ b/bunadi/src/main/java/com/woowacourse/bunadi/util/core/Cache.kt @@ -0,0 +1,29 @@ +package com.woowacourse.bunadi.util.core + +import com.woowacourse.bunadi.injector.DependencyKey +import com.woowacourse.bunadi.module.Module +import kotlin.reflect.KFunction + +data class Cache( + private val cache: MutableMap = mutableMapOf(), +) { + fun caching(module: Module, provider: KFunction<*>) { + val dependencyKey = DependencyKey.createDependencyKey(provider) + val dependency = provider.call(module) + + cache[dependencyKey] = dependency + } + + fun caching(dependencyKey: DependencyKey, dependency: Any? = null) { + cache[dependencyKey] = dependency + } + + operator fun get(dependencyKey: DependencyKey): Any? { + return cache[dependencyKey] + } + + fun clear(): Cache { + cache.clear() + return copy(cache = mutableMapOf()) + } +} diff --git a/domain/src/main/java/woowacourse/shopping/model/CartProduct.kt b/domain/src/main/java/woowacourse/shopping/model/CartProduct.kt new file mode 100644 index 000000000..20b012983 --- /dev/null +++ b/domain/src/main/java/woowacourse/shopping/model/CartProduct.kt @@ -0,0 +1,11 @@ +package woowacourse.shopping.model + +class CartProduct( + val id: Long, + val createdAt: Long, + product: Product, +) { + val name = product.name + val price = product.price + val imageUrl = product.imageUrl +} diff --git a/domain/src/main/java/woowacourse/shopping/model/Product.kt b/domain/src/main/java/woowacourse/shopping/model/Product.kt index c9f974276..52d9ea9ed 100644 --- a/domain/src/main/java/woowacourse/shopping/model/Product.kt +++ b/domain/src/main/java/woowacourse/shopping/model/Product.kt @@ -1,3 +1,7 @@ package woowacourse.shopping.model -class Product(val name: String, val price: Int, val imageUrl: String) +data class Product( + val name: String, + val price: Int, + val imageUrl: String, +) diff --git a/domain/src/main/java/woowacourse/shopping/repository/CartRepository.kt b/domain/src/main/java/woowacourse/shopping/repository/CartRepository.kt index 5f8dd0bd0..a4c79b861 100644 --- a/domain/src/main/java/woowacourse/shopping/repository/CartRepository.kt +++ b/domain/src/main/java/woowacourse/shopping/repository/CartRepository.kt @@ -1,10 +1,10 @@ package woowacourse.shopping.repository +import woowacourse.shopping.model.CartProduct import woowacourse.shopping.model.Product -// TODO: Step2 - CartProductDao를 참조하도록 변경 interface CartRepository { - fun addCartProduct(product: Product) - fun getAllCartProducts(): List - fun deleteCartProduct(id: Int) + suspend fun addCartProduct(product: Product) + suspend fun getAllCartProducts(): List + suspend fun deleteCartProduct(id: Long) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3eaf0e50a..baf59f811 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,3 +15,4 @@ dependencyResolutionManagement { rootProject.name = "android-di" include(":app") include(":domain") +include(":bunadi")