Android: zpracování JSON objektu - předělávka do Kotlinu

Po delší pauze jsem se opět pustil do vývoje aplikace Pobyty na Moravě. Než začnu přidávat nové funkce a vylepšovat ty stávající, rozhodl jsem se přepsat aplikaci do Kotlinu.

Android studio oficiálně podporuje Kotlin od verze 3.0, a navíc masivní nárůst používání Kotlinu mezi Android vývojáři nemohl uniknout mé pozornosti. Tomu nemohu nečinně přihlížet. Dobrým impulsem do začátku byl pro mě meetup, který pořádala GDG Brno.

Prvním pokusným králíkem budiž řešení pro zpracování JSON objektu, o kterém jsem už blogoval. Motivací k implementaci byla pro mě tenkrát opakující se potřeba kontrol na null spojená s odporem vůči copypastování. Při implementaci kontroly na nové příspěvky navíc nestahuji příspěvky celé, ale pouze jejich ID a název, čímž jsem zredukoval množství stažených dat z cca 2 kB na 150 bytů na příspěvek (o tom jsem ještě neblogoval, ale byla to dost dobrá škola Androidu, prakticky všechno, na co jsem sáhnul, bylo pro mě nové). To ale znamená, že se z null stala legální a očekávaná hodnota.

Při přepisu do Kotlinu jsem využil automatickou konverzi Java-Kotlin z Android studia včetně tipů na optimalizaci. S převedenými zdrojáky jsem pak experimentoval a zkoušel jsem jejich různé varianty, nejedná se tedy o tupou automatickou konverzi. Podívejme se, zda a jak předělávka do Kotlinu implementaci zjednoduší.

Převod JSONArray na seznam JSONObject

Původní implementace v Javě vypadala takto:

public static List<JSONObject> optJSONArray(JSONObject in, String key) {
    List<JSONObject> retval = new ArrayList<>();
    if(in != null) {
        JSONArray array = in.optJSONArray(key);
        if(array != null) {
            for(int i = 0; i < array.length(); i++) {
                JSONObject object = array.optJSONObject(i);
                if(object != null) {
                    retval.add(object);
                }
            }
        }
    }
    return retval;
}

Experimentováním jsem objevil dvě varianty, jak totéž implementovat v Kotlinu. Protože in je v Kotlinu klíčové slovo, přejmenoval jsem vstupní proměnnou na src. První varianta implementace je delší a provádí kontrolu na null před vytvořením výsledného seznamu.

fun optJSONArray(src: JSONObject?, key: String): List<JSONObject> {
    val array = src?.optJSONArray(key)
    return if(array != null) {
        (0 until array.length())
                .mapNotNull({ array.optJSONObject(it) })
    } else {
        emptyList()
    }
}

Druhá a kratší varianta pak přesouvá kontrolu na null přímo do vytváření výsledného seznamu.

fun optJSONArray(src: JSONObject?, key: String): List<JSONObject> {
    val array = src?.optJSONArray(key)
    return (0 until (array?.length() ?: 0))
            .mapNotNull({ array?.optJSONObject(it) })
}

V obou případech došlo předěláním do Kotlinu k výraznému zkrácení a zpřehlednění zdrojáku. Já za sebe však dávám přednost první variantě. Přestože je delší, připadne mi čitelnější a zřejmě bude i efektivnější.

Určitě namítnete, že i implementace v Javě může pro vytvoření výslednoho seznamu použít stream. To je sice pravda, ale aplikaci jsem začal vyvíjet pro Android od verze 4.0 (API level 14). Plnou podporu Java 8 zavádí až Android 7.0 (API level 24).

Aktualizace 18.5.2018: metodu jsem nakonec implementoval jako extension function pro třídu JSONObject. Její název jsem pozměnil, aby nedošlo ke kolizi se standardní metodou optJSONArray() s parametrem typu String.

fun JSONObject?.optJSONArrayToList(key: String): List {
    val array = this?.optJSONArray(key)
    return if(array != null) {
        (0 until array.length())
                .mapNotNull({ array.optJSONObject(it) })
    } else {
        emptyList()
    }
}

Získávání hodnot vnořených objektů

Původní implementace v Javě vypadala takto:

public static String optString(JSONObject in, String... keys) {
    if(in == null || keys.length == 0) {
        return null;
    }
    if(keys.length == 1) {
        return in.optString(keys[0]);
    }
    return optString(in.optJSONObject(keys[0]), Arrays.copyOfRange(keys, 1, keys.length));
}

Pokud bych měl zachovat metodu 1:1 v podobě, v jaké je, přepisem do Kotlinu neušetřím (ale jako vedlejší efekt se naučím, jak v Kotlinu používat varargs):

fun optString(src: JSONObject?, vararg keys: String): String? {
    if (src == null || keys.isEmpty()) {
        return null
    }
    return if (keys.size == 1) {
        src.optString(keys[0])
    } else {
        optString(src.optJSONObject(keys[0]), *keys.sliceArray(1 until keys.size))
    }
}

Pojďme ale vyjít z toho, co bylo původním smyslem implementace této metody. Implementace získávání hodnot vnořených objektů vypadá zhruba takto:

String img = null;
if(post != null) {
    JSONObject thumb = post.optJSONObject("thumbnail_images");
    if(thumb != null) {
        JSONObject full = thumb.optJSONObject("full");
        if(full != null) {
            img = full.optString("url");
        }
    }
}

To se mi ovšem nechce copypastovat, nehledě na to, že implementaci nepovažuji za nejpřehlednější. Chtěl jsem ji proto zkrátit a zjednodušit, aby se dala použít takto:

String img = JSONTools.optString(post, "thumbnail_images", "full", "url");

V Kotlinu ovšem takto zdlouhavá kaskáda vnořených if není potřebná. Opakovaná kontrola na null se dá implementovat podstatně jednodušeji:

val img = post?.optJSONObject("thumbnail_images")?.optJSONObject("full")?.optString("url")

A protože v Kotlinu nemáme rádi null:

val img = post?.optJSONObject("thumbnail_images")?.optJSONObject("full")?.optString("url") ?: ""

Nutnost speciální implementace získávání hodnot z vnořených objektů úplně vymizí. Devět řádků zredukovaných na nulu. Neberte to.

Aktualizace 18.5.2018: finální implementace vychází navíc z toho, že parametr post nemůže nabýt hodnotu null. Domněnka o redukci na nula řádků se potvrdila.

val img = post.optJSONObject("thumbnail_images")?.optJSONObject("full")?.optString("url") ?: ""

Tagy: Android, Pobyty na Moravě, Java, Programování, Kotlin

Tento web bude tebe a tvůj počítač krmit piškotkami, jelikož a protože je to slušný web a jako takový ví, že je potřeba návštěvu řádně pohostit, aby se u nás cítila dobře. Užíváním tohoto webu potvrzuješ, že netrpíš mentální anorexií, nedržíš žádnou obskurní dietu a že můžeš piškotki do sebe cpát kdykoli a v jakémkoli množství. Více informací...