174

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.

AdamHurwitz
  • 9,758
  • 10
  • 72
  • 134
Mario Rudman
  • 1,899
  • 2
  • 14
  • 18

11 Answers11

326

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.

mlykotom
  • 4,850
  • 1
  • 22
  • 27
  • 9
    Does every `ViewModel` class need its ViewModelFactory? – dmlebron Mar 16 '18 at 15:12
  • 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. – mlykotom Mar 17 '18 at 18:09
  • 8
    but every `ViewModel` could/will have different DI. How would you know which instance return on the `create()` method? – dmlebron Mar 17 '18 at 18:18
  • 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)` method – mlykotom Mar 17 '18 at 19:21
  • 1
    Your ViewModel will be recreated after change orientation. Your cant create factory every time. – Tim Aug 02 '18 at 17:57
  • 6
    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: https://developer.android.com/reference/android/arch/lifecycle/ViewModelProvider.html#get(java.lang.Class%3CT%3E) – mlykotom Aug 03 '18 at 09:05
  • `MyViewModelFactory` should just extend `ViewModelProvider.Factory` – Radu Topor Oct 30 '18 at 14:12
  • You are right, although it works for both variants (I'll edit the answer). – mlykotom Oct 30 '18 at 15:22
  • It causes the 'unchecked cast' warning. Any solution for this? – ivan8m8 Nov 19 '18 at 06:01
  • Unfortunately only solution I know is to suppress the warning for the class/method. – mlykotom Nov 19 '18 at 09:43
  • 3
    how about using `return modelClass.cast(new MyViewModel(mApplication, mParam))` to get rid of the warning – jackycflau Nov 21 '18 at 06:52
  • @jackycflau thanks. It's a good workaround. It also requires ``!!`` at the end. – ivan8m8 Nov 29 '18 at 15:40
  • Please @mlyko why do you pass the Application class? – Maxime Claude Mar 19 '19 at 01:43
  • Just for convenience, if you don't need it, don't pass it (e.g for getting strings in viewmodel) – mlykotom Mar 19 '19 at 06:12
  • `ViewModelProvides` is now deprecated! – G. Ciardini May 12 '20 at 15:59
  • 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 model – Ismail Iqbal Jul 28 '20 at 12:27
  • HI this is very nice post but can you please help me what we need to pass Application ? – MARSH Dec 11 '22 at 12:39
54

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
    }
}
Westy92
  • 19,087
  • 4
  • 72
  • 54
AdamHurwitz
  • 9,758
  • 10
  • 72
  • 134
  • 2
    While overriding create in the factory I get a warning saying Unchecked cast 'ItemViewModel to T' – Gilbert May 06 '20 at 21:15
  • 1
    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. – AdamHurwitz May 20 '20 at 05:26
  • I don't get why people see the need post unrelated answer. Question never asked for DI library implementation – Farid Oct 22 '22 at 14:25
18

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.

aruh
  • 396
  • 6
  • 10
rzehan
  • 638
  • 7
  • 8
  • 14
    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. – mlykotom May 17 '18 at 17:56
  • @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. – rzehan Jun 12 '18 at 10:24
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)
    // ...
}
aruh
  • 396
  • 6
  • 10
MFAL
  • 1,090
  • 13
  • 19
  • 1
    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?_ – AdamHurwitz Jul 12 '20 at 18:06
  • This is giving a warning for an unchecked cast to type `T`, am I missing something or is this a non-issue? – caly__pso May 26 '22 at 16:25
  • About the unchecked cast. This is how it's handled in some Google developer training code: https://github.com/android/architecture-components-samples/blob/0905d0e307ef457a4c37511a542edfe3bdb4d2a3/MADSkillsNavigationSample/app/src/main/java/com/android/samples/donuttracker/ViewModelFactory.kt#L22 and: https://github.com/google-developer-training/android-kotlin-fundamentals-apps/blob/5dc4c4a330da24c7d32c47a601125691cace80e2/MarsRealEstateFinal/app/src/main/java/com/example/android/marsrealestate/detail/DetailViewModelFactory.kt#L30 The first example shows, that the context its not needed – CyclingSir Jul 04 '22 at 09:37
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);
Danil Apsadikov
  • 274
  • 1
  • 4
  • 14
4

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...
  }
}
s-hunter
  • 24,172
  • 16
  • 88
  • 130
  • 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. – JCarlosR Oct 27 '22 at 21:40
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
        }
    }
}
Westy92
  • 19,087
  • 4
  • 72
  • 54
vilpe89
  • 4,656
  • 1
  • 29
  • 36
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.

rwst
  • 2,515
  • 2
  • 30
  • 36
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
        })
    }
}
Radu Topor
  • 1,504
  • 1
  • 11
  • 12
-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) }
Mateen Chaudhry
  • 631
  • 12
  • 32
-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)
USER249
  • 1,080
  • 7
  • 14
  • 4
    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. – Sanlok Lee Mar 04 '19 at 21:45
  • @Sanlok Lee Ok. How about adding a condition to the function to prevent initializing when unnecessary. Check my edited answer. – USER249 Mar 05 '19 at 04:37