Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[스캇] 1단계 자동 DI 미션 제출합니다 #10

Merged
merged 9 commits into from
Sep 5, 2023
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ android {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
"proguard-rules.pro",
)
}
}
Expand Down Expand Up @@ -68,4 +68,5 @@ dependencies {
implementation("com.github.bumptech.glide:glide:4.15.1")
// Robolectric
testImplementation("org.robolectric:robolectric:4.9")
implementation(kotlin("reflect"))
}
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />

<application
android:name=".ShoppingApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/java/woowacourse/shopping/ShoppingApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package woowacourse.shopping

import android.app.Application
import woowacourse.shopping.data.CartRepositoryImpl
import woowacourse.shopping.data.ProductRepositoryImpl
import woowacourse.shopping.di.module
import woowacourse.shopping.di.startDI
import woowacourse.shopping.repository.CartRepository
import woowacourse.shopping.repository.ProductRepository

class ShoppingApplication : Application() {
override fun onCreate() {
super.onCreate()
val dataModule = module {
provide<ProductRepository> { ProductRepositoryImpl() }
provide<CartRepository> { CartRepositoryImpl() }
}
startDI {
loadModule(dataModule)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package woowacourse.shopping.data

import woowacourse.shopping.model.Product
import woowacourse.shopping.repository.CartRepository

// TODO: Step2 - CartProductDao를 참조하도록 변경
class CartRepository {

private val cartProducts: MutableList<Product> = mutableListOf()
fun addCartProduct(product: Product) {
class CartRepositoryImpl(
private val cartProducts: MutableList<Product> = mutableListOf(),
) : CartRepository {
override fun addCartProduct(product: Product) {
cartProducts.add(product)
}

fun getAllCartProducts(): List<Product> {
return cartProducts.toList()
override fun deleteCartProduct(id: Int) {
cartProducts.removeAt(id)
}

fun deleteCartProduct(id: Int) {
cartProducts.removeAt(id)
override fun getAllCartProducts(): List<Product> {
return cartProducts.toList()
}
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,29 @@
package woowacourse.shopping.data

import woowacourse.shopping.model.Product
import woowacourse.shopping.repository.ProductRepository

class ProductRepository {
class ProductRepositoryImpl : ProductRepository {

private val products: List<Product> = listOf(
Product(
name = "우테코 과자",
price = 10_000,
imageUrl = "https://cdn-mart.baemin.com/sellergoods/api/main/df6d76fb-925b-40f8-9d1c-f0920c3c697a.jpg?h=700&w=700"
imageUrl = "https://cdn-mart.baemin.com/sellergoods/api/main/df6d76fb-925b-40f8-9d1c-f0920c3c697a.jpg?h=700&w=700",
),
Product(
name = "우테코 쥬스",
price = 8_000,
imageUrl = "https://cdn-mart.baemin.com/sellergoods/main/52dca718-31c5-4f80-bafa-7e300d8c876a.jpg?h=700&w=700"
imageUrl = "https://cdn-mart.baemin.com/sellergoods/main/52dca718-31c5-4f80-bafa-7e300d8c876a.jpg?h=700&w=700",
),
Product(
name = "우테코 아이스크림",
price = 20_000,
imageUrl = "https://cdn-mart.baemin.com/sellergoods/main/e703c53e-5d01-4b20-bd33-85b5e778e73f.jpg?h=700&w=700"
imageUrl = "https://cdn-mart.baemin.com/sellergoods/main/e703c53e-5d01-4b20-bd33-85b5e778e73f.jpg?h=700&w=700",
),
)

fun getAllProducts(): List<Product> {
override fun getAllProducts(): List<Product> {
return products
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package woowacourse.shopping.data
import androidx.room.Database
import androidx.room.RoomDatabase


@Database(entities = [CartProductEntity::class], version = 1, exportSchema = false)
abstract class ShoppingDatabase : RoomDatabase() {
abstract fun cartProductDao(): CartProductDao
Expand Down
56 changes: 56 additions & 0 deletions app/src/main/java/woowacourse/shopping/di/Injector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package woowacourse.shopping.di

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import woowacourse.shopping.di.service.DefaultService
import woowacourse.shopping.di.service.Service
import woowacourse.shopping.ui.util.ViewModelFactoryUtil
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass

object Injector {
private val services: MutableMap<KClass<*>, Service> = ConcurrentHashMap()
lateinit var declarations: Map<KClass<*>, Declaration<Any>>

fun <T : Any> getService(kClazz: KClass<out T>): Service {
return services[kClazz] ?: error(SERVICE_NOT_FOUNT_ERROR_MESSAGE + "$kClazz")
}

fun loadModules(modules: List<Module>) {
modules.forEach(::loadModule)
}
RightHennessy marked this conversation as resolved.
Show resolved Hide resolved

fun loadModule(module: Module) {
declarations = module.declarationRegistry
declarations.forEach { declaration ->
val service = getServiceFromDeclaration(declaration.key, declaration.value)
addService(service)
}
}

private fun addService(service: Service) {
services[service.type] = service
}

private fun <T : Any> getServiceFromDeclaration(
type: KClass<*>,
declaration: Declaration<T>,
): Service {
val instance = declaration.invoke()
return DefaultService.create(type, instance)
}

private const val SERVICE_NOT_FOUNT_ERROR_MESSAGE = "Unable to find definition of"
}

fun startDI(block: Injector.() -> Unit) = Injector.apply(block)

inline fun <reified T : Any> get(): T {
val service = Injector.getService(T::class)
return service.instance as T
}
Comment on lines +49 to +52
Copy link
Member

Choose a reason for hiding this comment

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

이건 어디 쓰이는 코드인거죠..?

Copy link
Author

Choose a reason for hiding this comment

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

아직은 쓰이지 않는 코드이지만, 나중에 Activity나 Fragment에서 최종적으로 의존성 주입을 할 때 사용할 코드입니다.

Copy link
Member

Choose a reason for hiding this comment

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

image


inline fun <reified T : ViewModel> ViewModelStoreOwner.viewModel(): Lazy<T> = lazy {
ViewModelProvider(this, ViewModelFactoryUtil.viewModelFactory<T>())[T::class.java]
}
31 changes: 31 additions & 0 deletions app/src/main/java/woowacourse/shopping/di/Module.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package woowacourse.shopping.di

import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass

typealias Declaration<T> = () -> T

fun module(block: Module.() -> Unit) = Module().apply(block)

// 여러 모듈 생성 가능, 최종적으로 사용할 서비스로 올라가기 전, 모듈의 declarationRegistry 에 임시로 저장한다.
open class Module {
val declarationRegistry: MutableMap<KClass<*>, Declaration<Any>> = ConcurrentHashMap()

inline fun <reified T : Any> provide(noinline declaration: Declaration<T>) {
declarationRegistry[T::class] = declaration
}

// 모듈 내에서 의존성 주입이 필요할 때
inline fun <reified T : Any> get(): T {
val declaration = declarationRegistry[T::class]
var instance = declaration?.invoke()

if (instance == null) {
instance = Injector.declarations[T::class]?.invoke()
?: error("Unable to find declaration of type ${T::class.qualifiedName}")
}
return instance as T
}

operator fun plus(module: Module) = listOf(module, this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package woowacourse.shopping.di.service

import kotlin.reflect.KClass
import kotlin.reflect.full.superclasses

// 최종적으로 Client(Activty 또는 뷰모델) 이 사용할 서비스
class DefaultService(
override val type: KClass<*>,
override val instance: Any,
) : Service {
companion object {
fun create(type: KClass<*>, instance: Any): Service {
if (instance.isMatchWith(type)) {
return DefaultService(type, instance)
}
throw IllegalStateException("MisMatch Type And Instance")
}

private fun Any.isMatchWith(type: KClass<*>): Boolean {
return this::class.superclasses.contains(type) || this::class == type
}
}
}
8 changes: 8 additions & 0 deletions app/src/main/java/woowacourse/shopping/di/service/Service.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package woowacourse.shopping.di.service

import kotlin.reflect.KClass

interface Service {
val type: KClass<*>
val instance: Any
}
10 changes: 3 additions & 7 deletions app/src/main/java/woowacourse/shopping/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,16 @@ import android.os.Bundle
import android.view.Menu
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import woowacourse.shopping.R
import woowacourse.shopping.databinding.ActivityMainBinding
import woowacourse.shopping.di.viewModel
import woowacourse.shopping.ui.cart.CartActivity

class MainActivity : AppCompatActivity() {

private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }

private val viewModel by lazy {
ViewModelProvider(this)[MainViewModel::class.java]
}

private val viewModel: MainViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
Expand All @@ -27,7 +24,6 @@ class MainActivity : AppCompatActivity() {
setupView()
}


override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.cart_menu, menu)
menu?.findItem(R.id.cart)?.actionView?.let { view ->
Expand Down Expand Up @@ -58,7 +54,7 @@ class MainActivity : AppCompatActivity() {
viewModel.products.observe(this) {
val adapter = ProductAdapter(
items = it,
onClickProduct = viewModel::addCartProduct
onClickProduct = viewModel::addCartProduct,
)
binding.rvProducts.adapter = adapter
}
Expand Down
5 changes: 2 additions & 3 deletions app/src/main/java/woowacourse/shopping/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package woowacourse.shopping.ui
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import woowacourse.shopping.data.CartRepository
import woowacourse.shopping.data.ProductRepository
import woowacourse.shopping.model.Product
import woowacourse.shopping.repository.CartRepository
import woowacourse.shopping.repository.ProductRepository

class MainViewModel(
private val productRepository: ProductRepository,
Expand All @@ -18,7 +18,6 @@ class MainViewModel(
private val _onProductAdded: MutableLiveData<Boolean> = MutableLiveData(false)
val onProductAdded: LiveData<Boolean> get() = _onProductAdded


fun addCartProduct(product: Product) {
cartRepository.addCartProduct(product)
_onProductAdded.value = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ package woowacourse.shopping.ui.cart
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import woowacourse.shopping.R
import woowacourse.shopping.databinding.ActivityCartBinding
import woowacourse.shopping.di.viewModel

class CartActivity : AppCompatActivity() {

private val binding by lazy { ActivityCartBinding.inflate(layoutInflater) }

private val viewModel by lazy {
ViewModelProvider(this)[CartViewModel::class.java]
}
private val viewModel: CartViewModel by viewModel()

private lateinit var dateFormatter: DateFormatter

Expand Down Expand Up @@ -60,7 +58,7 @@ class CartActivity : AppCompatActivity() {
val adapter = CartProductAdapter(
items = it,
dateFormatter = dateFormatter,
onClickDelete = viewModel::deleteCartProduct
onClickDelete = viewModel::deleteCartProduct,
)
binding.rvCartProducts.adapter = adapter
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package woowacourse.shopping.ui.cart
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import woowacourse.shopping.data.CartRepository
import woowacourse.shopping.model.Product
import woowacourse.shopping.repository.CartRepository

class CartViewModel(
private val cartRepository: CartRepository,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package woowacourse.shopping.ui.util

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import woowacourse.shopping.di.Injector
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.jvmErasure

object ViewModelFactoryUtil {
inline fun <reified T : ViewModel> viewModelFactory(): ViewModelProvider.Factory =
viewModelFactory {
initializer {
// 뷰모델의 주 생성자를 가져와요!!
val primaryConstructor =
T::class.primaryConstructor ?: throw IllegalStateException()

// 뷰 모델의 생성자의 파라미터들을 KClass 로 바꿔줘요!
val constructorParameterKClasses =
primaryConstructor.parameters.map { it.type.jvmErasure }

// 파라미터들의 KClass에 해당하는 객체를 주입해요!
val arguments = constructorParameterKClasses.map { kClazz ->
Injector.getService(kClazz).instance
}.toTypedArray()
primaryConstructor.call(*arguments)
}
}
}
Loading