본문 바로가기
개발/자세히 쳐다 보면...

토이프로젝트 1-9 viewmodel(vm) 만들기

by lonewhitedot 2023. 4. 23.
반응형

뷰 모델의 역할은 간단해, 뷰는 화면에 보이는 것들만 처리하고, 뷰 모델은 데이터 영역간에 연결?을 하는 역할이지

뷰에서는 불필요하게 데이터를 이렇게 저렇게 가져오는 부분이 있을 필요 없으니 그걸 다 뷰 모델에서 처리하게 하고, 뷰 모델에서 데이터를 읽어서 뷰에 필요한 부분만 가공한다던지, 뷰에서 넘어온 데이터를 DB에 맞게 변경해서 저장한다든지 하는거지.

네트워크로 데이터를 받아오는 경우에 뷰 모델에서 네트워크를 쓸지, 내부에 캐시된 DB를 쓸지 정하고 DB의 캐시 정보도 업데이트를 해주는거지. 그러면 뷰에서는 이게 어디서 온 데이터인지는 알 필요 없이 그냥 보여주기만 하면 되는거야.

그리고 뷰 모델이 너무 커지는 걸 막기 위해서 usecase라는 것도 쓰기는 하는데 일단 그건 다음번에 알아보고 오늘은 view 모델은 어떻게 만드는지, 뷰에서 어떻게 쓰면 되는지만 해볼려고.

 

먼저 적당한 위치에 대충 만들어. 지금은 일단 테스트 코드만 짜고 있는 거라서 나중에 뷰 구조 잡고, vm은 어디 드가는게 좋을지 정할꺼야.

 

난 이렇게.

뷰 모델은 ViewModel 클래스를 상속 받고, 그냥 내가 원하는 메소드를 쓰면 되는거야.

클래스를 상속 받으면 viewmodelscope 라던가 뭐 그런걸 쓸 수 있는데 그건 나중에 플로우 코루틴 하면서 할꺼고, 오늘은 그거 없이, 그냥 현재 저장된 아이템 개수만 가져오는 걸 만들었어. 아래처럼

 

package com.lonewhite.finhelper.test

import androidx.lifecycle.ViewModel
import com.lonewhite.finhelper.data.repository.Repository
import javax.inject.Inject

class InvestItemViewModel @Inject constructor(
    private val repository: Repository
) : ViewModel() {
    fun getItemCount(): Int {
        return repository.getAllData().size
    }
}

아이템을 DB에서 가져올려면 repository가 필요하겠지? 근데 그거 일일이 생성자에 넣어주기 귀찮으니깐 대거를 이용할꺼야. Inject 어노테이션으로 대거야 알아서 여기에 repository 넣어주삼 하는거지.

근데 여기서 inject의 위치가 지난번이랑 좀 다르지? 필드 인젝션이 아니라 생성자 인젝션으로 일단은 알아서 넣어줘라 뭐 그런 정도로만 인해하면 됨. 컴포넌트에서 inject를 구현 안해도 되고....

 

그리고 지금 우리가 vm을 만드는데... 사실 이게 진짜 만들어질때는 viewmodel factory라는 것을 이용하게 되어 있어. 그래서 이걸 내가 이후에 bind 어노테이션을 써서 할껀데...

뭐라 설명해야 하지..

viewmodel factory라는거는 추상화 되어 있고, 생성자에 넘겨줄게 없으면 그냥 쓰면 되지만, 만약에 있으면 그냥 쓸 수 가 없어. viewmodel factory를 상속받아서 이렇게 viewmodel 만드는거야 라고 알려주는 팩토리를 만들어야 하는거지. 근데 그게 여러개면 넘흐 귀찮자나. 그래서 그걸 또 추상화해서 여러 타입을 받게 하고, 팩토리는 viewmodel 프로바이더를 받아와서 알아서 만들어주게 하는 식으로 할꺼야. 일단 따라해보자.

 

여튼 이게 vm 만드는 건 끝이야. 간단하지?

 

그럼 이제 이 vm도 대거가 알아서 할 수 있게 생성해 주는거를 만들어보자.

 

밑에꺼 안봤으면 먼저 보삼

https://lonewhite.tistory.com/28

 

토이프로젝트 1-6 dagger 써보기 -1-

먼저 dagger를 쓰겠다고, build gradle에 추가해야겠지? app 모듈에 추가하고, 이제는 app에서 database를 빌드 해야하니깐 Room 추가하자. dependencies { implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.a

www.lonewhite.com

 

그럼 시작해보자.

 

먼저 view model 팩토리 부터 만들어 보자. 지금은 한개라 별 의미가 없지만 여러개의 뷰모델이 있을때 유용하지.

아래처럼

class ViewModelFactory @Inject constructor(
    private val creators: @JvmSuppressWildcards Map<Class<out ViewModel>, Provider<ViewModel>>) :
    ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        var creator: Provider<out ViewModel>? = creators[modelClass]
        if (creator == null) {
            for ((key, value) in creators) {
                if (modelClass.isAssignableFrom(key)) {
                    creator = value
                    break
                }
            }
        }

        requireNotNull(creator) { "unknown model class $modelClass" }

        return try {
            creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }
    }
}

저 팩토리에 뷰모델과 거기에 맞는 뷰 모델 프로바이더를 맵으로 받아서, 요청하는 뷰 모델을 만들어서 넘겨주겠다는 거야.

설명이 좀 어려운데... 빌드 돌리고 나서 나온 코드를 찬찬히 읽어보면 대충 이해할 수 있을꺼야. 일단 따라하자

 

저기서 받는 뷰모델-뷰모델프로바이더가 어떻게 매칭되는지도 알려줘야 해. 그리고 뷰모델프로바이더가 어떤건지도 알려줘야 하지. 그걸 모듈에서 한번에 아래처럼 정의 했어.

 

@Module
interface ViewModelModule {
    @Binds
    fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

    @Binds
    @IntoMap
    @ViewModelKey(InvestItemViewModel::class)
    fun bindInvestItemViewModel(viewModel: InvestItemViewModel): ViewModel
}

@Target(AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
annotation class ViewModelKey(val value: KClass<out ViewModel>)

 

ViewModelKey가 뷰모델-뷰모델프로바이더가 연결되었다는 어노테이션을 새로 만들어서 정의한거고, Binds로 추상화 되어 있으니깐 니가 알아서 만들어 써라 라고 하는거지. 특이점은 interface라는거야. 알아서 대거가 만들어야 하니깐 저렇게 해야함. 아니며 abstract로 하던가.

파일 위치는 여기에 했어

 

저 새롭게 만든 ViewModelModule을 Component에 연결해주자

이렇게

 

@Singleton
@Component(modules = [DataModule::class, ViewModelModule::class])
interface ApplicationComponent {
    @Component.Builder
    interface Builder {
        fun dataModule(dataModule: DataModule): Builder

        fun viewModelModule(viewModelModule: ViewModelModule): Builder

        fun build(): ApplicationComponent
    }

    fun inject(testActivity: TestActivity)
}

ViewModelModule관련해서 추가된거가 보이지?

어플리케이션은 이렇게...

class FinApplication: Application() {

    lateinit var applicationComponent: ApplicationComponent

    override fun onCreate() {
        super.onCreate()

        applicationComponent = DaggerApplicationComponent.builder()
            .dataModule(DataModule(this))
            .viewModelModule(ViewModelModule())
            .build()
    }
}

 

 

그럼 View에서는 어떻게 쓰는지 보자.

viewmodel은 fragment에 붙어. 그니깐 기존에 activity에서 테스트 하던걸 fragment로 바꿔야해

기존 테스트 액티비티 레이아웃을 프라그먼트 레이아웃으로 옮기고, 뷰 바인등 바꾸고 등등등을 아래처럼 했어.

 

testactivity는 프라그먼트 붙이게...

class TestActivity : AppCompatActivity() {
    private lateinit var binding: ActivityTestBinding

    @Inject
    lateinit var repository: Repository

    override fun onCreate(savedInstanceState: Bundle?) {
        (applicationContext as FinApplication).applicationComponent.inject(this)
        super.onCreate(savedInstanceState)
        binding = ActivityTestBinding.inflate(layoutInflater)
        setContentView(binding.root)
        val testFragment = TestFragment();
        val fragmentManager: FragmentManager = supportFragmentManager
        val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
        fragmentTransaction.add(binding.fragmentFrame.id, testFragment)
        fragmentTransaction.commit()
    }
}

activity_text.xml은

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".test.TestActivity">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/fragmentFrame"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

 

 

fragment는 기존 activity 형태로...

 

class TestFragment : Fragment() {
    private lateinit var binding: FragmentTestBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        (requireActivity().application as FinApplication).applicationComponent.inject(this)
        binding = FragmentTestBinding.inflate(layoutInflater)
        val view = binding.root
        binding.button1.setOnClickListener{
//            val items = repository.getAllData()
//            binding.textView.text = items.size.toString()
        }
//        val setButton = findViewById<Button>(R.id.button2)
        binding.button2.setOnClickListener{
//            val invItem = InvItem("test1", null, 55, Date(235235L), null)
//            repository.insertData(invItem)
        }
        return view
    }
}

fragment_test.xml은 이렇게

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".test.TestFragment">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="get"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:text="set"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button1" />

    <TextView
        android:id="@+id/textView"
        android:layout_width="250dp"
        android:layout_height="166dp"
        android:layout_marginTop="4dp"
        android:text="TextView"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button2" />

</androidx.constraintlayout.widget.ConstraintLayout>

 

구조는... 이렇게

 

이제 fragment에 viewmodel을 어떻게 쓰는지 보자.

그냥 아래 두줄만 추가해주면 됨

 

@Inject
lateinit var factory: ViewModelProvider.Factory
private val viewModel: InvestItemViewModel by viewModels { factory }

 

그런데 보이는 빨간줄!!!

 

요렇게 해결

https://lonewhite.tistory.com/31

 

Unresolved reference: viewModels 에러 해결법

야심차게 mvvm으로 작성하고 빌드하면 항상 만나는 에러! 뭐 이유는 간단하다. 기본으로 안스에서 프로젝트 만들면, 관련된걸 implement 안해줘서 그런거임 아래처럼 gradle에 추가해주면 됨 implementat

www.lonewhite.com

그러고 보니 이게 안되서 activity에서 안보였던건데.... 괜히 프라그먼트 만들었네...

 

프라그먼트가 추가되었고, 거기에 inject를 해야하니깐, ApplicationComponent에다가 아래 줄 추가

fun inject(testFragment: TestFragment)

 

뭐 여튼 fragment 코드 전체적으로 보면,

 

class TestFragment : Fragment() {
    private lateinit var binding: FragmentTestBinding

    @Inject
    lateinit var factory: ViewModelProvider.Factory
    private val viewModel: InvestItemViewModel by viewModels { factory }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        (requireActivity().application as FinApplication).applicationComponent.inject(this)
        binding = FragmentTestBinding.inflate(layoutInflater)
        val view = binding.root
        binding.button1.setOnClickListener{
//            val items = repository.getAllData()
//            binding.textView.text = items.size.toString()
            binding.textView.text = viewModel.getItemCount().toString()
        }
//        val setButton = findViewById<Button>(R.id.button2)
        binding.button2.setOnClickListener{
//            val invItem = InvItem("test1", null, 55, Date(235235L), null)
//            repository.insertData(invItem)
        }
        return view
    }
}

 

 

이렇게 하면 완성!

 

이제 빌드 돌려볼까??

 

하지만 실패...  내용을 보면... 안쓰는 아래 코드가 왜 드가 있냐?

fun viewModelModule(viewModelModule: ViewModelModule): Builder

 

아까 위에서 viewmodel은 팩토리가 만들고 어쩌고 했자나, 그래서 팩토리만 잘 만들어지면, 되는거라서... 사실 저걸 하면 안되는 거였음.

 

그래서 코드를 다시 보면, 아래처럼

 

@Singleton
@Component(modules = [DataModule::class, ViewModelModule::class])
interface ApplicationComponent {
    @Component.Builder
    interface Builder {
        fun dataModule(dataModule: DataModule): Builder
        fun build(): ApplicationComponent
    }

    fun inject(testActivity: TestActivity)
    fun inject(testFragment: TestFragment)
}

 

 

class FinApplication: Application() {

    lateinit var applicationComponent: ApplicationComponent

    override fun onCreate() {
        super.onCreate()

        applicationComponent = DaggerApplicationComponent.builder()
            .dataModule(DataModule(this))
            .build()
    }
}

(기존과 동일)

 

이렇게 해주면 빌드가 잘 돌고, viewmodel이 잘 드간건 확인할 수 있음!

 

그럼 다음은.... viewmodel도 활용할겸... room에서 flow로 받아서 viewmodel에서 가공하고 다시 flow로 넘기고, 그걸 코루틴을 이용해서 스코프 잡고 하는 식으로... 시간 될때 해볼께.

반응형

댓글