Argumentos adicionais do Android ViewModel

107

Existe uma maneira de passar argumentos adicionais para meu AndroidViewModelconstrutor personalizado, exceto o contexto do aplicativo. Exemplo:

public class MyViewModel extends AndroidViewModel {
    private final LiveData<List<MyObject>> myObjectList;
    private AppDatabase appDatabase;

    public MyViewModel(Application application, String param) {
        super(application);
        appDatabase = AppDatabase.getDatabase(this.getApplication());

        myObjectList = appDatabase.myOjectModel().getMyObjectByParam(param);
    }
}

E quando quero usar minha ViewModelclasse personalizada , uso este código em meu fragmento:

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)

Portanto, não sei como passar argumentos adicionais String parampara o meu costume ViewModel. Só posso passar o contexto do aplicativo, mas não argumentos adicionais. Eu realmente apreciaria qualquer ajuda. Obrigado.

Edit: Eu adicionei algum código. Espero que esteja melhor agora.

Mario rudman
fonte
adicione mais detalhes e código
hugo
Qual é a mensagem de erro?
Moses Aprico de
Não há nenhuma mensagem de erro. Eu simplesmente não sei onde definir argumentos para o construtor, já que ViewModelProvider é usado para criar objetos AndroidViewModel.
Mario Rudman

Respostas:

213

Você precisa ter uma classe de fábrica para seu ViewModel.

public class MyViewModelFactory implements ViewModelProvider.Factory {
    private Application mApplication;
    private String mParam;


    public MyViewModelFactory(Application application, String param) {
        mApplication = application;
        mParam = param;
    }


    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        return (T) new MyViewModel(mApplication, mParam);
    }
}

E ao instanciar o modelo de visualização, você faz assim:

MyViewModel myViewModel = ViewModelProvider(this, new MyViewModelFactory(this.getApplication(), "my awesome param")).get(MyViewModel.class);

Para kotlin, você pode usar a propriedade delegada:

val viewModel: MyViewModel by viewModels { MyViewModelFactory(getApplication(), "my awesome param") }

Há também outra nova opção - implementar HasDefaultViewModelProviderFactorye substituir getDefaultViewModelProviderFactory()com a instanciação de sua fábrica e então você ligaria ViewModelProvider(this)ou by viewModels()sem a fábrica.

Mlyko
fonte
4
Cada ViewModelclasse precisa de seu ViewModelFactory?
dmlebron
6
mas cada um ViewModelpoderia / terá um DI diferente. Como você saberia qual instância retorna no create()método?
dmlebron
1
Seu ViewModel será recriado após a mudança de orientação. Você não pode criar uma fábrica todas as vezes.
Tim
3
Isso não é verdade. Nova ViewModelcriação impede o método get(). Com base na documentação: "Retorna um ViewModel existente ou cria um novo no escopo (geralmente, um fragmento ou uma atividade), associado a este ViewModelProvider." consulte: developer.android.com/reference/android/arch/lifecycle/…
mlyko
2
que tal usar return modelClass.cast(new MyViewModel(mApplication, mParam))para se livrar do aviso
jackycflau
23

Implementar com injeção de dependência

Isso é mais avançado e melhor para código de produção.

Dagger2 , AssistedInject da Square, oferece uma implementação pronta para produção para ViewModels que pode injetar componentes necessários, como um repositório que lida com solicitações de rede e banco de dados. Também permite a injeção manual de argumentos / parâmetros na atividade / fragmento. Aqui está um esboço conciso das etapas para implementar com o código Gists com base na postagem detalhada de Gabor Varadi, Dagger Tips .

Punho de Adaga é a solução da próxima geração, em alfa a partir de 12/07/20, oferecendo o mesmo caso de uso com uma configuração mais simples assim que a biblioteca estiver no status de lançamento.

Implementar com Lifecycle 2.2.0 em Kotlin

Passando argumentos / parâmetros

// Override ViewModelProvider.NewInstanceFactory to create the ViewModel (VM).
class SomeViewModelFactory(private val someString: String): ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T = SomeViewModel(someString) as T
} 

class SomeViewModel(private val someString: String) : ViewModel() {
    init {
        //TODO: Use 'someString' to init process when VM is created. i.e. Get data request.
    }
}

class Fragment: Fragment() {
    // Create VM in activity/fragment with VM factory.
    val someViewModel: SomeViewModel by viewModels { SomeViewModelFactory("someString") } 
}

Habilitando SavedState com Argumentos / Parâmetros

class SomeViewModelFactory(
        private val owner: SavedStateRegistryOwner,
        private val someString: String) : AbstractSavedStateViewModelFactory(owner, null) {
    override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, state: SavedStateHandle) =
            SomeViewModel(state, someString) as T
}

class SomeViewModel(private val state: SavedStateHandle, private val someString: String) : ViewModel() {
    val feedPosition = state.get<Int>(FEED_POSITION_KEY).let { position ->
        if (position == null) 0 else position
    }
        
    init {
        //TODO: Use 'someString' to init process when VM is created. i.e. Get data request.
    }
        
     fun saveFeedPosition(position: Int) {
        state.set(FEED_POSITION_KEY, position)
    }
}

class Fragment: Fragment() {
    // Create VM in activity/fragment with VM factory.
    val someViewModel: SomeViewModel by viewModels { SomeViewModelFactory(this, "someString") } 
    private var feedPosition: Int = 0
     
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        someViewModel.saveFeedPosition((contentRecyclerView.layoutManager as LinearLayoutManager)
                .findFirstVisibleItemPosition())
    }    
        
    override fun onViewStateRestored(savedInstanceState: Bundle?) {
        super.onViewStateRestored(savedInstanceState)
        feedPosition = someViewModel.feedPosition
    }
}
Adam Hurwitz
fonte
Ao substituir criar na fábrica, recebo um aviso dizendo Unchecked cast 'ItemViewModel to T'
Ssenyonjo
1
Esse aviso não foi um problema para mim até agora. No entanto, examinarei isso mais detalhadamente quando refatorar a fábrica ViewModel para injetá-la usando Dagger, em vez de criar uma instância dela por meio do fragmento.
Adam Hurwitz
15

Para uma fábrica compartilhada entre vários modelos de visualização diferentes, eu estenderia a resposta de mlyko assim:

public class MyViewModelFactory extends ViewModelProvider.NewInstanceFactory {
    private Application mApplication;
    private Object[] mParams;

    public MyViewModelFactory(Application application, Object... params) {
        mApplication = application;
        mParams = params;
    }

    @Override
    public <T extends ViewModel> T create(Class<T> modelClass) {
        if (modelClass == ViewModel1.class) {
            return (T) new ViewModel1(mApplication, (String) mParams[0]);
        } else if (modelClass == ViewModel2.class) {
            return (T) new ViewModel2(mApplication, (Integer) mParams[0]);
        } else if (modelClass == ViewModel3.class) {
            return (T) new ViewModel3(mApplication, (Integer) mParams[0], (String) mParams[1]);
        } else {
            return super.create(modelClass);
        }
    }
}

E instanciar modelos de visualização:

ViewModel1 vm1 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), "something")).get(ViewModel1.class);
ViewModel2 vm2 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), 123)).get(ViewModel2.class);
ViewModel3 vm3 = ViewModelProviders.of(this, new MyViewModelFactory(getApplication(), 123, "something")).get(ViewModel3.class);

Com diferentes modelos de visualização com diferentes construtores.

Rzehan
fonte
8
Não recomendo essa maneira por alguns motivos: 1) os parâmetros de fábrica não são seguros para o tipo - dessa forma, você pode quebrar o código em tempo de execução. Sempre tente evitar essa abordagem quando possível 2) verificar os tipos de modelo de visualização não é realmente uma maneira OOP de fazer as coisas. Uma vez que os ViewModels são convertidos para o tipo base, novamente você pode quebrar o código durante o tempo de execução sem qualquer aviso durante a compilação. Nesse caso, eu sugeriria usar o Android Factory padrão e passar os parâmetros para o modelo de visualização já instanciado.
mlyko
@mlyko Claro, todas essas são objeções válidas e o (s) próprio (s) método (s) para configurar os dados do modelo de visão é sempre uma opção. Mas às vezes você quer ter certeza de que o viewmodel foi inicializado, daí o uso do construtor. Caso contrário, você mesmo deve lidar com a situação "viewmodel não inicializado ainda". Por exemplo, se viewmodel tem métodos que retornam LivedData e observadores são anexados a isso em vários métodos de ciclo de vida de View.
Rzehan
3

Com base em @ vilpe89, a solução Kotlin acima para casos AndroidViewModel

class ExtraParamsViewModelFactory(private val application: Application, private val myExtraParam: String): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T = SomeViewModel(application, myExtraParam) as T

}

Então, um fragmento pode iniciar o viewModel como

class SomeFragment : Fragment() {
 ....
    private val myViewModel: SomeViewModel by viewModels {
        ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")
    }
 ....
}

E então a classe ViewModel real

class SomeViewModel(application: Application, val myExtraParam:String) : AndroidViewModel(application) {
....
}

Ou em algum método adequado ...

override fun onActivityCreated(...){
    ....

    val myViewModel = ViewModelProvider(this, ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")).get(SomeViewModel::class.java)

    ....
}
MFAL
fonte
A questão pergunta como passar argumentos / parâmetros sem usar contexto que o acima não segue: Existe uma maneira de passar argumentos adicionais para meu construtor AndroidViewModel personalizado, exceto contexto de aplicativo?
Adam Hurwitz
3

Eu fiz uma classe em que o objeto já criado é passado.

private Map<String, ViewModel> viewModelMap;

public ViewModelFactory() {
    this.viewModelMap = new HashMap<>();
}

public void add(ViewModel viewModel) {
    viewModelMap.put(viewModel.getClass().getCanonicalName(), viewModel);
}

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
    for (Map.Entry<String, ViewModel> viewModel : viewModelMap.entrySet()) {
        if (viewModel.getKey().equals(modelClass.getCanonicalName())) {
            return (T) viewModel.getValue();
        }
    }
    return null;
}

E depois

ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);
Danil
fonte
Devemos ter um ViewModelFactory para cada ViewModel para passar os parâmetros para o construtor ??
K Pradeep Kumar Reddy
Não. Apenas um ViewModelFactory para todos os ViewModels
Danil
Existe algum motivo para usar o nome canônico como a chave hashMap? Posso usar class.simpleName?
K Pradeep Kumar Reddy
Sim, mas certifique-se de que não haja nomes duplicados
Danil
Este é o estilo recomendado de escrever o código? Você criou esse código por conta própria ou o leu nos documentos do Android?
K Pradeep Kumar Reddy
1

Eu escrevi uma biblioteca que deve tornar isso mais simples e mais limpo, sem multibindings ou clichês de fábrica necessários, enquanto trabalhava perfeitamente com argumentos ViewModel que podem ser fornecidos como dependências por Dagger: https://github.com/radutopor/ViewModelFactory

@ViewModelFactory
class UserViewModel(@Provided repository: Repository, userId: Int) : ViewModel() {

    val greeting = MutableLiveData<String>()

    init {
        val user = repository.getUser(userId)
        greeting.value = "Hello, $user.name"
    }    
}

Na vista:

class UserActivity : AppCompatActivity() {
    @Inject
    lateinit var userViewModelFactory2: UserViewModelFactory2

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_user)
        appComponent.inject(this)

        val userId = intent.getIntExtra("USER_ID", -1)
        val viewModel = ViewModelProviders.of(this, userViewModelFactory2.create(userId))
            .get(UserViewModel::class.java)

        viewModel.greeting.observe(this, Observer { greetingText ->
            greetingTextView.text = greetingText
        })
    }
}
Radu Topor
fonte
1

(KOTLIN) Minha solução usa um pouco de reflexão.

Digamos que você não queira criar a mesma classe de fábrica de aparência toda vez que criar uma nova classe ViewModel que precisa de alguns argumentos. Você pode fazer isso por meio do Reflection.

Por exemplo, você teria duas atividades diferentes:

class Activity1 : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val args = Bundle().apply { putString("NAME_KEY", "Vilpe89") }
        val viewModel = ViewModelProviders.of(this, ViewModelWithArgumentsFactory(args))
            .get(ViewModel1::class.java)
    }
}

class Activity2 : FragmentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val args = Bundle().apply { putInt("AGE_KEY", 29) }
        val viewModel = ViewModelProviders.of(this, ViewModelWithArgumentsFactory(args))
            .get(ViewModel2::class.java)
    }
}

E ViewModels para essas atividades:

class ViewModel1(private val args: Bundle) : ViewModel()

class ViewModel2(private val args: Bundle) : ViewModel()

Então, a parte mágica, a implementação da classe Factory:

class ViewModelWithArgumentsFactory(private val args: Bundle) : NewInstanceFactory() {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        try {
            val constructor: Constructor<T> = modelClass.getDeclaredConstructor(Bundle::class.java)
            return constructor.newInstance(args)
        } catch (e: Exception) {
            Timber.e(e, "Could not create new instance of class %s", modelClass.canonicalName)
            throw e
        }
    }
}
vilpe89
fonte
0

Por que não fazer assim:

public class MyViewModel extends AndroidViewModel {
    private final LiveData<List<MyObject>> myObjectList;
    private AppDatabase appDatabase;
    private boolean initialized = false;

    public MyViewModel(Application application) {
        super(application);
    }

    public initialize(String param){
      synchronized ("justInCase") {
         if(! initialized){
          initialized = true;
          appDatabase = AppDatabase.getDatabase(this.getApplication());
          myObjectList = appDatabase.myOjectModel().getMyObjectByParam(param);
    }
   }
  }
}

e use-o assim em duas etapas:

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)
Amr Berag
fonte
2
O objetivo de colocar parâmetros no construtor é inicializar o modelo de visualização apenas uma vez . Com sua implementação, se você ligar myViewModel.initialize(param)no onCreateda atividade, por exemplo, ele pode ser chamado várias vezes na mesma MyViewModelinstância como o usuário gira o dispositivo.
Sanlok Lee
@Sanlok Lee Ok. Que tal adicionar uma condição à função para evitar a inicialização quando desnecessária. Verifique minha resposta editada.
Amr Berag
0
class UserViewModelFactory(private val context: Context) : ViewModelProvider.NewInstanceFactory() {
 
    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return UserViewModel(context) as T
    }
 
}
class UserViewModel(private val context: Context) : ViewModel() {
 
    private var listData = MutableLiveData<ArrayList<User>>()
 
    init{
        val userRepository : UserRepository by lazy {
            UserRepository
        }
        if(context.isInternetAvailable()) {
            listData = userRepository.getMutableLiveData(context)
        }
    }
 
    fun getData() : MutableLiveData<ArrayList<User>>{
        return listData
    }

Call Viewmodel in Activity

val userViewModel = ViewModelProviders.of(this,UserViewModelFactory(this)).get(UserViewModel::class.java)

Para obter mais referências: Exemplo de Android MVVM Kotlin

Dhrumil Shah
fonte
A questão pergunta como passar argumentos / parâmetros sem usar contexto que o acima não segue: Existe uma maneira de passar argumentos adicionais para meu construtor AndroidViewModel personalizado, exceto contexto de aplicativo?
Adam Hurwitz
Você pode passar qualquer argumento / parâmetro em seu construtor de modelo de visão personalizado. Aqui, o contexto é apenas um exemplo. Você pode passar qualquer argumento personalizado no construtor.
Dhrumil Shah
Entendido. É uma prática recomendada não passar contexto, visualizações, atividades, fragmentos, adaptadores, visualizar ciclo de vida, observar observáveis ​​de ciclo de vida de visualização ou reter recursos (drawables, etc) no ViewModel, pois a visualização pode ser destruída e o ViewModel persistirá com desatualizado em formação.
Adam Hurwitz