top of page

(Part - III) Android: Split ViewModel into Delegates with Dagger

In the previous parts, we discussed how to split ViewModel functionalities into ViewModel Delegates and how to use Hilt for dependency injection. Now, we will continue from there and convert our implementation to use Dagger, a fully static, compile-time dependency injection framework for both Java and Android.


Dagger in ViewModel


Dagger provides a way to inject dependencies into Android classes, including ViewModel. Here is how we can use Dagger in our UserProfileViewModel:

class UserProfileViewModel @Inject constructor(

    override val delegateScope: CloseableCoroutineScope,

    userDetailDelegateFactory: ViewModelDelegateFactory<UserDetailViewModelDelegate>,

    changeAvatarDelegateFactory: ViewModelDelegateFactory<ChangeAvatarDelegate>,

    ) : ViewModel(delegateScope),

    UserDetailViewModelDelegate by userDetailDelegateFactory.create(delegateScope),

    ChangeAvatarDelegate by changeAvatarDelegateFactory.create(delegateScope) {

    // Extract viewmodel functionality to delegates

}

interface ViewModelDelegateFactory<out D : ViewModelDelegate> {

    fun create(delegateScope: CoroutineScope): D
}

In the above code, we are using @Inject to inject dependencies into our ViewModel. The `delegateScope` is of type CloseableCoroutineScope, which is a custom scope that we can cancel when the ViewModel is cleared. This is important because the same `delegateScope` needs to be injected into the ViewModel and ViewModelDelegates to ensure that all coroutines launched in this scope are cancelled when the ViewModel is cleared. This helps prevent memory leaks and ensures that our ViewModel and ViewModelDelegates do not continue to do work after the ViewModel has been cleared.


Providing Dependencies with Dagger


We can define a Dagger module to provide the dependencies for our ViewModel:

@Module
class ViewModelDelegateModule {

    @Provides
    fun provideDelegateScope(): CloseableCoroutineScope {

        return CloseableCoroutineScope()
    }

    @Provides
    fun provideUserDetailViewModelDelegateFactory(
        factory: UserDetailViewModelDelegateImpl.Factory
    ): ViewModelDelegateFactory<UserDetailViewModelDelegate> {

        return factory
    }

    @Provides
    fun provideChangeAvatarDelegateFactory(
        factory: ChangeAvatarDelegateImpl.Factory
    ): ViewModelDelegateFactory<ChangeAvatarDelegate> {

        return factory
    }
}

In the above module, we are providing a CloseableCoroutineScope and factories for our ViewModel Delegates. These factories will be used to create instances of our ViewModel Delegates with the same `delegateScope` as our ViewModel.


Implementing ViewModel Delegates with Dagger


Next, we implement our ViewModel Delegates. Each delegate is responsible for a specific task in the ViewModel. We use Dagger to inject the dependencies needed by each delegate:

interface UserDetailViewModelDelegate : ViewModelDelegate {
    //...
}

class UserDetailViewModelDelegateImpl @AssistedInject constructor(
    @Assisted
    override val delegateScope: CoroutineScope,
    private val getUserDetailUseCase: GetUserDetailUseCase
) : UserDetailViewModelDelegate {
    //...

    @AssistedFactory
    interface Factory : ViewModelDelegateFactory<UserDetailViewModelDelegate> {
        override fun create(
            @Assisted delegateScope: CoroutineScope
        ): UserDetailViewModelDelegateImpl
    }
}

interface ChangeAvatarDelegate : ViewModelDelegate {
    //...
}

class ChangeAvatarDelegateImpl @AssistedInject constructor(
    @Assisted
    override val delegateScope: CoroutineScope,
    private val changeAvatarUseCase: ChangeAvatarUseCase
) : ChangeAvatarDelegate {
    //...

    @AssistedFactory
    interface Factory : ViewModelDelegateFactory<ChangeAvatarDelegate> {
        override fun create(
            @Assisted delegateScope: CoroutineScope
        ): ChangeAvatarDelegateImpl
    }
}

In conclusion, by using Dagger with ViewModel Delegates, we can manage dependencies in a more efficient and maintainable way. This approach not only keeps our code clean but also makes it easier to test. The use of @AssistedInject ensures that a new delegateScope is created for each new ViewModel, which is crucial for the proper cancellation of coroutines when the ViewModel is cleared.

Comments


bottom of page