6

I have this function in kotlin extension file to pass method but it doesn't work. Please explain me how it make correctly, I try this:

fun showErrorClientScreen(context: Context, action : () -> Unit) {
    val intent = Intent(context, RestClientErrorActivity::class.java)

    val bundle = Bundle()
    bundle.putSerializable(UPDATE_CLIENT_ERROR, ErrorClientListener {  action })
    intent.putExtra(UPDATE_CLIENT_ERROR_BUNDLE, bundle)

    context.startActivity(intent)
}

use java interface

public interface ErrorClientListener extends Serializable {

    void tryAgainFunction();

}

and my activity where i need listen click button and try again send request:

class RestClientErrorActivity: BaseActivity(), View.OnClickListener {

    private lateinit var errorClientListener: ErrorClientListener

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_rest_client_error)

        try {
            val bundle = intent.getBundleExtra(UPDATE_CLIENT_ERROR_BUNDLE)
            errorClientListener = bundle?.getSerializable(UPDATE_CLIENT_ERROR) as ErrorClientListener
        } catch (e: Exception) {
            e.message
        }
    }

    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.ib_update -> errorClientListener.tryAgainFunction()
        }
    }

}
nicolas asinovich
  • 3,201
  • 3
  • 27
  • 37
  • Interesting question! I know tha serialize a lambda as Serializable & Runnable is possible in Java. I don't know how it works in Kotlin. But wonder if serialize a lambda expression to pass to an Intent is a good idea? what about using EventBus or Observables? – maheryhaja Jun 20 '18 at 12:50
  • 1
    thanks, but I would like to use native resources) – nicolas asinovich Jun 20 '18 at 13:01

3 Answers3

1

It is quite strange to package interfaces between activities and it is definitely not advisable. One reason why it is maybe not serializing between Activity A and Activity B is because the object was created in Activity A, it is treated as anonymous class creation and Activity A holds the reference to this object, hence preventing it from being serialised. This is good, because you can create references to objects within the interface callback whose reference in turn would be held by class instantiating it. Therefore, garbage collector won't be able to run collections on these objects and free up the space; causing a massive memory leak.

The alternative approach to your problem could be using clean architectures and a Singleton class pattern that is accessible by both activities and instantiated only once by say Activity A:

class SingletonErrorHandler private constructor(){
    var isError = false

    fun doOnError() {
        // do non view related stuff
        // like a network call or something          
    }

    companion object {
        val instance by lazy { SingletonErrorHandler() }
    }
}

in the activity you can define

class ActivityA : AppCompatActivity() {
    fun onError() {
        SingletonErrorHandler.instance.isError = true
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.a_activity)
    }
}

in activity B

class ActivityB : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.b_activity)

        val errorHandler = SingletonErrorHandler.instance
        if(errorHandler.isError)
             errorHandler.doOnError()
    }
}
HawkPriest
  • 272
  • 2
  • 8
  • 1
    thanks, but how specifically passing function as parameter use your approach? – nicolas asinovich Jun 20 '18 at 13:36
  • What is the `action` that you need to perform? – HawkPriest Jun 20 '18 at 13:57
  • the action can be any method in my case I have to send different requests when i reconnected – nicolas asinovich Jun 20 '18 at 14:13
  • 1
    If you are not updating any views in `ActivityA` then you can use a lambda variable `var callback : (() -> Unit)? = null` or an interface inside the `SingletonErrorHandler` that is set in `onError()` of `ActivityA` and call `callback?.invoke()` inside `doOnError()` – HawkPriest Jun 20 '18 at 14:20
  • 1
    thank you so much! it what I need! it turned out very nicely!)) – nicolas asinovich Jun 20 '18 at 14:51
  • Using Sigleton in Android is not advisable because you can loose state, leak memory, etc. Here you have an article discussing that problem. https://medium.com/@programmerr47/singletons-in-android-63ddf972a7e7 – Lucas Nov 13 '19 at 17:41
  • I agree singletons are not always a good solution. This would be best if it can be solved with scoped dependency injection whose references can be cleared once the dependent task completes. This also means that your state mutation has more visibility to the classes using them. Alternatively, you could also use `typedMaps` and pass serialisable objects `not functions` because they withhold references and functions themselves are anonymous so their references leak – HawkPriest Nov 14 '19 at 00:43
1

You can write factory method to start the activity like android studio generates factory method for fragment creation.

    class RestClientErrorActivity : AppCompatActivity() {

    companion object {
        private var completion: (() -> Unit)? = null
        fun start(context: Context, completion: (() -> Unit)?) {
            RestClientErrorActivity.completion = completion
            val bundle = Bundle()
    intent.putExtra(UPDATE_CLIENT_ERROR_BUNDLE, bundle)
     context.startActivity(intent)
        }
    }

    private lateinit var retryButton: Button

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retryButton = findViewById(R.id.btn_retry)
    }

    fun onRetryClick(view: View) {
        finish()
        completion?.invoke()
    }
}

Note: completion is not mandatory. so i made that as nullable. if you start activity without using factory method app will not crash.

0

I had the same problem. As mentioned in HawkPriest's Answer, your object is not serializable, because its an anonymous class. Another way to fix this is to simply implement a non-anonymous class that implements your interface. Here is my code:

Interface

interface MyInterface : Serializable {
    fun instruction()
}

Class

class MyClass : MyInterface {
    override fun instruction() {
        // does something
    }
}

Calling Activity

val myObject = MyClass()
val intent = Intent(context, MyActivity::class.java).putExtra("Tag", myObject)

context.startActivity(intent)

Activity

override fun onCreate(savedInstanceState: Bundle?) {
    val myObject = intent.getSerializableExtra("Tag") as MyInterface

    myObject.instruction()
}

Regarding the "native resources" as mentioned in your comment, you can make your instruction take parameters or pass them to your MyObject.

P.S. The problems I have with the Singleton solution:

  1. Singleton is not eligable for garbage collection, which means it lives on after its not needed anymore. (not 100% sure about that, but that's what I get from this answer)
  2. Using singleton would mean you cant have "multiple different uses" for your activity. If an interface is used, it is to be able to use multiple different implementations of that interface. A singleton wouldn't provide that, without using an interface architecture within your singleton, which would then again render it unnecessary, considering my proposed solution.
Benjamin Basmaci
  • 2,247
  • 2
  • 25
  • 46