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 미션 제출합니다 #5

Merged
merged 18 commits into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,31 @@
# android-di
# 0.5단계
## 기능 요구 사항
### 생성자 주입 - 수동
다음 문제점을 해결한다.

- [x] DB 없이 테스트하기 어렵다.
- [x] DB 객체를 교체하기 위해 또다른 객체를 만들어 바꿔줘야 한다. 즉, ViewModel에 직접적인 변경사항이 발생한다.

# 1단계
## 기능 요구 사항
### 생성자 주입 - 자동
다음 문제점을 해결한다.

- [x] ViewModel에서 참조하는 Repository가 정상적으로 주입되지 않는다.
- [x] Repository를 참조하는 다른 객체가 생기면 주입 코드를 매번 만들어줘야 한다.
- [x] ViewModel에 수동으로 주입되고 있는 의존성들을 자동으로 주입되도록 바꿔본다.
- [x] 특정 ViewModel에서만이 아닌, 범용적으로 활용될 수 있는 자동 주입 로직을 작성한다. (MainViewModel, CartViewModel 모두 하나의 로직만 참조한다)
- [x] 100개의 ViewModel이 생긴다고 가정했을 때, 자동 주입 로직 100개가 생기는 것이 아니다. 하나의 자동 주입 로직을 재사용할 수 있어야 한다.
- [x] 장바구니에 접근할 때마다 매번 CartRepository 인스턴스를 새로 만들고 있다.
- [x] 여러 번 인스턴스화할 필요 없는 객체는 최초 한 번만 인스턴스화한다. (이 단계에서는 너무 깊게 생각하지 말고 싱글 오브젝트로 구현해도 된다.)

## 선택 요구 사항
- [ ] TDD로 DI 구현
- [x] Robolectric으로 기능 테스트
- [ ] ViewModel 테스트
- [ ] 모든 도메인 로직, Repository 단위 테스트

## 프로그래밍 요구 사항
사전에 주어진 테스트 코드가 모두 성공해야 한다.
Annotation은 이 단계에서 활용하지 않는다.
5 changes: 4 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,7 @@ dependencies {
implementation("com.github.bumptech.glide:glide:4.15.1")
// Robolectric
testImplementation("org.robolectric:robolectric:4.9")

implementation("androidx.fragment:fragment-ktx:1.5.7")
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=".ui.ShoppingApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ package woowacourse.shopping.data
import woowacourse.shopping.model.Product

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

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

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

fun deleteCartProduct(id: Int) {
override fun deleteCartProduct(id: Int) {
cartProducts.removeAt(id)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,26 @@ package woowacourse.shopping.data

import woowacourse.shopping.model.Product

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
}
}
17 changes: 17 additions & 0 deletions app/src/main/java/woowacourse/shopping/di/Container.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package woowacourse.shopping.di

import kotlin.reflect.KClass

object Container {
private val instances = mutableMapOf<KClass<*>, Any>()
fun addInstance(type: KClass<*>, instance: Any) {
instances[type] = instance
}

fun getInstance(type: KClass<*>): Any =
instances[type] ?: throw NoSuchElementException("Unknown Instance")

fun clear() {
instances.clear()
}
Comment on lines +5 to +16
Copy link
Member

Choose a reason for hiding this comment

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

어디서나 접근할 수 있는 object 로 관리하면 instance 의 무결성을 보장할 수 없다고 생각합니다! 다음 미션에 참고해주세요!

}
25 changes: 25 additions & 0 deletions app/src/main/java/woowacourse/shopping/di/ViewModelInjector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package woowacourse.shopping.di

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.jvmErasure

object ViewModelInjector {
inline fun <reified T : ViewModel> getInjectedViewModelFactory(): ViewModelProvider.Factory {
val constructor = T::class.primaryConstructor
requireNotNull(constructor) { "Unknown ViewModel Class ${T::class}" }

val instances = constructor.parameters.map {
Container.getInstance(it.type.jvmErasure)
}

return viewModelFactory {
initializer {
constructor.call(*instances.toTypedArray())
}
}
}
}
10 changes: 4 additions & 6 deletions app/src/main/java/woowacourse/shopping/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import woowacourse.shopping.R
import woowacourse.shopping.databinding.ActivityMainBinding
import woowacourse.shopping.di.ViewModelInjector
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 by viewModels<MainViewModel> { ViewModelInjector.getInjectedViewModelFactory<MainViewModel>() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -27,7 +26,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 +56,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
1 change: 0 additions & 1 deletion app/src/main/java/woowacourse/shopping/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 20 additions & 0 deletions app/src/main/java/woowacourse/shopping/ui/ShoppingApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package woowacourse.shopping.ui

import android.app.Application
import woowacourse.shopping.data.CartRepository
import woowacourse.shopping.data.CartRepositoryImpl
import woowacourse.shopping.data.ProductRepository
import woowacourse.shopping.data.ProductRepositoryImpl
import woowacourse.shopping.di.Container

class ShoppingApplication : Application() {
override fun onCreate() {
super.onCreate()
inject()
}

private fun inject() {
Container.addInstance(CartRepository::class, CartRepositoryImpl())
Container.addInstance(ProductRepository::class, ProductRepositoryImpl())
Comment on lines +17 to +18
Copy link
Member

Choose a reason for hiding this comment

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

이제 생성되는 저장소가 추가되어도 유연하게 대응할 수 있겠네요!

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ package woowacourse.shopping.ui.cart

import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import woowacourse.shopping.R
import woowacourse.shopping.databinding.ActivityCartBinding
import woowacourse.shopping.di.ViewModelInjector

class CartActivity : AppCompatActivity() {

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

private val viewModel by lazy {
ViewModelProvider(this)[CartViewModel::class.java]
}
private val viewModel by viewModels<CartViewModel> { ViewModelInjector.getInjectedViewModelFactory<CartViewModel>() }

private lateinit var dateFormatter: DateFormatter

Expand Down Expand Up @@ -60,7 +59,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
@@ -0,0 +1,44 @@
package woowacourse.shopping.activity

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.ViewModelProvider
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import woowacourse.shopping.ui.cart.CartActivity
import woowacourse.shopping.ui.cart.CartViewModel

@RunWith(RobolectricTestRunner::class)
class CartActivityTest {

@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule() // AAC 컴포넌트들을 한 스레드에서 실행되도록 함

@Test
fun `Activity 실행 테스트`() {
// given
val activity = Robolectric
.buildActivity(CartActivity::class.java)
.create()
.get()

// then
assertThat(activity).isNotNull()
}

@Test
fun `ViewModel 주입 테스트`() {
// given
val activity = Robolectric
.buildActivity(CartActivity::class.java)
.create()
.get()
val viewModel = ViewModelProvider(activity)[CartViewModel::class.java]

// then
assertThat(viewModel).isNotNull()
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package woowacourse.shopping
package woowacourse.shopping.activity

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.ViewModelProvider
Expand All @@ -11,12 +11,11 @@ import org.robolectric.RobolectricTestRunner
import woowacourse.shopping.ui.MainActivity
import woowacourse.shopping.ui.MainViewModel


@RunWith(RobolectricTestRunner::class)
class MainActivityTest {

@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
var instantTaskExecutorRule = InstantTaskExecutorRule() // AAC 컴포넌트들을 한 스레드에서 실행되도록 함

@Test
fun `Activity 실행 테스트`() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package woowacourse.shopping
package woowacourse.shopping.activity

import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
Expand Down
59 changes: 59 additions & 0 deletions app/src/test/java/woowacourse/shopping/di/ViewModelInjectorTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package woowacourse.shopping.di

import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModel
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner

interface FakeRepository
class FakeRepositoryImpl : FakeRepository
class FakeViewModel(
val fakeRepository: FakeRepository,
) : ViewModel()

class FakeActivity : AppCompatActivity() {
val viewModel by viewModels<FakeViewModel> { ViewModelInjector.getInjectedViewModelFactory<FakeViewModel>() }
}

@RunWith(RobolectricTestRunner::class)
class ViewModelInjectorTest {

@Before
fun setup() {
Container.clear()
}

@Test
fun `Container에서 타입에 맞는 instance를 찾아 ViewModel 의존성을 주입한다`() {
// given
val repository = FakeRepositoryImpl()
Container.addInstance(FakeRepository::class, repository)
val activity = Robolectric
.buildActivity(FakeActivity::class.java)
.create()
.get()

// when
val viewModel = activity.viewModel

// then
assertNotNull(viewModel)
assertEquals(viewModel.fakeRepository, repository)
}

@Test(expected = NoSuchElementException::class)
fun `Container에 타입에 맞는 instance가 없으면 ViewModel 의존성 주입에 실패하여 에러가 발생한다`() {
val activity = Robolectric
.buildActivity(FakeActivity::class.java)
.create()
.get()

activity.viewModel
}
}
11 changes: 11 additions & 0 deletions domain/src/main/java/woowacourse/shopping/data/CartRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package woowacourse.shopping.data

import woowacourse.shopping.model.Product

interface CartRepository {
fun addCartProduct(product: Product)

fun getAllCartProducts(): List<Product>

fun deleteCartProduct(id: Int)
}
Loading