SoFunction
Updated on 2025-04-09

How to parse heterogeneous lists on Android

Preface

When developing business requirements, we encountered a completely different type of data structure in the list. We call this kind of list heterogeneous list. Take the chat history list as an example

[
	{
		"msgType" : "text",
		"id" : "1",
		"content" : "Hello world"
	},
	{
		"msgType" : "record",
		"id" : "2",
		"url" : "https://xxxx.mp4",
		"length" : 123450
	},
	{
		"msgType" : "image",
		"id" : "3",
		"url" : "",
		"size" : "300x300"
	}
]

If you want to parse the above JSON, manual parsing is not possible, but it is definitely not recommended. If you use parsing tools directly, such as Gson, no matter what data structure is defined, it seems that it does not conform to the list element above.

Can that be done? Let's tell Gson what data types each element in the list is, so that it knows how to parse it? Next, we implement custom parsing through Gson's TypeAdapter.

Implementation plan

First define various data types, and correspond to the msgType field one by one.

abstract class BaseMessage(val id: String?, val msgType: String?)

class TextMessage(id: String?, msgType: String?, val content: String?
) : BaseMessage(id, msgType)

class ImageMessage(id: String?, msgType: String?, val url: String?, val size: String?
) : BaseMessage(id, msgType)

class RecordMessage(id: String?, msgType: String?, val url: String?, val length: Long
) : BaseMessage(id, msgType)

Then customize a TypeAdapter.

class BaseMessageTypeAdapter : TypeAdapter<BaseMessage>() {
 override fun write(out: JsonWriter, value: BaseMessage?) {
 }

 override fun read(`in`: JsonReader): BaseMessage? {
 }
}

You can see that there are two methods: write() is responsible for serialization, and read() is responsible for deserialization. Let's focus on the implementation of read() first

The basic idea of ​​implementing read() is as follows

  1. Read msgType field
  2. Judging the corresponding data type according to msgType
  3. Obtain the TypeAdapter of parsing the type according to this data type
  4. Leave it to the corresponding type TypeAdapter parsing

According to the above ideas, you can write the basic implementation code of read(). Of course this is a rough implementation, and in fact there are other situations to consider

class BaseMessageTypeAdapter(private val gson: Gson, 
        private val skipPast: TypeAdapterFactory
) : TypeAdapter&lt;BaseMessage&gt;() {
 override fun read(`in`: JsonReader): BaseMessage? {
  // 1. Read the msgType field  val jsonObject = (`in`).asJsonObject
  val msgType = ("msgType")?.asString
  // 2. Obtain and parse the TypeAdapter of this type according to msgType  val adapter = getTypeAdapterByType(msgType)
  // 3. Leave it to the corresponding type TypeAdapter to parse  return adapter?.fromJsonTree(jsonObject)
 }
}

There is nothing to say about the write() method, just hand it over to the corresponding type TypeAdapter serialization

class BaseMessageTypeAdapter(private val gson: Gson, 
        private val skipPast: TypeAdapterFactory
) : TypeAdapter<BaseMessage>() {
 override fun write(out: JsonWriter, value: BaseMessage?) {
  if (value == null) {
   ()
   return
  }
  getTypeAdapterByType()?.write(out, value)
 }
}

Next, implement the getTypeAdapterByType() method.

 private fun getTypeAdapterByType(type: String?): TypeAdapter&lt;BaseMessage&gt;? {
  return when (type) {
   "text" -&gt; getTypeAdapter(TextMessage::)
   "image" -&gt; getTypeAdapter(ImageMessage::)
   "record" -&gt; getTypeAdapter(RecordMessage::)
   else -&gt; null
  }
 }

 private fun &lt;R : BaseMessage&gt; getTypeAdapter(clazz: Class&lt;R&gt;): TypeAdapter&lt;BaseMessage&gt; {
  // Get the TypeAdapter corresponding to this type in Gson  return SubTypeAdapterWrapper(clazz, (skipPast, (clazz)))
 }

The logic is also relatively simple. It should be noted that in the getTypeAdapter() method, you need to convert TypeAdapter<out BaseMessage> to TypeAdapter<BaseMessage>. Next, let's see how SubTypeAdapterWrapper is implemented

class SubTypeAdapterWrapper<T, R : T>(private val clazz: Class<R>,
          private val adapter: TypeAdapter<R>
) : TypeAdapter<T>() {
 override fun write(out: JsonWriter, value: T) {
  if (!(value)) {
   throw JsonSyntaxException("Expected a " +  + " but was " + value)
  }
  (out, value as R)
 }

 override fun read(`in`: JsonReader): T {
  return (`in`)
 }
}

It's actually a packaging category. Wrap a TypeAdapter parsing R type into a TypeAdapter parsing T type.

Finally, implement a TypeAdapterFactory and register it with Gson

class BaseMessageTypeAdapterFactory : TypeAdapterFactory {
 override fun <T : Any?> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T>? {
  if (!BaseMessage::()) {
   return null
  }
  return BaseMessageTypeAdapter(gson, this) as (TypeAdapter<T>)
 }
}

Write a test case to test it

Abstract encapsulation

In order to better reuse, we will remodel BaseMessageTypeAdapter next.

Define a new TypeAdapter subclass. The relationship between type and TypeAdapter is stored in Map and provides methods to external calls.

public class HeterogeneousTypeAdapter<T> extends TypeAdapter<T> {
 private final Gson mGson;
 private final TypeAdapterFactory mSkipPast;
 private final String mFieldName;
 private final Map<String, TypeAdapter<T>> mClassToAdapterMap = new HashMap<>();
 private final Map<String, TypeAdapter<T>> mFieldToAdapterMap = new HashMap<>();

 public HeterogeneousTypeAdapter(Gson gson, TypeAdapterFactory skipPast, String fieldName) {
  mGson = gson;
  mSkipPast = skipPast;
  mFieldName = fieldName;
 }

 public <R extends T> void addSubTypeAdapter(final String fieldValue,
            final Class<R> cls) {
  final TypeAdapter<R> typeAdapter = (mSkipPast, (cls));
  addSubTypeAdapter(fieldValue, cls, typeAdapter);
 }

 public <R extends T> void addSubTypeAdapter(final String fieldValue,
            final Class<R> cls,
            final TypeAdapter<R> typeAdapter) {
  final TypeAdapter<T> adapter = new SubTypeAdapterWrapper<>(cls, typeAdapter);
  ((), adapter);
  (fieldValue, adapter);
 }

 @Override
 public void write(JsonWriter out, T value) throws IOException {
  if (value == null) {
   ();
   return;
  }
  getTypeAdapterByClass(()).write(out, value);
 }

 @Override
 public T read(JsonReader in) throws IOException {
  if (() == ) {
   ();
   return null;
  }

  final JsonObject jsonObject = (in).getAsJsonObject();
  final JsonElement fieldElement = (mFieldName);
  if (fieldElement == null || ()) {
   throw new JsonSyntaxException("Field " + mFieldName + " is null or not found");
  }

  final String field = ().getAsString();
  final TypeAdapter<T> adapter = getTypeAdapterByField(field);
  if (adapter == null) {
   // Unknown field, just skip
   return null;
  }
  return (jsonObject);
 }

 private TypeAdapter<T> getTypeAdapterByClass(Class<?> cls) {
  TypeAdapter<T> adapter = (());
  if (adapter == null) {
   throw new JsonParseException("Unknown class : " + cls);
  }
  return adapter;
 }

 private TypeAdapter<T> getTypeAdapterByField(String field) {
  return (field);
 }
}

How to use

class BaseMessageTypeAdapterFactory : TypeAdapterFactory {
 override fun &lt;T : Any?&gt; create(gson: Gson, type: TypeToken&lt;T&gt;): TypeAdapter&lt;T&gt;? {
  if (!BaseMessage::()) {
   return null
  }
  val adapter = HeterogeneousTypeAdapter&lt;BaseMessage&gt;(gson, this, "msgType")
  // Register various types  ("text", TextMessage::)
  ("image", ImageMessage::)
  ("record", RecordMessage::)
  return adapter as (TypeAdapter&lt;T&gt;)
 }
}

Summarize

Through custom TypeAdapter, we implement the function of parsing heterogeneous lists. Avoid the tedious work of manual analysis and avoid unnecessary errors.

The above is the detailed content of how Android parses heterogeneous lists. For more information about Android parsing heterogeneous lists, please follow my other related articles!