Skip to content
Open
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@ fun bind(product: ...) {

### 선택 요구 사항
- [] `DateFormatter`가 Configuration Changes에도 살아남을 수 있도록 구현한다.
- [] Activity, ViewModel 외에도 다양한 컴포넌트(Fragment, Service 등)별 유지될 의존성을 관리한다.
- [] Activity, ViewModel 외에도 다양한 컴포넌트(Fragment, Service 등)별 유지될 의존성을 관리한다.

## 5단계 기능 요구 사항
- [] 지금까지 만든 쇼핑 장바구니 앱에 적용된 DI 라이브러리를 Hilt 코드로 교체한다. 기존에 만들어 둔 모듈과 테스트 코드를 삭제하진 않아도 된다. 이전 요구 사항을 동일하게 만족해야 한다.
6 changes: 6 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.hilt.android)
}

android {
Expand Down Expand Up @@ -76,6 +77,11 @@ dependencies {
androidTestImplementation(libs.assertj.core)
androidTestImplementation(libs.truth)

// Hilt
kapt(libs.hilt.compiler)
kapt(libs.androidx.hilt.compiler)
implementation(libs.hilt.android)

// Reflection
implementation(libs.kotlin.reflect)
// Coroutines
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/woowacourse/shopping/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package woowacourse.shopping

import android.app.Application
import android.content.Context
import dagger.hilt.android.HiltAndroidApp
import woowacourse.shopping.di.DatabaseModule
import woowacourse.shopping.di.RepositoryModule

@HiltAndroidApp
class App : Application() {
val container: Container by lazy { Container() }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package woowacourse.shopping.data.repository

import woowacourse.shopping.annotation.Inject
import woowacourse.shopping.data.CartProductDao
import woowacourse.shopping.data.CartProductEntity
import woowacourse.shopping.domain.model.CartProduct
import woowacourse.shopping.domain.repository.CartRepository
import javax.inject.Inject

class DefaultCartRepository
@Inject
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package woowacourse.shopping.data.repository

import woowacourse.shopping.annotation.Inject
import woowacourse.shopping.domain.model.CartProduct
import woowacourse.shopping.domain.repository.CartRepository
import javax.inject.Inject

class InMemoryCartRepository
@Inject
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package woowacourse.shopping.data.repository

import woowacourse.shopping.annotation.Inject
import woowacourse.shopping.domain.model.Product
import woowacourse.shopping.domain.repository.ProductRepository
import javax.inject.Inject

class ProductRepositoryImpl
@Inject
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/woowacourse/shopping/hilt/Qualifiers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package woowacourse.shopping.hilt

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class HiltInMemory

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class HiltRoomDB
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package woowacourse.shopping.hilt.module

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import woowacourse.shopping.data.repository.DefaultCartRepository
import woowacourse.shopping.data.repository.InMemoryCartRepository
import woowacourse.shopping.domain.repository.CartRepository
import woowacourse.shopping.hilt.HiltInMemory
import woowacourse.shopping.hilt.HiltRoomDB
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class CartRepositoryModule {
@Binds
@Singleton
@HiltRoomDB
abstract fun bindsDefaultCartRepository(impl: DefaultCartRepository): CartRepository

@Binds
@Singleton
@HiltInMemory
abstract fun inMemoryCartRepository(impl: InMemoryCartRepository): CartRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package woowacourse.shopping.hilt.module

import android.content.Context
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import woowacourse.shopping.data.CartProductDao
import woowacourse.shopping.data.ShoppingDatabase

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
fun provideShoppingDatabase(
@ApplicationContext context: Context,
): ShoppingDatabase = ShoppingDatabase.getInstance(context)

Comment on lines +12 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Room DB는 @singleton으로 제공해야 합니다

여러 인스턴스 생성 시 캐시/트랜잭션 혼란과 자원 낭비가 발생합니다. @singleton을 추가해 주세요.

적용 예:

 import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton

 @Module
 @InstallIn(SingletonComponent::class)
 object DatabaseModule {
-    @Provides
+    @Provides
+    @Singleton
     fun provideShoppingDatabase(
         @ApplicationContext context: Context,
     ): ShoppingDatabase = ShoppingDatabase.getInstance(context)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
fun provideShoppingDatabase(
@ApplicationContext context: Context,
): ShoppingDatabase = ShoppingDatabase.getInstance(context)
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import woowacourse.shopping.data.database.ShoppingDatabase
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideShoppingDatabase(
@ApplicationContext context: Context,
): ShoppingDatabase = ShoppingDatabase.getInstance(context)
}
🤖 Prompt for AI Agents
In app/src/main/java/woowacourse/shopping/hilt/module/DatabaseModule.kt around
lines 12 to 19, the Room database provider is missing a @Singleton scope which
can cause multiple instances and resource/transaction issues; update the
provider by annotating the provideShoppingDatabase function with @Singleton so
Hilt creates a single ShoppingDatabase instance, and add the necessary import
for javax.inject.Singleton if not already present.

@Provides
fun provideCartProductDao(database: ShoppingDatabase): CartProductDao = database.cartProductDao()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package woowacourse.shopping.hilt.module

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
import woowacourse.shopping.data.repository.ProductRepositoryImpl
import woowacourse.shopping.domain.repository.ProductRepository

@Module
@InstallIn(ViewModelComponent::class)
abstract class ProductRepositoryModule {
@Binds
@ViewModelScoped
abstract fun bindProductRepository(impl: ProductRepositoryImpl): ProductRepository
}
6 changes: 4 additions & 2 deletions app/src/main/java/woowacourse/shopping/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@ import android.os.Bundle
import android.view.Menu
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import dagger.hilt.android.AndroidEntryPoint
import woowacourse.shopping.R
import woowacourse.shopping.databinding.ActivityMainBinding
import woowacourse.shopping.di.injectViewModel
import woowacourse.shopping.ui.cart.CartActivity

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private val viewModel by injectViewModel<MainViewModel>()
private val viewModel: MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand Down
58 changes: 29 additions & 29 deletions app/src/main/java/woowacourse/shopping/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,43 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import woowacourse.shopping.annotation.Inject
import woowacourse.shopping.di.annotation.RoomDB
import woowacourse.shopping.domain.model.CartProduct
import woowacourse.shopping.domain.model.Product
import woowacourse.shopping.domain.repository.CartRepository
import woowacourse.shopping.domain.repository.ProductRepository
import woowacourse.shopping.hilt.HiltRoomDB
import javax.inject.Inject

class MainViewModel : ViewModel() {
@HiltViewModel
class MainViewModel
@Inject
private lateinit var productRepository: ProductRepository

@Inject
@RoomDB
private lateinit var cartRepository: CartRepository

private val _products: MutableLiveData<List<Product>> = MutableLiveData(emptyList())
val products: LiveData<List<Product>> get() = _products

private val _onProductAdded: MutableLiveData<Boolean> = MutableLiveData(false)
val onProductAdded: LiveData<Boolean> get() = _onProductAdded
constructor(
private val productRepository: ProductRepository,
@HiltRoomDB private val cartRepository: CartRepository,
) : ViewModel() {
private val _products: MutableLiveData<List<Product>> = MutableLiveData(emptyList())
val products: LiveData<List<Product>> get() = _products

private val _onProductAdded: MutableLiveData<Boolean> = MutableLiveData(false)
val onProductAdded: LiveData<Boolean> get() = _onProductAdded

fun addCartProduct(product: Product) {
viewModelScope.launch {
cartRepository.addCartProduct(product.toCartProduct())
_onProductAdded.value = true
}
}

fun addCartProduct(product: Product) {
viewModelScope.launch {
cartRepository.addCartProduct(product.toCartProduct())
_onProductAdded.value = true
fun getAllProducts() {
_products.value = productRepository.getAllProducts()
}
}

fun getAllProducts() {
_products.value = productRepository.getAllProducts()
private fun Product.toCartProduct(): CartProduct =
CartProduct(
name = name,
price = price,
imageUrl = imageUrl,
)
}

private fun Product.toCartProduct(): CartProduct =
CartProduct(
name = name,
price = price,
imageUrl = imageUrl,
)
}
14 changes: 8 additions & 6 deletions app/src/main/java/woowacourse/shopping/ui/cart/CartActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ package woowacourse.shopping.ui.cart
import android.os.Bundle
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import dagger.hilt.android.AndroidEntryPoint
import woowacourse.shopping.R
import woowacourse.shopping.annotation.Inject
import woowacourse.shopping.databinding.ActivityCartBinding
import woowacourse.shopping.di.DIActivity
import woowacourse.shopping.di.injectViewModel
import javax.inject.Inject

class CartActivity : DIActivity() {
@AndroidEntryPoint
class CartActivity : AppCompatActivity() {
private val binding by lazy { ActivityCartBinding.inflate(layoutInflater) }
private val viewModel by injectViewModel<CartViewModel>()
private val viewModel: CartViewModel by viewModels()

@Inject
private lateinit var dateFormatter: DateFormatter
lateinit var dateFormatter: DateFormatter
private val adapter by lazy {
CartProductAdapter(
onClickDelete = viewModel::deleteCartProduct,
Expand Down
43 changes: 24 additions & 19 deletions app/src/main/java/woowacourse/shopping/ui/cart/CartViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,37 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import woowacourse.shopping.di.annotation.RoomDB
import woowacourse.shopping.domain.model.CartProduct
import woowacourse.shopping.domain.repository.CartRepository
import woowacourse.shopping.hilt.HiltRoomDB
import javax.inject.Inject

class CartViewModel(
@RoomDB private val cartRepository: CartRepository,
) : ViewModel() {
private val _cartProducts: MutableLiveData<List<CartProduct>> =
MutableLiveData(emptyList())
val cartProducts: LiveData<List<CartProduct>> get() = _cartProducts
@HiltViewModel
class CartViewModel
@Inject
constructor(
@HiltRoomDB private val cartRepository: CartRepository,
) : ViewModel() {
private val _cartProducts: MutableLiveData<List<CartProduct>> =
MutableLiveData(emptyList())
val cartProducts: LiveData<List<CartProduct>> get() = _cartProducts

private val _onCartProductDeleted: MutableLiveData<Boolean> = MutableLiveData(false)
val onCartProductDeleted: LiveData<Boolean> get() = _onCartProductDeleted
private val _onCartProductDeleted: MutableLiveData<Boolean> = MutableLiveData(false)
val onCartProductDeleted: LiveData<Boolean> get() = _onCartProductDeleted

fun getAllCartProducts() {
viewModelScope.launch {
_cartProducts.value = cartRepository.getAllCartProducts()
fun getAllCartProducts() {
viewModelScope.launch {
_cartProducts.value = cartRepository.getAllCartProducts()
}
}
}

fun deleteCartProduct(id: Long) {
viewModelScope.launch {
cartRepository.deleteCartProduct(id)
_onCartProductDeleted.value = true
_cartProducts.value = cartRepository.getAllCartProducts()
fun deleteCartProduct(id: Long) {
viewModelScope.launch {
cartRepository.deleteCartProduct(id)
_onCartProductDeleted.value = true
_cartProducts.value = cartRepository.getAllCartProducts()
}
}
}
}
24 changes: 15 additions & 9 deletions app/src/main/java/woowacourse/shopping/ui/cart/DateFormatter.kt
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
package woowacourse.shopping.ui.cart

import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityRetainedScoped
import woowacourse.shopping.R
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject

class DateFormatter(context: Context) {
private val formatter =
SimpleDateFormat(
context.getString(R.string.date_format),
Locale.KOREA,
)
@ActivityRetainedScoped
class DateFormatter
@Inject
constructor(
@ApplicationContext context: Context,
) {
private val formatter =
SimpleDateFormat(
context.getString(R.string.date_format),
Locale.KOREA,
)

fun formatDate(timestamp: Long): String {
return formatter.format(Date(timestamp))
fun formatDate(timestamp: Long): String = formatter.format(Date(timestamp))
}
}
8 changes: 7 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ plugins {
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.ktlint) apply false
alias(libs.plugins.jetbrains.kotlin.jvm) apply false
alias(libs.plugins.hilt.android) apply false
}

allprojects {
apply(plugin = rootProject.libs.plugins.ktlint.get().pluginId)
apply(
plugin =
rootProject.libs.plugins.ktlint
.get()
.pluginId,
)
}
Loading