How can I initialize an androidx ViewModel from parcelable data?

Viewed 2182

In my Android app, I pass custom data (UByteArray) from one activity to another using the parcelable interface.

I am using this data inside multiple fragments, so I rewrote the data class to extend androidx ViewModel and expose LiveData properties to the fragments. Now the UI updates are a lot nicer, but I think I am using it wrong because I overwrite all ViewModel values inside onCreate.

Now my question: What do I need to change to initialize the ViewModel only once? The following is my current code (abbreviated and renamed for this question):

class ActivityB : AppCompatActivity() {
  private val bData: ViewModelB by viewModels()
  // ...

  override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    intent.getParcelableExtra<ViewModelB>("id")?.let {
      Log.e(TAG, "Found parceled bData $it")
      // This seems like a very stupid way to do it, is there a better one?
      bData.copyAll(it)
    }
  }
}

I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.

Should I change the initialization of tagData with by viewModels() to = ViewModelB(intent)?
Or do I need to extend the ViewModelFactory somehow?

Any tip here would be really appreciated, thanks.

2 Answers

I saw that it is possible to inject SavedState into the ViewModelB constructor, but I don't have a saved state until now, and the data needs to be passed only once.

The official solution would be to provide a SavedStateHandle that is initialized with the defaultArgs as the intent.extras of your Activity.

For that, you need to provide an AbstractSavedStateViewModelFactory implementation, OR use SavedStateViewModelFactory (in which case you must define the right constructor in order to have it instantiated via reflection).

class ActivityB : AppCompatActivity() {
  private val bData: ViewModelB by viewModels {
      SavedStateViewModelFactory(application, this, intent.extras)
  }
  // ...

  override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    // intent.getParcelableExtra<ViewModelB>("id")?.let {
    //      Log.e(TAG, "Found parceled bData $it")
  }
}

Then in your ViewModel

@Keep
class ViewModelB(val savedStateHandle: SavedStateHandle): ViewModel() {
    val uByteData = savedStateHandle.get<UByteArray>("id")
}

Or so. The "id" key must match the same key as is in the intent extras.

Since you have a ViewModel which implements Parcelable, you can get your ViewModelB instance directly from the Intent extra.

The Intent which is used for starting ActivityB may not be != null at the time when ActivityB is instantiated, but you can use

lateinit var bData: ViewModelB

Then in onCreate()

bData = if(intent.hasExtra("id")) intent.getParcelableExtra<ViewModelB>("id") else ViewModelProvider(this).get(ViewModelB::class.java)
Related