30

is there a way of bundling function references in Kotlin and Android so that the functions can be called from other fragments? For instance, my fragment factory method looks like this:

    fun newInstance(tryAgainFunction: () -> Unit): TimeOutHandlerFragment {
        val fragment = TimeOutHandlerFragment()
        val bundle = Bundle()

        return fragment
    }

I want to be able to save my tryAgainFunction in the bundle for further retrieval.

Thanks a lot!

Edit

In the end, the most suitable solution was using hotkey's answer and then in onViewCreated I initializing a listener with the passed function. The complete code is as follows:

companion object {
    val CALLBACK_FUNCTION: String = "CALLBACK_FUNCTION"

    fun newInstance(tryAgainFunction: () -> Unit): TimeOutHandlerFragment {
        val fragment = TimeOutHandlerFragment()
        val bundle = Bundle()
        bundle.putSerializable(CALLBACK_FUNCTION, tryAgainFunction as Serializable)
        fragment.arguments = bundle
        return fragment
    }
}

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    try {
        val callback: () -> Unit = arguments.getSerializable(CALLBACK_FUNCTION) as () -> Unit
        btnTryAgain.setOnClickListener { callback.invoke() }
    } catch (ex: Exception) {
        // callback has a wrong format
        ex.printStackTrace()
    }
}

Thanks to everyone for your help!

Milan Zelenka
  • 416
  • 1
  • 7
  • 11

7 Answers7

13

If tryAgainFunction is Serializable, then you can put it into the bundle using bundle.putSerializable("try_again_function", tryAgainFunction);.

It will actually be Serializable if it is a function reference (SomeClass::someFunction) or a lambda. But it might not be, if it is some custom implementation of the functional interface () -> Unit, so you should check that and handle the cases when it's not.

hotkey
  • 140,743
  • 39
  • 371
  • 326
  • 6
    I am passing this lamda: () -> LiveData> to my activity but it is throwing NotSerializableException. Can you please expand on how we can handle this case? – Ritesh Chandnani Feb 19 '18 at 20:26
  • @RiteshChandnani You can't do anything in that case. If your lambda has reference to a non-serializable object then it will crash. `LiveData` is not serializable so it will crash as expected. What you can do is to pass `List` as serializable in the intent and make your `T` implement `Serializable` – Farid Feb 10 '22 at 06:57
12

2022 Update:

  1. My original approach has a drawback, in my demo if I press the home button to put the app into the background, it will crash with NotSerializableException.
  2. Actually there is another API just for listening changes from one Fragment in the other, that is FragmentResultListener, offical tutorial is here: https://youtu.be/oP-zXjkT0C0?t=468.
  3. If FragmentResultListener cannot meet your need, Shared ViewModel is the final solution in there: https://youtu.be/THt9QISnIMQ.

1. You need to add id 'kotlin-parcelize' to your build.gradle (app), like this:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'kotlin-parcelize'    //here
}

2. Create a Class to wrap your Lambda function:

@Parcelize
class Listener(val clickAction: () -> Unit) : Parcelable {
    fun onClick() = clickAction()
}

3. Pass your Lambda:

val bundle = Bundle().apply {
    putParcelable("listener", Listener(clickAction))
}

private val clickAction: () -> Unit = {    
    //do your stuff
}

4. Retrieve it:

arguments?.getParcelable<Listener>("listener")?.onClick()

Demo (all Fragments): https://youtu.be/0OnaW4ZCnbk

Sam Chen
  • 7,597
  • 2
  • 40
  • 73
6

Maybe too late but:

Have you tried to put the val callback inside the companion object? Solution may be something like:

companion object {
    private lateinit var callback 

    fun newInstance(tryAgainFunction: () -> Unit): TimeOutHandlerFragment {
        val fragment = TimeOutHandlerFragment()
        this.callback = tryAgainFunction
        return fragment
    }
}

override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    btnTryAgain.setOnClickListener { callback.invoke() }
}
Javier Antón
  • 570
  • 4
  • 8
  • But remember, if you do so, the variable is going to be "static" and therefore shared between all object of the same class. In your case is fine because rarely you are going to have more than one instance of the same activity. – Javier Antón Dec 13 '18 at 13:59
  • can you please share how tryAgainFunction will be passed to the newInstance function – Gauri Gadkari Mar 22 '19 at 20:10
  • MyClass.newInstance() { tryAgainFunction() } – Javier Antón Mar 27 '19 at 09:08
  • Instead of a "static" var, why can't we use the function as a constructor parameter? It works fine until I found that in some devices (Xiaomi) I was getting `could not find Fragment constructor` then I switched to hotkey's solution. – GuilhE Nov 10 '20 at 10:29
  • 2
    But now I have a problem, my callbacks fail to `Serialize` when Fragment's Activity goes to background with `NotSerializableException`. Example of a callback: `(String?, List?) -> (Unit)`and `(Boolean) -> (Unit)` – GuilhE Nov 10 '20 at 12:26
3

No, as others say, there is no way of passing a function literal to Bundle.

But since enum implements Serializable, you can pass enum variables that can hold some methods. Try below codes;

enum class SampleEnum(val f: () -> Unit) {
    Sample0({
        Log.d("enum Test", "sample 0")
    }),
    Sample1({
        Log.d("enum Test", "sample 1")
    }),
}

in your from-activity (or fragment);

bundle.putSerializable("sample_enum",SampleEnum.Sample0)

in your to-activity (or fragment);

 // invoke your fuction
(bundle.getSerializable("sample_enum") as SampleEnum).f()
Yuta
  • 286
  • 1
  • 7
1

Yes, there is a simple solution to this that I've been using in my projects. The idea is to wrap any reference types/objects in a warper class that is Parcalabe and Serializable without actually implementing marshaling of the underlying reference type. Here is the wrapper class and how to use it without causing any potential memory leak:

@Parcelize @kotlinx.serialization.Serializable
data class TrackedReference<ReferenceType: Any> private constructor(
    private var uniqueID: Int = -1
) : Serializable, Parcelable {

    constructor(reference: ReferenceType) : this() {
        uniqueID = System.identityHashCode(this)
        referenceMap.set(uniqueID, reference)
    }

    val get: ReferenceType?
        get() = referenceMap.get(uniqueID) as? ReferenceType

    fun removeStrongReference() {
        get?.let { referenceMap.set(uniqueID, WeakReference(it)) }
    }

    companion object {
        var referenceMap = hashMapOf<Int, Any>()
            private set
    }

}

Any object can be wrapped in a TrackedReference object and passed as serializable or parcelable:

class ClassA {
    fun doSomething { }
}

// Add instance of ClassA to an intent
val objectA = ClassA()
intent.putExtra("key", TrackedReference(objectA))

You can also pass a weak or soft reference depending on the use case.

Use case 1:

The object (in this case objectA) is not guaranteed to be in memory by the time the receiver activity needs to use it (e.g. might be garbage collected).

In this case, the object is kept in memory until you explicitly remove the strong reference. Pass in an Intent or Bundle inside this wrapper and accesse in a straightforward manner, e.g. (psuedocode)

class ClassA {
    fun doSomething { }
}

// Add instance of ClassA to an intent
val objectA = ClassA()
intent.putExtra("key", TrackedReference(objectA))

// Retrieve in another activity
val trackedObjectA = intent.getSerializable("key") as TrackedReference<ClassA>
trackedObjectA.get?.doSomething()

// Important:- to prevent potential memory leak,
// remove *strong* reference of a tracked object when it's no longer needed.
trackedObjectA.removeStrongReference()

Use case 2:

The object is guaranteed to be in memory when needed, e.g. an activity that is housing a fragment is guaranteed to remain in memory for the lifecycle of the fragment.

Or, we do not care if the object is garbage collected, and we do not want to be the reason for its existence. In this case, pass a weak or soft reference when initializing a TrackedReference instance:

// Removing the strong reference is not needed if a week reference is tracked, e.g. 
val trackedObject = TrackedReference(WeakReference(objectA)) 
trackedObject.get?.doSomething()

Its best to understand why Intent requires Parcelable or Serializable and what's the best way to use this workaround in a given scenario.

It's certainly not ideal to serialize objects to allow delegation or callback communication between activities.

Here is the documentation on Intent but simply put, an Intent is passed to the Android System which then looks-up what to do with it and in this case starts the next Activity (which can be another app as far as the Android System receiving the intent is concerned). Therefore, the system needs to make sure that everything inside an intent can be reconstructed from parcels.

Rant:

IMO, Intent as a higher level abstraction for IPC (Inter Process Communication) maybe convenient and efficient internally but at the cost of these limitations. It's maybe comparing Apples to oranges but in iOS, ViewControllers (similar to Activities) are like any class and can be passed any Type (value or reference) as an argument. The developer is then responsible to avoid potential reference-cycles that might prevent ARC (memory management) to free unused references.

Lukas
  • 3,423
  • 2
  • 14
  • 26
0

1 - Create interface and extend serializable

interface ItemClickListener : Serializable {
    fun onItemClick(item: ItemModel)
}

2 - Pass to instance of fragment, dialogFragment or BottomSheetDialogFragment

companion object {

    const val KEY = "itemClickListener"

    @JvmStatic
    fun newInstance(itemClickListener: ItemClickListener) =
        ItemFragment().apply {
            arguments = Bundle().apply {                  
                putSerializable(KEY, itemClickListener)
            }
        }
}

3 - in fragment, dialogFragment or BottomSheetDialogFragment, onCreate

//declare nullable variable
private var itemClickListener: ItemClickListener? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    arguments?.let {      
        itemClickListener = it.getSerializable(KEY, ItemClickListener::class.java)
    }
}

By doing this, you will be safe if device change dark mode or light mode, configuration change, screen rotation.

Viroth
  • 429
  • 5
  • 14
-1

If it is not possible to put a function literal into a Bundle, however, you can pass the function to the Fragment through a setter like function in the Fragment.

fun setTryAgainFunction(tryAgainFunction: () -> Unit) {

}
Bob
  • 13,447
  • 7
  • 35
  • 45