How implement a LiveData Singleton - android

I need to pass a Bitmap between activities without write the image in the internal/external memory.
An Intent can't carry that size so the best option that I found is to use a Singleton Bitmap or extend Livedata and use it as singleton.
(I'm not that good with architecture so if you have a better solution...)
I'm trying to implement the LiveData option since the livedata observer will be useful and I'm following the
official documentation:
class StockLiveData(symbol: String) : LiveData<BigDecimal>() {
private val stockManager: StockManager = StockManager(symbol)
private val listener = { price: BigDecimal ->
value = price
}
override fun onActive() {
stockManager.requestPriceUpdates(listener)
}
override fun onInactive() {
stockManager.removeUpdates(listener)
}
companion object {
private lateinit var sInstance: StockLiveData
#MainThread
fun get(symbol: String): StockLiveData {
sInstance = if (::sInstance.isInitialized) sInstance else StockLiveData(symbol)
return sInstance
}
}
}
But I really don't understand the logic:
What's the listener will be used for?
What's the class StockManager?
If I need it only for a Bitmap do I need to use onActive() and onInactive() too?
I couldn't find a different implementation example anywhere, how can I implement that only for a Bitmap?
------------ UPDATE for the Sanlok Lee answer ----------------
I tried to implement your class BitmapCache example:
In my first activity I attach the observer
companion object {
val myCache = BitmapCache()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.mylayout)
myCache.getCachedBitmap().observe(this, Observer<Bitmap> { selfie: Bitmap? ->
Log.i(TAG, "TRIGGERED")
})
And in my second Activity I set the value like that:
companion object {
val myCache = BitmapCache()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.mylayout)
val bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.android)
Handler().postDelayed({
myCache.cacheBitmap(bitmap)
}, 3000)
}
But the observer is never triggered, are you sure I can create a Live data singleton like that? Thank you!

StockManager in the example is just a random custom class they made just for example purpose.
Just to give you a simpler example that uses a more familiar component, let's imagine that you need to create a custom LiveData that count (and emit the count) the number of user button press while the LiveData is active. It can look like this:
class ButtonClickLiveData(val button: Button) : LiveData<Int>() {
var clickCount = 0
private val listener = { v: View ->
clickCount++
value = clickCount
}
override fun onActive() {
// Set the click listener when LiveData is not active.
button.setOnClickListener(listener)
}
override fun onInactive() {
// Remove the click listener when LiveData is not active.
button.setOnClickListener(null)
}
}
And to explain your question
What's the listener will be used for?
That listener will be attached to the StockManager. When there is any change in StockManager, StockManager class is responsible for invoking this listener, and when the listener is invoked, it will update LiveData value.
What's the class StockManager?
Just an example class.
If I need it only for a Bitmap do I need to use onActive() and onInactive() too?
No. In fact I am guessing you would not need LiveData for transporting large object. Just as you pointed out, a simple singleton cache class is all you need. LiveData would make sense if you have a stream of Bitmap and you want the activities to automatically react to the stream. For example:
class BitmapCache { // This can be a singleton class.
private val bitmapLiveData = MutableLiveData<Bitmap>()
fun cacheBitmap(bmp: Bitmap) {
bitmapLiveData.value = bmp
}
fun getCachedBitmap(): LiveData<Bitmap> = bitmapLiveData as LiveData<Bitmap>
}
Edit:
Here's the singleton version of the class:
object BitmapCache {
private val bitmapLiveData = MutableLiveData<Bitmap>()
fun cacheBitmap(bmp: Bitmap) {
bitmapLiveData.value = bmp
}
fun getCachedBitmap(): LiveData<Bitmap> = bitmapLiveData as LiveData<Bitmap>
}
and it can be used like this:
// Activity A
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.mylayout)
BitmapCache.getCachedBitmap().observe(this, Observer<Bitmap> { selfie: Bitmap? ->
Log.i(TAG, "TRIGGERED")
})
// Activity B
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.mylayout)
val bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.android)
Handler().postDelayed({
BitmapCache.cacheBitmap(bitmap)
}, 3000)
}

Related

Sometimes, ConflatedBroadcastChannel fires recent value without any action

In Google's official codelab about advanced-coroutines-codelab sample, they've used ConflatedBroadcastChannel to watch a variable/object change.
I've used the same technique in one of my side projects, and when resuming the listening activity, sometimes ConflatedBroadcastChannel fires it's recent value, causing the execution of flatMapLatest body without any change.
I think this is happening while the system collects the garbage since I can reproduce this issue by calling System.gc() from another activity.
Here's the code
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
val tvCount = findViewById<TextView>(R.id.tv_count)
viewModel.count.observe(this, Observer {
tvCount.text = it
Toast.makeText(this, "Incremented", Toast.LENGTH_LONG).show();
})
findViewById<Button>(R.id.b_inc).setOnClickListener {
viewModel.increment()
}
findViewById<Button>(R.id.b_detail).setOnClickListener {
startActivity(Intent(this, DetailActivity::class.java))
}
}
}
MainViewModel.kt
class MainViewModel : ViewModel() {
companion object {
val TAG = MainViewModel::class.java.simpleName
}
class IncrementRequest
private var tempCount = 0
private val requestChannel = ConflatedBroadcastChannel<IncrementRequest>()
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
.asLiveData()
fun increment() {
requestChannel.offer(IncrementRequest())
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val button = findViewById<Button>(R.id.b_gc)
val timer = object : CountDownTimer(5000, 1000) {
override fun onFinish() {
button.isEnabled = true
button.text = "CALL SYSTEM.GC() AND CLOSE ACTIVITY"
}
override fun onTick(millisUntilFinished: Long) {
button.text = "${TimeUnit.MILLISECONDS.toSeconds(millisUntilFinished)} second(s)"
}
}
button.setOnClickListener {
System.gc()
finish()
}
timer.start()
}
}
Here's the full source code :
CoroutinesFlowTest.zip
Why is this happening?
What am I missing?
Quoting from the official response, (The simple and straightforward solution)
The problem here is that you are trying to use
ConflatedBroadcastChannel for events, while it is designed to
represent current state as shown in the codelab. Every time the
downstream LiveData is reactivated it receives the most recent state
and performs the incrementing action. Don't use
ConflatedBroadcastChannel for events.
To fix it, you can replace ConflatedBroadcastChannel with
BroadcastChannel<IncrementRequest>(1) (non-conflated channel, which is
Ok for events to use) and it'll work as you expect it too.
In addition to the answer of Kiskae:
This might not be your case, but you can try to use BroadcastChannel(1).asFlow().conflate on a receiver side, but in my case it led to a bug where the code on a receiver side didn't get triggered sometimes (I think because conflate works in a separate coroutine or something).
Or you can use a custom version of stateless ConflatedBroadcastChannel (found here).
class StatelessBroadcastChannel<T> constructor(
private val broadcast: BroadcastChannel<T> = ConflatedBroadcastChannel()
) : BroadcastChannel<T> by broadcast {
override fun openSubscription(): ReceiveChannel<T> = broadcast
.openSubscription()
.apply { poll() }
}
On Coroutine 1.4.2 and Kotlin 1.4.31
Without using live data
private var tempCount = 0
private val requestChannel = BroadcastChannel<IncrementRequest>(Channel.CONFLATED)
val count = requestChannel
.asFlow()
.flatMapLatest {
tempCount++
Log.d(TAG, "Incrementing number to $tempCount")
flowOf("Number is $tempCount")
}
Use Flow and Coroutine
lifecycleScope.launchWhenStarted {
viewModel.count.collect {
tvCount.text = it
Toast.makeText(this#MainActivity, "Incremented", Toast.LENGTH_SHORT).show()
}
}
Without using BroadcastChannel
private var tempCount = 0
private val requestChannel = MutableStateFlow("")
val count: StateFlow<String> = requestChannel
fun increment() {
tempCount += 1
requestChannel.value = "Number is $tempCount"
}
The reason is very simple, ViewModels can persist outside of the lifecycle of Activities. By moving to another activity and garbagecollecting you're disposing of the original MainActivity but keeping the original MainViewModel.
Then when you return from DetailActivity it recreates MainActivity but reuses the viewmodel, which still has the broadcastchannel with a last known value, triggering the callback when count.observe is called.
If you add logging to observe the onCreate and onDestroy methods of the activity you should see the lifecycle getting advanced, while the viewmodel should only be created once.

on kotlin, textview not initialized outside onCreate method

I am new at Kotlin and trying to implement MVP Architecture,
Currently I am having problem initializing/setting textview's value outside onCreate() method
here is my code
SplashActivity.kt
class SplashActivity : AppCompatActivity(), Splash.ViewInterface {
lateinit var appDetail: AppDetail
lateinit var textTitle: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textTitle = findViewById(R.id.splash_txt_title) as TextView
AppSingleton.appContext = applicationContext
var splashPresentation = SplashPresentation(this)
splashPresentation.getAppDetailFromService()
}
override fun fetchAppDetailSuccessful(response: SplashServiceObject) {
AppSingleton.initializeAppDetal(Gson().fromJson(response.json_string, AppDetail::class.java))
this.appDetail = AppSingleton.appDetail
}
override fun fetchAppDetailFailed(errMsg: String) {
textTitle.text = errMsg
}
}
SplashPresenter.kt
class SplashPresentation(private val view: Splash.ViewInterface) : Splash.PresentationInterface {
fun getAppDetailFromService() {
var splashService = SplashService()
splashService.getAppDetailFromAssets(this)
}
override fun fetchAppDetailFromServiceSuccessful(response: SplashServiceObject) {
view.fetchAppDetailSuccessful(response)
}
override fun fetchAppDetailFromServiceFailed(errMsg: String) {
view.fetchAppDetailFailed(errMsg)
}
}
SplashService.kt
class SplashService {
fun getAppDetailFromAssets(splashPresentation: SplashPresentation) {
val json_filename = "appdetail.json"
var jsonResponse: JsonResponse = AppSingleton.commonUtils.fetchJsonFromAssets(json_filename, AppSingleton.appContext!!)
if (jsonResponse.json_status) {
var splashServiceObj = SplashServiceObject
splashServiceObj.json_string = jsonResponse.json_info
splashServiceObj.response_msg = "JSON Successful fetched."
splashPresentation.fetchAppDetailFromServiceSuccessful(splashServiceObj)
} else {
splashPresentation.fetchAppDetailFromServiceFailed(jsonResponse.json_info)
}
}
}
in my SplashActivity().onCreate(), I am calling a Presenter that access Service, then the Service return a value to Presenter,
Then Presenter, return value to my SplashActivity's View, one of the function is, fetchAppDetailFailed(errMsg)
when I run the app, it crashes, saying the "textaa" is not yet initialized.
back in Java exp, when the variable is already instantiated on onCreate(), you can call this variable anywhere within the activity.
Thanks in advance!
You cannot instantiate Activities on Android. They are instantiated by the OS, and the OS calls the lifecycle methods on it.
In an MVP pattern, the View and Presenter both reference each other. Since Activity (the View) is the entry point of the application, your Activity should instantiate the Presenter and pass a reference of itself to the Presenter so communication can go both ways.
Also, the reference to the activity in the Presenter should be specified as a ViewInterface, not an Activity, or you're kind of defeating the purpose of using MVP.
class SplashPresentation(private val view: Splash.ViewInterface) : Splash.PresentationInterface {
//... methods that call functions on view
}
class SplashActivity : AppCompatActivity(), Splash.ViewInterface {
private val presenter = SplashPresentation(this)
//...
}

is observeForever lifecycle aware?

I'm working with MVVM, and I have made different implementations of it, but one thing that is still making me doubt is how do I get data from a Repository (Firebase) from my ViewModel without attaching any lifecycle to the ViewModel.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea because I think I should communicate from my repository to my ViewModel either with a callback or a Transformation.
I leave here an example where I fetch a device from Firebase and update my UI, if we can see here, I'm observing the data coming from the repo from the UI, but from the ViewModel I'm also observing data from the repo, and here is where I really doubt if I'm using the right approach, since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
UI
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
observeData(deviceId)
}
}
fun observeData(deviceId:String){
viewModel.fetchDeviceData(deviceId).observe(this, Observer {
textView.text = "Tipo: ${it.devType}"
})
ViewModel
class MainViewmodel: ViewModel() {
private val repo = Repo()
fun fetchDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
repo.getDeviceData(deviceId).observeForever {
mutableData.value = it
}
return mutableData
}
}
Repository
class Repo {
private val db = FirebaseDatabase.getInstance().reference
fun getDeviceData(deviceId:String):LiveData<Device>{
val mutableData = MutableLiveData<Device>()
db.child(deviceId).child("config/device").addListenerForSingleValueEvent(object: ValueEventListener{
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
})
return mutableData
}
}
This example just shows how to fetch the device from Firebase, it works, but from my ViewModel, it keeps making me think that observeForever() is not what I'm looking for to communicate data between the repository to the ViewModel.
I have seen Transformations, but I, in this case, I just need to deliver the entire Device object to my UI, so I don't need to transform the Object I'm retrieving to another Object
What should be here the right approach to communicate the repository and the ViewModel properly?
is observeForever lifecycle aware?
No, that's why it's called observeForever.
I have implemented observeForever() from the ViewModel, but I don't think that is a good idea
No, it's not, you should be using Transformations.switchMap {.
since I don't know if observeForever() will be cleared on onCleared() if my view is destroyed, so it won't keep the observer alive if the view dies.
Well if you're not clearing it in onCleared() using removeObserver(observer), then it won't clear itself, because it observes forever.
here is where I really doubt if I'm using the right approach,
No, you can do much better than this following a reactive approach.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
val deviceId = editText.text.toString().trim()
viewModel.onSelectedDeviceChanged(deviceId)
}
viewModel.selectedDevice.observe(this, Observer { device ->
textView.text = "Tipo: ${device.devType}"
})
}
And
class MainViewModel(
private val savedStateHandle: SavedStateHandle,
): ViewModel() {
private val repo = Repo() // TODO: move to Constructor Argument with ViewModelProvider.Factory
private val selectedDeviceId: MutableLiveData<String> = savedStateHandle.getLiveData<String>("selectedDeviceId")
fun onSelectedDeviceChanged(deviceId: String) {
selectedDeviceId.value = deviceId
}
val selectedDevice = Transformations.switchMap(selectedDeviceId) { deviceId ->
repo.getDeviceData(deviceId)
}
}
And
class Repo {
private val db = FirebaseDatabase.getInstance().reference // TODO: move to constructor arg? Probably
fun getDeviceData(deviceId:String) : LiveData<Device> {
return object: MutableLiveData<Device>() {
private val mutableLiveData = this
private var query: Query? = null
private val listener: ValueEventListener = object: ValueEventListener {
override fun onDataChange(dataSnapshot: DataSnapshot) {
val device = dataSnapshot.getValue(Device::class.java)
mutableLiveData.value = device
}
override fun onCancelled(dataError: DatabaseError) {
Log.e("Error","handle error callback")
}
}
override fun onActive() {
query?.removeEventListener(listener)
val query = db.child(deviceId).child("config/device")
this.query = query
query.addValueEventListener(listener)
}
override fun onInactive() {
query?.removeEventListener(listener)
query = null
}
}
}
}
This way, you can observe for changes made in Firebase (and therefore be notified of future changes made to your values) using LiveData, rather than only execute a single fetch and then not be aware of changes made elsewhere to the same data.
To use ObserveForever, you need to remove the observer inside onClear in the ViewModel.
In this case, I would suggest to use Transformation even though you just need a direct mapping without any processing of the data, which is actually the same as what you are doing with the observer for observerForever.
observeForever() is not Lifecycle aware and will continue to run until removeObserver() is called.
In your ViewModel do this instead,
class MainViewmodel: ViewModel() {
private val repo = Repo()
private var deviceData : LiveData<Device>? = null
fun fetchDeviceData(deviceId:String):LiveData<Device>{
deviceData = repo.getDeviceData(deviceId)
return deviceData!!
}
}

Good way to pass a reference to a ViewModel in an activity android

I have a ViewModel that inherits from a base class and I would like to have a corresponding Activity also inherit from a base class. The activity would call the same method of the derived ViewModel each time. So it would be something like this:
BaseViewModel:
abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {
protected val context = getApplication<Application>().applicationContext
protected var speechManager: SpeechRecognizerManager? = null
var _actionToTake : MutableLiveData<AnalyseVoiceResults.Actions> = MutableLiveData()
var actionToTake : LiveData<AnalyseVoiceResults.Actions> = _actionToTake
open fun stopListening() {
if (speechManager != null) {
speechManager?.destroy()
speechManager = null
}
open fun startListening() {
val isListening = speechManager?.ismIsListening() ?: false
if (speechManager == null) {
SetSpeechListener()
} else if (!isListening) {
speechManager?.destroy()
SetSpeechListener()
}
}
}
BaseActivity
class BaseActivity : AppCompatActivity() {
private lateinit var baseViewModel: BaseViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
fun goback() {
super.onBackPressed()
baseViewModel.stopListening()
finish()
}
fun startListening() {
baseViewModel.startListening()
}
override fun onDestroy() {
super.onDestroy()
baseViewModel.stopListening()
}
}
Derived Activity:
class DerivedActivity : BaseActivity() {
private val nextActivityViewModel: NextActivityViewModel by inject()
///^^inherits from BaseViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/*** pass reference ***/
baseViewModel = nexActivityViewModel
nextActivityViewModel.actionToTake.observe(this, object : Observer<AnalyseVoiceResults.Actions?> {
override fun onChanged(t: AnalyseVoiceResults.Actions?) {
if (t?.equals(AnalyseVoiceResults.Actions.GO_BACK) ?: false) {
goback()
}
}
})
startListening()
}
}
Will this cause memory leaks to have two instances of a view model for this activity? Is there a better way to do this? I don't want to keep repeating the same code for all my activities. (I would also have the same question if I was doing this with one base fragment).
make this var baseViewModel: BaseViewModel an abstract variable where all the children class must override it. So, when you call the startListening and stopListening, these methods will be called from children implementation.
Edit:
Make the BaseActivity an abstract class and the baseViewModel as an abstract variable
abstract class BaseActivity : AppCompatActivity() {
private abstract var baseViewModel: BaseViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
fun goback() {
super.onBackPressed()
baseViewModel.stopListening()
finish()
}
fun startListening() {
baseViewModel.startListening()
}
override fun onDestroy() {
super.onDestroy()
baseViewModel.stopListening()
}
}
So, your DerivedActivity must override the baseViewModel, and every call on father's class will trigger the child
class DerivedActivity : BaseActivity() {
override val baseViewModel: NextActivityViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Will this cause memory leaks to have two instances of a view model for
this activity?
No, there are no memory leaks with this approach. Nor do you have 2 instances of the ViewModel for the same activity. It's a single instance of ViewModel which is being referenced with different variables in BaseActivity and BaseViewModel.
Is there a better way to do this?
The first issue I see is that you have Android specific code in the ViewModels, which is not considered a good practice. You should move the speech manager code to the base activity itself, and ViewModel should only hold the "state" data that you want to retain on orientation changes. This will ensure all the Speech Management methods (create, resume, destroy) will be in the base activity. Concrete activity will only have observers if the state changes.
If you are following any architecture pattern (like MVP), once you move the Speech Manager code out to activity, it would become obvious to move this further out to the Presenter.
EDIT: I have not used the MVVM pattern in production, but this is a light variant of what you may want:
The basic idea is to move Speech management code in a lifecycle-aware component. All UI code in view/activity and business logic / non-android state in viewmodel. I don't see a point in having base activity or viewmodel based on the requirements you have shared so far.
/**
* All the speech related code is encapsulated here, so any new activity/fragment can use it by registering it's lifecycle
*/
class SpeechManager(private val context: Context): LifecycleObserver {
val TAG = "SpeechManager"
private var speechRecognizer: SpeechRecognizer? = null
fun registerWithLifecycle(lifecycle: Lifecycle) {
Log.e(TAG, "registerWithLifecycle")
lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_START)
fun start() {
Log.e(TAG, "start")
speechRecognizer = (speechRecognizer ?: SpeechRecognizer.createSpeechRecognizer(context)).apply {
// setRecognitionListener(object : RecognitionListener {
// //implement methods
// })
}
}
#OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun stop() {
Log.e(TAG, "stop")
speechRecognizer?.run {
stopListening()
destroy()
}
}
}
ViewModel:
class SpeechViewModel: ViewModel() {
val TAG = "SpeechViewModel"
//List all your "data/state" that needs to be restores across activity restarts
private val actions: MutableLiveData<Actions> = MutableLiveData<Actions>().apply { value = Actions.ActionA }
//Public API for getting observables and all use-cases
fun getActions() = actions
fun doActionA(){
//validations, biz logic
Log.e(TAG, "doActionA")
actions.value = Actions.ActionA
}
fun doActionB(){
Log.e(TAG, "doActionB")
actions.value = Actions.ActionB
}
}
sealed class Actions{
object ActionA: Actions()
object ActionB: Actions()
}
Activity/View:
class SpeechActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_speech)
setSupportActionBar(toolbar)
initSpeechManager()
}
private lateinit var speechManager: SpeechManager
private lateinit var speechViewModel: SpeechViewModel
/**
* Register lifecycle aware components and start observing state changes from ViewModel.
* All UI related code should ideally be here (or your view equivalent in MVVM)
*/
private fun initSpeechManager() {
speechManager = SpeechManager(this).apply {registerWithLifecycle(lifecycle)}
speechViewModel = ViewModelProviders.of(this).get(SpeechViewModel::class.java).apply {
getActions().observe(this#SpeechActivity, Observer<Actions>{
when(it){
is Actions.ActionA -> {
Log.e(TAG, "Perform ActionA")
speechManager.start()
}
is Actions.ActionB -> {
Log.e(TAG, "Perform ActionB")
speechManager.stop()
super.onBackPressed()
}
}
})
}
}
}

Kotlin - passing function as parameter via Intent

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()
}
}
}
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()
}
}
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.
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:
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)
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.

Categories

Resources