How to save and restore lambdas in Android?

2019-08-19 10:49发布

问题:

When implementing state restoration in Android, how can I save and restore a lambda?

I tried saving it as Serializable and Parcelable, but it throws a compile error.

Is there any way to save and restore them, or should I seek other approaches?

回答1:

Kotlin lambdas implement Serializable, so they can't be saved like:

override fun onSaveInstanceState(outState: Bundle) {
    outState.putSerializable("YOUR_TAG", myLambda as Serializable)
    super.onSaveInstanceState(outState)
}

Similarly, to restore them:

override fun onCreate(savedInstanceState: Bundle?) {
    myLambda = savedInstanceState?.getSerializable("YOUR_TAG") as (MyObject) -> Void
    super.onCreate(savedInstanceState)
}

(This can obviously be done in any of the lifecycle events that offer you the savedInstanceState, as this was just an example)

Some notes:

  • When saving them, they need to be casted, otherwise compiler complains (for some reason).
  • import java.io.Serializable is required.
  • The method where you're casting it back to your lambda type will throw a warning Unchecked cast: Serializable? to YourLambdaType. This cast is safe (assuming you infer the nullability correctly!), so you can safely supress this warning by using @Suppress("UNCHECKED_CAST")
  • MyObject must be Serializable or Parcelable, otherwise it crashes in runtime.

Now there's a detail that is not told anywhere and crashes in runtime with no helpful crash logs. The inner implementation of your lambda (i.e. what's inside the { } when you assign it) must not have references to objects that will be deallocated in a later moment. A classic example would be:

// In your MyActivity.kt…
myLambda = { handleLambdaCallback() } 
…

private fun handleLambdaCallback() {
    …
}

This will crash in runtime because handleLambdaCallback is implicitly accessing this, which would trigger an attempt to recursively serialize the entire object graph reachable by it, which would fail at some point during serialization time.

One solution to this problem is to send a reference in the lambda. Example:

// In your MyActivity.kt…
myLambda = { fragment -> (fragment.activity as MyActivity).handleLambdaCallback() }
…

private fun handleLambdaCallback() {
    …
}

This way, we are computing the reference when the lambda is invoked, rather than when it's assigned. Definitely not the cleanest solution, but it's the best I could come with, and it works.

Feel free to suggest improvements and alternative solutions!



回答2:

Should I seek other approaches?

Yes, there is not a really good reason to do it, your code won't be easily testable and you could introduce memory leaks.

Instead of saving the function, save the parameters (i.e. variables in the scope) that are needed to be saved, and invoke the function as you usually do.

Example

Instead of doing

val name = "John Smith"
val sayHello = { "Hi there, $name" }

startActivity(Intent().apply { putExtra("GREETER", sayHello as Serializable) })

Create a function that you can use elsewhere

fun sayHello(name: String) = { "Hi there, $name" }

And invoke with the restored name parameter later