Argumentos adicionales de Android ViewModel

108

¿Hay alguna forma de pasar un argumento adicional a mi AndroidViewModelconstructor personalizado, excepto el contexto de la aplicación? Ejemplo:

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);
    }
}

Y cuando quiero usar mi ViewModelclase personalizada , uso este código en mi fragmento:

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

Entonces no sé cómo pasar argumentos adicionales String parama mi costumbre ViewModel. Solo puedo pasar el contexto de la aplicación, pero no los argumentos adicionales. Realmente apreciaria cualquier ayuda. Gracias.

Editar: he agregado un código. Espero que esté mejor ahora.

Mario Rudman
fuente
agregar más detalles y código
hugo
¿Cuál es el mensaje de error?
Moses Aprico
No hay ningún mensaje de error. Simplemente no sé dónde establecer argumentos para el constructor, ya que ViewModelProvider se usa para crear objetos AndroidViewModel.
Mario Rudman

Respuestas:

214

Necesita tener una clase de fábrica para su 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);
    }
}

Y al crear una instancia del modelo de vista, te gusta esto:

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

Para kotlin, puede usar propiedad delegada:

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

También hay otra opción nueva: implementar HasDefaultViewModelProviderFactoryy anular getDefaultViewModelProviderFactory()con la creación de instancias de su fábrica y luego llamaría ViewModelProvider(this)o by viewModels()sin la fábrica.

mlyko
fuente
4
¿Cada ViewModelclase necesita su ViewModelFactory?
dmlebron
6
pero cada ViewModelpodría / tendrá diferente DI. ¿Cómo sabría qué instancia devuelve el create()método?
dmlebron
1
Su ViewModel se volverá a crear después de cambiar la orientación. No puedes crear una fábrica cada vez.
Tim
3
Eso no es cierto. La nueva ViewModelcreación impide el método get(). Basado en documentación: "Devuelve un ViewModel existente o crea uno nuevo en el alcance (generalmente, un fragmento o una actividad), asociado con este ViewModelProvider". ver: developer.android.com/reference/android/arch/lifecycle/…
mlyko
2
¿ return modelClass.cast(new MyViewModel(mApplication, mParam))
Qué tal
23

Implementar con inyección de dependencia

Esto es más avanzado y mejor para el código de producción.

Dagger2 , AssistedInject de Square ofrece una implementación lista para producción para ViewModels que puede inyectar los componentes necesarios, como un repositorio que maneja las solicitudes de la red y la base de datos. También permite la inyección manual de argumentos / parámetros en la actividad / fragmento. Aquí hay un resumen conciso de los pasos para implementar con Code Gists basado en la publicación detallada de Gabor Varadi, Dagger Tips .

Dagger Hilt , es la solución de próxima generación, en alfa a partir del 12/7/20, que ofrece el mismo caso de uso con una configuración más simple una vez que la biblioteca está en estado de lanzamiento.

Implementar con Lifecycle 2.2.0 en Kotlin

Pasar 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") } 
}

Habilitación de SavedState con 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
fuente
Al anular la creación en la fábrica, recibo una advertencia que dice Cast sin marcar 'ItemViewModel to T'
Ssenyonjo
1
Esa advertencia no ha sido un problema para mí hasta ahora. Sin embargo, lo analizaré más a fondo cuando refactorice la fábrica ViewModel para inyectarlo usando Dagger en lugar de crear una instancia de él a través del fragmento.
Adam Hurwitz
15

Para una fábrica compartida entre múltiples modelos de vista diferentes, extendería la respuesta de mlyko de esta manera:

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 vista:

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);

Con diferentes modelos de vista que tienen diferentes constructores.

rzehan
fuente
8
No recomiendo esta forma por un par de razones: 1) los parámetros de fábrica no son seguros para los tipos; de esta forma, puede romper su código en tiempo de ejecución. Siempre trate de evitar este enfoque cuando sea posible 2) verificar los tipos de modelos de vista no es realmente una forma de OOP de hacer las cosas. Dado que los ViewModels están convertidos al tipo base, nuevamente puede romper el código durante el tiempo de ejecución sin ninguna advertencia durante la compilación. En este caso, sugeriría usar la fábrica de Android predeterminada y pasar los parámetros al modelo de vista ya instanciado.
mlyko
@mlyko Seguro, todas estas son objeciones válidas y los métodos propios para configurar los datos del modelo de vista siempre son una opción. Pero a veces desea asegurarse de que el modelo de vista se haya inicializado, de ahí el uso de constructor. De lo contrario, debe manejar la situación "viewmodel aún no inicializado". Por ejemplo, si viewmodel tiene métodos que devuelven LivedData y los observadores están adjuntos a eso en varios métodos de ciclo de vida de View.
rzehan
3

Basado en @ vilpe89, la solución de Kotlin anterior para casos de 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

}

Entonces un fragmento puede iniciar viewModel como

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

Y luego la clase ViewModel real

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

O en algún método adecuado ...

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

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

    ....
}
MFAL
fuente
La pregunta pregunta cómo pasar argumentos / parámetros sin usar el contexto que lo anterior no sigue: ¿Hay alguna manera de pasar argumentos adicionales a mi constructor AndroidViewModel personalizado, excepto el contexto de la aplicación?
Adam Hurwitz
3

Lo convertí en una clase en la que se pasa el objeto ya creado.

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;
}

Y entonces

ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);
Danil
fuente
¿Deberíamos tener un ViewModelFactory para cada ViewModel para pasar los parámetros al constructor?
K Pradeep Kumar Reddy
No. Solo un ViewModelFactory para todos los ViewModels
Danil
¿Hay alguna razón para usar un nombre canónico como clave hashMap? ¿Puedo usar class.simpleName?
K Pradeep Kumar Reddy
Sí, pero debe asegurarse de que no haya nombres duplicados
Danil
¿Es este el estilo recomendado para escribir el código? ¿Se te ocurrió este código por tu cuenta o lo leíste en documentos de Android?
K Pradeep Kumar Reddy
1

Escribí una biblioteca que debería hacer que hacer esto sea más sencillo y más limpio, sin necesidad de múltiples enlaces o repetición de fábrica, mientras trabajaba sin problemas con los argumentos de ViewModel que Dagger puede proporcionar como dependencias: 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"
    }    
}

En la 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
fuente
1

(KOTLIN) Mi solución usa un poco de Reflexión.

Supongamos que no desea crear la misma clase Factory con el mismo aspecto cada vez que crea una nueva clase ViewModel que necesita algunos argumentos. Puede lograr esto a través de Reflection.

Por ejemplo, tendría dos actividades 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)
    }
}

Y ViewModels para esas actividades:

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

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

Luego, la parte mágica, la implementación de la clase 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
fuente
0

Por qué no hacerlo así:

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);
    }
   }
  }
}

y luego úselo así en dos pasos:

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)
Amr Berag
fuente
2
El objetivo de poner parámetros en el constructor es inicializar el modelo de vista solo una vez . Con su implementación, si llama myViewModel.initialize(param)a onCreatela actividad, por ejemplo, se puede llamar varias veces en la misma MyViewModelinstancia cuando el usuario gira el dispositivo.
Sanlok Lee
@Sanlok Lee Ok. ¿Qué le parece agregar una condición a la función para evitar la inicialización cuando no sea necesario? Verifique mi respuesta 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
    }

Llamar a Viewmodel en actividad

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

Para más referencia: Ejemplo de Android MVVM Kotlin

Dhrumil Shah
fuente
La pregunta pregunta cómo pasar argumentos / parámetros sin usar el contexto que lo anterior no sigue: ¿Hay alguna manera de pasar argumentos adicionales a mi constructor AndroidViewModel personalizado, excepto el contexto de la aplicación?
Adam Hurwitz
Puede pasar cualquier argumento / parámetro en su constructor de modelo de vista personalizado. Aquí el contexto es solo un ejemplo. Puede pasar cualquier argumento personalizado en el constructor.
Dhrumil Shah
Entendido. Es una buena práctica no pasar contexto, vistas, actividades, fragmentos, adaptadores, ver el ciclo de vida, observar los observables conscientes del ciclo de vida de la vista o retener recursos (elementos de diseño, etc.) en el modelo de vista, ya que la vista puede destruirse y el modelo de vista persistirá con obsoletos información.
Adam Hurwitz