When you have a big POJO with loads of variables (Booleans, Int, Strings) and you want to use the new Work Manager to start a job. You then create a Data file which gets added to the one time work request object.
What would be the best practices to build this data file? (It feels wrong to be writing 100 lines of code to just say put int on the builder for every variable.)
I ended up breaking apart my parcelable object as i thought this was the best implementation. I did not want to use gson lib as it would of added another layer of serialization to my object.
Data.Builder builder = new Data.Builder();
builder.putBoolean(KEY_BOOL_1, stateObject.bool1);
builder.putBoolean(KEY_BOOL_2, stateObject.bool2);
builder.putBoolean(KEY_BOOL_3, stateObject.bool3);
builder.putInt(KEY_INT_1, stateObject.int1);
builder.putInt(KEY_INT_2, stateObject.int2);
builder.putString(KEY_STRING_1, stateObject.string1);
return builder.build();
The partial answer to my question is as @CommonsWare pointed out :
The reason Parcelable is not supported is that the data is persisted.
Not sure what the detailed answer to Data not supporting parcelable?
- This answer explains its :
The Data is a lightweight container which is a simple key-value map and can only hold values of primitive & Strings along with their String version. It is really meant for light, intermediate transfer of data. It shouldn't be use for and is not capable of holding Serializable or Parcelable objects.
Do note, the size of data is limited to 10KB when serialized.
To pass an argument to a task, call the WorkRequest. Builder. setInputData(Data) method before you create the WorkRequest object. That method takes a Data object, which you create with Data.
A Parcelable is the Android implementation of the Java Serializable. It assumes a certain structure and way of processing it. This way a Parcelable can be processed relatively fast, compared to the standard Java serialization.
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.
Super easy with GSON: https://stackoverflow.com/a/28392599/5931191
// Serialize a single object.
public String serializeToJson(MyClass myClass) {
Gson gson = new Gson();
String j = gson.toJson(myClass);
return j;
}
// Deserialize to single object.
public MyClass deserializeFromJson(String jsonString) {
Gson gson = new Gson();
MyClass myClass = gson.fromJson(jsonString, MyClass.class);
return myClass;
}
I'm posting my solution here as I think it might be interesting for other people. Note that this was my first go at it, I am well aware that we could probably improve upon it, but this is a nice start.
Start by declaring an abstract class that extends from Worker like this:
abstract class SingleParameterWorker<T> : Worker(), WorkManagerDataExtender{
final override fun doWork(): WorkerResult {
return doWork(inputData.getParameter(getDefaultParameter()))
}
abstract fun doWork(t: T): WorkerResult
abstract fun getDefaultParameter(): T
}
The WorkManagerDataExtender is an interface that has extensions to Data. getParameter()
is one of these extensions:
fun <T> Data.getParameter(defaultValue: T): T {
return when (defaultValue) {
is ClassA-> getClassA() as T
is ClassB-> getClassB() as T
...
else -> defaultValue
}
}
Unfortunately I was not able to use the power of inlined + reified to avoid all the default value logic. If someone can, let me know in the comments.
getClassA()
and getClassB()
are also extensions on the same interface. Here is an example of one of them:
fun Data.getClassA(): ClassA {
val map = keyValueMap
return ClassA(map["field1"] as String,
map["field2"] as Int,
map["field3"] as String,
map["field4"] as Long,
map["field5"] as String)
}
fun ClassA.toMap(): Map<String, Any> {
return mapOf("field1" to field1,
"field2" to field2,
"field3" to field3,
"field4" to field4,
"field5" to field5)
}
(Then you can call toWorkData()
on the return of this extension, or make it return Data instead, but this way you can add more key value pairs to the Map before calling toWorkData()
And there you go, now all you have to do is create subclasses of the SingleParameterWorker and then create "to" and "from" extensions to Data to whatever class you need. In my case since I had a lot of Workers that needed the same type of POJO, it seemed like a nice solution.
This solution works without using JSON, and serializes directly to byte array.
package com.andevapps.ontv.extension
import android.os.Parcel
import android.os.Parcelable
import androidx.work.Data
import java.io.*
fun Data.Builder.putParcelable(key: String, parcelable: Parcelable): Data.Builder {
val parcel = Parcel.obtain()
try {
parcelable.writeToParcel(parcel, 0)
putByteArray(key, parcel.marshall())
} finally {
parcel.recycle()
}
return this
}
fun Data.Builder.putParcelableList(key: String, list: List<Parcelable>): Data.Builder {
list.forEachIndexed { i, item ->
putParcelable("$key$i", item)
}
return this
}
fun Data.Builder.putSerializable(key: String, serializable: Serializable): Data.Builder {
ByteArrayOutputStream().use { bos ->
ObjectOutputStream(bos).use { out ->
out.writeObject(serializable)
out.flush()
}
putByteArray(key, bos.toByteArray())
}
return this
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Parcelable> Data.getParcelable(key: String): T? {
val parcel = Parcel.obtain()
try {
val bytes = getByteArray(key) ?: return null
parcel.unmarshall(bytes, 0, bytes.size)
parcel.setDataPosition(0)
val creator = T::class.java.getField("CREATOR").get(null) as Parcelable.Creator<T>
return creator.createFromParcel(parcel)
} finally {
parcel.recycle()
}
}
inline fun <reified T : Parcelable> Data.getParcelableList(key: String): MutableList<T> {
val list = mutableListOf<T>()
with(keyValueMap) {
while (containsKey("$key${list.size}")) {
list.add(getParcelable<T>("$key${list.size}") ?: break)
}
}
return list
}
@Suppress("UNCHECKED_CAST")
fun <T : Serializable> Data.getSerializable(key: String): T? {
val bytes = getByteArray(key) ?: return null
ByteArrayInputStream(bytes).use { bis ->
ObjectInputStream(bis).use { ois ->
return ois.readObject() as T
}
}
}
Add proguard rule
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With