How you can parcel an OnClickListener in an Android Bundle — but shouldn’t!
Add undocumented, unreliable optimizations to the dangers of using custom Parcelable objects in bundles
tl;dr — If you create a custom Parcelable with a View.OnClickListener and send the object to a Fragment using a Bundle, the OS will pass the data copy-by-reference (not actually parceling the data) and the receiving fragment can access the original OnClickListener in nearly all cases. However, this is a reliance on an underlying optimization and is not the intention of the interface. So, it is not recommended.
Before describing more in depth the why it is definitely not recommended to “parcel” an OnClickListener within a custom parcelable, let me make the case that you should be very wary to use a custom parcelable in the first place.
The official Parcelables and Bundles documentation recommends “you should be careful to limit the data size” you are sending with a bundle to prevent TransactionTooLargeExceptions. If you are trying to retain as much control as you can over how much data goes into a bundle, then you should be wary of using an object that can encapsulate an arbitrary amount of data that can blow out your buffer and crash the app. These kinds of crashes can be difficult to debug because they aren’t necessarily reproducible — depending on what the user did to populate the bundle, it may or may not blow out the buffer. In the case of sending data between processes/apps, Google Developers even recommends that you do not even use custom parcelables! They cite the limited buffer size again and add a warning that one must be very careful to ensure “that the exact same version of the custom class is present on both the sending and receiving apps” so there aren’t any errors.
Let me now give a third warning that will transition us into how one can “parcel” an OnClickListener: Incomplete parcel implementation can lead to unpredictable and unreliable behavior. To understand why, we need to understand what is going on underneath the hood.
The official documentation says “Parcelable and Bundle objects are intended to be used across process boundaries such as with IPC/Binder transactions, between activities with intents, and to store transient state across configuration changes.” You could think of a bundle as a wrapper for a map that is marshalled into a byte array for transport/storage in the above cases. So, if you put thing A into the bundle, you should expect to get a reconstruction of thing A (note: not the original) on the other side. Another way of saying this is that the object (thing A) should be expected to be passed by value not by reference.
However, bundles can also be used to send arguments to Fragments. This is not an IPC process, so the underlying map need not be marshalled and unmarshalled. The original underlying map can be passed by reference (not by value) to avoid unnecessary work. Although it’s not documented, this is what the OS actually does. We can see in the implementation of BaseBundle.java#41 that the OS will avoid parceling the map when possible using something called mMap.
What this means is that if you’re not careful you can find yourself relying upon the map being passed by reference instead of relying on the map being passed by value (the documented intention). For an example of this, consider the following code. Notice that the OnClickListener is not and cannot be parceled in writeToParcel and unparceled in the constructor since Parceling is copy-by-value, not copy-by-reference.
Now imagine that you send this CustomParcelable to a fragment using a bundle — something like this:
In this case, DestinationFragment would be able to access the original OnClickListener! It would be able to access something that by the documentation should never have been accessible!
This example might seem far fetched, but in large codebases with many contributors that mostly work with fragments, it is conceivable to imagine how some might inadvertently rely on this. Misusing bundles in this way for a particular use case can cause serious headache for developers if any of the following happen:
- Android OS changes this underlying optimization in the future.
- The bundle is also used to send data to an activity.
- There is a configuration change.
In all of those cases, the data put into the bundle that cannot be or is not parceled would be lost. There may be a way around losing the data in the event of a configuration change by manually handling configuration changes (to prevent recreation of the activity and fragment), but that is not a good reason to manually handle configuration changes.
All that to say that this undocumented and unreliable optimization makes incomplete implementation of custom Parcelable objects in bundles unpredictable and unreliable. It allows you to “parcel” a OnClickListener — but not really… and only sometimes.
So what should you do if you are tempted to send an OnClickListener through a bundle? Ideally, the destination fragment should be responsible for its own business/view logic, and the caller should only pass relevant state. The caller shouldn’t try to pass logic (which is what an OnClickListener essentially encapsulates). So instead of trying to pass actual OnClickListeners (e.g. logic), they should instead pass state that the destination needs to create the appropriate OnClickListener itself.
Shout out to Charlie Lai for being an amazing mentor and editor for this article, and thanks to the following resources which helped me understand this topic more.