Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WorkManager Data.Builder does not support Parcelable

Tags:

android

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

Answer

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();

UPDATE

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.

like image 253
Gotama Avatar asked Jun 11 '18 12:06

Gotama


People also ask

How to pass parameter in Workmanager?

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.

How Parcelable works in Android?

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.

When to use Parcelable in Android?

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.


3 Answers

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;
}
like image 60
Arseny Levin Avatar answered Oct 12 '22 07:10

Arseny Levin


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.

like image 43
skm Avatar answered Oct 12 '22 08:10

skm


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;
}
like image 4
Ufkoku Avatar answered Oct 12 '22 07:10

Ufkoku