개발관련일지

android hilt (di) 학습 정리 본문

개발기록/안드로이드

android hilt (di) 학습 정리

BEECHANGBOT 2023. 7. 30. 23:30

힐트에 대해서 기본과 궁금했던 부분들 정리

힐트는 안드로이드에서 의존성주입 라이브러리이다. Dagger2에서 단순하게 만들기 위해서 만들어졌고 Dagger2 기반으로 빌드된다. Dagger2를 사용해본적은 없지만 힐트가 Dagger2에서 파생된거여서 아주 기본적인부분은 정리했다.

 

Dagger2

class MyApplication : Application() {
    val appContainer = AppContainer() // 컨테이너 선언
}

class LoginContainer(val userRepository: UserRepository) {

    val loginData = LoginUserData()
    val loginViewModelFactory = LoginViewModelFactory(userRepository) // 로그인뷰모델
}



class AppContainer {
    val userRepository = UserRepository(localDataSource, remoteDataSource)
    var loginContainer: LoginContainer? = null // 엑티비티의 생명주기를 따라가기 위해서 런타임중에 주입
}
-----------------------------

class LoginActivity: Activity() {

    private lateinit var loginViewModel: LoginViewModel
    private lateinit var loginData: LoginUserData
    private lateinit var appContainer: AppContainer

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        appContainer = (application as MyApplication).appContainer
        appContainer.loginContainer = LoginContainer(appContainer.userRepository)

        loginViewModel = appContainer.loginContainer.loginViewModelFactory.create()
        loginData = appContainer.loginContainer.loginData
    }

    override fun onDestroy() {
        appContainer.loginContainer = null // loginContainer의 생명주기는 , 엑티비티의 생명주기를 같이하기위함
        super.onDestroy()
    }
}

Dagger2를 사용하지 않고 안드로이드 앱 아키텍처를 따르는 방식으로 개발하면 AppContainer가 필요한 인스턴스들을 관리한다. LoginContainer는 엑티비티와 생명주기를 같이 하기 위해서 엑티비티에서 생성 후 AppContainer에 추가해준다.

만약 AppContainer가 없다면 로긴엑티비티에 객체들을 생성하는 코드들이 나열되어있을거다. 

AppContainer는 애플리케이션과 생명주기를 같이하고 어플리케이션 클래스에서 생성된다. 단점이라면 의존성주입에 필요한 순서대로 객체들을 생성해야하고 앱의 사이즈가 커질수록 앱컨테이너는 더 커지게된다.

수동적으로 객체를 생성하고 의존성주입을 관리하는 경우 늘어나는 객체생성코드들을 관리해줘야하고 실수나 오류발생 확률에 노출되게된다. 위의 프로세스를 도와주기위한 라이브러리가 Dagger2이다. 

 

Hilt

Dagger2 기반으로 빌드되며 안드로이드에 맞게 스코프 설정이 가능하다.

객체를 제공할 때 스코프내에 이미 생성된 인스턴스가 있으면 재사용한다. 스코프에서 엑티비티 -> 뷰컴포넌트가 있으면 엑티비티에서 생성한 A인스턴스가 하위 뷰컴포넌트에서 A를 사용할 경우 미리 생성된 A를 제공받음 

@HiltAndroidApp
class ExampleApplication : Application() { ... }

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { 
	@Inject lateinit var analytics: AnalyticsAdapter // not private
}
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService // 얜 인터페이스니까 바로 구현체가 필요함 -> 그래서 구현체를 정의해주고 따로 작업이 필요함 -> @Binds
) { ... }

interface AnalyticsService {
  fun analyticsMethods()
}

class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

// 인터페이스 주입경우 
@Module // 힐트 모듈 
@InstallIn(ActivityComponent::class) //사용 가능한 범위를 지정 
abstract class AnalyticsModule {

  @Binds // 힐트에 인터페이스의 인스턴스를 제공할 때 사용할 구현 방법을 전달역할 
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

// 외부클래스이거나 , 빌더패턴을 사용해서 생성자 주입이 불가능한 경우
@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
		...
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

 

안드로이드 클래스(엑티비티,프레그먼트,서비스,뷰 등)이 의존에 필요한 객체를 힐트로 부터 받기 위해서는 대거 컴포넌트(그래프)에 포함되어야한다. 이때 사용되는게 @AndroidEntryPoint 어노테이션이다. 

엔트리포인트를 설정하면 생성되는 클래스

/**
 * A generated base class to be extended by the @dagger.hilt.android.AndroidEntryPoint annotated class.
 If using the Gradle plugin, this is swapped as the base class via bytecode transformation.
 */
public abstract class Hilt_MainActivity extends AppCompatActivity implements GeneratedComponentManagerHolder { 

	// 생성자로 실행되면 1번
	Hilt_MainActivity() {
    super();
    _initHiltInternal();
  }

  Hilt_MainActivity(int contentLayoutId) {
    super(contentLayoutId);
    _initHiltInternal();
  }

// 2번
private void _initHiltInternal() {
		// 이부분이 엑티비티의 ComponentActivity extends androidx.core.app.ComponentActivity의 함수고
		// 해당 엑티비티의 사용 가능여부를 검증하는데 사용하는 걸로 판단된다.
    addOnContextAvailableListener(new OnContextAvailableListener() {
      @Override
      public void onContextAvailable(Context context) {
        inject();
      }
    });
  }

@Override
  public final Object generatedComponent() {
// 어플리케이션의 힐트 선언여부 확인하고 EntryPoints를 반환
    return this.componentManager().generatedComponent();

/*
ActivityComponentManager.createComponent
return EntryPoints.get(
            activityRetainedComponentManager, ActivityComponentBuilderEntryPoint.class)
        .activityComponentBuilder()
        .activity(activity)
        .build();
*/
  }

// 3번 주입하는 곳 
protected void inject() {
    if (!injected) {
      injected = true;
      ((MainActivity_GeneratedInjector) this.generatedComponent()).injectMainActivity(UnsafeCasts.<MainActivity>unsafeCast(this));
    }
  }
}


@OriginatingElement(
    topLevelClass = MainActivity.class
)
@GeneratedEntryPoint
@InstallIn(ActivityComponent.class)
public interface MainActivity_GeneratedInjector {
  void injectMainActivity(MainActivity mainActivity);
}

힐트로부터 생성되는 클래스고 엑티비티를 기준으로 ActivityComponetManager가 EntryPoint를 생성하고 MainActivity_GeneratenInjector에 의해서 해당 엑티비티가 컴포넌트에 포함된다.

 

힐트가이드에서는 모듈에 주입해야할 객체를 정의할 때 interface를 주입하거나 생성자로 주입이 가능한거는 @Binds를 사용하라고 하고있다. 반면에 외부 클래스나 빌더패턴으로 되어잇는 경우는 @Provide로 사용한다. 근데 @Binds로 되는것도 @Provides로 가능하다. 처음에는 동작하는 걸 봤을 떈 뭐 잘못알고 있는줄 알았다. 두방식의 차이는 

// 테스트용코드
interface AInterface {
    fun doSomething()
}

class AImpl @Inject constructor(private val cClass: C_Class) : AInterface {
    override fun doSomething() {
        Log.e("BImpl" , "doSomething ${cClass.toString()}")
    }
}

interface BInterface {
    fun doSomethingElse()
}

class BImpl(private val dClass: D_Class) : BInterface {
    override fun doSomethingElse() {
        Log.e("BImpl" , "doSomethingElse ${dClass.toString()}")
    }
}

class C_Class @Inject constructor() {
    // DClass의 구현
}

class D_Class @Inject constructor() {
    // DClass의 구현
}

@Module
@InstallIn(SingletonComponent::class)
interface AModule {
    @Binds
     fun bindAInterface(impl: AImpl): AInterface
}

@Module
@InstallIn(SingletonComponent::class)
object BModule {
    @Provides
    fun provideBInterface(dClass: D_Class): BInterface {
        return BImpl(dClass)
    }
}

A를 바인드로 B를 프로바이드로 진행

프로바이드를 사용하는 B를 제외하고는 객체들을 Dagger의 Factory를 상속해서 인스턴스를 제공해준다. 

// ------------------A (@Binds)----------------------
public final class AImpl_Factory implements Factory<AImpl> {
	  private final Provider<C_Class> cClassProvider;
	
	public AImpl_Factory(Provider<C_Class>cClassProvider) {
	    this.cClassProvider =cClassProvider;
	}
	  @Override
	  public AImpl get() {
	    returnnewInstance(cClassProvider.get());
	}
	
	  public static AImpl_Factory create(Provider<C_Class>cClassProvider) {
	    return new AImpl_Factory(cClassProvider);
	}
	
	  public static AImpl newInstance(C_ClasscClass) {
	    return new AImpl(cClass);
	}
}

// ------------------B (@Provides)----------------------
public final class BModule_ProvideBInterfaceFactory implements Factory<BInterface> {
  private final Provider<D_Class> dClassProvider;

  public BModule_ProvideBInterfaceFactory(Provider<D_Class> dClassProvider) {
    this.dClassProvider = dClassProvider;
  }

  @Override
  public BInterface get() {
    return provideBInterface(dClassProvider.get());
  }

  public static BModule_ProvideBInterfaceFactory create(Provider<D_Class> dClassProvider) {
    return new BModule_ProvideBInterfaceFactory(dClassProvider);
  }
// BModule 인스턴스 끌고와서 생성함
  public static BInterface provideBInterface(D_Class dClass) {
    return Preconditions.checkNotNullFromProvides(BModule.INSTANCE.provideBInterface(dClass));
  }
}

각각 생성되는 파일은 A : AImpl_Factory, B : BModule_ProcideBInterdaceFactory 이다.

B모듈은 BInterface 타입객체를 생성하기 위해 BInterface get()에서 B모듈의 인스턴스를 끌어다가 D클래스객체를 생성한다.

A모듈은 AImpl_Factory에서 직접 new AImpl(cClass); 으로 객체를 생성한다.

B같은 경우는 B모듈을 생성해서 가져오므로 빌더패턴이던 다른클래스던 어떤상황도 B모듈에 정의된 방식으로 객체를 생성한다.

참고한 링크 인 스택오버플로우에서도 질문이 있었는데 확인해보니 데거에 대해서 좀 몰라서 이해 가능한 부분이 많지 않았지만 , 이해한 부분만 작성하면 바인드가 간결해지고 Dagger의 내부모듈에서 프로바이드는 모듈을 인스턴스화 하기 때문에 메모리가 조금더 사용된다는 점이었다. 

 

또 의아했던점은 @ActivityRetainedScoped였다. 가이드의 이미지를 봤을 떄는 엑티비티 관련된 스코프가 두개였고 뷰모델하고 생명주기가 동일했다. 찾아보니 @ActivityRetainedScoped은 엑티비티가 재성성되는걸 무시하고 첫시작 -> 완전끝 까지의 생명주기를 가지고 있는거고 컴피규체인지, 다크모드와 같은 외부상황에서 영향을 받지 않는 생명주기를 말한다. 반대로 생각하면 @ActivityScoped는 컨피규체인지가 발생하면 의존성을 가지고 있는 객체들도 새로운 객체일거다. 

뷰모델스코프하고 동일해보이는데 차이가 머냐 생각을 처음엔 했는데 생명주기의 소유가 다르기 떄문에 의미가 없다. 엑티비티는 엑티비티고 뷰모델은 뷰모델이며 별도의 생명주기를 가지고 있으므로 쓰임이 동일해 보이지는 않았다. 각자에 맞는걸 쓰면된다. 

 

// 동일 타입일 경우 -> @Qualifier를 이용해서 이름을 지음 
// 퀄리파이어 본인이 사용할 퀄리파이어를 생성함 
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

  @AuthInterceptorOkHttpClient // 만든거를 @Binds or @Provides 위에 사용
  @Provides
  fun provideAuthInterceptorOkHttpClient(
    authInterceptor: AuthInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(authInterceptor)
               .build()
  }

  @OtherInterceptorOkHttpClient // 만든거를 @Binds or @Provides 위에 사용
  @Provides
  fun provideOtherInterceptorOkHttpClient(
    otherInterceptor: OtherInterceptor
  ): OkHttpClient {
      return OkHttpClient.Builder()
               .addInterceptor(otherInterceptor)
               .build()
  }
}

// 사용 시엔 인젝트 하기전에 적어놔야함 그래야 힐트가 맞는 객체로 리턴해줌 
@AuthInterceptorOkHttpClient
@Inject lateinit var okHttpClient: OkHttpClient

class ExampleServiceImpl @Inject constructor(
  @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient
)

// 안드로이드 생명주기 관련 퀄리파이어가 필요하면 아래꺼 사용하면됨
@ApplicationContext , @ActivityContext

동일한 타입일 경우에는 퀄리파이어를 사용해서 구분이 가능하다.