I seguenti esempi si basano su scenari comuni in cui utilizzi R8 per l'ottimizzazione, ma hai bisogno di indicazioni avanzate per scrivere le regole di conservazione.
Riflesso
In generale, per prestazioni ottimali, non è consigliabile utilizzare il riflesso. Tuttavia, in alcuni scenari, potrebbe essere inevitabile. I seguenti esempi forniscono indicazioni per le regole di conservazione in scenari comuni che utilizzano il riflesso.
Riflesso con classi caricate per nome
Le librerie spesso caricano le classi in modo dinamico utilizzando il nome della classe come String.
Tuttavia, R8 non è in grado di rilevare le classi caricate in questo modo e potrebbe rimuovere le classi che considera inutilizzate.
Ad esempio, considera il seguente scenario in cui hai una libreria e un'app che utilizza la libreria: il codice mostra un caricatore di librerie che crea un'istanza di un'interfaccia StartupTask implementata da un'app.
Il codice della libreria è il seguente:
// The interface for a task that runs once.
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(className: String) {
// R8 won't retain classes specified by this string value at runtime
val taskClass = Class.forName(className)
val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
task.run()
}
}
L'app che utilizza la libreria ha il seguente codice:
// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
override fun run() {
// This log will never appear if the class is removed by R8.
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is told to run the app's task by its name.
TaskRunner.execute("com.example.app.PreCacheTask")
}
In questo scenario, la libreria deve includere un file delle regole di conservazione del consumer con le seguenti regole di conservazione:
-keep class * implements com.example.library.StartupTask {
<init>();
}
Senza questa regola, R8 rimuove PreCacheTask dall'app perché l'app non utilizza la classe direttamente, interrompendo l'integrazione. La regola trova le classi che implementano l'interfaccia StartupTask della libreria e le conserva, insieme al costruttore senza argomenti, consentendo alla libreria di creare ed eseguire correttamente l'istanza di PreCacheTask.
Riflesso con ::class.java
Le librerie possono caricare le classi facendo in modo che l'app passi direttamente l'oggetto Class, che è un metodo più solido rispetto al caricamento delle classi per nome. In questo modo viene creato un riferimento forte alla classe che R8 può rilevare. Tuttavia, anche se ciò impedisce a R8 di rimuovere la classe, devi comunque utilizzare una regola di conservazione per dichiarare che la classe viene creata tramite riflesso e per proteggere i membri a cui si accede tramite riflesso, come il costruttore.
Ad esempio, considera il seguente scenario in cui hai una libreria e un'
app che utilizza la libreria: il caricatore di librerie crea un'istanza di un'StartupTask
interfaccia passando direttamente il riferimento della classe.
Il codice della libreria è il seguente:
// The interface for a task that runs once.
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(taskClass: Class<out StartupTask>) {
// The class isn't removed, but its constructor might be.
val task = taskClass.getDeclaredConstructor().newInstance()
task.run()
}
}
L'app che utilizza la libreria ha il seguente codice:
// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
override fun run() {
Log.d("AppTask", "Warming up the cache...")
}
}
fun onCreate() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
In questo scenario, la libreria deve includere un file delle regole di conservazione del consumer con le seguenti regole di conservazione:
# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
<init>();
}
Queste regole sono progettate per funzionare perfettamente con questo tipo di riflesso, consentendo la massima ottimizzazione e garantendo al contempo il corretto funzionamento del codice. Le regole consentono a R8 di offuscare il nome della classe e di ridurre o rimuovere l'implementazione della classe StartupTask se l'app non la utilizza mai. Tuttavia,
per qualsiasi implementazione, come PrecacheTask utilizzata nell'esempio,
conservano il costruttore predefinito (<init>()) che la libreria deve
chiamare.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: questa regola è destinata a qualsiasi classe che implementa l'interfacciaStartupTask.-keep class * implements com.example.library.StartupTask: questa regola conserva qualsiasi classe (*) che implementa l'interfaccia.,allowobfuscation: questa regola indica a R8 che, nonostante la conservazione della classe, può rinominarla o offuscarla. Questa operazione è sicura perché la libreria non si basa sul nome della classe, ma ottiene direttamente l'oggettoClass.,allowshrinking: questo modificatore indica a R8 che può rimuovere la classe se non viene utilizzata. In questo modo, R8 può eliminare in sicurezza un'implementazione diStartupTaskche non viene mai passata aTaskRunner.execute(). In breve, questa regola implica quanto segue: se un'app utilizza una classe che implementaStartupTask, R8 conserva la classe. R8 può rinominare la classe per ridurne le dimensioni ed eliminarla se l'app non la utilizza.
-keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: Questa regola è destinata a membri specifici delle classi identificate nella prima regola, in questo caso il costruttore.-keepclassmembers class * implements com.example.library.StartupTask: questa regola conserva membri specifici (metodi, campi) della classe che implementaStartupTaskinterfaccia, ma solo se la classe implementata stessa viene conservata.{ <init>(); }: questo è il selettore dei membri.<init>è il nome interno speciale di un costruttore nel bytecode Java. Questa parte è specificamente destinata al costruttore predefinito senza argomenti.- Questa regola è fondamentale perché il codice chiama
getDeclaredConstructor().newInstance()senza argomenti, che richiama tramite riflesso il costruttore predefinito. Senza questa regola, R8 vede che nessun codice chiama direttamentenew PreCacheTask(), presuppone che il costruttore non venga utilizzato e lo rimuove. In questo modo, l'app si arresta in modo anomalo in fase di runtime con un'eccezioneInstantiationException.
Riflesso basato sull'annotazione del metodo
Le librerie spesso definiscono annotazioni che gli sviluppatori utilizzano per taggare metodi o campi.
La libreria utilizza quindi il riflesso per trovare questi membri annotati in fase di runtime. Ad esempio, l'annotazione @OnLifecycleEvent viene utilizzata per trovare i metodi richiesti in fase di runtime.
Ad esempio, considera il seguente scenario in cui hai una libreria e un'app che utilizza la libreria: l'esempio mostra un bus di eventi che trova e richiama i metodi annotati con @OnEvent.
Il codice della libreria è il seguente:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent
class EventBus {
fun dispatch(listener: Any) {
// Find all methods annotated with @OnEvent and invoke them
listener::class.java.declaredMethods.forEach { method ->
if (method.isAnnotationPresent(OnEvent::class.java)) {
try {
method.invoke(listener)
} catch (e: Exception) { /* ... */ }
}
}
}
}
L'app che utilizza la libreria ha il seguente codice:
class MyEventListener {
@OnEvent
fun onSomethingHappened() {
// This method will be removed by R8 without a keep rule
Log.d(TAG, "Event received!")
}
}
fun onCreate() {
// Instantiate the listener and the event bus
val listener = MyEventListener()
val eventBus = EventBus()
// Dispatch the listener to the event bus
eventBus.dispatch(listener)
}
La libreria deve includere un file delle regole di conservazione del consumer che conserva automaticamente tutti i metodi che utilizzano le relative annotazioni:
-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
@com.example.library.OnEvent <methods>;
}
-keepattributes RuntimeVisibleAnnotations: questa regola conserva le annotazioni che devono essere lette in fase di runtime.-keep @interface com.example.library.OnEvent: questa regola conserva laOnEventclasse di annotazione stessa.-keepclassmembers class * {@com.example.library.OnEvent <methods>;}: Questa regola conserva una classe e membri specifici solo se la classe viene utilizzata e contiene questi membri.-keepclassmembers: questa regola conserva una classe e membri specifici solo se la classe viene utilizzata e contiene questi membri.class *: la regola si applica a qualsiasi classe.@com.example.library.OnEvent <methods>;: questa regola conserva qualsiasi classe che abbia uno o più metodi (<methods>) annotati con@com.example.library.OnEvente conserva anche i metodi annotati stessi.
Riflesso basato sulle annotazioni delle classi
Le librerie possono utilizzare il riflesso per cercare le classi che hanno un'annotazione specifica. In questo caso, la classe di esecuzione delle attività trova tutte le classi annotate con ReflectiveExecutor utilizzando il riflesso ed esegue il metodo execute.
Ad esempio, considera il seguente scenario in cui hai una libreria e un'app che utilizza la libreria.
La libreria ha il seguente codice:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ReflectiveExecutor
class TaskRunner {
fun process(task: Any) {
val taskClass = task::class.java
if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) {
val methodToCall = taskClass.getMethod("execute")
methodToCall.invoke(task)
}
}
}
L'app che utilizza la libreria ha il seguente codice:
// In consumer app
@ReflectiveExecutor
class ImportantBackgroundTask {
fun execute() {
// This class will be removed by R8 without a keep rule
Log.e("ImportantBackgroundTask", "Executing the important background task...")
}
}
// Usage of ImportantBackgroundTask
fun onCreate(){
val task = ImportantBackgroundTask()
val runner = TaskRunner()
runner.process(task)
}
Poiché la libreria utilizza il riflesso per ottenere classi specifiche, deve includere un file delle regole di conservazione del consumer con le seguenti regole di conservazione:
# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations
# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor
# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
public void execute();
}
Questa configurazione è molto efficiente perché indica a R8 esattamente cosa conservare.
Riflesso per supportare le dipendenze facoltative
Un caso d'uso comune per il riflesso è la creazione di una dipendenza soft tra una libreria principale e una libreria di componenti aggiuntivi facoltativa. La libreria principale può verificare se il componente aggiuntivo è incluso nell'app e, in caso affermativo, può attivare funzionalità aggiuntive. In questo modo, puoi distribuire moduli di componenti aggiuntivi senza forzare la libreria principale ad avere una dipendenza diretta da essi.
La libreria principale utilizza il riflesso (Class.forName) per cercare una classe specifica in base al nome. Se la classe viene trovata, la funzionalità viene attivata. In caso contrario, l'operazione non va a buon fine.
Ad esempio, considera il seguente codice in cui un AnalyticsManager principale verifica la presenza di una classe VideoEventTracker facoltativa per attivare l'analisi video.
La libreria principale ha il seguente codice:
object AnalyticsManager {
private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"
fun initialize() {
try {
// Attempt to load the optional module's class using reflection
Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
Log.d(TAG, "Video tracking enabled.")
} catch (e: ClassNotFoundException) {
Log.d(TAG,"Video tracking module not found. Skipping.")
} catch (e: Exception) {
Log.e(TAG, e.printStackTrace())
}
}
}
La libreria video facoltativa ha il seguente codice:
package com.example.analytics.video
class VideoEventTracker {
// This constructor must be kept for the reflection call to succeed.
init { /* ... */ }
}
Lo sviluppatore della libreria facoltativa è responsabile della fornitura della regola di conservazione del consumer necessaria. Questa regola di conservazione garantisce che qualsiasi app che utilizza la libreria facoltativa conservi il codice che la libreria principale deve trovare.
# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
Senza questa regola, è probabile che R8 rimuova VideoEventTracker dalla libreria facoltativa, poiché nulla in questo modulo la utilizza direttamente. La regola di conservazione conserva la classe e il relativo costruttore, consentendo alla libreria principale di crearne correttamente l'istanza.
Riflesso per accedere ai membri privati
L'utilizzo del riflesso per accedere a codice privato o protetto che non fa parte dell'API pubblica di una libreria può introdurre problemi significativi. Questo codice è soggetto a modifiche senza preavviso, il che può causare comportamenti imprevisti o arresti anomali nell'applicazione.
Quando utilizzi il riflesso per le API non pubbliche, potresti riscontrare i seguenti problemi:
- Aggiornamenti bloccati: le modifiche al codice privato o protetto possono impedirti di eseguire l'aggiornamento a versioni più recenti della libreria.
- Vantaggi mancanti: potresti perdere nuove funzionalità, correzioni importanti degli arresti anomali o aggiornamenti di sicurezza essenziali.
Ottimizzazioni R8 e riflesso
Se devi riflettere nel codice privato o protetto di una libreria, presta molta attenzione alle ottimizzazioni di R8. Se non sono presenti riferimenti diretti a questi membri, R8 potrebbe presupporre che non vengano utilizzati e quindi rimuoverli o rinominarli.
Ciò può causare arresti anomali in fase di runtime, spesso con messaggi di errore fuorvianti come NoSuchMethodException o NoSuchFieldException.
Ad esempio, considera il seguente scenario che mostra come potresti accedere a un campo privato da una classe di libreria.
Una libreria di cui non sei proprietario ha il seguente codice:
class LibraryClass {
private val secretMessage = "R8 will remove me"
}
La tua app ha il seguente codice:
fun accessSecretMessage(instance: LibraryClass) {
// Use Java reflection from Kotlin to access the private field
val secretField = instance::class.java.getDeclaredField("secretMessage")
secretField.isAccessible = true
// This will crash at runtime with R8 enabled
val message = secretField.get(instance) as String
}
Aggiungi una regola -keep nell'app per impedire a R8 di rimuovere il campo privato:
-keepclassmembers class com.example.LibraryClass {
private java.lang.String secretMessage;
}
-keepclassmembers: questa regola conserva membri specifici di una classe solo se la classe stessa viene conservata.class com.example.LibraryClass: questa regola è destinata alla classe esatta contenente il campo.private java.lang.String secretMessage;: questa regola identifica il campo privato specifico in base al nome e al tipo.
Java Native Interface (JNI)
Le ottimizzazioni di R8 possono presentare problemi quando si utilizzano le chiamate upcall dal codice nativo (C/C++) a Java o Kotlin. Sebbene sia vero anche il contrario, ovvero le chiamate downcall da Java o Kotlin al codice nativo possono presentare problemi, il file predefinito proguard-android-optimize.txt include la seguente regola per mantenere il funzionamento delle chiamate downcall. Questa regola impedisce l'eliminazione dei metodi nativi.
-keepclasseswithmembernames,includedescriptorclasses class * {
native <methods>;
}
Interazione con il codice nativo tramite Java Native Interface (JNI)
Quando l'app utilizza JNI per effettuare chiamate upcall dal codice nativo (C/C++) a Java o Kotlin, R8 non è in grado di vedere quali metodi vengono chiamati dal codice nativo. Se non sono presenti riferimenti diretti a questi metodi nell'app, R8 presuppone erroneamente che questi metodi non vengano utilizzati e li rimuove, causando l'arresto anomalo dell'app.
Il seguente esempio mostra una classe Kotlin con un metodo destinato a essere chiamato da una libreria nativa. La libreria nativa crea un'istanza di un tipo di applicazione e passa i dati dal codice nativo al codice Kotlin.
package com.example.models
// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
/**
* This method is called from the native side.
* R8 will remove it if it's not kept.
*/
fun onNativeEvent(data: NativeData) {
Log.d(TAG, "Received event from native code: $data")
}
// Use 'external' to declare a native method
external fun startNativeProcess()
companion object {
init {
// Load the native library
System.loadLibrary("my-native-lib")
}
}
}
In questo caso, devi informare R8 per impedire l'ottimizzazione del tipo di applicazione. Inoltre, se i metodi chiamati dal codice nativo utilizzano le tue classi nelle firme come parametri o tipi restituiti, devi anche verificare che queste classi non vengano rinominate.
Aggiungi le seguenti regole di conservazione all'app:
-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
public void onNativeEvent(com.example.model.NativeData);
}
-keep class NativeData{
<init>(java.lang.Integer, java.lang.String);
}
Queste regole di conservazione impediscono a R8 di rimuovere o rinominare il metodo onNativeEvent e, soprattutto, il relativo tipo di parametro.
-keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: questa regola conserva membri specifici di una classe solo se la classe viene creata tramite istanza prima nel codice Kotlin o Java. Indica a R8 che l'app utilizza la classe e che deve conservare membri specifici della classe.-keepclassmembers: questa regola conserva membri specifici di una classe solo se la classe viene creata tramite istanza prima nel codice Kotlin o Java. Indica a R8 che l'app utilizza la classe e che deve conservare membri specifici della classe.class com.example.JniBridge: questa regola è destinata alla classe esatta contenente il campo.includedescriptorclasses: questo modificatore conserva anche tutte le classi trovate nella firma o nel descrittore del metodo. In questo caso, impedisce a R8 di rinominare o rimuovere la classecom.example.models.NativeData, utilizzata come parametro. SeNativeDatavenisse rinominata (ad esempio, ina.a), la firma del metodo non corrisponderebbe più a quella prevista dal codice nativo, causando un arresto anomalo.public void onNativeEvent(com.example.models.NativeData);: questa regola specifica la firma Java esatta del metodo da conservare.
-keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: Mentreincludedescriptorclassesgarantisce che la classeNativeDatastessa venga conservata, tutti i membri (campi o metodi) all'interno diNativeDataa cui si accede direttamente dal codice JNI nativo richiedono le proprie regole di conservazione.-keep class NativeData: questa regola è destinata alla classe denominataNativeDatae il blocco specifica i membri all'interno della classeNativeDatada conservare.<init>(java.lang.Integer, java.lang.String): questa è la firma del costruttore. Identifica in modo univoco il costruttore che accetta due parametri: il primo è unIntegere il secondo è unaString.
Chiamate di piattaforma indirette
Trasferire i dati con un'implementazione di Parcelable
Il framework Android utilizza il riflesso per creare istanze degli oggetti Parcelable. Nello sviluppo Kotlin moderno, devi utilizzare il plug-in kotlin-parcelize, che genera automaticamente l'implementazione Parcelable necessaria, inclusi il campo CREATOR e i metodi di cui il framework ha bisogno.
Ad esempio, considera il seguente esempio in cui il plug-in kotlin-parcelize viene utilizzato per creare una classe Parcelable:
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
val name: String,
val age: Int
) : Parcelable
In questo scenario, non è consigliabile utilizzare una regola di conservazione. Il plug-in Gradle kotlin-parcelize genera automaticamente le regole di conservazione richieste per le classi annotate con @Parcelize. Gestisce la complessità per te, assicurandosi che i costruttori e CREATOR generati vengano conservati per le chiamate di riflesso del framework Android.
Se scrivi manualmente una classe Parcelable in Kotlin senza utilizzare @Parcelize,
sei responsabile della conservazione del campo CREATOR e del costruttore che
accetta un Parcel. Se non lo fai, l'app si arresta in modo anomalo quando il sistema tenta di deserializzare l'oggetto. L'utilizzo di @Parcelize è la prassi standard e più sicura.
Quando utilizzi il plug-in kotlin-parcelize, tieni presente quanto segue:
- Il plug-in crea automaticamente i campi
CREATORdurante la compilazione. - Il file
proguard-android-optimize.txtcontiene le regolekeepnecessarie per conservare questi campi per un corretto funzionamento. - Gli sviluppatori di app devono verificare che siano presenti tutte le regole
keeprichieste, in particolare per eventuali implementazioni personalizzate o dipendenze di terze parti.
Librerie più diffuse
Le librerie che utilizzano il riflesso o le trasformazioni di bytecode accedono al codice in modo dinamico in fase di runtime. Se R8 rimuove o rinomina classi, campi o metodi a cui si accede in questo modo, l'app può arrestarsi in modo anomalo.
Tuttavia, le librerie di terze parti più diffuse (come Gson, Retrofit e Kotlinx Serialization) raggruppano automaticamente le proprie regole di conservazione del consumer R8. Quando utilizzi le versioni recenti di queste librerie, non devi aggiungere manualmente regole di conservazione al progetto.
Gson
Gson è una libreria di serializzazione e deserializzazione JSON che si basa in gran parte sul riflesso. Quando utilizzi la modalità completa per ottimizzare l'app, vengono rimossi le firme dei tipi generici, i costruttori predefiniti e i campi non annotati, a meno che non venga indicato diversamente.
Per garantire il corretto funzionamento di Gson, aggiungi regole specifiche per conservare i campi non transitori nelle classi del modello di dati e conservare la gerarchia TypeToken:
# Preserve generic type information required for deserialization
-keepattributes Signature
# Keep all non-transient fields in your data model classes for reflection
-keepclassmembers class com.example.models.** {
!transient <fields>;
}
# Keep TypeToken itself and any anonymous classes extending it
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
I campi contrassegnati con il modificatore transient vengono ignorati da Gson durante
la serializzazione e la deserializzazione, motivo per cui la regola di conservazione è specificamente
destinata ai campi non transitori (!transient).
Retrofit
Retrofit è una libreria di rete che ispeziona i metodi dell'interfaccia di servizio annotati con annotazioni HTTP (come @GET o @POST) utilizzando il riflesso per creare richieste di rete e convertire le risposte.
Retrofit genera dinamicamente le implementazioni delle interfacce API in fase di runtime utilizzando Proxy.newProxyInstance(). Poiché R8 non vede alcuna classe che implementa staticamente queste interfacce, potrebbe rimuovere i metodi o i relativi tipi restituiti generici.
Regole di conservazione raggruppate
Retrofit si basa sul riflesso in fase di runtime per ispezionare i parametri generici, le annotazioni dei metodi e le annotazioni dei parametri. Senza una configurazione corretta, la modalità completa R8 può rimuovere completamente le firme generiche dai tipi restituiti, dalle continuazioni Kotlin e dalle classi di risposta o persino sostituire i valori dell'interfaccia con null, poiché le interfacce Retrofit vengono create dinamicamente con un proxy.
A partire da Retrofit 2.10.0, la libreria raggruppa automaticamente le regole di conservazione ufficiali necessarie per conservare i valori predefiniti delle annotazioni, i parametri dei metodi di servizio e i metadati delle classi necessari. Per maggiori informazioni, vedi Regole utilizzate da Retrofit.
Conservare i tipi restituiti generici
Retrofit ispeziona la firma generica del tipo restituito (ad esempio,
Observable<Data>) per deserializzare correttamente la risposta di rete. Se R8 rimuove la firma generica, Retrofit sostituirà l'oggetto creato con null.
Per impedire alla modalità completa R8 di rimuovere la firma generica dei tipi restituiti, utilizza la seguente regola condizionale:
# Preserve generic type information for Call/Observable return types
-keepattributes Signature
# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>)
-if interface * {
@retrofit2.http.* public *** *(...);
}
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
Anche la classe del modello di dati effettivo restituita (ad esempio, Data in
Observable<Data>) deve essere conservata, poiché verrà creata tramite riflesso da
l convertitore (come Gson).
Coroutine
Quando utilizzi le coroutine Kotlin, il compilatore Kotlin trasforma le funzioni suspend aggiungendo un parametro Continuation alla firma del metodo compilato.
Quando le librerie come Retrofit leggono tramite riflesso la firma generica di una funzione suspend, si basano su questo parametro Continuation. Quando utilizzi la modalità completa, l'attributo Signature viene conservato solo per le classi conservate in modo esplicito. Poiché Continuation è un parametro sintetico, R8 rimuove la relativa firma per impostazione predefinita, interrompendo il riflesso.
Per impedire la rimozione della firma e garantire la compatibilità in fase di runtime in modalità completa, includi la seguente regola:
# Keep the signature attribute globally
-keepattributes Signature
# Explicitly keep the Continuation class so its signature is not stripped
-keep class kotlin.coroutines.Continuation