Kotlin-Updates: Android Studio 4.2

Weiter geht’s mit Teil 6 der Kotlin-Updates-Serie. Diesmal gehe ich auf die Neuerungen ein, die sich mit Android Studio 4.2 ergeben. Den größten Änderungsbedarf verursacht das Gradle-Plugin kotlin-android-extensions, das jetzt deprecated ist.

Dieser Text bezieht sich auf die folgenden Versionsnummern:

Android Studio: 4.2.2
Kotlin: 1.5.21
Gradle: 6.7

Neue Projekte

Wenn Sie in Android Studio 4.2.2 mit allen Update ein neues Projekt einrichten, kommen Kotlin 1.5.21 und Gradle 6.7 zum Einsatz. build.gradle (Projekt) enthält zwar weiterhin den Verweis auf das jcenter-Repository, aber auch den Hinweis, dass diese Paketquelle demnächst nicht mehr aktiv sein wird. Am besten kommentieren Sie die Quelle aus.

Bei meinen Experimenten trat immer wieder die Fehlermeldung Path.op() not supported auf. Offenbar muss in Android Studio die neue Rendering Engine aktiviert werden: Settings/Experimental/Use new Layout Rendering Engine.

Beachten Sie, dass Android Studio 4.2 merkwürdigerweise selbst bei neuen Projekten das »View Binding« (siehe unten) nicht automatisch aktiviert und Sie diesbezüglich im Regen stehen lässt. Merkwürdig!

Sie müssen die unten beschriebenen Aktionen auch in neuen Projekten durchführen. Das gilt insbesondere für die Datei build.gradle (Module), in die Sie buildFeatures { viewBinding true } einfügen müssen, als auch für MainActivity.kt, wo Sie die Variable binding deklarieren und den Code in onCreate entsprechend ändern müssen.

Vorhandene Projekte aktualisieren

Um zu testen, wie einfach die Aktualisierung älterer Projekte gelingt, habe ich in den Gradle-Dateien des Währungsumrechner (siehe Kapitel 25 im Kotlin-Buch) sämtliche Versionsnummern auf den neuesten Stand aktualisiert. Offen gesagt war ich auf Kompatibilitätsprobleme eingestellt, aber überraschenderweise funktionierte der Code auch nach dem Umbau fehlerfrei.

kotlin-android-extensions is deprecated

So weit, so gut! Allerdings liefert das Build-System eine neue Warnung:

The kotlin-android-extensions Gradle plugin is deprecated. Please use this migration guide (https://goo.gle/kotlin-android-extensions-deprecation) to start working with View Binding (https://developer.android.com/topic/libraries/view-binding) and the kotlin-parcelize plugin.

Vorerst können Sie diese Warnung ignorieren. Auch wenn das Gradle-Plugin deprecated ist — noch funktioniert es ja. Ich habe die Vermutung, dass dies aufgrund unzähliger Projekte, die davon abhängig sind, noch eine Weile so bleiben wird. Aber längerfristig ist die Verwendung von deprecated-Komponenten selten eine gute Idee. Und bei neuen Projekten fehlt das Plugin ohnedies. Es wird Ihnen also nicht erspart bleiben, sich an neue Arbeitstechniken zu gewöhnen.

Welche Funktion hatte nun diese kotlin-android-extension? Dieses Gradle-Plugin stellte in der Vergangenheit sicher, dass Sie im Code unmittelbar auf Steuerelemente zugreifen konnten. Wenn Sie also ein TextView-Steuerelement mit mytext benannten (id-Eigenschaft), dann konnten Sie in der Folge im Code mit mytext.text = "abc" den dort angezeigten Text anzeigen. import kotlinx.android.synthetic.main.fragment_about.view.* und vom Plugin erzeugter synthetischer Code stellten sicher, dass der Steuerelementzugriff wie von Zauberhand funktionierte.

Die nunmehr empfohlene Vorgehensweise sieht wie folgt aus: Zuerst bauen Sie in build.gradle auf Modulebene die folgenden Zeilen ein. (Merkwürdigerweise fehlt dieser Code auch bei neu eingerichteten Projekten.) Es ist keine Referenz auf neue Bibliotheken oder Plugins erforderlich.

// Datei build.gradle (Module)
android {
    ...
    // die folgenden drei Zeilen hinzufügen, um den neuen 
    // View-Binding-Mechanismus zu aktivieren
    buildFeatures {
        viewBinding true
    }
}

In der Klassendatei für die Aktivität brauchen Sie eine neue binding-Variable. Deren Datentyp setzt sich aus dem Namen der Layout-Datei aus. Wenn es also bei einem Minimalprojekt die Datei activity_main.xml gibt, hat die resultierende Binding-Klasse den Namen ActivtyMainBinding.

Diese Variable initialisieren Sie in onCreate. In der Folge können Sie dann auf alle Steuerelemente in der Form binding.<name> zugreifen. Der Code zum Hello-World-Projekt aus Kapitel 21, wo nach dem Klick auf einen Button in einem Textfeld Datum und Uhrzeit angezeigt werden, sieht damit so aus:

// Datei MainActivity.kt
...
import info.kofler.test_as_42.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

    // layout/activity_main.xml -> ActivityMainBinding usw.
    private lateinit var binding: ActivityMainBinding

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

        // Zugriff auf Steuerelemente via binding
        binding = ActivityMainBinding.inflate(layoutInflater)

        // den folgenden Code ausführen, wenn 'mybutton' angeklickt wird
        binding.mybutton.setOnClickListener {
            val loc = Locale.getDefault()
            val fmt = DateFormat.getDateTimeInstance(
                DateFormat.LONG, DateFormat.LONG, loc)
            binding.mytext.text = "Datum und Uhrzeit:\n" + fmt.format(Date())
        }

        setContentView(binding.root)
    }
}

Die zu ändernden Code-Passagen finden Sie am schnellsten, wenn Sie die Anweisung import kotlinx.android.synthetic.main.activity_main.* auskommentieren. Alle Zugriffe auf Steuerelemente ohne binding werden dann als Fehler markiert.

Für Fragmente gilt an sich die gleiche Vorgehensweise. Allerdings ist der Zugriff auf binding erst ab dem Aufruf von onCreateView() und nur bis zum Aufruf von onDestroyView() erlaubt. Die Dokumentation empfiehlt deswegen, binding als Property zu implementieren und eine zusätzliche Variable _binding einzuführen. Der Code sieht dann so aus:

class MyFragment : Fragment(), CoroutineScope by MainScope() {

    // layout/fragment_myname.xml -> FragmentMynameBinding usw.
    private var _binding: FragmentMynameBinding? = null
    // This property is only valid between onCreateView and
    // onDestroyView!
    private val binding get() = _binding!!

    override fun onCreateView(inflater: LayoutInflater,
                              container: ViewGroup?,
                              savedInstanceState: Bundle?): View?
    {
        _binding = FragmentAboutBinding.inflate(inflater, container, false)

        // Zugriff auf Steuerelemente via binding
        binding.mytext.text = "abc"
        binding.mybutton.setOnClickListener { ... }

        return binding.root
    }
}

Ein wenig diffiziler ist die Umstellung von Adapter- und ViewHolder-Klassen zur Darstellung von Listen. Das folgende, stark gekürzte Listing zeigt die prinzipielle Vorgehensweise anhand für das Layout item_currency.xml und die zugehörige Code-Datei CurrencyAdapter.kt des Währungsumrechners (Kapitel 25):

// an die ViewHolder-Klasse binding statt view übergeben
class CurrencyViewHolder(val binding: ItemCurrencyBinding)
    : RecyclerView.ViewHolder(binding.root)
{
  val txtCurrency : TextView = binding.txtCurrency
  val imgFlag : ImageView = binding.imgFlag
}

// in der Adapter-Klasse in onCreateViewHolder mit Bindings arbeiten
class CurrencyAdapter(private val cc: CurrencyCalculator,
                      private var selectedCurrency: MutableLiveData<String>,
                      private val context: Context)
  : RecyclerView.Adapter<CurrencyViewHolder>()
{
  ...

  override fun onCreateViewHolder(
    parent: ViewGroup, viewType: Int): CurrencyViewHolder
  {
    // layout/item_currency.xml -> ItemCurrencyBinding usw.
    val binding = ItemCurrencyBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    binding.root.setOnClickListener {
      selectedCurrency.value = binding.txtCurrency.text.toString()
      notifyDataSetChanged()   // alles neu zeichnen
    }
    return CurrencyViewHolder(binding)
  }
}

Um ein vorhandenes Projekt auf die neue Binding-Technologie umzustellen, bauen Sie zuerst in build.gradle die Anweisung viewBinding true ein. Dann bauen Sie eine Activity- bzw. Fragment-Klasse nach der anderen um. (Ein Mischbetrieb zwischen der veralteten Kotlin-Android-Extension und Bindings ist erlaubt.) Zuletzt entfernen Sie die kotlin-android-extensions-Zeile aus build.gradle.

Ich habe den Währungsumrechner (Kapitel 25) innerhalb einer halben Stunde entsprechend umgestellt. Wenn man den neuen Mechanismus einmal verstanden hat, ist das keine Hexerei. Dennoch ist es natürlich ein mühsamer Prozess, und es ist ärgerlich, dass es für derartige Arbeiten keinen Assistenten gibt. Xcode bietet diesbezüglich viel mehr Komfort als Android Studio.

build.gradle (Module)

Zum Abschluss als Referenz noch die vollständige Datei build.gradle für das Modul des Währungsumrechners:

plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlinx-serialization'
    // id 'kotlin-android-extensions' -> ersetzt durch View Binding
}


android {
    compileSdkVersion 30
    defaultConfig {
        applicationId "info.kofler.currencyconverter"
        minSdkVersion 26
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    buildFeatures {
        viewBinding true
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'androidx.core:core-ktx:1.6.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
    implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'

    // Koroutinen
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'
    implementation 'com.google.code.gson:gson:2.8.6'

    // Serialisierungs-Runtime
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.2.1"
    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1"
}

Beachten Sie, dass die in der Vergangenheit übliche Zeile implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" nicht mehr erforderlich ist. Das Build-System kümmert sich selbst um den Import der Kotlin-Standardbibliothek in der richtigen Version.

Updates/Links

Download

Die aktualisierten und auf View-Bindung umgestellten Beispielprojekte der Kapitel 21 bis 25 können Sie hier herunterladen:

https://kofler.info/uploads/kotlin/kap21-25-mit-view-binding.zip

Ich habe auch den Evaluation-Client aus Kapitel 30 auf View-Binding umgestellt. Die Projekte finden Sie im ktor-Update-Artikel zum Download.

Die Kotlin-Updates-Serie

Weitere Kotlin-Update-Artikel finden Sie hier auf meiner Website:

https://kofler.info/tag/kotlin-updates