개인 광고 영역

이번 Google I/O에서는 Android Architecture Components를 소개하였습니다.

AAC를 이용해서 ViewModel/Lifecycle/LiveData을 제공해주고 있습니다.

이중 ViewModel 초기화 방법을 정리하려고 합니다.

aac-background

제가 만든 라이브러리는 GitHub - LifecycleExtensions에서 확인 가능합니다.


ViewModel 적용하기

Components 추가하는 방법Lifecycle, LiveData, ViewModel을 함께 사용하려면 다음의 dependencies을 추가해주어야 합니다.

dependencies {
	compile "android.arch.lifecycle:runtime:1.0.0-alpha5"
	compile "android.arch.lifecycle:extensions:1.0.0-alpha5"
	annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha5"
}


ViewModel 클래스 생성하기

Google GitHub 샘플인 Android Architecture Components samples 통해서 AAC을 적용하는 방법을 살펴볼 수 있습니다. BasicSample, RxJava 적용 샘플, DataBinding, Room이 포함되어있는 다양한 샘플들이 있으니, AAC 적용에 참조할 수 있습니다.

저는 그중 RxJavaSample을 살펴보았고, 샘플을 공부하면서 다음의 글을 정리하였습니다.

먼저 ViewModel은 아래와 같이 구현하며, lifecycle.ViewModel을 별도로 상속받습니다.

그리고 생성자에는 (name, age)을 함께 초기화하는 코드로 작성합니다.

import android.arch.lifecycle.ViewModel

class ListViewModel(name: String, age: Int) : ViewModel() {
	// 생략
}

일단 new로 접근하는 방법으로 아래와 같이 생성해보도록 하겠습니다.

class ViewFragment : LifecycleFragment() {

	private lateinit var viewMode: ListViewModel

	override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
		viewMode = ListViewModel("Name", 0)
	}
}

하지만 Google lifecycle.ViewModel은 Android lifecycle을 함께 처리하기 위한 로직을 가지고 있습니다.

그러나 위와 같이 초기화한다면 lifecycle과 무관하게 동작하여, architecture Lifecycle을 사용하는 게 의미가 없게 됩니다.(onResume/onDestroy/onPause 등...)

일단 ViewModel 가이드를 살펴보니 다음과 같이 적용하라고 나옵니다.

ViewModelProviders.of(this)로 초기화를 진행해야 하며, class을 get 메소드에 넘겨서 초기화를 진행합니다. 내부에서 class을 전달받아 객체 생성을 진행합니다.

class ViewFragment : LifecycleFragment() {

	private lateinit var viewMode: ListViewModel

	override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
		viewModel = ViewModelProviders.of(this).get(ListViewModel::class.java)
	}
}


생성자에 초기값들도 있는데요?

네 위와 같이 ViewModelProviders.of(this).get(ViewModel::class.java)을 사용하면 생성자 정의를 별도로 할 수는 없습니다.

ViewModelProviders에는 아래와 같은 2개의 추가 get() 메소드가 존재합니다.

@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment, @NonNull Factory factory) {
    return new ViewModelProvider(ViewModelStores.of(fragment), factory);
}

@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
				@NonNull Factory factory) {
		return new ViewModelProvider(ViewModelStores.of(activity), factory);
}		

바로 ViewModel.Factory을 함께 초기화할 수 있고, 별도의 Factory를 초기화하지 않으면, 기본적으로 DefaultFactory의 아래 코드를 통해

modelClass.getConstructor(Application.class).newInstance(mApplication);

생성자를 초기화하고 있습니다. 그래서 별도의 생성자의 초가화 코드 사용이 불가한 것이죠.

public static class DefaultFactory extends ViewModelProvider.NewInstanceFactory {

    private Application mApplication;

    /**
     * Creates a {@code DefaultFactory}
     *
     * @param application an application to pass in {@link AndroidViewModel}
     */
    public DefaultFactory(@NonNull Application application) {
        mApplication = application;
    }

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
            //noinspection TryWithIdenticalCatches
            try {
                return modelClass.getConstructor(Application.class).newInstance(mApplication);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (InstantiationException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            }
        }
        return super.create(modelClass);
    }
}


ViewModelProvider.Factory 초기화 하는 방법

AAC 샘플의 Factory 코드 참고 - ViewModelFactory

ViewModelProvider.Factory을 상속받은 또 다른 ViewModelFactory에 아래와 같이 create 메소드를 상속 구현해줍니다.

create에서 new UserViewModel(data)와 같은 형태로 초기화 코드를 추가해둡니다.

public class ViewModelFactory implements ViewModelProvider.Factory {

    private final UserDataSource mDataSource;

    public ViewModelFactory(UserDataSource dataSource) {
        mDataSource = dataSource;
    }

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        if (modelClass.isAssignableFrom(UserViewModel.class)) {
            return (T) new UserViewModel(mDataSource);
        }
        throw new IllegalArgumentException("Unknown ViewModel class");
    }
}

그러면 아래와 같이 ViewModel을 초기화하는 과정에서 ViewModelFactory을 함께 사용할 수 있게 됩니다.

mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(UserViewModel.class);

아무래도 ViewModelProviders을 초기화하는 과정에 많은 class을 생성하게 됩니다.

구글 샘플에서는 Injection 클래스도 만들어 두고 있어, 아래와 같이 초기화를 진행하고 있죠.

mViewModelFactory = Injection.provideViewModelFactory(this);
mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(UserViewModel.class);

Injection.class 참고 - Injection

public class Injection {
	public static UserDataSource provideUserDataSource(Context context) {
		UsersDatabase database = UsersDatabase.getInstance(context);
		return new LocalUserDataSource(database.userDao());
	}

	public static ViewModelFactory provideViewModelFactory(Context context) {
		UserDataSource dataSource = provideUserDataSource(context);
		return new ViewModelFactory(dataSource);
	}
}

조금이나 new Class을 분리하기 위해서 Injection을 추가해둔 것 같은데 만들어야 할 파일들이 많아서 좋아 보이지는 않습니다.


구글 RxJava Sample에서 ViewModel을 생성하기 위해서는 다음과 같은 순서와 클래스를 사용하게 됩니다.

구현체
- ViewModel : ViewModel을 상속받아 구현
- ViewModelFactory : ViewModelProvider.Factory을 상속받아 구현하며, ViewModel을 생성
- Injection : ViewModelFactory와 DataSource 클래스를 생성하는 역할

생성 순서
- Injection을 통해 DataSource/ViewModelFactory을 생성
- ViewModelFactory을 통해 ViewModel을 생성
- ViewModelProviders.of(this, ViewModelFactory).get(ViewModel::class.java)을 통해 lifecycle 적용한 ViewModel을 사용 가능

3개의 클래스를 넘나들면서 Injection을 진행합니다.

조금 복잡하고 불필요해 보이는 클래스를 단순화하기 위해 저는 Kotlin infix notation을 사용하여 간단하게 inject 할 수 있는 라이브러리를 만들어보았습니다.


Lifecycle Extensions

Lifecycle Extensions을 만들어보았습니다.

jcenter()에 배포하게 되어 아래와 같이 추가해주시면 되겠습니다.

dependencies {
	compile 'tech.thdev.lifecycle.extensions:android-lifecycle-extensions:1.0.0-alpha1'
}

제 라이브러리는 ViewModelProvider.Factory 상속을 받아 create을 구현하는 부분을 줄인 코드입니다.

ViewModelProviders.of(this, object : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>?): T {
        return SearchViewModel(..., ..., ...) as T
    }
}).get(ViewModel::class.java)

kotlin의 infix notation을 사용하여 다음과 같이 줄여두었습니다.

fun <T : ViewModel> T.inject(fragment: Fragment): T {
    val model = this
    ViewModelProviders.of(fragment, object : ViewModelProvider.Factory {
        override fun <T : ViewModel?> create(modelClass: Class<T>?): T {
            return model as T
        }
    }).get(this.javaClass)
    return this
}

그래서 매번 ViewModelFactory을 만들 필요 없이 Activity/Fragment에서 아래와 같이 사용할 수 있습니다.

Activity에서 사용하는 방법

ViewModel은 기본 객체 생성과 동일하고, 생성된 객체에서 inject(this)을 한번 추가해줍니다.

import android.arch.lifecycle.LifecycleActivity

class MainActivity : LifecycleActivity() {
    // ...
    val viewModel = ViewModel(...).inject(this)
    // ...
}

Fragment에서 사용하는 방법

ViewModel은 기본 객체 생성과 동일하고, 생성된 객체에서 inject(this)을 한번 추가해줍니다.

import android.arch.lifecycle.LifecycleFragment

class MainActivity : Fragment() {
    // ...
    val viewModel = ViewModel(...).inject(this)
    // ...
}

Java에서는?

java는 infix notation이 별도로 없기에 아래와 같이 초기화를 해야 합니다.

public class Sample extends LifecycleActivity {

    // ...
    ViewModel viewModel = ExtensionsKt.inject(new ViewModel(...), this);
    // ...
}

Fragment인 경우도 동일합니다.

public class Sample extends LifecycleFragment {

    // ...
    ViewModel viewModel = ExtensionsKt.inject(new ViewModel(...), this);
    // ...
}


마무리

간단하게 Android Architecture Components의 ViewModel을 초기화하는 방법을 살펴보았고, Factory가 너무 길어서 줄이는 코드를 추가해보았습니다.

Download

Dagger를 사용하면 제가 만든 코드가 필요치는 않아서! Dagger 공부를 해야겠습니다.

제가 만든 라이브러리는 GitHub - LifecycleExtensions에서 확인 가능합니다.


개인 광고 영역

Tae-hwan

Android, Kotlin .. Create a content development.