Search code examples
androidkotlinlayoutfragmentviewmodel

Why is my ViewModel null in Fragment but not Fragment's bound layout?


I have a FooActivity: AppCompatActivity() that uses a FooViewModel to look up a Foo from a database and then present information about it in a few Fragments. Here's how I set it up:

private lateinit var viewModel: FooViewModel

override fun onCreate(savedInstanceState: Bundle?) {

    super.onCreate(savedInstanceState)
    // Get the Intent that started this activity and extract the FOO_ID
    val id = intent.getLongExtra(FOO_ID, 1L)
    val viewModelFactory = FooViewModelFactory(
        id,
        FooDatabase.getInstance(application).fooDao,
        application)
    viewModel = ViewModelProviders.of(
        this, viewModelFactory).get(FooViewModel::class.java)

    // FooViewModel is bound to Activity's Fragments, so must
    // create FooViewModelFactory before trying to bind/inflate layout
    binding = DataBindingUtil.setContentView(this, R.layout.activity_foo)
    binding.lifecycleOwner = this
    binding.viewModel = viewModel
}

And in the FooInfoFragment class:

private val viewModel: FooViewModel by activityViewModels()

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    super.onCreateView(inflater, container, savedInstanceState)
    val binding = FragmentFooInfoBinding.inflate(inflater)
    binding.setLifecycleOwner(this)
    binding.viewModel = viewModel
    // normally viewModel.foo shouldn't be null here, right?
    return binding.root
}

So far so good. My layout shows the various info of the Foo, eg @{viewModel.foo.name}.

The issue is that in my FooInfoFragment.onCreateView when I attempt to access viewModel.foo.value?.name after binding, viewModel is null.

    binding.viewModel = viewModel
    Log.wtf(TAG, "${viewModel.foo.value?.name} shouldn't be null!")
    return binding.root

I don't understand why it's null in my Fragment but not in my layout. Help?


Solution

  • Reading between the lines here that your problem is that you don't expect viewModel.foo.value to be nullable, not that you are getting a Java NPE for a null viewModel when you access viewModel.foo.

    If foo is a LiveData, its value parameter is always nullable, even if its type is not. That is how the LiveData class is defined. value is nullable because it defaults to null before it has been set. After it is set, it won't be null again. If you are sure that you always set its value before accessing it, you can use value!! or value ?: error("Value was accessed before it was set.").

    Or you can write an extension property to access it more cleanly:

    val <T: Any> LiveData<T>.requiredValue: T get() = 
        value ?: error("Non-null value cannot be accessed before value is first set.")
    

    However, LiveData values should rarely need to be accessed directly. The intent with LiveData is that changes to the value will be observed, in which case, you would never run into this issue.