How to save and restore lambdas in Android?
Asked Answered
I

4

7

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?

Irresolution answered 27/12, 2018 at 3:55 Comment(0)
I
9

Kotlin lambdas implement Serializable, so they can 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 the 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 suppress this warning by using @Suppress("UNCHECKED_CAST")
  • MyObject must be Serializable or Parcelable, otherwise it crashes at 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!

Irresolution answered 27/12, 2018 at 3:55 Comment(4)
If you try to serialize a lambda that captures this, it will try to recursively serialize the entire object graph reachable from it. I expect that to fail right away, not at deserialization time. The referent being GCd is not a problem because the whole point of parcelization is reconstructing dead objects.Hilten
The reason why you must cast the lambda to Serializable should be that this signals to Kotlin to create a serializable lambda in the first place. Lambdas aren't serializable by default.Hilten
You're right, it fails on serialization time, not at deserialization. Interesting point on serializing the entire object graphIrresolution
For me it is failing when activity goes to onStop lifecycle event. Also I was able to successfully pass, retrieve and typeCast lambda in a fragment with the argument bundleNapoli
S
2

Update for Kotlin 2.0.0+

If you're using Kotlin 2.0.0 or higher, lambas no longer implement Serializable by default. The changelog states:

To retain the legacy behavior of generating lambda functions, you can either:

  • Annotate specific lambdas with @JvmSerializableLambda.

  • Use the compiler argument -Xlambdas=class to generate all lambdas in a module using the legacy method.

Shipshape answered 27/5 at 6:9 Comment(1)
You save my dayOrson
S
0

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

Salvia answered 27/12, 2018 at 4:55 Comment(12)
I don't understand how the lambda approach is inherently untestable and vulnerable to memory leaks.Hilten
I think that my answer improves the memory leaking, specifically because now it's mandatory to not have a reference to this. I think your approach, however, makes the child (e.g. fragment) aware of its parent (i.e. the fragment would have to call (activity as MyActivity).sayHello(name)), which's obviously not good. Either this, or we'd lead to "delegation" (listener) approach. Am I missing something?Irresolution
@MarkoTopolnik There is nothing wrong with lambdas by definition, but from the moment you start using variables in the scope instead of parameters, things get messy:Salvia
@MarkoTopolnik 1. You can't look at the definition of the lambda and tell what it is supposed to do,Salvia
@MarkoTopolnik 2. For the same reason is hard to test, it's hard to know what are the dependencies (parameters) to mock and you have to define them as variables to be available in the scope (instead of just invoking the function)Salvia
@MarkoTopolnik 3. You need to be really careful what variables from the scope you reference, if it's an Activity, Fragment, Context... you could potentially create memory leaks, but if you pass them as parameters you won't create leaks because you never held a referenceSalvia
@RogerOba Your solution is the same as having a function (the same thing I'm talking about), because instead of accessing variables in the scope you are passing them as parameters, so why you need to serialize it?? If you look at it there is no need at all, just invoke the lambda/function with the deserialized parametersSalvia
@OmarMainegra consider that in my solution the private functions are kept private because they don't need to be exposed, and the activity is aware of itself, and the fragment isn't aware of the activity. Using functions requires a more complex logic to determine which activity to invoke if the fragment is reused in multiple activities, plus it creates the necessity of exposing the function as public, needlessly.Irresolution
@RogerOba if your functions are private and you still need them, then you are doing something wrong, and about the fragment-activity comunication you should be using and interface as the contract, otherwise is really unclear what the protocol is (whats functions should the fragment/activity call)Salvia
@RogerOba It looks like you need a little bit of architecture in your code, and with some design pattern you will not need to do this ever, and your code will be easy to understand, to mantain, safe from memory leaks, easy to test, etc...Salvia
@OmarMainegra maybe you're right. We were using MVP before and banished it because it was roughly ~15-20x slower than a more declarative approach, and the code is a lot harder to comprehend/read/followIrresolution
Let us continue this discussion in chat.Salvia
L
0

There are various alternatives,

  1. You may reassign the lambdas on the parent's onAttachFragment method, or via callbacks on the fragment's onAttach method.

  2. You may create a ViewModel for the fragment that hosts that data so that it can be saved between states

  3. You may use a FragmentFactory that receives the object with lambdas so that new fragments recreated regain access to that data which is not destroyed from the factory.

  4. There's an old deprecated way of simply using retainInstance so that you don't care of fragment being destroyed during these state changes. Of course this consumes more data for your app.

Latter answered 24/8, 2022 at 22:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.