Matt
Matt

Reputation: 1839

How to use viewmodel + room with fragments?

I'm looking through the tutorial for Android room with a view, and trying to extend their model for using ViewModels to multiple fragments, but not really sure how.

MyApplication

class myApplication : Application() {


    companion object {
        var database: myDatabase? = null
        var repository: myRepository? = null
    }

    override fun onCreate() {
        super.onCreate()
        database = MyDatabase.getInstance(this)
        repository = MyRepository(database!!.myDatabaseDao)

    }
}

MyViewModel

class MyViewModel(private val repository: MyRepository) : ViewModel() {


    val allWords: LiveData<List<Words>> = repository.allWords.asLiveData()

    fun insert(word: Word) = viewModelScope.launch {
        repository.insert(word)
    }
}

class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
            @Suppress("UNCHECKED_CAST")
            return MyViewModel(repository) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}

HomeFragment

class HomeFragment : Fragment() {

    private val myViewModel: MyViewModel by activityViewModels()

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

        // Inflate the layout for this fragment
        var rootView = inflater.inflate(R.layout.fragment_home, container, false)

        return rootView
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        myViewModel.allWords.observe(viewLifecycleOwner) { words ->
            // Update the cached copy of the words in the adapter.
            words.let { Log.d("fragment", it.toString()) }
        }

    }
}

I have a couple of other fragments that will hopefully share the same ViewModel as HomeFragment. I've tried many different approaches, such as using

myViewModel = ViewModelProviders.of(activity!!).get(MyViewModel::class.java)

but all of them give me Caused by: java.lang.InstantiationException: java.lang.Class<com.example.tabtester.ViewModels.MyViewModel> has no zero argument constructor. I can't find any SO posts or documentation that shows me how to provide a constructor in Kotlin.

Also conceptually I can't find any description for what exactly is happening and how the viewmodel is being constructed (and by what). In the Room with a View tutorial, the example given is in MainActivity:

private val wordViewModel: WordViewModel by viewModels {
    WordViewModelFactory((application as WordsApplication).repository)
}

This makes sense, to me; you're using the Factory to instantiate a ViewModel to use in the MainActivity. But for any description of how to use ViewModels in Fragments, I don't see where the ViewModel is being constructed. If you have multiple fragments who is constructing the ViewModel? If I use Fragments then does that mean I also need an activity to construct the ViewModel, then somehow share between the Fragments?

Would appreciate any help, or documentation that explains this more clearly.

Upvotes: 1

Views: 2005

Answers (1)

ianhanniballake
ianhanniballake

Reputation: 200020

The underlying APIs of by viewModels(), by activityViewModels() and the (now deprecated) ViewModelProviders.of() all feed into one method: the ViewModelProvider constructor:

ViewModelProvider(viewModelStore: ViewModelStore, factory: ViewModelProvider.Factory)

This constructor takes two parameters:

  • The ViewModelStore controls the storage and scoping of the ViewModel you create. For example, when you use by viewModels() in a Fragment, it is the Fragment which is used as the ViewModelStore. Similarly, by activityViewModels() uses the Activity as the ViewModelStore.
  • The ViewModelProvider.Factory controls the construction of the ViewModel if one has not already been created for that particular ViewModelStore.

Therefore if you need a custom Factory, you must always pass that Factory into all places that could create that ViewModel (remember, due to process death and recreation, there's no guarantee that your HomeFragment will be the first fragment to create your ViewModel).

private val myViewModel: MyViewModel by activityViewModels() {
    MyViewModelFactory(MyApplication.repository!!)
}

As long as you're using activityViewModels(), the storage of your ViewModel will always be at the activity level, no matter what Factory you are using.

Upvotes: 4

Related Questions