Android ViewModel additional arguments
Asked Answered
A

11

183

Is there a way to pass additional argument to my custom AndroidViewModel constructor except Application context. Example:

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

And when I want to user my custom ViewModel class I use this code in my fragment:

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

So I don't know how to pass additional argument String param into my custom ViewModel. I can only pass Application context, but not additional arguments. I would really appreciate any help. Thank you.

Edit: I've added some code. I hope it's better now.

Aside answered 18/9, 2017 at 16:17 Comment(3)
add more details and codePalate
What's the error message?Predecessor
There is no error message. I simply don't know where to set arguments for constructor as ViewModelProvider is used for creating AndroidViewModel objects.Aside
F
334

You need to have a factory class for your 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);
    }
}

And when instantiating the view model, you do like this:

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

For kotlin, you may use delegated property:

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

There's also another new option - to implement HasDefaultViewModelProviderFactory and override getDefaultViewModelProviderFactory() with the instantiation of your factory and then you would call ViewModelProvider(this) or by viewModels() without the factory.

Flagon answered 12/10, 2017 at 8:18 Comment(14)
Does every ViewModel class need its ViewModelFactory?Sharpsighted
No, it's a factory, so it's up to you which ViewModel will you instantiate. You can have one factory for 2 ViewModels and e.g. some sort of flag to recognise which ViewModel will be returned from the create() method.Flagon
but every ViewModel could/will have different DI. How would you know which instance return on the create() method?Sharpsighted
I'm not sure what you mean? If you want to use it with DI then you need to do different implementation which would inject requested parameters. If you don't and just want to share one factory among several different viewModels, you can distinguish them based on the modelClass parameter which is passed from the get(MyViewModel.class) methodFlagon
Your ViewModel will be recreated after change orientation. Your cant create factory every time.Protector
That's not true. New ViewModel creation prevents method get(). Based on documentation: "Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or an activity), associated with this ViewModelProvider." see: developer.android.com/reference/android/arch/lifecycle/…Flagon
MyViewModelFactory should just extend ViewModelProvider.FactoryConsolute
You are right, although it works for both variants (I'll edit the answer).Flagon
It causes the 'unchecked cast' warning. Any solution for this?Invite
Unfortunately only solution I know is to suppress the warning for the class/method.Flagon
how about using return modelClass.cast(new MyViewModel(mApplication, mParam)) to get rid of the warningPlace
ViewModelProvides is now deprecated!Ceremonious
If we are sharing the same view model with two fragment and only one has the fields to initialize it how could the second fragment create the same view model to access the data the first fragment needs to pass data to the second fragment via the view modelErstwhile
HI this is very nice post but can you please help me what we need to pass Application ?Mariko
U
57

Implement with Dependency Injection

This is more advanced and better for production code.

Dagger2, Square's AssistedInject offers a production-ready implementation for ViewModels that can inject necessary components such as a repository that handles network and database requests. It also allows for the manual injection of arguments/parameters in the activity/fragment. Here's a concise outline of the steps to implement with code Gists based on Gabor Varadi's detailed post, Dagger Tips.

Dagger Hilt, is the next generation solution, in alpha as of 7/12/20, offering the same use case with a simpler setup once the library is in release status.

Implement with Lifecycle 2.2.0 in Kotlin

Passing Arguments/Parameters

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

Enabling SavedState with Arguments/Parameters

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
    }
}
Unjust answered 8/2, 2020 at 19:56 Comment(3)
While overriding create in the factory I get a warning saying Unchecked cast 'ItemViewModel to T'Scottscotti
That warning has not been an issue for me so far. However, I will look into it further when I refactor the ViewModel factory to inject it using Dagger rather than creating an instance of it via the fragment.Unjust
I don't get why people see the need post unrelated answer. Question never asked for DI library implementationBairam
P
19

For one factory shared between multiple different view models I'd extend mlyko's answer like this:

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

And instantiating view models:

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

With different view models having different constructors.

Preterhuman answered 16/5, 2018 at 14:53 Comment(2)
I don't recommend this way because couple of reasons: 1) parameters in factory are not type safe - this way you can break your code on runtime. Always try to avoid this approach when possible 2) checking view model types is not really OOP way of doing things. Since the ViewModels are casted to base type, again you can break code during runtime without any warning during compilation.. In this case I'd suggest using default android factory and pass the parameters to already instantiated view model.Flagon
@mlyko Sure, these are all valid objections and own method(s) to set up the viewmodel data is always an option. But sometimes you want to make sure that viewmodel has been initialized, hence the use of constructor. Otherwise you must yourself handle situation "viewmodel not initialized yet". For example if viewmodel has methods that return LivedData and observers are attached to that in various View lifecycle methods.Preterhuman
J
12

Based on @vilpe89 the above Kotlin solution for AndroidViewModel cases

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
}

Then a fragment can initiate the viewModel as

class SomeFragment : Fragment() {
    
    // ...

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

    // ...

}

And then the actual ViewModel class

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

Or in some suitable method ...

override fun onActivityCreated(...){
    // ...
    val myViewModel = ViewModelProvider(this, ExtraParamsViewModelFactory(this.requireActivity().application, "some string value")).get(SomeViewModel::class.java)
    // ...
}
Jo answered 5/5, 2020 at 11:25 Comment(3)
The question asks how to pass arguments/parameters without using context which the above does not follow: Is there a way to pass additional argument to my custom AndroidViewModel constructor except Application context?Unjust
This is giving a warning for an unchecked cast to type T, am I missing something or is this a non-issue?Abott
About the unchecked cast. This is how it's handled in some Google developer training code: github.com/android/architecture-components-samples/blob/… and: github.com/google-developer-training/… The first example shows, that the context its not neededExtramarital
S
5

The proper way is to use a dependency injection framework such as Dagger hilt. If not using a DI framework, then do it with ViewModelFactory.

With Dagger Hilt:

A ViewModel with parameters

@HiltViewModel
class MyViewModel @Inject constructor(
    private val myRepository: MyRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() { ... }

A Repository

class MyRepository @Inject constructor(
    private val myRemoteDataSource: MyDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) { ... }

A Module for providing the dependencies/parameters so they can be injected into repositories and ViewModels.

@InstallIn(ViewModelComponent::class)
@Module
object MyProvideModule {
    @Provides
    fun provideMyDataSource(@ApplicationContext context: Context): MyDataSource {
        //code to create MyDataSource... 
        return MyDataSource(context)
    }

    @Provides
    fun provideCoroutineDispatcher(): CoroutineDispatcher {
        return Dispatchers.IO
    }
}

A module for binding the repository

@Module
@InstallIn(ViewModelComponent::class)
interface RepositoryModules {
    @Binds
    fun provideMyRepository(repository: MyRepository): MyRepository
}

Initiating Dagger hilt with the application with the @HiltAndroidApp annotation.

@HiltAndroidApp
class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()
    }

}

Getting the ViewModel in activities

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  private val myViewModel: MyViewModel by viewModels()
  // Other code...
}

Getting the ViewModel in fragments

@AndroidEntryPoint
class MyFragment : Fragment() {
  private val myViewModel: MyViewModel by activityViewModels()
  // Other code...
}

With ViewModelFactory:

A ViewModel with parameter messageDataStore, where MessageDataStore is a DataStore class or it can be anything else that you want to pass into the ViewModel.

class MyViewModel(
    private val messageDataStore: MessageDataStore,
): ViewModel() { ... }

The ViewModel factory class for creating ViewModels

/**
 * Factory for all ViewModels.
 */
@Suppress("UNCHECKED_CAST")
class ViewModelFactory constructor(
    private val messageDataStore: MessageDataStore,
    owner: SavedStateRegistryOwner,
    defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
    override fun <T : ViewModel> create(
        key: String,
        modelClass: Class<T>,
        handle: SavedStateHandle
    ) = with(modelClass) {
        when {
            isAssignableFrom(MyViewModel::class.java) ->
                MyViewModel(messageDataStore)
            else ->
                throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
        }
    } as T
}

The application class for creating the dependencies/parameters

class MyApp : Application() {
    val messageDataStore: MessageDataStore
        get() = MessageDataStore.getInstance(this)

}

Extension functions for getting the factory class in activities and fragments, MyExt.kt

fun AppCompatActivity.getViewModelFactory(savedInstanceState: Bundle?): ViewModelFactory {
    val messageDataStore = (applicationContext as MyApp).messageDataStore
    return ViewModelFactory(messageDataStore, this, savedInstanceState)
}

fun Fragment.getViewModelFactory(savedInstanceState: Bundle?): ViewModelFactory {
    val messageDataStore = (requireContext().applicationContext as MyApp).messageDataStore
    return ViewModelFactory(messageDataStore, this.requireActivity(), savedInstanceState)
}

Getting the ViewMode in activities

class MainActivity : AppCompatActivity() {

  private lateinit var myViewModel: MyViewModel
  // Other code...

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val vm by viewModels<MyViewModel> { getViewModelFactory(savedInstanceState) }
    myViewModel = vm
    // Other code...
  }
}

Getting the ViewModel in Fragments.

class MyFragment : Fragment() {
    private lateinit var myViewModel: MyViewModel
    //Other code...

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
      val vm by activityViewModels<MyViewModel> { getViewModelFactory(savedInstanceState) }
      myViewModel = vm
      //Other code...
  }
}
Sixpenny answered 28/3, 2022 at 2:54 Comment(1)
Adding Hilt to a multi-module project is not very straightforward. I managed to setup that way but didn't like adding the "not transitive" dependencies to all modules. This time I will go with the Factory class approach. Thanks for sharing both.Orissa
D
4

I made it a class in which the already created object is passed.

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

And then

ViewModelFactory viewModelFactory = new ViewModelFactory();
viewModelFactory.add(new SampleViewModel(arg1, arg2));
SampleViewModel sampleViewModel = ViewModelProviders.of(this, viewModelFactory).get(SampleViewModel.class);
Dalessio answered 5/7, 2019 at 18:15 Comment(7)
We should have a ViewModelFactory for every ViewModel to pass the parameters to the constructor ??Telephony
No. Only one ViewModelFactory for all ViewModelsDalessio
Is there any reason for using canonical name as the hashMap key ? Can i use class.simpleName ?Telephony
Yes, but you must make sure there are no duplicate namesDalessio
Is this the recommended style of writing the code ? You came up with this code on your own or you read it in android docs ?Telephony
I invented it myselfDalessio
Ok, then what is the android docs recommended way of doing it ? Any idea ? For now, i'm following your answer, as i don't have any other better solution.Telephony
R
1

(KOTLIN) My solution uses little bit of Reflection.

Lets say you don't want to create the same looking Factory class every time you create new ViewModel class which needs some arguments. You can accomplish this via Reflection.

For example you would have two different Activities:

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

And ViewModels for those Activities:

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

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

Then the magic part, Factory class's implementation:

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
        }
    }
}
Runthrough answered 13/2, 2019 at 10:22 Comment(0)
R
1

In Kotlin, since the caller of the ViewModel and the ViewModel itself run in different coroutines, it is more natural and convenient to pass data between them using kotlinx.coroutines.channels.Channel:

class NewViewModel : ViewModel() {
    private val newData: MutableLiveData<Service.DataEntry?> by lazy {
        MutableLiveData<Service.DataEntry?>().also {
            viewModelScope.launch {
                val channel = Service.ParamChannel   // type Channel<Params>
                val params = channel.receive()
                it.value = Service.postSomething(params)
            }
        }
    }

    fun getData(): LiveData<Service.DataEntry?> {
        return newData
    }
}

// Calling code:
val model: NewViewModel by viewModels()
model.getData().observe(this) { newData ->
    if (newData != null) {
        ...
    }
    else
    {
        ...
    }
}
runBlocking {
    Service.ParamChannel.send(theParams)
}

This is part of working code which I anonymized for demo purposes.

Rosariarosario answered 22/3, 2022 at 16:56 Comment(0)
J
1

this is the most clean way

class ImagesViewModel(application: Application,private val fragmentType: FragmentType) : AndroidViewModel(application) {


class Factory(private val application: Application, private val fragmentType: FragmentType) :
    ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T =
        ImagesViewModel(application, fragmentType) as T
}

}

and create instance

         val viewModel: ImagesViewModel by viewModels { ImagesViewModel.Factory(requireActivity().application ,fragmentType) }
Jabot answered 11/6, 2023 at 5:18 Comment(0)
C
0

I wrote a library that should make doing this more straightforward and way cleaner, no multibindings or factory boilerplate needed, while working seamlessly with ViewModel arguments that can be provided as dependencies by 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"
    }    
}

In the view:

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
        })
    }
}
Consolute answered 21/11, 2018 at 19:15 Comment(0)
F
-2

Why not do it like this:

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

and then use it like this in two steps:

MyViewModel myViewModel = ViewModelProvider.of(this).get(MyViewModel.class)
myViewModel.initialize(param)
Friedlander answered 3/3, 2019 at 3:57 Comment(2)
The whole point of putting parameters in the constructor is to initialize the view model only once. With your implementation, if you call myViewModel.initialize(param) in onCreate of the activity, for example, it can be called multiple times on the same MyViewModel instance as the user rotates the device.Haakon
@Sanlok Lee Ok. How about adding a condition to the function to prevent initializing when unnecessary. Check my edited answer.Friedlander

© 2022 - 2024 — McMap. All rights reserved.