why the different parceable get instantiated when getParcelable is on different one - android

in android having some parcelable classes
#Parcelize
class ParcelableClassA(private val member: Int) : IAParcelable {
init {
Log.e("+++", "+++ ParcelableClassA::init{}, this: $this")
}
#Parcelize
class ParcelableClassB(private val member: String) : IBParcelable {
init {
Log.e("+++", "+++ ParcelableClassB::init{}, this: $this")
}
in some where it is instantiated and put in a bundle for an activity's creation.
Intent(context, TheActivity::class.java).apply {
val bundle = Bundle()
bundle.putParcelable(EXTRAS_CLASS_A, ParcelableClassA(1))
bundle.putParcelable(EXTRAS_CLASS_B, ParcelableClassB("B")
...
putExtras(bundle)
context.startActivity(this)
}
in the activity class:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val classA = intent.extras?.getParcelable(EXTRAS_CLASS_A) as? IAParcelable
val classB = intent.extras?.getParcelable(EXTRAS_CLASS_B) as? IBParcelable
}
was expect there would be only one printout "+++ ParcelableClassA::init{}, this: $this"
but actually see two instances of the ParcelableClassA are created, one at the
intent.extras?.getParcelable(EXTRAS_CLASS_A)
and the other also happened when the
intent.extras?.getParcelable(EXTRAS_CLASS_B)
is called. So both ParcelableClassA::init{}, and ParcelableClassB::init{} are called twice.
and if there are more data put in the extra, the more ParcelableClassA's instantiation is called, which are just waste.
Is this expected behvior or the way it is done here is not correct?
update:
seems in the BaseBundle's, if in the unparcel() and the mParcelledData is not null it will have this behavior. But not sure why it happens.
if debug it step by step seems mParcelledData is always null and do not have this problem, but if just run it it will show this problem.
void unparcel() {
synchronized (this) {
final Parcel source = mParcelledData;
if (source != null) {
initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative);
} else {
if (DEBUG) {
Log.d(TAG, "unparcel "
+ Integer.toHexString(System.identityHashCode(this))
+ ": no parcelled data");
}
}
}
}

looks like the use of intent.extra? caused the problem.
if change to
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var bundle = intent.extras
val classA = bundle?.getParcelable(EXTRAS_CLASS_A) as? IAParcelable
val classB = bundle?.getParcelable(EXTRAS_CLASS_B) as? IBParcelable
}
seems dont see the dups.

Related

How to get result using registerForActivityResult from within ktor's Routing call running in a non-activity class?

How to get result from another activity (registerForActivity) from with in ktor's Routing API call (eg. /POST) running in a non-activity class?
Background: For an Android app, I run ktor server engine 'netty' in a non-activity class HttpServer.kt. I need to call another app's activity from with in ktor's Routing' POST handler, so I pass 'appCompatActivity' from MainActivity.kt. That's done, just because, I assume, registerForActivityResult() has dependency on UI/life cycle class.
Problem arises when running this as below, as registerForActivityResult() requires to be run earlier (like onCreate() ?), and I don't have such a class in this non-activity class. Moreover, the callback to run when ActivityResult is returned needs to call ktor ApplicationCall's respond which is also a suspend function.
class HttpServer(
private val applicationContext: AppCompatActivity
) {
private val logger = LoggerFactory.getLogger(HttpServer::class.java.simpleName)
private val server = createServer()
private fun ApplicationCall.startSaleActivityForResult() { // <====== *
val activityLauncherCustom =
applicationContext.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK || result.resultCode == Activity.RESULT_CANCELED) {
val transactionResultReturned = result.data
// Handle the returned result properly using transactionResultReturned
GlobalScope.launch {
respond(status = HttpStatusCode.OK, TransactionResponse())
}
}
}
val intent = Intent()
// Ignoring statements to create proper action/data intent
activityLauncherCustom.launch(intent) // <====== *
}
fun start() = server.start()
fun stop() = server.stop(0, 0)
private fun createServer(): NettyApplicationEngine {
return GlobalScope.embeddedServer(Netty) {
install(CallLogging)
install(ContentNegotiation) {
gson {
setPrettyPrinting()
}
}
routing {
route("/") {
post {
call.startSaleActivityForResult() // <====== *
}
}
}
}
}
private fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration>
CoroutineScope.embeddedServer(
factory: ApplicationEngineFactory<TEngine, TConfiguration>,
module: Application.() -> Unit
): TEngine {
val environment = applicationEngineEnvironment {
this.parentCoroutineContext = coroutineContext + parentCoroutineContext
this.log = logger
this.module(module)
connector {
this.port = 8081
}
}
return embeddedServer(factory, environment)
}
}
Above is what I tried, but gives below error. And I don't have onCreate on this non-activity class.
java.lang.IllegalStateException: LifecycleOwner com.youtap.upti.MainActivity#38dcf06 is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.
Any suggestions to resolve this problem would be grateful.
Below same above snippet as a screenshot to display helper text on declaration/param types from Android Studio:
And I invoke this server class from onCreate() of MainActivity:
To solve your problem and to hide the complexity you can create an intermediate class for launching activity and waiting for a result to come:
import kotlinx.coroutines.channels.Channel
class Repository(private val activity: MainActivity) {
private val channel = Channel<Int>(1)
suspend fun get(input: String): Int {
activity.activityLauncher.launch(input)
return channel.receive()
}
suspend fun callback(result: Int) {
channel.send(result)
}
}
You can store a reference to a repository and an activity launcher in the MainActivity class:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
CoroutineScope(Dispatchers.IO).launch {
HttpServer(this#MainActivity).also { it.start() }
}
}
val activityLauncher = registerForActivityResult(MySecondActivityContract()) { result ->
GlobalScope.launch {
repository.callback(result!!)
}
}
val repository = Repository(this)
}
My second activity and a contract looks like the following:
class ChildActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_child)
val result = Intent()
result.putExtra("name", 6666)
result.data = Uri.parse("http://mydata")
setResult(Activity.RESULT_OK, result)
finish()
}
}
class MySecondActivityContract : ActivityResultContract<String, Int?>() {
override fun createIntent(context: Context, input: String?): Intent {
return Intent(context, ChildActivity::class.java)
.putExtra("my_input_key", input)
}
override fun parseResult(resultCode: Int, intent: Intent?): Int? = when {
resultCode != Activity.RESULT_OK -> null
else -> intent?.getIntExtra("name", 42)
}
override fun getSynchronousResult(context: Context, input: String?): SynchronousResult<Int?>? {
return if (input.isNullOrEmpty()) SynchronousResult(42) else null
}
}
The most simplest part is routing handler:
routing {
route("/") {
post {
val result = (applicationContext as MainActivity).repository.get("input")
call.respondText { result.toString() }
}
}
}
This solution works but only one request is processed at the same time and it's not robust because Activity may be destroyed before HTTP server or repository objects.

Android fragment onCreate called twice

In my app I have two activities. The main activity that only has a search button in the Appbar and a second, searchable, activity. The second activity hold a fragment that fetches the data searched in it's onCreate call. My problem is that the fragment fetches the data twice. Inspecting the lifecycle of my activities, I concluded that the searchable activity gets paused at some point, which obviously determines the fragment to be recreated. But I have no idea what causes the activity to be paused.
Here are my activities
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
val root = binding.root
setContentView(root)
//Setup the app bar
setSupportActionBar(binding.toolbar);
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return initOptionMenu(menu, this)
}
}
fun initOptionMenu(menu: Menu?, context: AppCompatActivity): Boolean {
val inflater = context.menuInflater;
inflater.inflate(R.menu.app_bar_menu, menu)
// Get the SearchView and set the searchable configuration
val searchManager = context.getSystemService(Context.SEARCH_SERVICE) as SearchManager
(menu?.findItem(R.id.app_bar_search)?.actionView as SearchView).apply {
// Assumes current activity is the searchable activity
setSearchableInfo(searchManager.getSearchableInfo(context.componentName))
setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
}
return true;
}
SearchActivity.kt
class SearchActivity : AppCompatActivity() {
private lateinit var viewBinding: SearchActivityBinding
private var query: String? = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = SearchActivityBinding.inflate(layoutInflater)
val root = viewBinding.root
setContentView(root)
// Setup app bar
supportActionBar?.displayOptions = ActionBar.DISPLAY_SHOW_CUSTOM
supportActionBar?.setCustomView(R.layout.search_app_bar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
//Get the query string
if (Intent.ACTION_SEARCH == intent.action) {
intent.getStringExtra(SearchManager.QUERY).also {
//Add the query to the appbar
query = it
updateAppBarQuery(it)
}
}
//Instantiate the fragment
if (savedInstanceState == null) {
val fragment = SearchFragment.newInstance();
val bundle = Bundle();
bundle.putString(Intent.ACTION_SEARCH, query)
fragment.arguments = bundle;
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment)
.commitNow()
}
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
return initOptionMenu(menu, this)
}
private fun updateAppBarQuery(q: String?) {
supportActionBar?.customView?.findViewById<TextView>(R.id.query)?.apply {
text = q
}
}
}
As you can see, I am using the built in SearchManger to handle my search action and switching between activities. I haven't seen anywhere in the docs that during search, my searchable activity might get paused or anything like that. Does anyone have any idea why this happens? Thanks in advance!
edit: Here is my onCreate method for the SearchFragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val query = arguments?.getString(Intent.ACTION_SEARCH);
//Create observers
val searchResultObserver = Observer<Array<GoodreadsBook>> {
searchResultListViewAdapter.setData(it)
}
viewModel.getSearchResults().observe(this, searchResultObserver)
GlobalScope.launch { //Perform the search
viewModel.search(query)
}
lifecycle.addObserver(SearchFragmentLifecycleObserver())
}
Here, searchResultListViewAdapter is the adapter for a RecyclerViewand searchResult is a livedata in the view-model holding the search result
Here is the stack trace for the first call of onCreate() on SearchFragment:
And here is for the second call:
Here is the ViewModel for the SearchFragment:
class SearchViewModel() : ViewModel() {
private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
MutableLiveData<Array<GoodreadsBook>>();
}
fun getSearchResults(): LiveData<Array<GoodreadsBook>> {
return searchResults;
}
// TODO: Add pagination
suspend fun search(query: String?) = withContext(Dispatchers.Default) {
val callback: Callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
// TODO: Display error message
}
override fun onResponse(call: Call, response: Response) {
// TODO: Check res status
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
// Create the bitmap from the imageUrl
searchResults.postValue(parsedRes)
}
}
launch { searchBook(query, callback) }
}
}
I made some changes to the app since posted this and right now the search doesn't work for some reason in the main branch. This ViewModel it's from a branch closer to the time I posted this. Here is the current ViewModel, although the problem is present in this variant as well:
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
// private val searchResults: MutableLiveData<Array<GoodreadsBook>> by lazy {
//// MutableLiveData<Array<GoodreadsBook>>();
//// }
companion object {
private const val SEARCH_RESULTS = "searchResults"
}
fun getSearchResults(): LiveData<Array<GoodreadsBook>> =
savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)
// TODO: Add pagination
fun search(query: String?) {
val searchResults = savedStateHandle.getLiveData<Array<GoodreadsBook>>(SEARCH_RESULTS)
if (searchResults.value == null)
viewModelScope.launch {
withContext(Dispatchers.Default) {
//Handle the API response
val callback: Callback = object : Callback {
override fun onFailure(call: Call, e: IOException) {
// TODO: Display error message
}
override fun onResponse(call: Call, response: Response) {
// TODO: Check res status
val gson = Gson();
val parsedRes = gson.fromJson(
response.body?.charStream(),
Array<GoodreadsBook>::class.java
);
searchResults.postValue(parsedRes)
}
}
launch { searchBook(query, callback) }
}
}
}
}
The searchBook function just performs the HTTP request to the API, all the data manipulation is handled in the viewModel
try this way
Fragment sf = SearchFragment.newInstance();
Bundle args = new Bundle();
args.putString(Intent.ACTION_SEARCH, query);
sf.setArguments(args);
getFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, sf).addToBackStack(null).commit();
If your activity is getting paused in between then also onCreate of your activity should not be called and that's where you are instantiating the fragment.i.e Fragment is not created again(view might be created again).
As as you have subscribed live data in onCreate of Fragment it should also not trigger an update(onChanged() won't be called for liveData) again.
Just to be sure about live data is not calling onChanged() again try below (i feel that's the culprit here as i can't see any other update happening)
As you will not want to send the same result to your search page again so distinctUntilChanged is a good check for your case.
viewModel.getSearchResults().distinctUntilChanged().observe(viewLifecycleOwner,
searchResultObserver)
Do subscription of live data in onActivityCreated of
fragment.(reference)
Instead of using globalScope you can use viewModelScope and launch from inside your ViewModel.(just a suggestion for clean code)
And what's SearchFragmentLifecycleObserver?
P.S - If you can share the ViewModel code and how the search callback's are triggering data it will be great.But Current lifecycle should not effect the creation of new fragment.
Use SaveStateHandle in your ViewModel to persist the loaded data, and don't use GlobalContext to do the fetching, encapsulate the fetching in VieModel. GlobalContext should only be used for fire and forget actions, which are not bound the any views or lifecycle.
How your SearchViewModel could look like:
#Parcelize
class SearchResult(
//fields ...
) : Parcelable
class SearchViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private var isLoading : Boolean = false
fun searchLiveData() : LiveData<SearchResult> = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)
fun fetchSearchResultIfNotLoaded() { //do this in onCreate
val liveData = savedStateHandle.getLiveData<SearchResult>(EXTRA_SEARCH)
if(liveData.value == null) {
if(isLoading)
return
isLoading = true
viewModelScope.launch {
try {
val result = withContext(Dispatchers.IO) {
//fetching task
SearchResult()
}
liveData.value = result
isLoading = false
}catch (e : Exception) {
//log
isLoading = false
}
}
}
}
companion object {
private const val EXTRA_SEARCH = "EXTRA_SEARCH"
}
}
And in your Search Fragment onCreate
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val searchResultObserver = Observer<Array<GoodreadsBook>> {
searchResultListViewAdapter.setData(it)
}
viewModel.searchLiveData().observe(viewLifeCycleScope, searchResultObserver)
viewModel.fetchSearchResultIfNotLoaded()
}
I think the Android team in charge of the documentation should really do a better job. I went ahead and just removed the SearchManager from the SearchViewand use the onQueryTextListener directly, only to see that with this approach I also get my listener called twice. But thanks to this post, I saw that apparently it's a bug with the emulator (or with the way SearchView handles the submit event). So if I press the OSK enter button everything works as expected.
Thanks everyone for their help!

Activity Results API returned data is null

I am trying out the new Activity Results API by trying to return a parcelable dataesque class from a child activity. Using Alpha4 of the library.
I have setup the Intent with a custom contract 'AddAttendeeContract' as per my understanding of the docs. It compiles and runs and as far as I can see the correct methods are being called but the data is just null.
What might I be missing?
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
... //boilerplate setup nonsense
fab.setOnClickListener {
addAttendee()
}
}
private val addAttendee = registerForActivityResult(AddAttendeeContract()) { attendee: AttendeeData? ->
println("Attendee") // this does not print out
println(attendee) // this does not either
}
}
And the contract
class AddAttendeeContract : ActivityResultContract<Void?, AttendeeData?>() {
override fun createIntent(
context: Context,
input: Void?
): Intent =
Intent(context, AddAttendeeActivity::class.java)
override fun parseResult(
resultCode: Int,
intent: Intent?
): AttendeeData? = when {
resultCode != Activity.RESULT_OK -> null
else -> intent?.getParcelableExtra<AttendeeData>("attendee")
}
}
Finally is the invocation in the child activity class.
class AddAttendeeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
... //boilerplate
add.setOnClickListener { //button on a form
val name: String = view.name.text.toString().trim()
val rate: Double = view.rate.text.toString().trim().toDouble()
val number: Int = view.number.text.toString().trim().toInt()
val intent = Intent(this, MainActivity::class.java).apply {
putExtra("attendee", AttendeeData(name=name, rate=rate, number=number))
}
setResult(Activity.RESULT_OK, intent)
startActivity(intent)
}
}
}
Any insights as to what is going on?
This is solved. The problem was that the second activity was startign a new intent, rather than finishing and returning to the old one.
In the second/child activity had to change the line:
startActivity(intent)
to
finish() and things all worked as expected.

How implement a LiveData Singleton

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)
}

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