top of page

(Part - II) Android: Split ViewModel into Delegates with Hilt

In the previous part, we discussed how to split ViewModel functionalities into ViewModel Delegates to make our code more modular, easier to read, and easier to test. Now, we will continue from there and convert our implementation to use Hilt, a dependency injection library built on top of Dagger.


Hilt in ViewModel

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

@HiltViewModel
class UserProfileViewModel @Inject constructor(
    override val delegateScope: CoroutineScope,
    userDetailDelegate: UserDetailViewModelDelegate,
    changeAvatarDelegate: ChangeAvatarDelegate
) : ViewModel(delegateScope),
    UserDetailViewModelDelegate by userDetailDelegate,
    ChangeAvatarDelegate by changeAvatarDelegate {
    // Extract viewmodel functionality to delegates
}

In the above code, we are using @HiltViewModel to inject dependencies into our ViewModel. The delegateScope is of type CoroutineScope 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 Hilt

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

@Module
@InstallIn(ViewModelComponent::class)
class ViewModelDelegateModule {
    @ViewModelScoped
    @Provides
    fun provideDelegateScope(): CoroutineScope {
        return CloseableCoroutineScope()
    }
}

In the above module, we are providing a CloseableCoroutineScope and binding it to the CoroutineScope interface. This scope will be used by our ViewModel Delegates. The @ViewModelScoped annotation ensures that a new instance of CloseableCoroutineScope is created for each new ViewModel. This is crucial for the proper cancellation of coroutines when the ViewModel is cleared.


Implementing ViewModel Delegates with Hilt

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

class UserDetailViewModelDelegateImpl @Inject constructor(
    override val delegateScope: CoroutineScope,
    private val getUserDetailUseCase: GetUserDetailUseCase
) : UserDetailViewModelDelegate {
    //...
}
class ChangeAvatarDelegateImpl @Inject constructor(
    override val delegateScope: CoroutineScope,
    private val changeAvatarUseCase: ChangeAvatarUseCase
) : ChangeAvatarDelegate {
    //...
}
@Module
@InstallIn(ViewModelComponent::class)
interface ViewModelDelegateModule {

    @Binds
    fun bindUserDetailViewModelDelegate(
        impl: UserDetailViewModelDelegateImpl
    ): UserDetailViewModelDelegate

    @Binds
    fun bindChangeAvatarDelegate(
        impl: ChangeAvatarDelegateImpl
    ): ChangeAvatarDelegate

    companion object {

        @ViewModelScoped
        @Provides
        fun provideDelegateScope(): CoroutineScope {
            return CloseableCoroutineScope()
        }
    }
}

In conclusion, by using Hilt 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 @ViewModelScoped 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.



Komentar


bottom of page