Как передать параметры времени выполнения в конструктор ViewModel при использовании Hilt для внедрения зависимостей?

#android #android-fragments #dependency-injection #android-viewmodel #dagger-hilt

#Android #android-фрагменты #внедрение зависимостей #android-viewmodel #кинжал-рукоять

Вопрос:

Мне интересно, как передать параметры времени выполнения конструктору ViewModel при использовании Hilt для DI? До использования Hilt у меня была ViewModel, которая выглядит следующим образом:

 class ItemViewModel(private val itemId: Long) : ViewModel() {
    private val repo = ItemRepository(itemId) 
}

class ItemViewModelFactory(private val itemId: Long) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
    if (modelClass.isAssignableFrom(ItemViewModel::class.java)) {
        return ItemViewModel(itemId) as T
    }
    throw IllegalArgumentException("Unknown ViewModel class")
}
 

Я создаю вышеупомянутую ViewModel в своем фрагменте следующим образом:

 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {

    val args: ItemScreenFragmentArgs by navArgs()
    val itemId = args.itemId

    //Create the view model factory
    val viewModelFactory = ItemViewModelFactory(application, itemId)

    // Get a reference to the ViewModel associated with this fragment.
    val itemViewModel = ViewModelProvider(this, viewModelFactory).get(ItemViewModel::class.java)
}
 

Если бы в моем конструкторе ItemViewModel не было параметра ItemId, моя ViewModel и фрагментация с использованием Hilt выглядели бы так:

 class ItemViewModel
@ViewModelInject
constructor(private val repo: ItemRepository) : ViewModel() { }

@AndroidEntryPoint
class ItemFragment : Fragment() {
    private val itemViewModel: ItemViewModel by viewModels ()
}
 

Я пытаюсь выяснить, как передать ItemId, который я получаю из наваргов ItemFragment, в конструктор ItemViewModel? Есть ли способ сделать это с помощью Hilt?

Ответ №1:

Для тех, кто еще хочет передать параметры времени выполнения ViewModel при использовании Dagger Hilt, вот как я это сделал:

Я следовал коду из этого примера, в котором используется библиотека AssistedInject .

Теперь мой код выглядит следующим образом:

 class ItemViewModel
@AssistedInject
constructor(private val repo: ItemRepository, @Assisted private val itemId: Long) : ViewModel() {
    init {
        repo.itemId = itemId
    }

    @AssistedInject.Factory
    interface AssistedFactory {
        fun create(itemId: Long): ItemViewModel
    }

    companion object {
        fun provideFactory(
            assistedFactory: AssistedFactory,
            itemId: Long
        ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                return assistedFactory.create(itemId) as T
            }
        }
    }
}

@InstallIn(FragmentComponent::class)
@AssistedModule
@Module
interface AssistedInjectModule {}

@AndroidEntryPoint
class ItemFragment : Fragment() {
    private val args: ItemScreenFragmentArgs by navArgs()      
    @Inject lateinit var itemViewModelAssistedFactory: ItemViewModel.AssistedFactory        
    private val itemViewModel: ItemViewModel by viewModels {
            ItemViewModel.provideFactory(itemViewModelAssistedFactory, args.itemId)
    }    
}
 

Комментарии:

1. спасибо, но я получил ошибку, подобную приведенной ниже, с hilt 2.35: конструктор ViewModel должен быть аннотирован с помощью [at]Inject вместо [at]AssistedInject . Обработка [Hilt] не завершена. Подробности см. в разделе Ошибка выше.

2. @gturedi вам нужно удалить HiltViewModel в вашей viewmodel, чтобы использовать AssistedInject, но в последнем обновлении вы могли бы передать аргумент с помощью SavedStateHandle . смотрите это здесь, github.com/google/dagger/issues/2287#issuecomment-771671159

3. да, я заметил это позже, и я работал. спасибо

4. Я следовал этому подходу. Я продолжаю получать AssistedInjectModule is missing an @InstallIn annotation , хотя я сохранил @InstallIn(SingletonComponent::class)

Ответ №2:

Вспомогательная инъекция теперь поддерживается Dagger, а InflationInjection получил собственное репозиторий. Теперь синтаксис будет выглядеть так :

 import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
...




class ItemViewModel
@AssistedInject
constructor(private val repo: ItemRepository, @Assisted private val itemId: Long) : ViewModel() {
    init {
        repo.itemId = itemId
    }

    //-@AssistedInject.Factory
    @AssistedFactory
    interface AssistedFactory {
        fun create(@Named("item_id") itemId: Long): ItemViewModel
    }

    companion object {
        fun provideFactory(
            assistedFactory: AssistedFactory,
            itemId: Long
    ): ViewModelProvider.Factory = object : ViewModelProvider.Factory {
         override fun <T : ViewModel?> create(modelClass: Class<T>): T {
             return assistedFactory.create(itemId) as T
         }
       }
    }
}

@InstallIn(FragmentComponent::class)
//-@AssistedModule
@Module
interface AssistedInjectModule {}

@AndroidEntryPoint
class ItemFragment : Fragment() {
    private val args: ItemScreenFragmentArgs by navArgs()      
    @Inject lateinit var itemViewModelAssistedFactory:ItemViewModel.AssistedFactory        
    private val itemViewModel: ItemViewModel by viewModels {
        ItemViewModel.provideFactory(itemViewModelAssistedFactory, args.itemId)
    }    
}
 

Основывайтесь на ответе Redek. Подробнее об этом здесь

Ответ №3:

На самом деле вы можете использовать шаблон проектирования фабрики для создания объекта, который нужно передать

Это работает, но я не уверен, правильно это или нет

 class ItemRepository constructor(private val id: Int) {

}

class RepositoryFactory @Inject constructor() {

    private var id: Int = 0

    fun setId(id: Int) {
        this.id = id
    }

    fun create(): ItemRepository = ItemRepository(id)

}


class ItemViewModel @ViewModelInject constructor(private val repositoryFactory: RepositoryFactory) : ViewModel() {

    private var itemRepository: ItemRepository


    init {
        repositoryFactory.setId(45)
        itemRepository = repositoryFactory.create()
    }

}

@AndroidEntryPoint
class ItemFragment : Fragment() {
    private val viewModel: ItemViewModel by viewModels ()
}
 

Комментарии:

1. Проблема в том, что идентификатор поступает не из ViewModel, а из фрагмента, создающего его

Ответ №4:

Есть лучшее решение без AssistedInject, просто используйте SavedStateHandle .

если аргументы в вашем фрагменте содержат идентификатор пользователя:

 val args: UserFragmentArgs by navArgs()
...
args.userId
 

Тогда просто в вашем savedStateHandle он будет доступен без дополнительной работы.

 class UserViewModel @Inject constructor (
    private val repo: UserRepo,
    private val state: SavedStateHandle
) : ViewModel() {
    val user = repo.getUser(
        state.get<String>("userId")
    )
}
 

Полная информация о реализации: https://mattrobertson.dev/passing-safe-args-to-your-viewmodel-with-hilt-366762ff3f57

Комментарии:

1. Добавление к этому: теперь вы даже можете сделать это типобезопасным способом. val args = UserFragmentArgs.fromSavedStateHandle(state); val userId = args.user