18

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.

Gotama
  • 421
  • 4
  • 13
  • 1
    IMHO, you only put stuff in the `Data` that is *unique to the job*. Everything else belongs in other model objects that gets loaded as part of the work, where those model objects are stored with Room, Realm, Retrofit, or even things that don't start with R. :-) The reason `Parcelable` is not supported is that the data is persisted. And from what I can tell, keys are unique, so your two `putString()` calls should be replaced by one `putStringArray()` call. – CommonsWare Jun 11 '18 at 13:07
  • @CommonsWare i guess the answer to my question is your _The reason Parcelable is not supported is that the data is persisted. _ Thanks for the help. Also note that the two strings is just an example.. Ill update my post now to reflect new information – Gotama Jun 11 '18 at 15:01
  • So what will be the solution for sending serializable extra through work? – noob-Sci-Bot Nov 06 '18 at 13:07
  • @noob-Sci-Bot if you dont need to worry about parcelable then gson is probably the best solution. – Gotama Nov 29 '18 at 10:46
  • We get why `Parcelable` isn't supported, but just punting on the entire problem is a big step backwards. Now every developer has to build their own serialization method, and JSON while being the easiest to code will perform much worse than parcelable. – Jeffrey Blattman Feb 14 '20 at 17:18

7 Answers7

9

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;
}
Arseny Levin
  • 664
  • 4
  • 10
  • This is an option i thought of doing but didnt go this route as my POJO implements parcelable and didnt want to add another layer of "serialization" But this should be the accepted answer if you have a POJO that does not implement a layer of serialization. – Gotama Jun 11 '18 at 15:06
  • Surprisingly, gson converted string hasn't worked for me also. – tahsinRupam Jul 28 '19 at 11:57
  • JSON is an inefficient serialization form with regard both to space and time. Not to mention, simply leaving it up to each developer to figure out a serialization method is a big step backwards. – Jeffrey Blattman Feb 14 '20 at 17:13
7

This solution works without using JSON, and serializes directly to byte array.

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

@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;
}
Ufkoku
  • 2,384
  • 20
  • 44
3

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.

skm
  • 151
  • 5
  • `toWorkData()` was deprecated and removed in `alpha07`: https://developer.android.com/reference/kotlin/androidx/work/ktx/package-summary#toworkdata – Ivan Semkin Nov 23 '18 at 03:25
1

Accepted answer is correct. But new android developer can not understand easily, So thats why i given another answer with proper explanation.

My Requirement is pass Bitmap object. (You can pass as per your requirement)

Add dependency in your gradle file

Gradle:

dependencies {
  implementation 'com.google.code.gson:gson:2.8.5'
}

Use below method for serialize and de-serialize object

 // Serialize a single object.
    public static String serializeToJson(Bitmap bmp) {
        Gson gson = new Gson();
        return gson.toJson(bmp);
    }

    // Deserialize to single object.
    public static Bitmap deserializeFromJson(String jsonString) {
        Gson gson = new Gson();
        return gson.fromJson(jsonString, Bitmap.class);
    }

Serialize object.

 String bitmapString = Helper.serializeToJson(bmp);

Pass to data object.

 Data.Builder builder = new Data.Builder();
 builder.putString("bmp, bitmapString);
 Data data = builder.build();
        OneTimeWorkRequest simpleRequest = new OneTimeWorkRequest.Builder(ExampleWorker.class)
                .setInputData(data)
                .build();
        WorkManager.getInstance().enqueue(simpleRequest);

Handle your object in your Worker class.

Data data = getInputData();
String bitmapString = data.getString(NOTIFICATION_BITMAP);
Bitmap bitmap = Helper.deserializeFromJson(bitmapString);

Now your bitmap object is ready in Worker class.

Above is example, how to pass object in your worker class.

Yogesh Rathi
  • 6,331
  • 4
  • 51
  • 81
1

In Kotlin, thats how I do it

Object to Json

inline fun Any.convertToJsonString():String{
 return Gson().toJson(this)?:""
}

To Convert back to model,

inline fun <reified T> JSONObject.toModel(): T? = this.run {
  try {
    Gson().fromJson<T>(this.toString(), T::class.java)
  }
    catch (e:java.lang.Exception){ e.printStackTrace()
    Log.e("JSONObject to model",  e.message.toString() )

    null }
}


inline fun <reified T> String.toModel(): T? = this.run {
  try {
    JSONObject(this).toModel<T>()
   }
   catch (e:java.lang.Exception){
    Log.e("String to model",  e.message.toString() )

    null
 }
}
Aziz
  • 1,976
  • 20
  • 23
0

First you need to know that the value holder for data is private val mValues which you can not exactly work with to add parcelable to the data but there is a workaround to make the process at least less tedious

val Data.parcelables by lazy {
   mutableMapOf<String, Parcelable>()
}

fun Data.putParcelable(key:String, parcelable:Parcelable) : Data{
  parcelables[key] = parcelable
  // to allow for chaining 
  return this
}
// in order to get value in the Work class created  Wrok.doWork method
fun Data.getParcelable(key:String): Parcelable? = parcelables[key]

 // build  process

 /// you can not add putParcelable to Builder class either since mValues in the builder is also private and Builder.build() return Data(mValues)
 val data = Data.Builder()
            .putBoolean("one",false)

            .build()
            .putParcelable("name",parcelable)

val request = OneTimeWorkRequest.Builder().setInputData(data).build()
Breimer
  • 506
  • 8
  • 14
0

I solved the issue of sending arbitrary data to a worker by creating a static synchronized object queue map with an integer key. The key is then given as a parameter to the data object. And the object can then be retrieved from the object queue inside the worker. After retrieving the object it has to be removed from the queue.

An example below:

public class MyWorker extends Worker { 
    
    private final static @NonNull String DATA_KEY_QUEUE_INDEX = "queueIndex";
    private final static @NonNull Map<Integer, MyObject> queueMap = Collections.synchronizedMap(new HashMap<>());
    private static int nextQueueIndex = 0;

    public MyWorker(@NonNull Context context, @NonNull WorkerParameters parameters) {
        super(context, parameters);
    }


    @NonNull
    @Override
    public Result doWork() {
        var data = getInputData();
        var queueIndex = data.getInt(DATA_KEY_QUEUE_INDEX, -1);
        var myObject = queueMap.get(queueIndex);
        if (myObject == null) { return Result.failure(); }
        queueMap.remove(queueIndex);

        // DO work with myObject
        
        return Result.success();
    }

    public static void startWork(@NonNull Context context, @NonNull MyObject myObject) {
        var workManager = WorkManager.getInstance(context);
        var workRequest = new OneTimeWorkRequest.Builder(MyWorker.class);
        var inputData = new Data.Builder();
        
        var index = nextQueueIndex;
        inputData.putInt(DATA_KEY_QUEUE_INDEX, index);
        queueMap.put(index, myObject);
        nextQueueIndex += 1;
        workRequest.setInputData(inputData.build());
        workManager.enqueue(workRequest.build());
    }
    
}

Harmen
  • 81
  • 1
  • 3