paint-brush
Specific Problems with UMA Requests on Androidby@faridebagirova

Specific Problems with UMA Requests on Android

by Faride BagirovaNovember 5th, 2024
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

In this specific issue, I wan to discuss such specific issues with AMA requests on Android as: Handling arrays via a decoder. Dynamically extendable data in a response. Types mismatch issues with the AMA response system. I hope this helps you with your questions.
featured image - Specific Problems with UMA Requests on Android
Faride Bagirova HackerNoon profile picture


In this text, I want to discuss such specific issues with AMA requests on Android as:


  • Handling arrays via a decoder
  • Dynamically extendable data in a response
  • Types mismatch issues

Handling arrays via a decoder

Let's suppose we have to get a list of users where our presentation layer model will look like this:


data class UserItemModel(
   val id: Int,
   val fullName: String,
   val phone: List<String>,
   val telegram: List<String>,
   val email: List<String>
)


UMA will send us a response:


{
 "field_name": "id",
 "schema": [
   "users_table",
   "id"
 ],
 "type": "integer"
},
{
 "field_name": "fullName",
 "schema": [
   "users_table",
   "fullName"
 ],
 "type": "string"
},
{
 "field_name": "array_agg",
 "schema": [
   "users_table",
   "array_agg"
 ],
 "type": "ARRAY"
},
{
 "field_name": "array_agg",
 "schema": [
   "users_table",
   "array_agg"
 ],
 "type": "ARRAY"
},
{
 "field_name": "array_agg",
 "schema": [
   "emails",
   "array_agg"
 ],
 "type": "ARRAY"
}


Now let's remember what our basic decoder function looks like:


inline fun <reified T> Result.toDecoded(): List<T> {
    val list = arrayListOf<T>()

    if (data != null) {
        for (i in data) {
         val templateResponse = 
            T::class.constructors.find { it.parameters.isEmpty() }?.call()

            meta?.forEachIndexed { index, meta ->
                val fieldValue = i[index]
                val field: Field = T::class.java.getDeclaredField(meta.field_name)
                field.isAccessible = true
                field.set(templateResponse, fieldValue)
            }
            templateResponse?.let { list.add(it) }
        }
    }
    return list
}


As you might have noticed, we may receive fields with identical field names "field_name" : "array_agg" and even totally identical meta as we have for phone and telegram:


{
 "field_name": "array_agg",
 "schema": [
   "users_table",
   "array_agg"
 ],
 "type": "ARRAY"
},


However, in our decoder, we parse data by the field name, supposing that it is unique. So we need to handle arrays by the schema name in the email case and by data value check in the phone and telegram case.


First of all, let's add some constants:


const val ARRAY_AGG = "array_agg"
const val USERS_TABLE = "users_table"
const val EMAILS = "emails"
const val TELEGRAM = "telegram"
const val PHONE = "phone"


Where we will use the EMAILS, TELEGRAM and PHONE values in our response class like this:


data class UserResponse(
   var id: Int?,
   var fullName: String,
   var phone: List<String>?,
   var telegram: List<String>?,
   var email: List<String>?
)


Now, instead of just:


val field: Field = T::class.java.getDeclaredField(meta.field_name)


We will add a when statement and add conditions:


val field: Field =
   if (meta.field_name == ARRAY_AGG) {
       when (meta.schema[0]) {
           EMAILS -> T::class.java.getDeclaredField(EMAILS)
           USERS_TABLE -> {
               val formattedValue = 
                   value.toString().replace("[\\[\\],\\s+]".toRegex(), "")
               if (formattedValue.isDigitsOnly()) T::class.java.getDeclaredField(PHONE)
               else T::class.java.getDeclaredField(TELEGRAM)
           }
           else -> T::class.java.getDeclaredField(meta.field_name)
       }
   } else { 
       T::class.java.getDeclaredField(meta.field_name)
}


Here, we check if the meta's field name is array_agg. If so, we check the first schema’s value. If it's an email, find the same name in the response class.


For phone and telegram we have identical meta values. So the question is how to distinguish between them.

  1. We know the order. The third field in the response will be the phone number and the fourth telegram, but we cannot check the order since the situation may be the same, but the response may have a different order and number of fields.
  2. Since we have to distinguish only 2 fields like phone and telegram, we can do it by checking if one of them consists of only digits.


So our next step will be checking if the value equals users_table, then if the data value under this meta consists only of digits. If so, we will search for the PHONE field differently than for TELEGRAM.

Dynamically extendable data in response

Imagine we have a list of websites with the number of users that were online on certain days (the range we specify in the request, let's say 8.10.2024 - 11.10.2024, so it will return 3 days)

And we get the following response:


{
   "result": {
       "data": [
           [
               "facebook",
               456,
               280,
               653,
               942
           ],
           [
               "pinterest",
               556,
               299,
               342,
               673
           ],
       ],
       "meta": [
           {
               "field_name": "website_name",
               "schema": [
               "online_data_tbl",
               "project_name"
               ],
               "type": "straing"
           },
           {
               "field_name": "day_1",
               "schema": [
               "online_data_tbl",
               "day_1"
               ],
               "type": "int"
           },
           {
               "field_name": "day_2",
               "schema": [
               "online_data_tbl",
               "day_2"
               ],
               "type": "int"
           },
           {
               "field_name": "day_3",
               "schema": [
               "online_data_tbl",
               "day_3"
               ],
               "type": "int"
           },
           {
               "field_name": "total_users",
               "schema": [
               "online_data_tbl",
               "total_users"
               ],
               "type": "int"
           },
       ]
   }
}


You can see that we have a separate field for each day containing an int value, not a list of ints. To handle this type of response, we must first of all create a response class:


class UsersOnlineResponse {
   var website_name: String?
   var total_users: Int?
   var daysList: MutableList<String>?

   /**
    * We need empty constructor for mapping the response on Result .toDecoded()
    */
   constructor() : this("", 0, mutableListOf())

   constructor(
       website_name: String?,
       total_users: Int?,
       daysList: MutableList<String>
   ) {
       this.website_name = website_name
       this.total_users = total_users
       this.daysList = daysList
   }

   /**
    * addDayElement() called on Result .toDecoded()
    */
   fun addDayElement(element: String) {
       daysList?.add(element)
   }
}


We will wait for daysList as for the usual list, but we added the addDayElement function, which extends the list. This function will be called in our decoder. But first, let's add some constants:


const val DAY_ELEMENT = "day_"
const val ADD_DAY_ELEMENT_FUNC_NAME = "addDayElement"


ADD_DAY_ELEMENT_FUNC_NAME in this field we have to store the exact name of our addDayElement function.

In the .toDecoded() function we will add a check for DAY_ELEMENT


inline fun <reified T> Result.toDecoded(): List<T> {
   val list = arrayListOf<T>()

   if (data != null) {
       for (i in data) {
           val templateResponse = T::class.constructors.find { it.parameters.isEmpty() }?.call()
          meta?.forEachIndexed { index, meta ->
               val fieldValue = i[index]
               val field: Field =
                   if (meta.field_name == ARRAY_AGG) {
                       when (meta.schema[0]) {
                           EMAILS -> T::class.java.getDeclaredField(EMAILS)
                           USERS_TABLE -> {
                               val formattedValue =
                                   value.toString().replace("[\\[\\],\\s+]".toRegex(), "")
                               if (formattedValue.isDigitsOnly()) T::class.java.getDeclaredField(
                                   PHONE
                               )
                               else T::class.java.getDeclaredField(TELEGRAM)
                           }

                           else -> T::class.java.getDeclaredField(meta.field_name)
                       }
                   } else {
                       T::class.java.getDeclaredField(meta.field_name)
                   }

               field.isAccessible = true
               field.set(templateResponse, fieldValue)

               if (meta.field_name.startsWith(DAY_ELEMENT)) {
                   val method = T::class.java.getDeclaredMethod(
                       ADD_DAY_ELEMENT_FUNC_NAME,
                       String::class.java
                   )
                   method.invoke(templateResponse, fieldValue)
               }
           }
           templateResponse?.let { list.add(it) }
       }
   }
   return list
}


This is what our final decoder function looks like.

Types mismatch issues

In UMA responses, we often get improper types. For example, meta says we receive an integer, but we may get double inside, and if in our response class we expect an int but get a float, the value will not be parsed. For enum, we'll receive string values and as for strings, the meta may say that it's a string and a text type. To handle this situation, we’ll add a type check to the decoder. Let's replace our val fieldValue = i[index] with this:


// loop and constructor code

meta?.forEachIndexed { index, meta ->
   val value = i[index]
   val fieldValue = when (meta.type) {
       "boolean" -> {
           value as Boolean
       }

       "integer" -> {
           if (value is Double)
               value.toInt()
           else value
       }

       "string", "text" -> {
           value as String?
       }

       "enum" -> {
           value as String?
       }

       else -> {
           value
       }
   }

   // rest of the code related to field


In this article, we looked at the main issues you might encounter when working with UMA. In future articles, we'll discuss how to work with pagination, sorting, and how to build your own request using table joins.