Kotlin delegates nello sviluppo Android –  Parte 1

Google I/O 2017 è stato un punto di svolta importante nel mondo Android: Kotlin è diventato un linguaggio ufficialmente supportato per lo sviluppo delle app! Da dopo l’annuncio l’interesse verso questo linguaggio è cresciuto molto, ovviamente ci sono degli ottimi motivi per cui Google ha scelto di puntare su Kotlin. Grazie a data classes, properties, extension function e delegates una classe Kotlin è solitamente più piccola e più facile da leggere rispetto all’equivalente classe Java. In questo post vedremo come usare le delegate properties di Kotlin per semplificare alcuni costrutti tipici di Android. Se non siete già familiari con questo argomento potete dare un’occhiata alla documentazione ufficiale o questo post.

Lazy delegate

La Kotlin standard library contiene alcuni delegate molto utili, per esempio usando la function lazy è possibile creare una property inizializzata solo quando viene utilizzata la prima volta. Questo delegate può essere utile per inizializzare una property gestita usando Dagger:

private val navigationController by lazy { 
  (applicationContext as GithubApp).component.navigationController() 
}

Usando Dagger nel modo standard non c’è bisogno di un costrutto simile perchè la property verrebbe popolata grazie a una invocazione di un metodo inject di un component. In un progetto di demo disponibile su GitHub ho provato a utilizzare Dagger in un modo semplificato rispetto al solito; le dipendenze di Activity e Fragment sono popolate manualmente usando il component di Dagger.
Il codice del precedente esempio può essere semplificato definendo due extension properties per accedere al component di Dagger partendo da un Context o da un Fragment:

val Context.component: AppComponent
    get() = (applicationContext as GithubApp).component

val Fragment.component: AppComponent
    get() = activity.component

Sfruttando queste due definizioni la property può essere riscritta in un modo ancora più semplice e leggibile:

private val navigationController by lazy {
    component.navigationController() 
}

Se una classe contiene pochi field popolati con Dagger (e se ne contiene molti probabilmente ha troppe responsabilità!) è possibile popolarli usando un lazy delegate, non è necessario definire e invocare il metodo inject, la proprietà non ha bisogno di annotation (e quindi niente annotation processing su quella classe) e può essere dichiarata private e senza usare lateinit.

Map delegate

Un altro delegate definito nella Kotlin standard library è il map delegate: permette di usare una mappa chiave-valori come se fosse una classe fortemente tipata. A volte è necessario gestire una mappa con un insieme predefinito di chiavi, per esempio usando Firebase Cloud Messaging i parametri inviati dal server sono disponibili in una mappa:

class MyMessagingService : FirebaseMessagingService() {
    
    override fun onMessageReceived(message: RemoteMessage?) {
        super.onMessageReceived(message)
        val data = (message?.data ?: emptyMap()).withDefault { "" }
        
        val title = data["title"]
        val content = data["content"]
        
        print("$title $content")
    }
}

Definendo le chiavi come stringhe è facile sbagliare, il compilatore non ci aiuta. Lo so che è possibile definire una costante da qualche parte ma definire campi static final (o qualcosa in un object di Kotlin) è molto Java 1.4 style!
E’ possibile migliorare questo codice usando una classe con due proprietà gestite usando un map delegate:

class NotificationParams(val map: Map<String, String>) {
    val title: String by map
    val content: String by map
}

Il codice del precedente esempio può essere riscritto usando questa classe, ci sarà un errore in compilazione nel caso di un errore nel nome del campo (che corrisponde alla chiave nella mappa):

override fun onMessageReceived(message: RemoteMessage?) {
    super.onMessageReceived(message)
    val data = (message?.data ?: emptyMap()).withDefault { "" }

    val params = NotificationParams(data)

    print("${params.title} ${params.content}")
}

Shared Preferences

Su GitHub ci sono già molte librerie che possono essere usate per semplificare l’utilizzo delle SharedPreferences in Kotlin. Nel prossimo esempio vediamo come un custom delegate può essere molto utile per scrivere una proprietà salvata nelle shared preference. Prima di tutto definiamo una extension function della classe SharedPreferences che definisce un delegate:

fun SharedPreferences.int(
    defaultValue: Int = 0,
    key: String? = null
): ReadWriteProperty<Any, Int> {
  return object : ReadWriteProperty<Any, Int> {
      override fun getValue(thisRef: Any, property: KProperty<*>) =
          getInt(key ?: property.name, defaultValue)

      override fun setValue(thisRef: Any, property: KProperty<*>, 
              value: Int) =
          edit().putInt(key ?: property.name, value).apply()
  }
}

Questo codice non è proprio banale da capire per chi non ha familiarità con i costrutti di Kotlin (e forse anche per chi li conosce bene!). Definisce un nuovo extension method int che può essere invocato su un oggetto di tipo SharedPreferences, questo metodo ritorna un oggetto ReadWriteProperty che definisce come la proprietà sarà letta e scritta. Ci sono due argomenti opzionali: il valore di default e la chiave usata per memorizzare il valore nelle shared preference (il nome della proprietà è usata se la chiave non è passata).
Usando questo metodo è possibile definire una classe che contiene una property connessa alle shared preference con poche righe di codice:

class MyClass(prefs: SharedPreferences) {
    var count by prefs.int()
}

Ogni volta che la proprietà è invocata il valore è letto (o scritto) dalle shared preferences. E’ possibile definire metodi simili per gli altri tipi, per evitare il copia incolla è possibile usare un metodo generico (lo so, questo metodo è ancora più complicato di quello del precedente esempio 🙁 ):

private inline fun <T> SharedPreferences.delegate(
    defaultValue: T, 
    key: String? = null,
    crossinline getter: SharedPreferences.(String, T) -> T,
    crossinline setter: Editor.(String, T) -> Editor
): ReadWriteProperty<Any, T> {
  return object : ReadWriteProperty<Any, T> {
    override fun getValue(thisRef: Any, property: KProperty<*>) =
        getter(key ?: property.name, defaultValue)

    override fun setValue(thisRef: Any, property: KProperty<*>, 
            value: T) =
        edit().setter(key ?: property.name, value).apply()
    }
}

I parametri aggiuntivi sono due funzioni (entrambe sono extension function per semplificare il modo in cui sono passate quando il delegate è invocato):

  • un getter per leggere il valore dalle shared preferences
  • un setter per scrivere il valore usando un Editor

Questa function è definita come inline per evitare un overhead runtime, usando questa keyword i parametri getter e setter non sono trasformati in classi interne nel bytecode (maggiori informazioni sono disponibili nella documentazione ufficiale).
Adesso i metodi per tutti i tipi gestibili con le shared preference possono essere scritti facilmente (sfruttando il fatto che in Kotlin anche i tipi primitivi possono essere gestiti con i generics):

fun SharedPreferences.int(def: Int = 0, key: String? = null) =
   delegate(def, key, SharedPreferences::getInt, Editor::putInt)

fun SharedPreferences.long(def: Long = 0, key: String? = null) =
   delegate(def, key, SharedPreferences::getLong, Editor::putLong)
//...

Questi delegate possono essere usati per scrivere una classe che memorizza un token e conta quante volte il token viene salvato (non è troppo utile nella realtà ma fa comodo vedere come implementarlo):

class TokenHolder(prefs: SharedPreferences) {
    var token by prefs.string()
        private set

    var count by prefs.int()
        private set

    fun saveToken(newToken: String) {
        token = newToken
        count++
    }
}

Quando count++ è invocato stiamo in realtà leggendo il valore dalle shared preference e salvando quel valore incrementato di uno, qualcosa di simile a questo (ma in una forma leggibile e compatta!):

prefs.edit().putInt("count", prefs.getInt("count", 0) + 1).apply()

SharedPreferences è una classe dell’Android SDK, usando questo delegate lo stiamo usando in molte classi, alcune di queste sono probabilmente classi della business logic dell’app. Molti sviluppatori cercano di mantenere queste classi Android-free per testarle usando un test su JVM. Usando questo delegate la classe TokenHolder è testabile su JVM anche se utilizza una classe dell’Android SDK. Possiamo scrivere un test su JVM usando una implementazione fake delle SharedPreferences che usa una map per memorizzare i valori:

@Test fun shouldCount() {
    val prefs = FakeSharedPreferences()
    val tokenHolder = TokenHolder(prefs)

    tokenHolder.saveToken("a")
    tokenHolder.saveToken("b")

    assertThat(tokenHolder.count).isEqualTo(2)
    assertThat(prefs.getInt("count", 0)).isEqualTo(2)
}

La versione Kotlin della classe FakeSharedPreferences è disponibile qui mentre la versione Java originale è contenuta nel repository di Calendar (59 contro 166 linee di code!).
E questo è tutto per la prima parte di questo post, nella seconda parte vedremo come i delegate di Kotlin possono semplificare l’utilizzo degli Architecture Components (se volete una anteprima potete dare un’occhiata a questa demo su GitHub).

Fabio Collini

Software Architect con esperienza su piattaforma J2EE e attualmente focalizzato principalmente in progetti di sviluppo di applicazioni Android. Attualmente sono in Nana Bianca dove mi occupo dello sviluppo di alcune app Android. Coautore della seconda edizione di Android Programmazione Avanzata e docente di corsi di sviluppo su piattaforma Android. Follow me on Twitter - LinkedIn profile