Altre considerazioni

Sebbene la migrazione da Views a Compose sia puramente correlata all'interfaccia utente, ci sono molte cose da tenere in considerazione per eseguire una migrazione sicura e incrementale. Questa pagina contiene alcune considerazioni da tenere presenti durante la migrazione dell'app basata su View a Compose.

Eseguire la migrazione del tema dell'app

Material Design è il sistema di progettazione consigliato per la tematizzazione delle app Android.

Per le app basate su View, sono disponibili tre versioni di Material:

Per le app Compose, sono disponibili due versioni di Material:

Ti consigliamo di utilizzare l'ultima versione (Material 3) se il sistema di progettazione della tua app è in grado di farlo. Sono disponibili guide alla migrazione sia per Views che per Compose:

Quando crei nuove schermate in Compose, indipendentemente dalla versione di Material Design che utilizzi, assicurati di applicare un MaterialTheme prima di qualsiasi elemento componibile che emetta l'interfaccia utente dalle librerie Compose Material. I componenti Material (Button, Text e così via) dipendono da un MaterialTheme e il loro comportamento non è definito senza di esso.

Tutti gli esempi di Jetpack Compose utilizzano un tema Compose personalizzato basato su MaterialTheme.

Per saperne di più, consulta Sistemi di progettazione in Compose e Eseguire la migrazione dei temi XML a Compose.

Se utilizzi il componente Navigation nella tua app, consulta Navigare con Compose - Interoperabilità ed Eseguire la migrazione di Jetpack Navigation a Navigation Compose per ulteriori informazioni.

Testare l'interfaccia utente mista Compose/Views

Dopo aver eseguito la migrazione di alcune parti dell'app a Compose, i test sono fondamentali per assicurarti di non aver danneggiato nulla.

Quando un'attività o un fragment utilizza Compose, devi utilizzare createAndroidComposeRule anziché utilizzare ActivityScenarioRule. createAndroidComposeRule integra ActivityScenarioRule con un ComposeTestRule che ti consente di testare il codice Compose e View contemporaneamente.

class MyActivityTest {
    @Rule
    @JvmField
    val composeTestRule = createAndroidComposeRule<MyActivity>()

    @Test
    fun testGreeting() {
        val greeting = InstrumentationRegistry.getInstrumentation()
            .targetContext.resources.getString(R.string.greeting)

        composeTestRule.onNodeWithText(greeting).assertIsDisplayed()
    }
}

Per saperne di più sui test, consulta Testare il layout di Compose. Per l'interoperabilità con i framework di test dell'interfaccia utente, consulta Interoperabilità con Espresso e Interoperabilità con UiAutomator.

Integrare Compose con l'architettura dell'app esistente

I pattern di architettura del flusso di dati unidirezionale (UDF) funzionano perfettamente con Compose. Se l'app utilizza altri tipi di pattern di architettura, come Model View Presenter (MVP), ti consigliamo di eseguire la migrazione di quella parte dell'interfaccia utente a UDF prima o durante l'adozione di Compose.

Utilizzare un ViewModel in Compose

Se utilizzi la libreria Architecture Components ViewModel, puoi accedere a un ViewModel da qualsiasi elemento componibile chiamando la funzione viewModel(), come spiegato in Compose e altre librerie.

Quando adotti Compose, fai attenzione a utilizzare lo stesso tipo di ViewModel in elementi componibili diversi, poiché gli elementi ViewModel seguono gli ambiti del ciclo di vita di View. L'ambito sarà l'attività host, il fragment o il grafico di navigazione se viene utilizzata la libreria Navigation.

Ad esempio, se gli elementi componibili sono ospitati in un'attività, viewModel() restituisce sempre la stessa istanza, che viene cancellata solo al termine dell'attività. Nell'esempio seguente, lo stesso utente ("user1") viene salutato due volte perché la stessa istanza di GreetingViewModel viene riutilizzata in tutti gli elementi componibili nell'attività host. La prima istanza di ViewModel creata viene riutilizzata in altri elementi componibili.

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

        setContent {
            MaterialTheme {
                Column {
                    GreetingScreen("user1")
                    GreetingScreen("user2")
                }
            }
        }
    }
}

@Composable
fun GreetingScreen(
    userId: String,
    viewModel: GreetingViewModel = viewModel(  
        factory = GreetingViewModelFactory(userId)  
    )
) {
    val messageUser by viewModel.message.observeAsState("")
    Text(messageUser)
}

class GreetingViewModel(private val userId: String) : ViewModel() {
    private val _message = MutableLiveData("Hi $userId")
    val message: LiveData<String> = _message
}

class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return GreetingViewModel(userId) as T
    }
}

Poiché i grafici di navigazione definiscono anche l'ambito degli elementi ViewModel, gli elementi componibili che sono una destinazione in un grafico di navigazione hanno un'istanza diversa di ViewModel. In questo caso, l'ambito di ViewModel è il ciclo di vita della destinazione e viene cancellato quando la destinazione viene rimossa dallo stack di back. Nell'esempio seguente, quando l'utente passa alla schermata Profilo, viene creata una nuova istanza di GreetingViewModel.

@Composable
fun MyApp() {
    NavHost(rememberNavController(), startDestination = "profile/{userId}") {
        /* ... */
        composable("profile/{userId}") { backStackEntry ->
            GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "")
        }
    }
}

Fonte attendibile dello stato

Quando adotti Compose in una parte dell'interfaccia utente, è possibile che il codice di Compose e del sistema View debba condividere i dati. Se possibile, ti consigliamo di incapsulare lo stato condiviso in un'altra classe che segua le best practice di UDF utilizzate da entrambe le piattaforme, ad esempio in un ViewModel che espone uno stream dei dati condivisi per emettere aggiornamenti dei dati.

Tuttavia, non è sempre possibile se i dati da condividere sono modificabili o sono strettamente associati a un elemento dell'interfaccia utente. In questo caso, un sistema deve essere la fonte attendibile e deve condividere tutti gli aggiornamenti dei dati con l'altro sistema. Come regola generale, la fonte attendibile deve essere di proprietà dell'elemento più vicino alla radice della gerarchia dell'interfaccia utente.

Compose come fonte attendibile

Utilizza l' SideEffect elemento componibile per pubblicare lo stato di Compose nel codice non Compose. In questo caso, la fonte attendibile viene mantenuta in un elemento componibile, che invia gli aggiornamenti dello stato.

Ad esempio, la tua libreria di analisi potrebbe consentirti di segmentare la popolazione di utenti allegando metadati personalizzati (in questo esempio proprietà utente) a tutti gli eventi di analisi successivi. Per comunicare il tipo di utente dell'utente corrente alla libreria di analisi, utilizza SideEffect per aggiornarne il valore.

@Composable
fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics {
    val analytics: FirebaseAnalytics = remember {
        FirebaseAnalytics()
    }

    // On every successful composition, update FirebaseAnalytics with
    // the userType from the current User, ensuring that future analytics
    // events have this metadata attached
    SideEffect {
        analytics.setUserProperty("userType", user.userType)
    }
    return analytics
}

Per saperne di più, consulta Effetti collaterali in Compose.

Sistema View come fonte attendibile

Se il sistema View è proprietario dello stato e lo condivide con Compose, ti consigliamo di racchiudere lo stato negli oggetti mutableStateOf per renderlo thread-safe per Compose. Se utilizzi questo approccio, le funzioni componibili vengono semplificate perché non hanno più la fonte attendibile, ma il sistema View deve aggiornare lo stato modificabile e le View che utilizzano questo stato.

Nell'esempio seguente, un CustomViewGroup contiene un TextView e un ComposeView con un elemento componibile TextField all'interno. Il TextView deve mostrare il contenuto di ciò che l'utente digita in TextField.

class CustomViewGroup @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : LinearLayout(context, attrs, defStyle) {

    // Source of truth in the View system as mutableStateOf
    // to make it thread-safe for Compose
    private var text by mutableStateOf("")

    private val textView: TextView

    init {
        orientation = VERTICAL

        textView = TextView(context)
        val composeView = ComposeView(context).apply {
            setContent {
                MaterialTheme {
                    TextField(value = text, onValueChange = { updateState(it) })
                }
            }
        }

        addView(textView)
        addView(composeView)
    }

    // Update both the source of truth and the TextView
    private fun updateState(newValue: String) {
        text = newValue
        textView.text = newValue
    }
}

Eseguire la migrazione dell'interfaccia utente condivisa

Se esegui la migrazione graduale a Compose, potresti dover utilizzare elementi dell'interfaccia utente condivisi sia in Compose che nel sistema View. Ad esempio, se la tua app ha un componente CallToActionButton personalizzato, potresti doverlo utilizzare sia nelle schermate basate su Compose che su View.

In Compose, gli elementi dell'interfaccia utente condivisi diventano elementi componibili che possono essere riutilizzati nell'app, indipendentemente dal fatto che l'elemento sia stilizzato utilizzando XML o sia una visualizzazione personalizzata. Ad esempio, creeresti un elemento componibile CallToActionButton per il componente di invito all'azione personalizzato Button.

Per utilizzare l'elemento componibile nelle schermate basate su View, crea un wrapper di visualizzazione personalizzato che estenda da AbstractComposeView. Nell'elemento componibile Content sostituito, inserisci l'elemento componibile che hai creato racchiuso nel tema Compose, come mostrato nell'esempio seguente:

@Composable
fun CallToActionButton(
    text: String,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = MaterialTheme.colorScheme.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf("")
    var onClick by mutableStateOf({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

Tieni presente che i parametri dell'elemento componibile diventano variabili modificabili all'interno della visualizzazione personalizzata. In questo modo, la visualizzazione personalizzata CallToActionViewButton è espandibile e utilizzabile, come una visualizzazione tradizionale. Di seguito è riportato un esempio di questo con View Binding:

class ViewBindingActivity : ComponentActivity() {

    private lateinit var binding: ActivityExampleBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityExampleBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.callToAction.apply {
            text = getString(R.string.greeting)
            onClick = { /* Do something */ }
        }
    }
}

Se il componente personalizzato contiene uno stato modificabile, consulta Fonte attendibile dello stato.

Assegnare la priorità alla separazione dello stato dalla presentazione

Tradizionalmente, una View è con stato. Una View gestisce i campi che descrivono cosa visualizzare, oltre a come visualizzarlo. Quando converti un View in Compose, cerca di separare i dati di cui viene eseguito il rendering per ottenere un flusso di dati unidirezionale, come spiegato ulteriormente in innalzamento dello stato.

Ad esempio, una View ha una proprietà visibility che descrive se è visibile, invisibile o scomparsa. Questa è una proprietà intrinseca della View. Sebbene altre parti del codice possano modificare la visibilità di una View, solo la View stessa sa realmente qual è la sua visibilità attuale. La logica per garantire che una View sia visibile può essere soggetta a errori e spesso è legata alla View stessa.

Al contrario, Compose semplifica la visualizzazione di elementi componibili completamente diversi utilizzando la logica condizionale in Kotlin:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    if (showCautionIcon) {
        CautionIcon(/* ... */)
    }
}

Per progettazione, CautionIcon non deve sapere o preoccuparsi del motivo per cui viene visualizzato e non esiste il concetto di visibility: è presente nella composizione o non lo è.

Separando chiaramente la gestione dello stato e la logica di presentazione, puoi modificare più liberamente la modalità di visualizzazione dei contenuti come conversione dello stato in interfaccia utente. La possibilità di sollevare lo stato quando necessario rende anche gli elementi componibili più riutilizzabili, poiché la proprietà dello stato è più flessibile.

Promuovere componenti incapsulati e riutilizzabili

Gli elementi View spesso hanno un'idea di dove si trovano: all'interno di un'Activity, una Dialog, un Fragment o da qualche parte all'interno di un'altra gerarchia View. Poiché vengono spesso espansi da file di layout statici, la struttura complessiva di una View tende a essere molto rigida. Ciò comporta un accoppiamento più stretto e rende più difficile la modifica o il riutilizzo di una View.

Ad esempio, una View personalizzata potrebbe presupporre di avere una visualizzazione secondaria di un determinato tipo con un determinato ID e modificarne le proprietà direttamente in risposta a un'azione. Questo accoppia strettamente questi elementi View: la View personalizzata potrebbe arrestarsi in modo anomalo o essere danneggiata se non riesce a trovare la visualizzazione secondaria e quest'ultima probabilmente non può essere riutilizzata senza la View personalizzata principale.

Questo è un problema minore in Compose con elementi componibili riutilizzabili. I genitori possono specificare facilmente lo stato e i callback, quindi puoi scrivere elementi componibili riutilizzabili senza dover conoscere il luogo esatto in cui verranno utilizzati.

@Composable
fun AScreen() {
    var isEnabled by rememberSaveable { mutableStateOf(false) }

    Column {
        ImageWithEnabledOverlay(isEnabled)
        ControlPanelWithToggle(
            isEnabled = isEnabled,
            onEnabledChanged = { isEnabled = it }
        )
    }
}

Nell'esempio precedente, tutte e tre le parti sono più incapsulate e meno accoppiate:

  • ImageWithEnabledOverlay deve solo sapere qual è lo stato isEnabled attuale. Non deve sapere che esiste ControlPanelWithToggle o anche come è controllabile.

  • ControlPanelWithToggle non sa che esiste ImageWithEnabledOverlay. Potrebbero esserci zero, uno o più modi in cui viene visualizzato isEnabled e ControlPanelWithToggle non dovrebbe cambiare.

  • Per il genitore, non importa quanto siano nidificati ImageWithEnabledOverlay o ControlPanelWithToggle. Questi figli potrebbero animare le modifiche, sostituire i contenuti o passare i contenuti ad altri figli.

Questo pattern è noto come inversione del controllo, di cui puoi scoprire di più nella documentazione CompositionLocal.

Gestire le modifiche delle dimensioni dello schermo

Avere risorse diverse per dimensioni delle finestre diverse è uno dei modi principali per creare layout View adattabili. Sebbene le risorse qualificate siano ancora un'opzione per le decisioni di layout a livello di schermo, Compose semplifica notevolmente la modifica completa dei layout nel codice con la normale logica condizionale. Per saperne di più, consulta Utilizzare le classi di dimensioni delle finestre.

Inoltre, consulta Supportare diverse dimensioni di visualizzazione per scoprire le tecniche offerte da Compose per creare interfacce utente adattive.

Scorrimento nidificato con Views

Per ulteriori informazioni su come abilitare l'interoperabilità dello scorrimento nidificato tra gli elementi View scorrevoli e gli elementi componibili scorrevoli, nidificati in entrambe le direzioni, consulta Interoperabilità dello scorrimento nidificato.

Compose in RecyclerView

Gli elementi componibili in RecyclerView sono efficienti dalla versione 1.3.0-alpha02 di RecyclerView. Assicurati di utilizzare almeno la versione 1.3.0-alpha02 di RecyclerView per usufruire di questi vantaggi.

Interoperabilità di WindowInsets con Views

Potresti dover sostituire gli inset predefiniti quando la schermata contiene sia codice Views che Compose nella stessa gerarchia. In questo caso, devi specificare esplicitamente quale deve utilizzare gli inset e quale deve ignorarli.

Ad esempio, se il layout più esterno è un layout View di Android, devi utilizzare gli inset nel sistema View e ignorarli per Compose. In alternativa, se il layout più esterno è un elemento componibile, devi utilizzare gli inset in Compose e aggiungere il padding agli elementi componibili AndroidView di conseguenza.

Per impostazione predefinita, ogni ComposeView utilizza tutti gli inset a livello di utilizzo WindowInsetsCompat. Per modificare questo comportamento predefinito, imposta ComposeView.consumeWindowInsets su false.

Per saperne di più, consulta la documentazione di WindowInsets in Compose.