0

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?

Escher
  • 5,418
  • 12
  • 54
  • 101
  • `activityViewModels()`(i suppose this is from fragemnt-ktx ) has its own implementation i think its failing to create one since you have a parameterized constructor. – ADM Oct 18 '20 at 08:18
  • @ADM I know the viewModel is getting created `by activityViewModels()` because I am seeing the `Foo` information being rendered in the `Fragment` layout after binding. `activityViewModels()` uses a `ViewModelFactory` to create the `ViewModel`. See https://codelabs.developers.google.com/codelabs/kotlin-android-training-view-model/index.html?index=..%2F..android-kotlin-fundamentals#7 – Escher Oct 18 '20 at 09:08
  • 1
    Is it really the viewmodel that is null? In the line `viewModel.foo.value?.name`, it could be also `foo` or `foo.value`. And if it were `viewmodel` or `viewModel.foo`, the app would crash. – ferini Oct 20 '20 at 07:34
  • `viewModel` itself is null? or the `viewModel.foo.value?.name` is null? if `viewModel.foo.value` then it might be because the live data is not set yet. Aka the data from the database is not loaded yet. – Arpan Sarkar Oct 22 '20 at 19:40

2 Answers2

0

The "viewModel" in FooInfoFragment Fragment is not yet initialized and that's why it's null in Fragment.

binding.viewModel = viewModel 
//ViewModel is not initialized 
Hamza Israr
  • 411
  • 2
  • 7
  • 1
    `by activityViewModels()` guarantees that it is initialized when you access it the first time. – Tenfour04 Oct 21 '20 at 16:17
  • Thanks for letting me know, Really.. Now @Tenfour04 viewModel.LiveData.value will always be null outside Observer even if it is not nullable (I learned it the hard way). It can only confirm that it holds data while in an Observer. If you want to use one-shot asynchronous operations than use Flow or Transformations.switchMap. – Hamza Israr Oct 22 '20 at 22:30
0

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.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Thanks for the tips. The root cause what that the async operation to fetch the Foo from the database hadn't terminated at the time onCreate returned, but did terminate before rendering (so all the LiveData in the layout rendered correctly, making me forget that these data's availability depended on the async operation). – Escher Oct 24 '20 at 09:34
  • hey need help here --> https://stackoverflow.com/questions/64537639/spinner-value-using-retrofit-and-viewmodel – android dev Oct 27 '20 at 09:01