UI 상태 저장하기

이 글은 Android Developers 사이트의 Saving UI States 의 한국어 번역본입니다. 아주 많은 의역과 오역이 들어갈 수 있으므로, 만일 틀린 부분이 있다면 댓글로 지적 부탁드립니다.


시스템에서 시작한 Activity나 애플리케이션이 파괴될 때 마다 Activity의 UI의 상태를 적시에 보존하고 복원하는 것은 사용자 경험(User Experience) 에 있어서 중요한 부분입니다. 이러한 경우, 사용자는 UI 상태가 그대로 유지될 것이라고 예상하지만 시스템은 Activity와 Activity에 보관된 모든 상태를 파괴합니다.

사용자 기대와 시스템 동작 간의 차이를 메꾸기 위해 ViewModel 객체, onSaveInstanceState() 메서드 및 로컬 저장소를 조합하여 사용해서 이러한 애플리케이션 및 Activity 인스턴스의 전환에 있어 UI 상태를 유지할 수 있습니다. 이러한 옵션(ViewModel, onSavedInstanceState, 로컬 저장소)들을 결합하는 방법을 결정하는 데에는 UI 상태 데이터의 복잡성, 앱의 사용 사례 및 검색 속도, 메모리 사용 속도 측면에서 비교하고 결정하게 됩니다.

어떤 방법을 사용하든 관계 없이 앱이 UI 상태와 관련하여 사용자의 기대치를 충족하는지 확인하고 원할한 UI를 제공해야 합니다. (특히 화면 회전과 같이 자주 발생하는 설정의 변경(Configuration Changes) 후에 데이터를 다시 불러오는 데에 걸리는 시간을 피해야 합니다.) 대부분의 경우, ViewModel와 onSaveInstanceState를 모두 사용해야 합니다.

이 페이지에서는 UI 상태를 보존하는 데에 사용할 수 있는 옵션, 각각의 절충점 및 제한 사항을 소개합니다.

사용자가 기대하는 것과 시스템의 동작

사용자가 취하는 행동에 따라 사용자는 해당 Activity의 상태가 지워지거나 상태가 보존되기를 기대합니다. 경우에 따라 시스템은 사용자가 예상하는 작업을 자동으로 수행하는 반면, 다른 경우에는 시스템이 사용자가 기대하는 것과 반대되는 행동을 할 수 있습니다.

사용자에 의한 UI 상태 제거

사용자는 Activity를 시작할 때에 완전히 제거할 때 까지 해당 Activity의 UI 상태가 동일하게 유지될 것으로 예상합니다. 완전히 제거하는 예는 다음과 같습니다.

  • 뒤로 버튼 누르기
  • 최근 내역(Recents) 에서 끌어내리기
  • Navigation의 Up 활동
  • 애플리케이션 설정 화면에서 강제종료
  • 완료 화면 등에서의 화면 종료 (다시 말하면 Activity.finish())

이러한 완전 제거에 대해서는 사용자가 이 Activity를 완전히 종료하고, 다시 생성시켰을 때 깨끗한 상태에서 시작될 것으로 기대한다는 것입니다. 이러한 시나리오에 대한 기본 시스템 동작은 사용자의 기대 사항과 일치합니다. 즉, Activity의 인스턴스Activity에 저장된 모든 상태Activity와 관련된 상태 레코드삭제되고 메모리에서 제거되는 것을 의미합니다.

완전 제거 규칙에는 몇 가지 예외가 있습니다. 예를 들어, 뒤로 버튼을 누른다고 해도 브라우저 같은 앱에서는 Activity가 종료되는 것이 아닌 전 페이지를 보여줍니다.

시스템에 의한 UI 상태 제거

사용자는 화면 회전 또는 멀티태스킹 모드 전환으로 같은 설정의 변경(Configuration Changes) 에서 Activity의 UI 상태가 동일하게 유지될 것으로 기대합니다. 그러나 기본적으로 시스템은 이러한 설정의 변경이 일어날 경우 Activity의 UI 상태를 제거하고 Activity를 제거합니다. 설정의 변경에 대해서는 설정 참조 페이지 를 참조하세요. 참고로, 설정의 변경에 대해 기본 동작을 재정의하는 것은 권장되지는 않지만 가능합니다. 자세한 내용은 설정의 변경에 대한 재정의 를 참조하세요.

또한 사용자는 임시로 다른 앱으로 전환한 다음에 앱으로 돌아올 경우 Activity의 UI 상태가 동일하게 유지될 것으로 기대합니다. 예를 들어 사용자는 검색 Activity에서 검색을 수행한 다음 홈 버튼을 누르거나 전화 통화에 응답합니다. 다시 검색 Activity에 돌아가면 이전과 마찬가지로 검색 키워드와 그에 맞는 결과가 보여질 것으로 기대합니다.

이 시나리오에서는 앱이 백그라운드에서 실행되며 시스템이 앱 프로세스를 메모리에 저장하기 위해 최선을 다합니다. 그러나 시스템은 사용자가 앱과 상호 작용하지 않는 동안 애플리케이션 프로세스를 파괴할 수 있습니다. 이러한 경우 Activity 인스턴스는 해당 Activity에 저장된 모든 상태와 함께 제거됩니다. 사용자가 앱을 다시 실행하면 예기치 않게도 깨끗한 상태가 됩니다. 프로세스 중단에 대한 자세한 내용은 프로세스 및 애플리케이션 라이프사이클 주기를 참조하세요.

UI 상태 보존 옵션

UI 상태에 대한 사용자의 기대가 기본 시스템 동작과 일치하지 않는 경우 시스템 시작 파괴가 사용자에게 영향을 미치지 않도록 사용자의 UI 상태를 저장 및 복원해야 합니다.

UI상태를 보존하기 위한 각 옵션은 사용자 경험에 영향을 미치는 차원에 따라 달라집니다.

UI 상태 저장하기

ViewModelSavedStateInstance로컬 스토리지
저장 위치메모리직렬화되어 디스크 내부디스크 또는 네트워크
설정 변경의 경우 살아남음
시스템에 의해 제거될 경우 살아남음아니오
사용자가 제거할 경우 살아남음아니오아니오
데이터 제한복잡한 객체도 괜찮지만 메모리 공간에 의해 제한될 수 있음Primitive Type나 작은 객체(String)만 해당디스크 공간 및 네트워크 리소스의 쿼리 시간에 의해 제한될 수 있음
읽기/쓰기 시간빠름(메모리 엑세스만 필요)느림((역)직렬화 및 디스크 엑세스 필요)느림 (디스크 엑세스 또는 네트워크 트랜잭션 필요)

ViewModel를 사용하여 설정 변경의 경우에 대한 처리

ViewModel은 사용자가 애플리케이션을 사용하는 동안 OS 관련 데이터를 저장하고 관리하는 데 적합합니다. 이를 통해 UI 데이터에 빠르게 액세스 할 수 있으며, 화면 회전이나, 창 크기 조정 및 기타 일반적으로 발생하는 설정 변경을 방지할 수 있습니다. ViewModel을 구현하는 방법에 대한 자세한 내용은 ViewModel 가이드를 참조하세요.

ViewModel은 데이터를 메모리에 유지하므로 디스크나 네트워크의 데이터보다 접근하는 것보다 더 저렴합니다. ViewModel은 Activity 또는 다른 LifecycleOwner과 연결되며, 시스템은 설정 변경으로 인해 발생한 새 Activity 인스턴스와 ViewModel을 자동으로 연결합니다.

ViewModel은 사용자가 Activity또는 Fragment을 사용하지 않거나 finish()를 요청하면 시스템에 의해 자동으로 제거됩니다. 즉, 이러한 시나리오에서 사용자가 예상하는 대로 상태가 지워집니다.

onSavedInstanceState와 달리 ViewModels는 시스템에 의해 제거될 경우 같이 제거됩니다. 따라서 ViewModel과 함께 onSaveInstanceState (또는 다른 저장 방법)을 사용하여 onSavedInstanceState에 ID를 저장하여 ViewModel의 데이터 복구를 도우는 등의 행동을 해야 합니다.

설정 변경 때 UI 상태를 저장하기 위한 메모리 내 방법이 이미 있다면 ViewModel를 굳이 사용할 필요는 없습니다.

onSavedInstanceState를 백업으로 사용하여 시스템에 의해 제거될 경우에 대한 처리

onSaveInstanceState() 메서드에는 시스템이 UI 컨트롤러(Activity 또는 Fragment)를 제거한 후 다시 생성하는 경우 UI 컨트롤러 상태를 다시 로드하는 데 필요한 데이터가 저장됩니다. 저장된 인스턴스 상태를 구현하는 방법에 대한 자세한 내용은 Activity 라이프 사이클 가이드에서 Activity 상태 저장 및 복원을 참조하세요.

SavedInstanceBundle은 설정 변경 및 시스템에 의한 제거 둘 다 유지하지만 디스크에 데이터를 보존하기 때문에 저장 용량과 속도에 따라 제한됩니다. 직렬화할 개체가 복잡한 경우 직렬화하는 데 많은 메모리가 소모될 수 있습니다. 이 프로세스는 설정 변경 중에 메인 스레드에서 발생하므로, 직렬화하는 데 너무 오래 걸릴 경우 프레임 손실과 시각적 스터칭이 발생할 수 있습니다.

onSaveInstanceState()를 사용하여 대량의 데이터를 저장하거나 긴 (역)직렬화가 필요한 복잡한 데이터 구조를 저장하지 마십시오. 대신 Primitive 유형과 String와 같은 단순하고 작은 객체만 저장하십시오. 따라서 onSaveInstanceState()를 사용하여 ID와 같이 최소한의 데이터만 저장하고 다른 지속성 메커니즘(ViewModel 등)이 실패할 경우 UI를 이전 상태로 복원하는 데 사용하세요. 대부분의 앱은 시스템에 의해 제거된 경우를 처리하기 위해 onSaveInstanceState()를 구현해야 합니다.

애플리케이션의 사용 사례에 따라 onSaveInstanceState()를 전혀 사용할 필요가 없을 수도 있습니다. 예를 들어 브라우저를 사용하면 사용자가 브라우저를 종료하기 전에 보던 웹 페이지로 돌아갈 수 있습니다. Activity가 이러한 방식으로 수행되는 경우 onSaveInstanceState()를 사용하지 않고 모든 작업을 로컬로 유지할 수 있습니다.

또한 Intent를 통해 Activity를 시작할 때 설정 변경과 시스템에 의해 제거된 경우를 복원할 때 모두 Activity Bundle가 제공됩니다. Activity를 시작할 때 UI 상태 데이터가 추가 용도로 전달된 경우에는 SavedInstanceBundle 대신 Extra Bundle을 사용할 수 있습니다. Intent Extras에 대한 자세한 내용은 Intent 및 Intent Filter를 참조하세요.

이러한 시나리오 중 하나에서는 설정 변경중에 데이터베이스에서 데이터를 다시 로드하는 로직이 낭비되지 않도록 ViewModel을 사용해야 합니다.

보존할 UI 데이터가 단순하고 가벼울 경우onSaveInstanceState()를 통해서 데이터를 보존할 수 있습니다.

참조: 이제 Saved State 모듈을 사용하여 ViewModel 객체에 Saved State를 제공할 수 있습니다. 저장된 상태는 SavedStateHandle라는 객체를 통해 접근할 수 있습니다. Android Lifecycle-aware components 코드랩 에서 어떻게 작동하는지 확인할 수 있습니다.

로컬 스토리지를 사용하여 복잡하거나 큰 데이터에 대한 프로세스 중단 처리

데이터베이스 또는 SharedPreference 과 같은 영구 로컬 스토리지는 애플리케이션이 사용자 기기에 설치되어 있는 동안에는 유지됩니다. 이러한 로컬 스토리지는 사용자에 의하거나 시스템에 의해 제거된 Activity에 대해 모두 대응할 수 있지만, 로컬 스토리지에서 메모리로 읽어 와야 하므로 접근하는 데 많은 비용이 들 수 있습니다. 이러한 영구 로컬 스토리지는 Activity를 열고 닫을 때 손실되지 않아야 할 모든 데이터를 저장하는 애플리케이션의 주 아키텍쳐의 일부일 수 있습니다.

ViewModel 또는 onSaveInstanceState()는 모두 장기적인 임시 스토리지 솔루션이므로 데이터베이스와 같은 로컬 스토리지를 대체할 수 없습니다. UI 상태를 일시적으로 저장하는 데에는 이러한 임시 스토리지 솔루션을 활용할 수 있지만, 보존되어야 할 앱 데이터에는 영구 스토리지를 사용해야 합니다. 로컬 스토리지를 활용하여 애플리케이션 모델 데이터를 장기간 유지하는 방법에 대한 자세한 내용은 애플리케이션 아키텍쳐 가이드를 참조하세요.

UI 상태 관리: 분할 및 정복

Activity를 다양한 유형의 지속성 메커니즘으로 나누어 효율적으로 UI 상태를 저장하고 복원할 수 있습니다. 대부분의 경우, 이러한 메커니즘은 데이터 복잡성, 접근 속도 및 수명의 절충점에 기반하여 Activity에 사용되는 다양한 유형의 데이터를 저장해야 합니다.

  • 로컬 스토리지: 앱을 열고 닫을 때에도 손실되지 않아야 할 데이터
    • 예: 노래 객체, 오디오 파일이나 메타데이터
  • ViewModel: 연결된 UI 컨트롤러를 표시하는 데 필요한 모든 데이터
    • 예: 가장 최근에 검색한 노래와 그 검색어
  • onSavedInstanceState: Activity가 중지되었다가 다시 생성되는 경우 Activity의 상태를 쉽게 로드하는 데 필요한 소량의 데이터. 여기에 복잡한 객체를 저장하는 대신 로컬 스토리지에 복잡한 객체를 유지하고 이러한 객체에 대한 고유 ID를 onSavedInstanceState에 저장합니다.
    • 에: 가장 최근에 검색하는데 사용한 검색어

예를 들어, 노래 라이브러리를 검색하는 Activity를 떠올려 보세요. 다양한 이벤트를 처리하는 방법은 다음과 같습니다.

사용자가 노래를 추가하면 ViewModel은 즉시 이 데이터를 로컬로 유지합니다. 새로 추가된 이 곡이 UI에 표시해야 하는 곡인 경우 ViewModel 객체의 데이터도 업데이트하여 곡을 추가해야 합니다. 메인 스레드에서 모든 데이터베이스 삽입을 수행해야 하는 것을 잊지 마세요.

사용자가 노래를 검색할 때 UI 컨트롤러를 위해 데이터베이스에서 로드한 복잡한 노래 데이터는 즉시 ViewModel 객체에 저장되어야 합니다. 또한 검색 조회 자체를 ViewModel 객체에 저장해야 합니다.

활동이 백그라운드로 진행되면 onSaveInstanceState()를 통해 검색 쿼리를 저장해야 합니다. 적은 양의 데이터에 있어서는 저장하기 쉽습니다. 이 데이터는 Activity를 이전의 상태로 되돌리는 데 필요한 모든 정보라고 할 수 있습니다.

복잡한 상태의 복원 : 부품을 다시 조립하다

사용자가 Activity로 돌아갈 시간이 되면 Activity을 다시 만드는 데 사용할 수 있는 두가지 시나리오가 있습니다.

  • 시스템에 의해 중지된 후 Activity이 재생성 되는 경우: Activity에 저장된 쿼리가onSaveInstanceState() 번들로 제공되며 쿼리를 ViewModel에 전달해야 합니다. ViewModel은 캐시 된 검색 결과가 없음을 확인하고 검색 쿼리를 사용하여 검색 결과를 로드합니다.
  • Activity가 설정 변경에 의해 재성성 되는 경우: ViewModel가 이미 캐시된 검색 결과가 있으므로 데이터베이스에 다시 요청할 필요가 없습니다.

참조: Activity가 처음 생성되면 onSavedInstanceState() 번들에는 데이터가 없으며 ViewModel 또한 비어있습니다. ViewModel 객체를 생성할 때 빈 쿼리를 통과하여 ViewModel 객체에 아직 로드할 데이터가 없음을 알 수 있습니다. 따라서 깔끔한 상태에서 Activity가 시작됩니다.

추가 링크

UI 상태 저장에 대한 자세한 내용을 알고 싶다면, 다음 링크를 참조하세요.

DaggerAutoInject – Contributing to AndroidInjection with Annotation

도입

Dagger 를 사용하기 위해서는 주입할 의존성을 @Provides 나 @Binds 를 통하여 제공하는 것 말고도, 주입될 대상을 제공해야 하는데, Members-injection methods (dagger/api/latest/dagger/Component.html) 를 사용하거나 각 안드로이드 구성요소 (Activity, Service) 들에 대한 별도의 Subcomponent 를 만들고 @IntoSet 어노테이션을 이용해 DispatchingAndroidInjector 의 injectorFactories 에 해당 Subcomponent 를 추가해야 합니다.

두 가지 방법, Members-injection methods 와 Subcomponet 가 공통점을 가지고 있다면 빠르게 개발 해야 하는 입장에서는 상당히 고역이란 점입니다.

Members-injection methods 는 Component 내부에 직접 적어주긴 하나 작성해야 하는 코드 양이 적어 괜찮다고 볼 수 있습니다. 다음 코드에서 void inject ~ 코드가 하나의 Members-injection methods 를 나타냅니다.

public interface AppComponent {
    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(MainApplication application);
        AppComponent build();
    }
 
    void inject(MainApplication mainApp);
    void inject(MenuView menuView);
}

Subcomponent 로 오게 되면 작성해야 하는 코드는 매우 많아집니다.

  • MainActivity 에 대한 Subcomponent 를 만드는데, 이 Subcomponent 는 AndroidInjector<MainActivity> 를 상속하고 있어야 하고, abstract 클래스인 Builder 를 구현해야 함
  • 만든 MainActivitySubCompoent.Builder 를 @Binds, @IntoMap, @Activitykey 등의 메서드로 제공하는데, 내부적으로 Map<Class<?>, Provider<Activity> > 를 가지고 있어, 필요한 때에 Provider를 제공해야 함
  • 위 두 가지를 모두 포함한 클래스를 만들고, 해당 클래스를 @Module로 설정한 다음 Module 의 파라미터로 MainActivitySubcomponet 를 제공해야 함.

위 세 가지를 모두 반영한 것이 아래 코드입니다.

package com.github.windsekirun.daggerautoinject;

import android.app.Activity;
import com.github.windsekirun.daggerautoinject.sample.MainActivity;
import dagger.Binds;
import dagger.Module;
import dagger.Subcomponent;
import dagger.android.ActivityKey;
import dagger.android.AndroidInjector;
import dagger.multibindings.IntoMap;

@Module(subcomponents = ActivityModule_Contribute_MainActivity.MainActivitySubcomponent.class)
public abstract class ActivityModule_Contribute_MainActivity {
  private ActivityModule_Contribute_MainActivity() {}

  @Binds
  @IntoMap
  @ActivityKey(MainActivity.class)
  abstract AndroidInjector.Factory<? extends Activity> bindAndroidInjectorFactory(
      MainActivitySubcomponent.Builder builder);

  @Subcomponent
  public interface MainActivitySubcomponent extends AndroidInjector<MainActivity> {
    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<MainActivity> {}
  }
}

아무리 Dependency Injection 가 좋다고 해도, 도입에 있어 조금 겁이 날 수 있는 부분이라 생각합니다.

그래서 Dagger-Android 모듈에서는 @ContributesAndroidInjector 어노테이션을 제공하는데, 하나의 전체 모듈을 만들고 @ContributesAndroidInjector 어노테이션을 부착한 메서드를 각 안드로이드 구성요소당 하나씩 만들어 주면 나머지 세 가지 코드에 대해서는 자동으로 생성하는 기능을 가지고 있습니다.

package com.github.windsekirun.daggerautoinject;

import com.github.windsekirun.daggerautoinject.sample.MainActivity;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;

@Module
public abstract class ActivityModule {
  @ContributesAndroidInjector
  abstract MainActivity contribute_MainActivity();
}

하지만, 이와 같은 @ContributeAndroidInjector 에도 개선할 점은 있습니다. 앱에 Activity 가 여러 개 있다면, Activity 를 하나 작성할 때 마다 ActivityModule 란 곳에 작성해주는 것도 꽤나 고역이라고 생각됩니다.

그래서 찾은 라이브러리가 florent37/DaggerAutoInject (https://github.com/florent37/DaggerAutoInject) 이고, 이 라이브러리가 Activity / Fragment 만 제공하던 것을 좀 더 확장해 Activity / Fragment / Service / Broadcast Receiver / ContentProvider / ViewModel (Android Architecture components) 에 대해 제공하게 한 것이 WindSekirun/DaggerAutoInject (https://github.com/WindSekirun/DaggerAutoInject) 입니다.

이번 글에서는 DaggerAutoInject 라이브러리가 어떻게 작동하는지 살펴보고 적용 방법을 설명하려 합니다.

구현 원리 및 사용법

이전 글인 Generate Kotlin Code with KotlinPoet uses Annotation Processor (https://pyxispub.uzuki.live/generate-kotlin-code-with-kotlinpoet-uses-annotation-processor-1/) 에서도 설명한 Annotation Processor 로 특정 Annotation 가 붙은 클래스를 모두 찾아서 각 타입에 맞게 ActivityModule / ServiceModule / FragmentModule / ViewModelModule 를 생성하는 것입니다.

ActivityModule 의 구현 방법

ActivityModule / ServiceModule / FragmentModule / BroadcastReceiverModule / ContentProviderModule 에 대해서는 모두 같은 구현 방법을 취합니다.

  1. @InjectActivity 를 모두 찾아 ContributesHolder 란 객체에 담고, Map<ClassName, ContributesHolder> 로 가지고 있는다.
  2. 각 어노테이션 별 TypeSpec (클래스를 생성할 스펙) 를 만들고 map 를 반복문에 통과시켜서 @ContributesAndroidInjector abstract SimpleName contributes_SimpleName(); 라는 메서드를 생성한다.
  3. TypeSpec 를 Java 파일로 만든다.

위 과정을 모두 포함하는 것이 다음 코드입니다.

static <A extends Annotation> void processHolders(RoundEnvironment env, Class<A> cls, Map<ClassName, ContributesHolder> map) {
    for (Element element : env.getElementsAnnotatedWith(cls)) {
        final ClassName classFullName = ClassName.get((TypeElement) element);
        final String className = element.getSimpleName().toString();
        map.put(classFullName, new ContributesHolder(element, classFullName, className));
    }
}

static void constructContributesAndroidInjector(String className, Collection<ContributesHolder> holders, Filer filer) {
    final TypeSpec.Builder builder = TypeSpec.classBuilder(className)
            .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
            .addAnnotation(Constants.DAGGER_MODULE);

    for (ContributesHolder contributesHolder : holders) {
        builder.addMethod(MethodSpec.methodBuilder(Constants.METHOD_CONTRIBUTE + contributesHolder.className)
                .addAnnotation(Constants.DAGGER_ANDROID_ANNOTATION)
                .addModifiers(Modifier.ABSTRACT)
                .returns(contributesHolder.classNameComplete)
                .build()
        );
    }

    final TypeSpec newClass = builder.build();
    final JavaFile javaFile = JavaFile.builder(Constants.PACKAGE_NAME, newClass).build();

    try {
        javaFile.writeTo(System.out);
        javaFile.writeTo(filer);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

첫 번째 메서드가 1번의 과정을 가지고 있으며, 두 번째 메서드가 2, 3번의 과정을 가지고 있습니다.

사용 방법

먼저, AppComponent 의 @Component 어노테이션에 ActivityModule 들을 삽입합니다.

@Singleton
@Component(modules = {
        AppModule.class,

        AndroidInjectionModule.class,
        AndroidSupportInjectionModule.class,

        ActivityModule.class,
        FragmentModule.class,
        ViewModelModule.class,
        ServiceModule.class,
        BroadcastReceiverModule.class,
        ContentProviderModule.class
})
public interface AppComponent {
    void inject(MainApplication application);

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(Application application);

        AppComponent build();
    }
}

그 다음, 적용할 Activity 나 Service 들에 @InjectActivity / @InjectFragment / @InjectService / @InjectBroadcastReceiver / @InjectContentProvider 를 부착합니다.

@InjectActivity
public class MainActivity extends BaseActivity {

    @Inject SharedPreferences sharedPreferences;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d("MainActivity", sharedPreferences.getAll());
    }

그 다음 Application 내에 @InjectApplication 어노테이션을 부착한 뒤  아래 필드를 삽입하고, HasActivityInjector, HasServiceInjector,
HasBroadcastReceiverInjector, HasContentProviderInjector 인터페이스들을 구현합니다.

@Inject DispatchingAndroidInjector<Activity> mActivityDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<Service> mServiceDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<BroadcastReceiver> mBroadcastReceiverDispatchingAndroidInjector;
@Inject DispatchingAndroidInjector<ContentProvider> mContentProviderDispatchingAndroidInjector;

그 다음, Application.onCreate 에서 생성한 AppComponent 를 DaggerAutoInject 란 클래스에 넘겨줍니다.

전체 코드는 다음과 같습니다.

@InjectApplication(component = AppComponent.class)
public class MainApplication extends Application implements HasActivityInjector, HasServiceInjector,
        HasBroadcastReceiverInjector, HasContentProviderInjector {

    @Inject DispatchingAndroidInjector<Activity> mActivityDispatchingAndroidInjector;
    @Inject DispatchingAndroidInjector<Service> mServiceDispatchingAndroidInjector;
    @Inject DispatchingAndroidInjector<BroadcastReceiver> mBroadcastReceiverDispatchingAndroidInjector;
    @Inject DispatchingAndroidInjector<ContentProvider> mContentProviderDispatchingAndroidInjector;

    @Override
    public void onCreate() {
        super.onCreate();

        final AppComponent appComponent = DaggerAppComponent.builder()
                .application(this)
                .build();

        DaggerAutoInject.init(this, appComponent);
    }

    @Override
    public AndroidInjector<Activity> activityInjector() {
        return mActivityDispatchingAndroidInjector;
    }

    @Override
    public AndroidInjector<Service> serviceInjector() {
        return mServiceDispatchingAndroidInjector;
    }

    @Override
    public AndroidInjector<BroadcastReceiver> broadcastReceiverInjector() {
        return mBroadcastReceiverDispatchingAndroidInjector;
    }

    @Override
    public AndroidInjector<ContentProvider> contentProviderInjector() {
        return mContentProviderDispatchingAndroidInjector;
    }
}

그 다음 Fragment 의 경우에는 앱의 BaseActivity 클래스에 HasSupportFragmentInjector 를 구현하고, DispatchingAndroidInjector<Fragment> dispatchingFragmentInjector;필드를 삽입해서 supportFragmentInjector() 메서드에 반환합니다.

public class BaseActivity extends AppCompatActivity implements HasSupportFragmentInjector {

    @Inject
    DispatchingAndroidInjector<Fragment> dispatchingFragmentInjector;

    @Override
    public AndroidInjector<Fragment> supportFragmentInjector() {
        return dispatchingFragmentInjector;
    }
}

이 단계에서 DaggerAutoInject 를 사용할 준비는 모두 마쳤으며, 각 구성요소들의 onCreate 에서 AndroidInjection.inject(this);를 호출하면 내부적으로 Dependency Injection 를 시행합니다. 단, Activity / Fragment 는 자동으로 inject 메서드를 호출하므로 따로 할 필요가 없습니다.

주의할 점은 이 dispatching 필드들은 Type Parameter 에 있는 클래스가 앱에 하나라도 존재 해야 작동합니다. 만일 앱에 Service 가 없는데 DispatchingAndroidInjector<Service> 를 사용하려 한다면 컴파일 단계에서 오류가 나옵니다.

ViewModelModule 의 구현 방법

ViewModel 의 경우에는 조금 다른 구현 방법 및 용도를 가집니다. ActivityModule 는 그 자체가 Contribute 의 용도를 가지지만 ViewModelModule 는 위에서도 언급했던 Map<Class<?>, Provider<ViewModel>> 를 구성하게 도와주는 용도로 사용됩니다.

processHolders 메서드까지는 같지만, 생성하는 부분은 다른 메서드를 사용합니다.

private void constructViewHolderModule() {
    final TypeSpec.Builder builder = TypeSpec.classBuilder(Constants.VIEWHOLDER_MODULE)
            .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
            .addAnnotation(Constants.DAGGER_MODULE);

    for (ContributesHolder contributesHolder : mViewModelHolders.values()) {
        TypeName typeName = contributesHolder.classNameComplete;
        String parameterName = String.valueOf(contributesHolder.className.charAt(0)).toLowerCase() +
                contributesHolder.className.substring(1);

        builder.addMethod(MethodSpec.methodBuilder(Constants.METHOD_BIND + contributesHolder.className)
                .addAnnotation(Constants.DAGGER_BINDS)
                .addParameter(typeName, parameterName)
                .addAnnotation(Constants.DAGGER_INTOMAP)
                .addAnnotation(AnnotationSpec.builder(ViewModelKey.class)
                        .addMember("value", contributesHolder.className + ".class").build())
                .addModifiers(Modifier.ABSTRACT)
                .returns(Constants.VIEWMODEL)
                .build()
        );
    }

    final TypeSpec newClass = builder.build();
    final JavaFile javaFile = JavaFile.builder(Constants.PACKAGE_NAME, newClass).build();

    try {
        javaFile.writeTo(System.out);
        javaFile.writeTo(mFiler);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

위 결과로 생성된 ViewModelModule 는 아래와 같은 형태를 가지게 됩니다.

@Module
public abstract class ViewModelModule {

  @Binds
  @IntoMap
  @ViewModelKey(MainViewModel.class)
  abstract ViewModel bind_MainViewModel(MainViewModel mainViewModel);
}

이 ViewModelModule 를 사용하기 위해서는 아래 요소가 필요합니다.

  1. ViewModelModule 로 통하여 제공된 Map<Class<?>, Provider<ViewModel>> 를 사용하여 실제 ViewModel 의 객체를 반환할 Factory 클래스. 이 클래스는 ViewModelProvider.Factory 를 상속하여 Android Architecture components 의 ViewModelProvider.of 로 가져올 수 있게 합니다.
  2. 1번에서 생성할 Factory 클래스를 주입할 Module 클래스.

이 요소를 구현한 것이 다음 코드입니다. 설명은 주석으로 갈음합니다.

@Singleton // 한번 의존성이 생성되고 난 후에는 기존 인스턴스를 그대로 사용
/*
 * AAC 의 ViewModelProvider.Factory 를 상속하는 클래스를 제작.
 * 생성자로는 Map<Class<*>, Provider<ViewModel>> 를 받는데, 이 생성자는 미리 @IntoMap 와 @ViewModelKey 를 부착하여
 * Module 에 제공된 ViewModel 클래스의 클래스 객체와 그 ViewModel 의 생성자를 제공하는 Provider 객체를 각각 key, value로서 받는다.
 * 따라서, Map에는 MainViewModel.class.java 라는 키에 Provider<MainViewModel> 가 제공됨

 * 이 Provider 는 해당 ViewModel 에 대한 생성자를 제공할 수 있는 기능을 가지고 있기 때문에,
 * 실제로 ViewModel 의 생성자가 수십개 이상 있어도 그 생성자가 Dagger 에 의해 제공된다면 실제로는 의존성만 가져오면 됨
 */
class DaggerViewModelFactory @Inject constructor(private val creators: Map<Class<*>,
        @JvmSuppressWildcards Provider<ViewModel>>) : ViewModelProvider.Factory { 

    override fun <T : ViewModel> create(modelClass: Class<T>): T { 
        /*
         * creators 에서 주어진 key로 찾는데, 해당 값이 없으면
         * creators 에 modelClass 가 접근 가능한 요소를 찾아서 값을 얻어낸다.
         * 그래도 값이 없으면, IllegalArgumentException 예외를 발생시킨다.
         * creator 의 실제 타입이 나오지 않았는데, Kotlin에서는 타입 추론이 가능하기 때문에
         * 실제 타입을 명시하지 않아도 된다. 이 경우 추론된 타입은 Provider<T>, 즉 Provider<ViewModel> 이다.
         */
        val creator = creators[modelClass] ?:
                creators.asIterable().firstOrNull { modelClass.isAssignableFrom(it.key) }?.value
                ?: throw IllegalArgumentException("unknown model class " + modelClass)

        // 찾은 Provider<ViewModel> 반환
        return try {
            creator.get() as T
        } catch (e: Exception) {
            throw RuntimeException(e)
        }

    }
}
@Module
public abstract class BaseBindsModule {
    // 생성한 DaggerViewModelFactory 클래스를 @Binds 를 통해 @Module 에 설정
    @Binds
    abstract ViewModelProvider.Factory bindViewModelFactory(DaggerViewModelFactory factory);
}

마지막으로 ViewModel 에 @InjectViewModel 를 부착하고, Activity 에서 ViewModelProvider.of 로 가져옵니다.

@InjectViewModel
public class MainViewModel extends AndroidViewModel {

    @Inject
    public MainViewModel(@NonNull MainApplication application) {
        super(application);
    }
}
@InjectActivity 
public class MainActivity extends BaseActivity { 
    @Inject ViewModelProvider.Factory mViewModelFactory; 
    private MainViewModel mViewModel; 

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
        mViewModel = ViewModelProviders.of(this, mViewModelFactory).get(MainViewModel.class);
    }
}

이 방법의 장점은 해당 ViewModel 의 생성자가 Dagger 에 의해 주입된다면 Activity 에서 ViewModel 의 인스턴스를 가져올 때 생성자를 신경쓰지 않아도 된다는 점입니다. 단순히 보기에는 작성할 코드가 많지만, 실제 구현체인 MainActivity, MainViewModel 를 제외하면 프로젝트 특성을 가진 코드가 아니므로 Base화를 하여 구성해도 문제가 없습니다.

마무리

어떻게 보면 최종 사용자 (End-Developer) 의 할 일을 많이 줄였지만, 아래의 개선점은 있습니다.

첫번째로, 위에서도 소개했던 Member-injections methods 의 자동화 여부 입니다. 코드가 적다고 해도 나름대로 일은 일이기 때문입니다.

두번째로, 주입될 의존성들은 앱 전역으로 inject 가 되는데, 이를 일부 범위에서 inject 되게 할 수 있는 Scope  기능을 적용하지 못합니다. 단, 이쪽은 특정 Scope 어노테이션을 부착하기만 하면 되므로 InjectActivity 의 파라미터로 제공하게 하면 문제가 없을거라 판단됩니다.

개선점을 찾게 되면 글을 업데이트 하도록 하겠습니다.

이 라이브러리를 통하여 자동화를 하게 되면, 각 Activity 에 대한 Scope 를 지정할 수 없다는 것이 문제가 되는 것은 알고 있지만, 어떻게 해야 좋은 방법일지는 아직까지 고민중입니다.


특성 이미지 출처는 https://proandroiddev.com/dagger-2-part-three-new-possibilities-3daff12f7ebf 입니다.