Kotlin delegates nello sviluppo Android –  Parte 2

Al Google I/O 2017 ci sono stati due importanti annunci per gli sviluppatori Android: il supporto ufficiale a Kotlin e gli Architecture Components. Nella seconda parte di questo post (la prima parte è disponibile qui) vedremo come usare i delegate di Kotlin per semplificare l’integrazione dei ViewModel e dei LiveData in una applicazione Android.

ViewModel delegate

Un ViewModel può essere creato usando una ViewModelProvider.Factory, una interfaccia che contiene un singolo metodo. Questo metodo crea (o riusa dopo un cambio di configurazione) una istanza di un ViewModel basandosi sulla classe passata come parametro. L’implementazione di default di questa interfaccia usa la reflection per creare una nuova istanza.
Quando il ViewModel è gestito usando Dagger è necessario definire una implementazione custom di questa interfaccia. E’ possibile usare una singola factory per creare tutti i ViewModel in un progetto, ma questa implementazione conterrà una serie di if per verificare la classe passata come parametro.
Una soluzione alternativa consiste nel creare una singola factory per ogni ViewModel, un metodo che trasforma una funzione generica in un ViewModelProvider.Factory può essere scritto facilmente:

inline fun  viewModelFactory(
    crossinline f: () -> VM) =
        object : ViewModelProvider.Factory {
            override fun  create(aClass: Class) = 
                f() as T
        }

Questo metodo può essere usato dentro un lazy delegate per creare un ViewModel usando una invocazione a un metodo di un component di Dagger (o a un campo popolato con un Provider):

private val viewModel by lazy {
    val factory = viewModelFactory { component.myViewModel() }
    ViewModelProviders.of(this, factory)
            .get(MyViewModel::class.java)
}

In questo esempio component è una extension function che può essere usata per recuperare il component di Dagger dalla Application, la definizione è disponibile nella prima parte di questo post. Usando questo metodo possiamo evitare la scrittura di una singola factory per tutti i ViewModel nel progetto. Ogni Activity (o Fragment) contiene la sua factory creata con il metodo viewModelfactory con una lambda come parametro usata per creare il ViewModel se necessario.
Il codice del precedente esempio può essere semplificato spostando la creazione della factory in un custom delegate che contiene un parametro con il metodo da invocare:

inline fun  Fragment.viewModelProvider(
        crossinline provider: () -> VM) = lazy {
    ViewModelProviders.of(this, object : ViewModelProvider.Factory {
        override fun  create(aClass: Class) =    
            provider() as T1
    }).get(VM::class.java)
}

L’utilizzo in una Activity (o un Fragment) è molto semplice, utilizza il viewModelProvider delegate passando una lambda con il metodo per creare una nuova istanza:

private val viewModel by viewModelProvider { 
    component.myViewModel() 
}

Il lazy delegate è thread safe di default, in questo esempio la property viewModel è usata sempre da un Activity/Fragment. Usando il pattern MVVM (o anche MVP) questi metodi sono solitamente callback del lifecycle o aggiornamenti della view, in entrambi i casi sono eseguiti sul main thread e quindi la sincronizzazione non è necessaria. Per evitare l’overhead è possibile aggiungere un parametro con il valore di default impostato a NONE:

inline fun  Fragment.viewModelProvider(
        mode: LazyThreadSafetyMode = NONE,
        crossinline provider: () -> VM) = lazy(mode) {
    ViewModelProviders.of(this, object : ViewModelProvider.Factory {
        override fun  create(aClass: Class) =    
            provider() as T1
    }).get(VM::class.java)
}

LiveData

Un altro architecture component che è usato spesso con i ViewModel è il LiveData, i dettagli su come utilizzarlo sono disponibili in scritto da Ian Lake e in molti altri post.
Un delegate può essere usato per semplificare l’utilizzo dei LiveData, è possibile scriverlo in poche righe di codice:

class LiveDataDelegate(
        initialState: T,
        private val liveData: MutableLiveData =
            MutableLiveData()
) {
    init {
        liveData.value = initialState
    }
    fun observe(owner: LifecycleOwner, observer: (T) -> Unit) =
            liveData.observe(owner, Observer { observer(it!!) })
    operator fun setValue(ref: Any, p: KProperty<*>, value: T) {
        liveData.value = value
    }
    operator fun getValue(ref: Any, p: KProperty<*>): T =
            liveData.value!!
}

Questo esempio non contiene un oggetto ReadWriteProperty ma solo due funzioni definite usando la keyword operator. Per evitare valori nulli nel LiveData il parametro T è definito come una sottoclasse di Any e il costruttore contiene un parametro con il valore di default non nullable.
Questo delegate è un po’ diverso rispetto a quelli visti fino ad adesso, è necessario invocare oltre ai consueti getter e setter anche un altro metodo (la UI invoca il metodo observe per registrarsi come listener). Il modo più semplice per risolvere questo problema consiste nell’utilizzo di due property nel ViewModel, uno creato usando il delegate e l’altro con l’oggetto LiveDataDelegate:

class MyViewModel : ViewModel() {
    val liveData = LiveDataDelegate(0)
    private var state by liveData
    fun increment() {
        state++
    }
}

Come nell’esempio delle SharedPreferences della prima parte di questo post stiamo invocando l’operatore ++, in realtà stiamo leggendo e aggiornando il valore dal LiveData.
Nell’Activity (o nel Fragment) è possibile osservare il valore invocando il metodo observe sulla property di tipo LiveDataDelegate:

viewModel.liveData.observe(this) {
    textView.text = it.toString()
}

In questo esempio la classe MyViewModel contiene un solo LiveData, è possibile eseguire un refactoring per semplificare il codice (e provare un’altra feature di Kotlin!).
Iniziamo creando una interfaccia partendo dalla classe LiveDataDelegate:

interface LiveDataObservable {
    fun observe(owner: LifecycleOwner, observer: (T) -> Unit)
}

Adesso è possibile usare la class delegation di Kotlin per definire una singola property nel ViewModel, questa classe implementa l’interfaccia LiveDataObservable in modo che dall’esterno sia possibile invocare il metodo observe direttamente sul ViewModel:

class MyViewModel(
        liveData: LiveDataDelegate = LiveDataDelegate(0)
) : ViewModel(), LiveDataObservable by liveData {
    private var state by liveData
    fun increment() {
        state++
    }
}

C’è un altro modo per evitare di definire due property, è definito in questa domanda su StackOverflow. Tuttavia viene utilizzata la reflection e, per questo motivo, deve essere aggiunta anche la libreria Kotlin reflect al progetto.

Conclusioni

Usando i due delegate di questo post è possibile scrivere una Activity con un TextView connesso al valore di un LiveData. Cliccando sulla TextView il valore è incrementato, la view è automaticamente aggiornata grazie al LiveData:

class DemoActivity : LifecycleActivity() {

    private val viewModel by viewModelProvider { 
        component.myViewModel() 
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val textView = TextView(this).apply {
            textSize = 80 * resources.displayMetrics.density
            gravity = CENTER
            setOnClickListener { viewModel.increment() }
        }

        setContentView(textView, 
            LayoutParams(MATCH_PARENT, MATCH_PARENT))

        viewModel.observe(this) {
            textView.text = it.toString()
        }
    }
}

Un progetto di demo con alcuni esempi di utilizzo di questi due delegate è disponibile su GitHub.
I delegate di Kotlin sono molto potenti e possono essere usati per semplificare lo sviluppo su Android. In questi due post abbiamo visto alcuni esempi, spero di trovarne altri per poter scrivere un altro post su questo argomento (se avete suggerimenti lasciate un commento! 🙂 ).

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 e docente di corsi di sviluppo su piattaforma Android. -