I N T O U C H

Caricamento

Via Mario Giuntini, 29 - 56021 Navacchio (PI) info@intouch-srl.com

Android: Kotlin 10 Extension Functions Che Ripuliranno Davvero il Tuo Codice

Nel panorama dello sviluppo Android moderno, Kotlin è diventato lo standard per creare applicazioni robuste, leggibili e scalabili. Una delle sue funzionalità più potenti sono le extension functions, strumenti che permettono di aggiungere comportamenti a classi preesistenti senza modificarne l’implementazione.

Molti team non sfruttano appieno questo meccanismo, perdendo l’opportunità di ridurre drasticamente il boilerplate, centralizzare comportamenti comuni e rendere il codice più espressivo. Le estensioni non sono una scorciatoia: rappresentano un modo per creare un vero e proprio linguaggio interno al progetto, condiviso tra sviluppatori e facile da mantenere.

In questo articolo vedremo 10 extension functions realmente utili, tratte da casi d’uso frequenti, insieme a un capitolo avanzato che mostra come applicarle in una struttura architetturale moderna composta da ViewModel, Repository e Networking.

1. Visibilità delle View più leggibile

Gestire la visibilità con View.VISIBLE o View.GONE genera ripetizioni che appesantiscono il codice. Queste estensioni rendono il tutto più espressivo:

fun View.show() { visibility = View.VISIBLE }
fun View.hide() { visibility = View.GONE }
fun View.invisible() { visibility = View.INVISIBLE }

E l’uso diventa più naturale:

loader.show()
button.hide()

Tre funzioni semplici che migliorano sensibilmente la leggibilità della UI.


2. Click listener compatti e chiari

Un listener tradizionale richiede spesso più righe. Una piccola estensione semplifica tutto:

fun View.onClick(action: () -> Unit) =
    setOnClickListener { action() }

E il codice chiamante appare più ordinato:

submitButton.onClick { viewModel.submit() }

3. Conversione tra dp e px senza ripetere calcoli

Queste estensioni eliminano duplicazioni comuni in tutta l’app:

val Int.dp: Int
get() = (this * Resources.getSystem().displayMetrics.density).toInt()
val Int.px: Int
    get() = (this / Resources.getSystem().displayMetrics.density).toInt()

Particolarmente utili nelle custom view e nelle animazioni.


4. Consumo degli eventi nei listener

Molti listener richiedono un Boolean di ritorno che determina il comportamento dell’evento. Una estensione può rendere più chiaro il flusso:

inline fun <T> T.consume(block: T.() -> Unit): Boolean {
   block()
    return true
}

E il suo utilizzo:

view.setOnTouchListener { _, _ ->
    consume {
        // logica del touch
    }
}


5. Inflazione dei layout nei ViewHolder più pulita

Un adapter può risultare molto più leggibile se si aggiunge questa estensione:

fun ViewGroup.inflate(@LayoutRes res: Int): View =
    LayoutInflater.from(context).inflate(res, this, false)

Il ViewHolder si semplifica:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
     
MyViewHolder(parent.inflate(R.layout.item_row))


6. Recupero sicuro degli extra da Intent

Molti cast possono essere eliminati:

inline fun <reified T> Intent.getExtraSafe(key: String): T? =
  
extras?.get(key) as? T

Chiaro, sicuro e leggibile.


7. Gestione errori nei form con TextInputLayout

Impostare errori ripetendo sempre la stessa logica non è necessario:

fun TextInputLayout.showError(message: String?) {
    
    error = message

    isErrorEnabled = message != null

}

E l’uso diventa naturale:

emailInput.showError("Email non valida")


8. Funzioni semantiche per le liste

A volte è utile aggiungere comportamenti descrittivi:

fun <T> List<T>.ifNotEmpty(block: (List<T>) -> Unit) {

   if (isNotEmpty()) block(this)

}

Adatta a casi dove presenza o assenza di dati cambia il flusso.


9. Collezione sicura dei Flow in fragment o activity

Una delle estensioni più utili per evitare crash legati al lifecycle:

fun <T> Flow<T>.collectWhenStarted(
  
    owner: LifecycleOwner,

    collector: suspend (T) -> Unit

) {

    owner.lifecycleScope.launch {

        owner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {

            collect { collector(it) }

        }

    }

}

Esempio:

viewModel.uiState.collectWhenStarted(viewLifecycleOwner) { render(it) }


10. Un logger coerente per tutti i layer

Questa estensione centralizza il formato dei log:

inline fun <reified T> T.log(message: String) {

Log.d(T::class.java.simpleName, message)

}

``

Più chiaro e uniforme.

Applicare le Extension Functions a ViewModel, Repository e Networking

Le estensioni esprimono tutta la loro potenza quando vengono integrate in una Clean Architecture moderna. Possono infatti:

    • standardizzare la gestione degli errori

    • semplificare l’uso dei Flow tra ViewModel e UI

    • ridurre boilerplate nelle chiamate Retrofit

    • migliorare la leggibilità dei repository

    • rendere più chiara la conversione tra DTO e model di dominio

Vediamo alcuni esempi.


1. Una sealed class Result centralizzata

Per gestione coerente degli stati:

sealed class Result<out T> {
   
    data class Success<T>(val data: T) : Result<T>()

    data class Error(val throwable: Throwable) : Result<Nothing>()

    object Loading : Result<Nothing>()

}

2. Estensione per gestire il Result nel ViewModel

Molto utile per aggiornare lo UI State:

inline fun <T> Result<T>.onResult(

    success: (T) -> Unit = {},

    error: (Throwable) -> Unit = {},

    loading: () -> Unit = {}

) {

    when (this) {

        is Result.Success -> success(data)

        is Result.Error -> error(throwable)

        is Result.Loading -> loading()

    }

}

Esempio d’uso:

repository.getUser().onResult(
 
    success = { user -> _uiState.value = UiState(user = user) },

    error = { _uiState.value = UiState(error = it) },

    loading = { _uiState.value = UiState(isLoading = true) }

)

3. safeCall per le chiamate Retrofit

Una sola estensione evita try/catch sparsi:

suspend fun <T> safeCall(

   call: suspend () -> Response<T>

): Result<T> {

    return try {

        val response = call()

        if (response.isSuccessful && response.body() != null) {

            Result.Success(response.body()!!)

        } else {

            Result.Error(Exception(response.errorBody()?.string()))

        }

    } catch (e: Exception) {

        Result.Error(e)

    }

}

Repository molto più pulito:


class UserRepository(private val api: UserApi) 
    
     suspend fun getUser(): Result<User> =

        safeCall { api.getUser() }

}

4. Estensione sugli interceptor per header dinamici


fun OkHttpClient.Builder.addAuthHeader(tokenProvider: () -> String): OkHttpClient.Builder {

     addInterceptor { chain ->

        val newRequest = chain.request().newBuilder()

            .addHeader("Authorization", "Bearer ${tokenProvider()}")

            .build()
        chain.proceed(newRequest)
    }
    return this
}

5. DTO to Domain con estensioni e map

fun UserDto.toDomain(): User =
       User(id, name, email)

Abbinato a:

inline fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> =
        when (this) {
        is Result.Success -> Result.Success(transform(data))
        else -> this
    }

Repository:

suspend fun getUser(): Result<User> =
safeCall { api.getUser() }.map { it.toDomain() }

6. Validazione dei dati backend

fun String?.orUnknown(default: String = "N/A"): String =
    
   this?.takeIf { it.isNotBlank() } ?: default

Applicata in:

fun UserDto.toDomain() = User(
id = id,
    name = name.orUnknown(),
    email = email.orUnknown()
)

Lascia un commento