루피도 코딩한다

(Part 1) Server-Driven UI: Android with Dynamic Views 본문

Android

(Part 1) Server-Driven UI: Android with Dynamic Views

xiaolin219 2023. 11. 9. 21:50

📌 Backgrounds

Hi! I'm a android developer at'LGTM', and our team wanted to implement A/B testing on the Home screen. Unlike web services, we can't force Android app users to update simultaneously. So, we decided to leverage the 'Server-Driven UI' (SDUI) technique.

Today, I'd like to share how I applied the 'SDUI' approach to our project. Let's dive in!

1️⃣ Defined ViewTypes and Properties

To dynamically rearrange UI components, we had to define the necessary view types. This allowed us to match the prepared UI components on the client side with the API response. Here's the list we used:

  1. SectionTitle
  2. SectionItem
  3. SectionCloser
  4. SectionEmpty
  5. Unknown (In preparation for potential errors)

You might find it easier to understand when you refer to this image:

2️⃣ Specify the Required ViewType in the Enum Class

  • Remember to make necessary edits if new view types are added later.
  • Before introducing the ViewType enum class, developers need to define corresponding data classes required for each view type as like SectionTitleVO, SectionEmptyVO etc.
enum class SduiViewType(
    val viewType: String, private val viewTypeClassType: Type
) {
    TITLE("sectionTitle", SectionTitleVO::class.java),
    EMPTY("empty", SectionEmptyVO::class.java),
    CLOSER("sectionCloser", SectionCloserVO::class.java),
    ITEM("sectionItem", SectionItemVO::class.java),
    UNKNOWN("sectionUnknown", SectionUnknownVO::class.java);

    companion object {
        fun getViewTypeByOrdinal(ordinalNum: Int): SduiViewType {
            return values()[ordinalNum]
        }

        fun SduiViewType.getViewTypeClassType(): Type {
            return this.viewTypeClassType
        }

        fun findClassByItsName(viewTypeString: String?): SduiViewType {
            return values().find { it.viewType == viewTypeString } ?: UNKNOWN
        }
    }
}

3️⃣ Preparing the Response Class to Receive SDUI api

data class SduiVO(
    val sceneName: String, val contents: List<SduiItemVO>
)

data class SduiItemVO(
    val viewType: SduiViewType,
    val theme: SduiTheme,
    val content: SduiContent
)

interface SduiContent
  • Note that the sceneName isn't crucial on the Android side. Our team used it to double-check that server's response matches each screen (activity or fragment).
  • Within the Content structure, each SduiItemVO includes the view type, theme, and content.
  • It's worth mentioning that all these classes are located in the domain module, as this information is needed not only in the Presentation Layer but also in the Data Layer.

4️⃣ Delving Deeper into the SduiContent Interface

  • Can you guess why I chose SduiContent as an interface type?
  • Usually, we create data classes to facilitate JSON deserialization from the backend.
  • However, in the case of SDUI (Server Driven UI), we remain uncertain about the necessary data class until the JSON reaches the client level. Think of it as a runtime decision.
  • There could be required data classes for each already decided view type, such as SectionTitle, SectionItem, etc. Once these classes like SectionTitle and SectionItem are created, they inherit from SduiContent.
  • This approach enables us to deserialize runtime-determined classes in real-time! 🎉🎉
  • This interface serves as a beacon in the data module. Moving forward, let's explore the classes within the data module.

5️⃣ Set Custom Deserializer on Network Connection

  • Can you guess what we have to handle when applying SDUI?
  • It's a custom deserializer, especially if you're using the 'GSON' library.
image-20230823175408042
  • I'm using the GSON library, well-known among Android developers.

  • Gson doesn't support deserializing all classes.

  • As we used an interface in our response data class, deserialization might not work correctly.

  • So, we're going to handle GsonConverterFactory to let it know how to parse the interface into the proper data class.

  • let's delve deeper into this by exploring some real code below.

SduiViewTypeDeserializer

  • My class is structured like the code below.
  • First, we extract the viewTypeName and theme string from the JSON.
  • Next, we find the detailed data class that matches the viewTypeName just before this step.
    • At this stage, we utilize a function named findClassByItsName() defined in the SduiViewType enum class.
    • As my project uses a multi-module and clean architecture, I can easily use the SduiViewType enum class located in the domain module on the app module, which contains SduiViewTypeDeserializer.
  • Finally, reconstruct SduiItemVO with using viewType, theme, and detailed data class inheriting the SduiContent interface.
class SduiViewTypeDeserializer : JsonDeserializer<SduiItemVO> {
    @Throws(JsonParseException::class)
    override fun deserialize(
        json: JsonElement?, typeOfT: Type, context: JsonDeserializationContext
    ): SduiItemVO {
        val jsonObject = json?.asJsonObject ?: throw IllegalArgumentException("Json Parsing 실패")
        val viewTypeString: String = jsonObject.get("viewTypeName").asString
        val viewType: SduiViewType = SduiViewType.findClassByItsName(viewTypeString)
        val themeString: String = jsonObject.get("theme").asString
        val theme: SduiTheme = SduiTheme.findClassByItsName(themeString)
        val decidedViewType = viewType.getViewTypeClassType()
        val content = jsonObject.get("content").asJsonObject
        val sduiContent: SduiContent = Gson().fromJson(content, decidedViewType)
        return SduiItemVO(viewType, theme, sduiContent)
    }
}

Great! If you've followed those steps carefully, you should now be able to receive a proper API response on the client side.

Now, it's time to dive into the UI part in our next post. See you there!

Comments