반응형

이 글은 이전 포스팅([Android] 화면 회전해도 데이터 유지하기 - AAC ViewModel)에 이어지는 글입니다.

 

ViewModel 클래스를 상속하여 정의한 클래스는 개발자가 직접 생성자를 통하여서 인스턴스를 생성할 수 없고, ViewModelProvider.Factory 인터페이스를 필요로 합니다.

 

이번 포스팅을 통하여서 안드로이드는 어떻게 ViewModel 을 관리하고, 생성하는지 알아보고 필요에 따라 어떤 방식으로 뷰모델 객체를 생성해야 하는지 살펴보도록 하겠습니다.

 

ViewModelStoreOwner / ViewModelStore

ViewModel 은 ViewModelStore 라는 객체에서 관리를 합니다.

ViewModelStore 클래스는 내부적으로 HashMap<String, ViewModel> 를 두어 ViewModel 을 관리합니다.

그러면 이 ViewModelStore 객체는 누가 어떻게 만들고 관리할까요?

 

그건 바로 ViewModelStoreOwner 라는 녀석이 합니다.

ViewModelStoreOwner 는 다음과 같이 생긴 인터페이스이고, FragmentActivity 의 부모격인 ComponentActivityFragment 클래스가 이를 구현(Implement) 하고 있습니다. 

// Java Code
public interface ViewModelStoreOwner {

    @NonNull
    ViewModelStore getViewModelStore();
}

 

액티비티와 프래그먼트가 바로 이 ViewModelStoreOwner 를 구현하고 있기 때문에, 우리는 ViewModel 객체를 생성할 때 액티비티나 프래그먼트를 필요로 하고, 어떤 Owner 를 통해 생성하냐에 따라 ViewModel 의 Scope 가 결정됩니다.

 

그렇다면 액티비티나 프래그먼트만 있으면 ViewModel 인스턴스를 생성할 수 있을까요?

아니겠죠. 앞서 말씀 드린것처럼 ViewModel 을 생성할 때는 실질적으로 ViewModel 인스턴스를 생성하는 역할을 하는 팩토리를 필요로 합니다. (팩토리 패턴이 낯선 분들은 여기를 참고하세요)

 

ViewModelProvider 안에 정의되어 있는 팩토리는 다음과 같이 정의되어 있습니다. 

// Java Code
public class ViewModelProvider {

    public interface Factory {

        @NonNull
        <T extends ViewModel> T create(@NonNull Class<T> modelClass);
    }
}

 

이제 ViewModel 인스턴스를 생성하기 위한 재료들은 전부 확인 했으니, 실습을 통해 직접 생성해보도록 하겠습니다.

 

ViewModelProviders -> Deprecated

정상적인 실습을 하기에 앞서, 많은 블로그들과 심지어 구글 공식 Developer 사이트에서도 아직까지 예제로 사용되고 있는 ViewModelProviders 에 대해 짚고 넘어가겠습니다.

ViewModelProviders.of(this).get(AnyViewModel::class.java) --> deprecated !!

 

ViewModelProviders 클래스는 Deprecated 되었습니다. "Deprecated 되었다" 라는 말은 해당 클래스(또는 메소드)를 더이상 개발하지 않는 것을 뜻하며 특정 버전 이후로 호환되지 않을 수 있기 때문에 더이상 사용하지 않아야한다는 것을 의미합니다.

 

대체로 deprecated 되는 기능들에 대해서는 대안이 함께 제시되는데요. 이 경우에는 ViewModelProviders 대신에 ViewModelProvider 를 사용하도록 권장하고 있습니다. 따라서 우리는 ViewModelProvider 를 통해 ViewModel 객체를 생성하는 방법에 대해서만 다루도록 하겠습니다.

 

1. 파라미터가 없는 ViewModel - Lifecycle Extensions

이 방법은 가장 편한 방법 중 하나입니다. androidx.lifecycle의 lifecycle-extensions 모듈을 가져와 사용하면 됩니다.

먼저, module 수준의 build.gradle 에 다음과 같이 디펜던시를 추가해줍니다.

dependencies {
    // ...
    implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"
}

 

다음으로 예제에 사용할 ViewModel 클래스를 정의하겠습니다. 파라미터와 멤버 변수, 함수를 갖고있지 않은 심플한 ViewModel 클래스입니다.

// 파라미터가 없는 ViewModel class
class NoParamViewModel : ViewModel()

 

이제 이 뷰모델 클래스를 통하여 액티비티에서 객체를 생성해주겠습니다.

class MainActivity : AppCompatActivity() {

    private lateinit var noParamViewModel: NoParamViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /* use ViewModelProvider's constructor provided from lifecycle-extensions package */
        noParamViewModel = ViewModelProvider(this).get(NoParamViewModel::class.java)
    }
}

 

보시다시피 ViewModelProvider 생성자에 this 를 포함해주고, get() 메소드 내에 생성하고자 하는 뷰모델 클래스 타입을 넣어주면 됩니다.

이때 this 는 ViewModelStoreOwner 타입이기 때문에 액티비티프래그먼트를 넣어주시면 됩니다. 

 

2. 파라미터가 없는 ViewModel - ViewModelProvider.NewInstanceFactory

이번에 살펴볼 방법은 NewInstanceFactory 입니다.

이는 안드로이드가 기본적으로 제공해주는 팩토리 클래스이며, ViewModelProvider.Factory 인터페이스를 구현하고 있습니다. 따라서 ViewModel 클래스가 파라미터를 필요로 하지 않거나, 특별히 팩토리를 커스텀 할 필요가 없는 상황에서는 1번 방법을 사용하거나, 2번 방법을 사용하면 되겠습니다.

 

2번 방법은 1번에서 추가해준 lifecycle-extenstions 모듈을 추가하지 않아도 사용가능한 방법입니다.

class MainActivity : AppCompatActivity() {

    private lateinit var noParamViewModel: NoParamViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        noParamViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
            .get(NoParamViewModel::class.java)
    }
}

 

3. 파라미터가 없는 ViewModel - ViewModelProvider.Factory

이번에는 직접 ViewModelProvider.Factory 인터페이스를 구현하여 보도록 하겠습니다.

이 방법의 장점은 하나의 팩토리로 다양한 ViewModel 클래스를 관리할 수도 있고, 원치 않는 상황에 대해서 컨트롤 할 수 있습니다.

class NoParamViewModelFactory : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return if (modelClass.isAssignableFrom(NoParamViewModel::class.java)) {
            NoParamViewModel() as T
        } else {
            throw IllegalArgumentException()
        }
    }
}

 

위 코드는 NoParamViewModel 클래스가 아니면 IllegalArgumentException 을 던지도록 구현되어 있습니다. 이는 어디까지나 개발자의 마음대로 구현하면 되는 부분이며, 어떤 타입의 클래스가 전달되더라도 인스턴스를 생성하도록 구현할 수도 있습니다.

 

4. 파라미터가 있는 ViewModel - ViewModelProvider.Factory

3번 방법의 연장선상에서, ViewModelProvider.Factory 를 구현하면 파라미터를 소유하고 있는 ViewModel 객체의 인스턴스를 생성할 수 있습니다. 직접 구현한 Factory 클래스에 파라미터를 넘겨주어 create() 내에서 인스턴스를 생성할 때 활용하면 됩니다.

 

그럼 이번에는 파라미터가 있는 ViewModel 을 정의하고 그에 대한 객체를 생성하는 예제를 작성해보겠습니다.

 

ViewModel

class HasParamViewModel(val param: String) : ViewModel()

 

ViewModelFactory

class HasParamViewModelFactory(private val param: String) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return if (modelClass.isAssignableFrom(HasParamViewModel::class.java)) {
            HasParamViewModel(param) as T
        } else {
            throw IllegalArgumentException()
        }
    }
}

 

Activity (or Fragment)

class MainActivity : AppCompatActivity() {

    private lateinit var hasParamViewModel: HasParamViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val sampleParam = "Ready Story"

        hasParamViewModel = ViewModelProvider(this, HasParamViewModelFactory(sampleParam))
            .get(HasParamViewModel::class.java)
    }
}

 

3번과 4번 예제에서는 각각 파라미터 유무에 따라 별도의 팩토리 클래스를 구현하였지만, 꼭 그럴 필요 없이 하나의 팩토리 클래스로 두 가지 상황에 대한 처리를 한꺼번에 할 수도 있습니다.

 

5. 파라미터가 없는 AndroidViewModel - AndroidViewModelFactory

이번에는 조금 다른 ViewModel 을 살펴보겠습니다.

사실 developer 사이트에 의하면 ViewModel 클래스에서 Context 객체를 소유하거나 접근하는 것에 있어서 권장하지 않고 있습니다. 하지만 정말 불가피하게 필요한 경우가 있을 수 있는데요. ViewModel 에서 Context 를 사용해야할 필요성이 있을 때는 AndroidViewModel 클래스를 사용하면 됩니다. 

 

그리고 안드로이드에서는 이러한 AndroidViewModel 객체에 대한 생성을 위해 ViewModelProvider.AndroidViewModelFactory 라는 별도의 팩토리를 제공합니다. 예제를 통해 살펴보겠습니다.

 

먼저 AndroidViewModel 을 상속하는 ViewModel 클래스를 정의해줍니다.

class NoParamAndroidViewModel(application: Application) : AndroidViewModel(application)

보시다시피 AndroidViewModel 은 Application 객체를 필요로 합니다.

 

이번에는 AndroidViewModelFactory 를 이용하여 뷰모델 객체를 생성해보겠습니다.

class MainActivity : AppCompatActivity() {

    private lateinit var noParamAndroidViewModel: NoParamAndroidViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        noParamAndroidViewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application))
            .get(NoParamAndroidViewModel::class.java)
    }
}

 

AndroidViewModelFactory 내부 코드를 살펴보면 2번에서 살펴본 NewInstanceFactory 를 상속한 코드라는 걸 확인할 수 있습니다.

 

6. 파라미터가 있는 AndroidViewModel

드디어 마지막입니다. 파라미터가 있는 AndroidViewModel 객체를 생성하는 방법인데요.

사실 4번의 방법으로도 가능합니다만, 이번에는 5번에서 살펴본 AndroidViewModelFactory 와 유사한 방식으로 커스텀 팩토리를 구현해보도록 하겠습니다.

 

먼저 파라미터가 있는 AndroidViewModel 클래스를 준비합니다.

class HasParamAndroidViewModel(application: Application, val param: String)
    : AndroidViewModel(application)

 

다음으로 Custom Factory를 구현해줍니다.

class HasParamAndroidViewModelFactory(private val application: Application, private val param: String)
    : ViewModelProvider.NewInstanceFactory() {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        if (AndroidViewModel::class.java.isAssignableFrom(modelClass)) {
            try {
                return modelClass.getConstructor(Application::class.java, String::class.java)
                    .newInstance(application, param)
            } catch (e: NoSuchMethodException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            } catch (e: IllegalAccessException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            } catch (e: InstantiationException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            } catch (e: InvocationTargetException) {
                throw RuntimeException("Cannot create an instance of $modelClass", e)
            }
        }
        return super.create(modelClass)
    }
}

 

제법 복잡해보이지만, catch 문을 많이 분기했을 뿐이지 실상은 복잡하지 않습니다.

위 코드에서는 ViewModelProvider.NewInstanceFactory 클래스를 상속하여 구현했지만, ViewModelProvider.Factory 인터페이스를 구현하여도 무방합니다.

 


긴 글이었습니다. 어떤 방법으로든 각자의 상황에 맞게 사용하시면 되고, 이번 포스팅에서 다루지는 않았지만 android-ktx / fragment-ktx 모듈을 사용하면 보다 편리하게 뷰모델 인스턴스를 생성할 수도 있습니다.

 

위에서 작성된 모든 예제 코드는 Github 저장소에서 확인하실 수 있습니다.

 

 

반응형
  • 큐큐이큐 2020.10.28 02:38

    와 ㅠㅠㅠ 인터넷에 있는 예제보고 제가 직접 만들어보려고 하니까 안되서 한참 찾았었는데
    이렇게 잘 정리를 해주시다니.. 정말 감사합니다 ㅠㅠ

  • Favicon of https://dunge.tistory.com BlogIcon Jiib Studio 2021.03.03 23:06 신고

    아.. 너무 간결하게 잘 정리해주셔서 속이 다 뻥 뚫리는것같습니다. 감사합니다!!

  • Favicon of https://yuar.tistory.com BlogIcon 헤이메어 2021.04.03 15:26 신고

    글작성 하실때 이런 코드나 제목 부분 어떻게 하시는지 알려주실수있나요?
    가독성 높게 정리 정말 잘하시는것같아요.
    전 이렇게 안되더라구요.

    • Favicon of https://readystory.tistory.com BlogIcon Dev. Ready Kim 2021.04.04 22:07 신고

      https://post.naver.com/viewer/postView.nhn?volumeNo=30990142&memberNo=34904471

      좋은 참고글이 있어 공유드립니다 ㅎㅎ

  • Favicon of https://kesio.tistory.com BlogIcon BlueBright 2021.05.18 14:11 신고

    지금껏 그냥 잘 작동하는 예제 골라서 사용하고 있었는데, 이렇게 많은 종류가 있었네요.
    좋은 정보 감사합니다. ^^

  • Favicon of https://whyprogrammer.tistory.com BlogIcon 상추님 2021.08.18 11:17 신고

    글 잘 봤습니다 ㅎㅎ
    android-ktx / fragment-ktx 모듈을 사용하는 방법도 궁금하네요 ㅎ

반응형