Saving and retrieving list of custom objects in Java / Android - android

I am new to the world of Generics and am trying to write a utility class that will take a list of Objects and persist it to the store and then retrieve it back.
This is what I wrote to save the list:
public static void saveListToStore(Context ctx, String fileName, list<Object> listToStore) throws IOException
{
String elemValue = "";
Gson gson = new Gson();
try {
FileOutputStream fileOutputStream = ctx.openFileOutput(fileName, ctx.MODE_PRIVATE);
elemValue= gson.toJson(listToStore);
fileOutputStream.write(elemValue.getBytes());
objectOutputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
However when I try to retrieve, I will not be aware of the type of object that was there in the list and cannot rebuild it back. I do not want to put type comparisons as I would like to save any type of custom class and the list can be huge.
I want to deduce the type from the content itself. I was thinking of saving the type as the first line and then the data. So on retrieve I can get the type first and then typecast the objects. However is there any other cleaner way of achieving this ?

Ur Object should implement Serializable and the below code can help you to read and write
public static void readListToStore(Context ctx, String fileName, List<Object> listToStore) throws IOException {
SharedPreferences storeDataPref = ctx.getSharedPreferences("UR_KEY", Context.MODE_PRIVATE);
String elemValue = storeDataPref.getString("UR_NAME", null);
if (elemValue != null) {
Type listType = new TypeToken<ArrayList<Object>>() {
}.getType();
listToStore = new Gson().fromJson(elemValue, listType);
}
}
public static void saveListToStore(Context ctx, String fileName, List<Object> listToStore) throws IOException {
String elemValue = "";
Gson gson = new Gson();
try {
FileOutputStream fileOutputStream = ctx.openFileOutput(fileName, ctx.MODE_PRIVATE);
elemValue = gson.toJson(listToStore);
SharedPreferences storeDataPref = ctx.getSharedPreferences("UR_KEY", Context.MODE_PRIVATE);
Editor storeDataEditor = storeDataPref.edit();
storeDataEditor.clear();
storeDataEditor.putString("UR_NAME", elemValue).apply();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}

Taken from this question: Trouble with Gson serializing an ArrayList of POJO's
"You need to give Gson information on the specific generic type of List you're using (or any generic type you use with it). Particularly when deserializing JSON, it needs that information to be able to determine what type of object it should deserialize each array element to.
Type listOfTestObject = new TypeToken<List<TestObject>>(){}.getType();
String s = gson.toJson(list, listOfTestObject);
List<TestObject> list2 = gson.fromJson(s, listOfTestObject);
This is documented in the Gson user guide."
You can write and read the string to a file. The List can be of any collection type implementing the List interface

Related

Deserialize in-string JSON using Gson

I am using Retrofit 2 with GsonConverter. The problem is I have this response:
"responseData": {
"data": "<json array>"
}
As you can see one of the parameters is a JSON array, but it is a string. Should I use TypeAdapter and override the read and write methods? If so can you show how I can do this?
As you can see one of the parameters is a JSON array, but it is a string.
If the response generator is under your control, you should definitely change the response format (both for well-formedness and performance (Gson does not allow to read/write string literals as raw values)).
Should I use TypeAdapter and override the read and write methods?
If you cannot control your server response, you have to implement a custom type adapter with the read method implemented only. To align with that response format, you could define custom mappings like these:
final class Response<T> {
final ResponseData<T> responseData = null;
}
final class ResponseData<T> {
// This is where we're telling Gson to apply the special read strategy, not to all types
#JsonAdapter(RawJsonTypeAdapterFactory.class)
final T data = null;
}
As you can see, you just have to bind a custom type adapter to a specific field only. Despite the JsonAdapter annotation accepts TypeAdapter classes as well, you cannot bind TypeAdapter directly because you need Gson and Type instances.
final class RawJsonTypeAdapterFactory
implements TypeAdapterFactory {
// Gson will instantiate it itself without any issues
private RawJsonTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// No additional checks here, we're assuming the necessary fields are properly annotated
final Type type = typeToken.getType();
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value) {
throw new UnsupportedOperationException();
}
#Override
public T read(final JsonReader in)
throws IOException {
return gson.fromJson(in.nextString(), type);
}
}.nullSafe(); // And making the type adapter null-safe
}
}
Now JSON documents like
{
"responseData": {
"data": "[1,2,3]"
}
}
can be easily parsed in plain Java:
private static final Type intArrayResponseType = new TypeToken<Response<int[]>>() {
}.getType();
private static final Gson gson = new Gson();
...
try ( final JsonReader jsonReader = getPackageResourceJsonReader(Q43456942.class, "stringified.json") ) {
final Response<int[]> response = gson.fromJson(jsonReader, intArrayResponseType);
System.out.println(Arrays.toString(response.responseData.data));
}
Output:
[1, 2, 3]
Your Retrofit-bound service might have a method declared this (no type tokens necessary since Retrofit is smart enough to pass the Call parameterization to the underlying Gson converter):
Call<Response<int[]>> getIntArray();

Sharedpreference byte[] value cleared after app killed via 'Force Stop' or task manager

Problem
I'm saving a byte[] in my shared preferences. I am able to close the app and reopen it with the value persisting in the Shared Preferences. When running the app and closing it via the 'Task Manager' or 'Force Close', the Shared Preference value for the byte[] is cleared. I don't understand this because other values persist fine.
This lead me to believe that this was due to some gson or Shared Preference issue with the byte[] so I converted it to a String and I still have the issue.
Edit:
I save the data during normal activity usage... after onCreate(), for example. It's not during onPuse() or onDestroy() I forgot to mention this. It would make sense if I did call it here and one or both of those weren't being called on the 'Force Close' scenario.
Shared Preference Code
Slightly modified to remove app specific implementation and data
private static final String SHARED_PREFERENCES_FILE_NAME = "SharedPreferenceName";
public static void setSharedPreferenceObjectBase64Encoded(Context context, String key, Object object) throws Exception {
// Need an editor to update shared preference values
SharedPreferences.Editor editor = context.getSharedPreferences(SHARED_PREFERENCES_FILE_NAME, Context.MODE_PRIVATE).edit();
Gson gson = new GsonBuilder().serializeNulls().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create();
String encodedKey = Base64.encodeToString(key.getBytes(), 0, key.getBytes().length, Base64.DEFAULT);
String stringObject = gson.toJson(object);
String encodedObject = Base64.encodeToString(stringObject.getBytes(), 0, stringObject.getBytes().length, Base64.DEFAULT);
editor.putString(encodedKey, encodedObject);
editor.apply();
}
public static Object getSharedPreferenceObjectBase64Encoded(Context context, String key, Class<? extends Serializable> objectClass) throws Exception {
// Need an editor to update shared preference values
SharedPreferences prefs = context.getSharedPreferences(SHARED_PREFERENCES_FILE_NAME, Context.MODE_PRIVATE);
Gson gson = new GsonBuilder().serializeNulls().setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").create();
String encodedKey = Base64.encodeToString(key.getBytes(), 0, key.getBytes().length, Base64.DEFAULT);
String encodedObject = prefs.getString(encodedKey, null);
if (encodedObject == null) {
throw new NullPointerException("encodedObject is null : No shared preference exists for key.");
}
String decodedObject = new String(Base64.decode(encodedObject, Base64.DEFAULT));
if(decodedObject == null){
throw new NullPointerException("decodedObject is null : Json decoding error.");
}
Object resultObject = gson.fromJson(decodedObject, objectClass);
if (resultObject == null) {
throw new NullPointerException("resultObject is null : Json decoding error.");
}
return resultObject;
}
`byte[]` Code
public static final String VALUE_KEY= "value.key";
public static void saveTheValue(Context context, byte[] encryptedPin) {
try {
USharedPreferenceManager.setSharedPreferenceObjectBase64Encoded(context, VALUE_KEY, encryptedPin);
} catch (Exception e) {
}
}
public static byte[] getTheValue(Context context) {
try {
return (byte[]) USharedPreferenceManager.getSharedPreferenceObjectBase64Encoded(context, VALUE_KEY, byte[].class);
} catch (Exception e) {
return null;
}
}
Any input would be greatly appreciated..
Sadly, I haven't been able to make any progress here. Any thoughts?
Update:
As per Super-califragilistic recommendation, I iterated through the key/value pairs in the SharedPreferences immediately before retrieving the value. I was Base64 encoding my key and value values; in order to read the key to ensure the value was in the SharedPreferences I had to use the keys in plain text. This solved the issue for me as the byte[] value was now being retrieved properly.
This seems strange to me but I can use it as a solution. I would still like to Base64 encode the keys, but it's not incredibly important.
Current Solution:
Removed the Base64 encoding of the SharedPreference Key for storage and retrieval and the value is now persisting in all cases.
This line of code String encodedObject = prefs.getString(encodedKey, null); means if the key does not exist it should return null, hence your key that you are checking does not exist.
To validate if your key/value exist use this code
for(Entry<String, ?> en : sharedPref.getAll().entrySet()){
en.getKey();//key
en.getValue();//value
}
you could stop that from happening override onPause() in the Activity or Fragment and call saveTheValue(Context context, byte[] encryptedPin) if you detect you need to save data or have already tried saving data eg.
private boolean forceSaveInOnPause= false;//global variable
//in your saving method
....//after you save
forceSaveInOnPause = true;
//in your onPause of Activity
if(forceSaveInOnPause){
//re-save
forceSaveInOnPause = false;
but since you already have a solution scratch all that :)
Try once with editor.commit() instead of apply(), see if that works
I think using Base64.NO_PADDING instead of Base64.DEFAULT both while reading and writing may solve the problem.

Serializing a SparseArray<T> with GSON

I want to serialize a custom Java object, so I can use SharedPreferences to store it and retreive it in another Activity. I don't need persistant storage, the SharedPreferences, I wipe them when my application is closed. I'm currently using GSON for this, but it doesn't seem to work well with Android's SparseArray type.
My objects:
public class PartProfile {
private int gameId;
// Some more primitives
private SparseArray<Part> installedParts = new SparseArray<Part>();
// ...
}
public class Part {
private String partName;
// More primitives
}
Serialization:
Type genericType = new TypeToken<PartProfile>() {}.getType();
String serializedProfile = Helpers.serializeWithJSON(installedParts, genericType);
preferences.edit().putString("Parts", serializedProfile).commit();
serializeWithJSON():
public static String serializeWithJSON(Object o, Type genericType) {
Gson gson = new Gson();
return gson.toJson(o, genericType);
}
Deserialization:
Type genericType = new TypeToken<PartProfile>() {}.getType();
PartProfile parts = gson.fromJson(preferences.getString("Parts", "PARTS_ERROR"), genericType);
SparseArray<Part> retreivedParts = parts.getInstalledParts();
int key;
for (int i = 0; i < retreivedParts.size(); i++) {
key = retreivedParts.keyAt(i);
// Exception here:
Part part = retreivedParts.get(key);
// ...
}
Exception:
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.mypackage.objects.Part
I don't understand why Gson wants to cast a LinkedTreeMap to my object, I never use one in my entire program. I used to have a HashMap<Integer,Part> before I switched to the SparseArray<Part>, and never had issues with that. Are SparseArrays not supported by Gson, or is there an error on my side?
Edit: It seems that the SparseArray gets deserialized correctly, but not the objects inside. Instead of LinkedTreeMaps, these should be of type Part.
Really there is a way to serialize any kind of SparseArray, here is an example code:
public class SparseArrayTypeAdapter<T> extends TypeAdapter<SparseArray<T>> {
private final Gson gson = new Gson();
private final Class<T> classOfT;
private final Type typeOfSparseArrayOfT = new TypeToken<SparseArray<T>>() {}.getType();
private final Type typeOfSparseArrayOfObject = new TypeToken<SparseArray<Object>>() {}.getType();
public SparseArrayTypeAdapter(Class<T> classOfT) {
this.classOfT = classOfT;
}
#Override
public void write(JsonWriter jsonWriter, SparseArray<T> tSparseArray) throws IOException {
if (tSparseArray == null) {
jsonWriter.nullValue();
return;
}
gson.toJson(gson.toJsonTree(tSparseArray, typeOfSparseArrayOfT), jsonWriter);
}
#Override
public SparseArray<T> read(JsonReader jsonReader) throws IOException {
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull();
return null;
}
SparseArray<Object> temp = gson.fromJson(jsonReader, typeOfSparseArrayOfObject);
SparseArray<T> result = new SparseArray<T>(temp.size());
int key;
JsonElement tElement;
for (int i = 0; i < temp.size(); i++) {
key = temp.keyAt(i);
tElement = gson.toJsonTree(temp.get(key));
result.put(key, gson.fromJson(tElement, classOfT));
}
return result;
}
}
and to use it you need to register it in your Gson object, like this:
Type sparseArrayType = new TypeToken<SparseArray<MyCustomClass>>() {}.getType();
Gson gson = new GsonBuilder()
.registerTypeAdapter(sparseArrayType, new SparseArrayTypeAdapter<MyCustomClass>(MyCustomClass.class))
.create();
you can find this example in this gist.
P.S.: I know it's not optimized at all, but it's only an example to give an idea on how to achieve what you need.
It seems that the SparseArray gets deserialized correctly, but not the
objects inside. Instead of LinkedTreeMaps, these should be of type
Part.
Your observation is correct, since SparseArray contains Object (not Part), Gson won't have any clue to make Part as your object type. Hence it map your list as its infamous internal type LinkedTreeMap.
To solve it, I think you won't be able to use SparseArray... Or you may try retreivedParts.get(key).toString(), then use gson to parse the object again. But I don't think it's efficient to do that
As pointed out in the other answers SparseArray's internal implementation uses an Object[] to store the values so Gson cannot deserialize it correctly.
This can be solved by creating a custom Gson TypeAdapterFactory:
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import android.util.SparseArray;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
public class SparseArrayTypeAdapterFactory implements TypeAdapterFactory {
public static final SparseArrayTypeAdapterFactory INSTANCE = new SparseArrayTypeAdapterFactory();
private SparseArrayTypeAdapterFactory() { }
#Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
// This factory only supports (de-)serializing SparseArray
if (type.getRawType() != SparseArray.class) {
return null;
}
// Get the type argument for the element type parameter `<E>`
// Note: Does not support raw SparseArray type (i.e. without type argument)
Type elementType = ((ParameterizedType) type.getType()).getActualTypeArguments()[0];
TypeAdapter<?> elementAdapter = gson.getAdapter(TypeToken.get(elementType));
// This is safe because check at the beginning made sure type is SparseArray
#SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new SparseArrayTypeAdapter<>(elementAdapter);
// call nullSafe() to make adapter automatically handle `null` SparseArrays
return adapter.nullSafe();
}
private static class SparseArrayTypeAdapter<E> extends TypeAdapter<SparseArray<E>> {
private final TypeAdapter<E> elementTypeAdapter;
public SparseArrayTypeAdapter(TypeAdapter<E> elementTypeAdapter) {
this.elementTypeAdapter = elementTypeAdapter;
}
#Override
public void write(JsonWriter out, SparseArray<E> sparseArray) throws IOException {
out.beginObject();
int size = sparseArray.size();
for (int i = 0; i < size; i++) {
out.name(Integer.toString(sparseArray.keyAt(i)));
elementTypeAdapter.write(out, sparseArray.valueAt(i));
}
out.endObject();
}
#Override
public SparseArray<E> read(JsonReader in) throws IOException {
in.beginObject();
SparseArray<E> sparseArray = new SparseArray<>();
while (in.hasNext()) {
int key = Integer.parseInt(in.nextName());
E value = elementTypeAdapter.read(in);
// Use `append(...)` here because SparseArray is serialized in ascending
// key order so `key` will be > previously added key
sparseArray.append(key, value);
}
in.endObject();
return sparseArray;
}
}
}
This factory serializes SparseArrays as JSON objects with the key as JSON property name and the value serialized with the respective adapter as JSON value, e.g.:
new SparseArray<List<String>>().put(5, Arrays.asList("Hello", "World"))
    ↓ JSON
{"5": ["Hello", "World"]}
You then use this TypeAdapterFactory by creating your Gson instance using a GsonBuilder on which you register the TypeAdapterFactory:
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory​(SparseArrayTypeAdapterFactory.INSTANCE)
.create();

Gson fromJson only loading 1 element of ArrayList in object

I have an ArrayList<CustomClass> inside of ParentClass which I have written to a file using Gson.toJson(). However when I try to de-serialize the JSON using Gson.fromJson() I only get 1 element of the ArrayList<CustomClass>.
For example I will do the following
public class ParentClass {
private ArrayList<CustomClass> myList = new ArrayList<CustomClass>();
private GrandParentClass nested;
public ParentClass() {
myList.add(new CustomClass("adsf"));
myList.add(new CustomClass("fdsa"));
nested = new GrandParentClass();
}
public int arraySize() {
return myList.size();
}
}
public class GrandParentClass {
private ArrayList<OtherCustomClass> myList = new ArrayList<OtherCustomClass>();
public GrandParentClass() {
myList.add(new CustomClass("asdfasdf.."));
myList.add(new CustomClass("fdsafdsa..."));
}
public int arraySize() {
return myList.size();
}
}
Then when I instantiate a new instance of ParentClass, I use the following to write it to a file.
ParentClass pc = new ParentClass();
Gson gson = new Gson();
String writeThis = gson.toJson(pc); // Produces a perfect JSON reflection myList
FileOutputStream fos = new FileOutputStream(new File("writeto.json"));
fos.write(writeThis);
fos.close();
The JSON object is written in plain text to the .json file
FileInputStream fis = new FileInputStream(new File("writeto.json"));
char c;
StringBuffer sb = new StringBuffer();
while ((c = fis.read()) != -1)
sb.append((char) c);
//Now this is where I only get 1 element of the ArrayList
Gson gson = new Gson();
ParentClass pc = gson.fromJson(sb.toString(), ParentClass.class);
Log.i("SIZE", "Size is " + pc.arraySize()); // Log output: 'Size is 1'
Now, even though I have verified that there are indeed two elements in the ArrayList in the JSON file, only 1 gets loaded into the object using fromJson.
I am serializing these just fine, I would however like to deserialize the ArrayList<OtherCustomClass> inside of the GrandParentClass, which is inside of the ParentClass, in one fell swoop.
Basically I want to serialize ArrayLists nested possibly 3 or 4 layers down in this object heirarchy, and deserialize them into 1 ParentClass that contains these nested ArrayLists<?>. How would this be accomplished?
Thanks
You need to do some additional work when you want to deserialize a collection when generics are involved. This is explained here.
However, I am not sure how this applies to your case, since you have the collection "nested" within a top-level non-generic class.
public class ParentClass {
public static ArrayList<CustomClass> myList = new ArrayList<CustomClass>();
private GrandParentClass nested;
public ParentClass() {
myList.add(new CustomClass("adsf"));
myList.add(new CustomClass("fdsa"));
nested = new GrandParentClass();
}
public int arraySize() {
return myList.size();
}
}
public class GrandParentClass {
public GrandParentClass() {
ParentClass.myList.add(new CustomClass("asdfasdf.."));
ParentClass.myList.add(new CustomClass("fdsafdsa..."));
}
public int arraySize() {
return ParentClass.myList.size();
}
}
As you are again declaring and iniatilizing same array, so there possibility of getting lost the data. try this one out

Gson IllegalStateException

I'm a noob when it comes to basically all forms of storage aside from SharedPreferences and some SQLite. I did some searching and found that JSON+GSON was a fast way to parse Objects and their fields into storable Strings.
So, in my game, I have a Player object which has fields that are also my own classes:
public class Player {
private int something_game_related = 1;
private Skill equipped_skill;
private Item equipped_weapon;
public Player () {}
}
I suspect those classes are the problem, because when I try to run a simple save method:
private class ItemSerializer implements JsonSerializer<Item> {
public JsonElement serialize( Item src, Type typeOfSrc, JsonSerializationContext context ) {
return new JsonPrimitive(src.toString());
}
}
private class SkillSerializer implements JsonSerializer<Skill> {
public JsonElement serialize( Skill src, Type typeOfSrc, JsonSerializationContext context ) {
return new JsonPrimitive(src.toString());
}
}
public void doSave() {
GsonBuilder gson = new GsonBuilder();
//Both custom classes have zero-arg constructors so we don't need to register those
gson.registerTypeAdapter( Item.class, new ItemSerializer() );
gson.registerTypeAdapter( Skill.class, new SkillSerializer() );
Gson g = gson.create();
String mPlayer = "";
Type player = new TypeToken<Player>(){}.getType();
try{
mPlayer = g.toJson( GameView.mPlayer, player );
}
catch (Exception e) {e.printStackTrace();}
}
I get this Exception: java.lang.IllegalStateException: How can the type variable not be present in the class declaration!
My Question is..
How do I get these custom serializers to work? Like I said, I'm a noob.. but it looks like I did it right..
In the docs it says (kind of in the fine print) that static fields are excluded: http://sites.google.com/site/gson/gson-user-guide#TOC-Excluding-Fields-From-Serialization
You can do something like "excludeFieldsWithModifier(Modifier.STATIC)" in the GSON builder to include them.

Categories

Resources