Modularizzazione Android nel 2026: Architettura Multi-Modulo e Domande da Colloquio

Best practice per la modularizzazione Android con convention plugin, Gradle version catalog e feature module. Include domande da colloquio per sviluppatori senior.

Modularizzazione Android nel 2026: Architettura Multi-Modulo e Domande da Colloquio

La modularizzazione Android trasforma codebase monolitiche in sistemi manutenibili e scalabili dove i team possono lavorare in parallelo senza interferire con il codice degli altri. Un'app Android ben modularizzata compila più velocemente, permette test isolati e consente ai nuovi sviluppatori di diventare produttivi in giorni anziché settimane.

Modularizzazione in Sintesi

La modularizzazione divide un'app Android in moduli Gradle indipendenti con confini ben definiti. Basso accoppiamento tra moduli e alta coesione all'interno di ciascun modulo sono i principi guida. L'app di riferimento Now in Android di Google dimostra questo pattern con oltre 40 moduli.

Perché l'Architettura Multi-Modulo è Importante per le App Android

Un'app Android con un singolo modulo ricompila tutto quando cambia una sola riga. Una modifica al layer di rete innesca la ricompilazione dell'UI. Due sviluppatori che modificano feature diverse creano conflitti di merge in file condivisi. I tempi di build si allungano oltre i 4 minuti per le build incrementali.

L'architettura multi-modulo affronta direttamente questi problemi:

  • Build più veloci: Gradle ricompila solo i moduli interessati. Una modifica in :feature:checkout lascia :feature:profile invariato.
  • Sviluppo parallelo: I team possiedono moduli separati con API definite. I conflitti di merge si riducono significativamente.
  • Test isolati: I test unitari vengono eseguiti su un singolo modulo senza caricare l'intera app.
  • Riutilizzo del codice: Un modulo design system :core:ui diventa una libreria utilizzabile in più app.

La guida ufficiale alla modularizzazione Android raccomanda di strutturare i moduli attorno a feature o layer che cambiano insieme, seguendo il Principio di Singola Responsabilità.

Tipi di Moduli e le Loro Responsabilità

Il repository Now in Android stabilisce una tassonomia di moduli collaudata che scala fino alle app enterprise.

settings.gradle.kts - Struttura dei modulikotlin
include(":app")

// Moduli core - utility condivise
include(":core:common")
include(":core:data")
include(":core:database")
include(":core:datastore")
include(":core:designsystem")
include(":core:domain")
include(":core:model")
include(":core:network")
include(":core:ui")

// Moduli feature - logica specifica per schermata
include(":feature:home")
include(":feature:search")
include(":feature:bookmarks")
include(":feature:settings")

Modulo app: Contiene MainActivity, setup della navigazione e scaffolding a livello app. Dipende da tutti i moduli feature e dai moduli core necessari.

Moduli core: Forniscono funzionalità condivise tra le feature. :core:network gestisce le chiamate API. :core:database gestisce le entity Room. :core:designsystem definisce i componenti visivi dell'app.

Moduli feature: Incapsulano una singola schermata o flusso utente. Ogni modulo feature dipende solo dai moduli core di cui ha bisogno, mai da altri moduli feature.

Questa struttura impone dipendenze unidirezionali: le feature dipendono da core, e app dipende dalle feature.

Gradle Version Catalog per la Gestione delle Dipendenze

Gestire le dipendenze su oltre 25 moduli senza conflitti di versione richiede una configurazione centralizzata. I Gradle Version Catalog risolvono questo problema con un singolo file libs.versions.toml.

toml
# gradle/libs.versions.toml
[versions]
agp = "8.7.0"
kotlin = "2.1.0"
compose-bom = "2026.09.00"
ksp = "2.1.0-1.0.29"
hilt = "2.54"
room = "2.7.0"
retrofit = "2.11.0"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version = "1.15.0" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
retrofit-core = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }

[bundles]
compose = ["androidx-compose-ui", "androidx-compose-material3"]
room = ["room-runtime", "room-ktx"]

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }

I moduli poi referenziano le dipendenze con accessor type-safe:

feature/home/build.gradle.ktskotlin
dependencies {
    implementation(libs.bundles.compose)
    implementation(libs.hilt.android)
    ksp(libs.hilt.compiler)
}

Gradle genera questi accessor durante la sincronizzazione, abilitando l'autocompletamento dell'IDE e la validazione a compile-time.

Pronto a superare i tuoi colloqui su Android?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Convention Plugin: Configurazione Build DRY

Un progetto con 25 moduli senza convention plugin ripete gli stessi compileSdk, minSdk, composeOptions e applicazioni di plugin in ogni build.gradle.kts. Moduli diversi possono accidentalmente usare configurazioni differenti.

I convention plugin centralizzano questa logica in un included build build-logic.

build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.ktkotlin
import com.android.build.api.dsl.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure

class AndroidLibraryConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply("com.android.library")
            pluginManager.apply("org.jetbrains.kotlin.android")

            extensions.configure<LibraryExtension> {
                compileSdk = 35
                defaultConfig {
                    minSdk = 26
                    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
                }
                compileOptions {
                    sourceCompatibility = JavaVersion.VERSION_17
                    targetCompatibility = JavaVersion.VERSION_17
                }
            }
        }
    }
}

Registrazione del plugin in build-logic/convention/build.gradle.kts:

build-logic/convention/build.gradle.ktskotlin
plugins {
    `kotlin-dsl`
}

dependencies {
    compileOnly(libs.android.gradlePlugin)
    compileOnly(libs.kotlin.gradlePlugin)
}

gradlePlugin {
    plugins {
        register("androidLibrary") {
            id = "myapp.android.library"
            implementationClass = "AndroidLibraryConventionPlugin"
        }
        register("androidFeature") {
            id = "myapp.android.feature"
            implementationClass = "AndroidFeatureConventionPlugin"
        }
        register("androidHilt") {
            id = "myapp.android.hilt"
            implementationClass = "AndroidHiltConventionPlugin"
        }
    }
}

I moduli feature ora richiedono configurazione minima:

feature/home/build.gradle.ktskotlin
plugins {
    alias(libs.plugins.myapp.android.feature)
    alias(libs.plugins.myapp.android.hilt)
}

dependencies {
    implementation(projects.core.data)
    implementation(projects.core.designsystem)
}

Il build-logic di Now in Android di Google utilizza esattamente questo pattern.

Pattern di Comunicazione tra Moduli

I moduli feature non devono dipendere direttamente l'uno dall'altro. Questo vincolo preserva la compilazione parallela e previene le dipendenze circolari. Tre pattern gestiscono la comunicazione cross-feature.

core/navigation/src/main/kotlin/Routes.ktkotlin
object Routes {
    const val HOME = "home"
    const val PRODUCT_DETAIL = "product/{productId}"
    const val CHECKOUT = "checkout"
    
    fun productDetail(productId: String) = "product/$productId"
}

I moduli feature importano le route da :core:navigation senza sapere quale modulo implementa ciascuna destinazione.

Modelli di Dominio Condivisi

core/model/src/main/kotlin/Product.ktkotlin
data class Product(
    val id: String,
    val name: String,
    val price: BigDecimal,
    val imageUrl: String
)

Sia :feature:catalog che :feature:checkout dipendono da :core:model per la classe Product.

Event Bus o Shared ViewModel

Per la comunicazione a runtime, un event bus condiviso in :core:common o un ViewModel con scope gestisce gli eventi cross-feature:

core/common/src/main/kotlin/CartEventBus.ktkotlin
object CartEventBus {
    private val _events = MutableSharedFlow<CartEvent>()
    val events: SharedFlow<CartEvent> = _events.asSharedFlow()
    
    suspend fun emit(event: CartEvent) {
        _events.emit(event)
    }
}

sealed interface CartEvent {
    data class ItemAdded(val productId: String) : CartEvent
    data class ItemRemoved(val productId: String) : CartEvent
}

Domande da Colloquio sulla Modularizzazione Android

La modularizzazione appare frequentemente nei colloqui Android senior. Gli intervistatori valutano la comprensione dei sistemi di build, le decisioni architetturali e i trade-off pratici.

Consiglio per il Colloquio

Quando si discute di modularizzazione nei colloqui, è importante fare riferimento a metriche concrete: miglioramenti nei tempi di build, incrementi nella velocità del team, o conteggi specifici di moduli da progetti reali. Risposte astratte sulla "migliore separazione delle responsabilità" sono meno convincenti di "i tempi di build sono scesi da 4 minuti a 45 secondi dopo la modularizzazione."

D: Qual è la differenza tra dipendenze api e implementation in Gradle?

Le dipendenze implementation sono interne al modulo. I consumatori del modulo non possono accedervi. Le dipendenze api sono esposte transitivamente: se il modulo A usa api(libs.retrofit), il modulo B che dipende da A può usare direttamente le classi Retrofit.

Regola generale: usare implementation di default. Usare api solo quando la dipendenza fa parte dell'API pubblica del modulo. L'uso eccessivo di api rompe l'incapsulamento e rallenta le build.

D: Come viene gestita la navigazione tra moduli feature che non dipendono l'uno dall'altro?

Si crea un modulo :core:navigation contenente definizioni di route e interfacce di navigazione. I moduli feature dipendono da questo modulo condiviso. Il modulo app collega le destinazioni alle implementazioni. Questo segue il Principio di Inversione delle Dipendenze: le feature dipendono da astrazioni, non da implementazioni concrete.

D: Quali sono i trade-off di avere troppi moduli?

Ogni modulo aggiunge overhead di configurazione Gradle: file di build, elaborazione risorse, merge del manifest. I tempi di sync iniziali aumentano. Il punto di svolta varia per progetto, ma 50+ moduli sono comuni nelle app grandi. I benefici (build parallele, test isolati, ownership chiara) tipicamente superano l'overhead per team di 5+ sviluppatori.

D: Come differiscono i convention plugin da buildSrc?

Le modifiche a buildSrc innescano un rebuild completo del progetto. I convention plugin in un included build ricompilano solo quando cambia il codice del plugin, non quando cambiano i moduli consumatori. Questo rende i convention plugin più veloci per lo sviluppo iterativo. Inoltre, i convention plugin possono essere pubblicati come artifact separato per il riutilizzo tra repository.

D: Come si testa un modulo feature in isolamento?

I moduli feature dipendono dai moduli core attraverso interfacce. Si creano implementazioni fake nelle test fixture:

core/data/src/testFixtures/kotlin/FakeProductRepository.ktkotlin
class FakeProductRepository : ProductRepository {
    private val products = mutableListOf<Product>()
    
    override suspend fun getProducts(): List<Product> = products
    
    fun addProduct(product: Product) {
        products.add(product)
    }
}

I test del modulo feature iniettano i fake tramite le API di testing di Hilt o constructor injection manuale.

Per altre domande di pratica sull'architettura Android, consultare le domande sul colloquio architettura MVVM e il modulo dependency injection.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Punti Chiave per Progetti Android Multi-Modulo

  • Strutturare i moduli per feature (:feature:home, :feature:checkout) e layer (:core:data, :core:ui). Le feature dipendono da core, mai l'una dall'altra.
  • Usare Gradle Version Catalog (libs.versions.toml) per centralizzare le versioni delle dipendenze. I bundle raggruppano dipendenze correlate per file di build più puliti.
  • Implementare convention plugin in build-logic per eliminare configurazione duplicata tra i moduli. Una modifica al plugin aggiorna tutti i moduli consumatori.
  • Imporre dipendenze unidirezionali: app dipende dalle feature, le feature dipendono da core. Strumenti come Dependency Guard possono automatizzare l'applicazione.
  • Misurare le performance di build prima e dopo la modularizzazione. Tracciare i tempi di build incrementali, non solo le clean build, per validare i miglioramenti.
  • Mantenere stabili i confini dei moduli. Ristrutturazioni frequenti annullano i benefici della modularizzazione.
Sfida del giorno

Sapresti trovare il bug in Android?

Uno snippet reale, un bug nascosto, un tentativo al giorno. Senza account per provare.

Anthony Fillion-Maillet

Scritto da

Anthony Fillion-Maillet

Fondatore di SharpSkill

Sviluppatore fullstack da oltre 10 anni. Guida SharpSkill e risponde di tutto ciò che vi viene pubblicato.

Aggiornato il 17 settembre 2026

Condividi

Articoli correlati