Przyspiesz pisanie kodu z Kotlin Extensions

Pewnie nie raz słyszałeś, że tworząc aplikacje na Androida, warto korzystać z języka Kotlin. Przyspieszy on pracę nad kodem. To stwierdzenie jest jak najbardziej prawdziwe. Z tą różnicą, że nie pozbędziesz się starych nawyków, chyba że wspomożesz się dodatkami takimi jak Kotlin Extensions.

Tworząc kurs RecyclerView w Androidzie, opierałem się na tym rozwiązaniem. Dziś wyjaśnię Ci czym on jest i dlaczego warto z niego korzystać.

Czym jest Kotlin Extensions?

Po pierwsze, to wtyczka do Kotlina, która pozwoli na łączenie widoków z pliku XML z kodem w aktywności lub fragmencie.

Rozszerzenie wygeneruje dodatkowy kod, który pozwoli Ci uzyskać dostęp do widoków w układzie XML, tak jakby były one właściwościami o nazwie id używanej w definicji układu.

Tworzy także pamięć podręczną widoku lokalnego. Przy pierwszym użyciu właściwości. Wywoła znaną metodę findViewById(). Następnym razem, gdy widok przy skorzystaniu z danego widety, element zostanie odzyskany z pamięci podręcznej, więc dostęp będzie szybszy.

Jak tej wtyczki używać?

Domyślnie wtyczka jest zintegrowana z Android Studio. Podczas tworzenia nowego projektu z obsługą Kotlina. Odpowiedni wpis jest dodawana do pliku gradle. Jeżeli nie ma takiego zapisu z różnych przyczyn, musisz zrobić to ręcznie. W tym celu do pliku build.gradle (Module) dodaj:

apply plugin: 'kotlin-android-extensions'

Teraz wystarczy pozwolić Gradle zbudować projekt i to wszystko, czego potrzebujesz! Jesteś teraz gotowy, aby zacząć pracować z Kotlin Extensions.

Jak działa Kotlin Extensions?

Jeżeli miałeś styczność z procesem tworzenia aplikacji dla Androida, doskonale znasz metodę findViewById(). Od tego momentu nie ma potrzeby jej wywoływania. Teraz wystarczy, że użyjesz identyfikatora widoku zdefiniowanego w pliku XML bezpośrednio w aktywności.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <EditText
            android:id="@+id/editText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Your Name"
            android:gravity="center"
    />

    <Button
            android:id="@+id/button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Send"
    />

    <TextView
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center"
            android:textSize="24sp"
    />
</LinearLayout>
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // Old method
        val textView_old = findViewById<TextView>(R.id.textView)
        textView_old.setText("Maciej")
        
        // New method
        val textView_new = textView
        textView_old.setText("Maciej")
        
        // Better
        textView.text = "Maciej"
    }
}

Jak widzisz, skróciliśmy sobie drogę maksymalnie i wystarczy tylko jedna linijka 🙂 Nie tylko zyskujesz szybkość pisania kodu, ale także jego czytelność.

Nie musisz używać metody findViewById() ani adnotacji @BindView – jeżeli korzystasz z biblioteki ButterKnife. Jedna linia kodu dla każdego użytego widoku. Brzmi wspaniale!

Jeśli używałeś w przeszłości Butter Knife, będziesz wiedział, że pomaga to również w wiązaniu widoków. Jednak wiązanie odbywa się w inny sposób. Więcej o tym dodatku pisałem tutaj.

Wracając do naszego przykładu, IDE automatycznie doda import.

import kotlinx.android.synthetic.main.activity_main.*

Jest to informacja dla kompilatora, który będzie wiedział, że chcesz skorzystać z odpowiedniego rozszerzenia. Zobaczmy, co kryje się pod maską.

Buduj aplikacje na system Android za pomocą komponentów:

Kotlin Extensions pod maską

Android Studio posiada bardzo przydatną funkcję, która pozwala nam sprawdzić, jak będzie się prezentował nasz kod po kompilacji. W tym celu wybierz z menu: Tools -> Kotlin -> Show Kotlin Bytecode.

Decompile Kotlin Bytecode

Na obrazie po prawej stronie masz częściowy widok bytecodu. Taki zapis oczywiście jest nieczytelny. Dlatego za pomocą przycisku Decompile, możemy skonwertować nasz kod do kodu w Javie. Otrzymamy nastepujący wynik:

public final class MainActivity extends AppCompatActivity {
  
  private HashMap _$_findViewCache;

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(-1300009);
      ((Button)this._$_findCachedViewById(id.button)).setOnClickListener((OnClickListener)(new OnClickListener() {
         public final void onClick(View text) {
            EditText var10000 = (EditText)MainActivity.this._$_findCachedViewById(id.editText);
            Intrinsics.checkExpressionValueIsNotNull(var10000, "editText");
            Editable name = var10000.getText();
            TextView var3 = (TextView)MainActivity.this._$_findCachedViewById(id.textView);
            Intrinsics.checkExpressionValueIsNotNull(var3, "textView");
            var3.setText((CharSequence)name);
         }
      }));
   }

   public View _$_findCachedViewById(int var1) {
      if (this._$_findViewCache == null) {
         this._$_findViewCache = new HashMap();
      }

      View var2 = (View)this._$_findViewCache.get(var1);
      if (var2 == null) {
         var2 = this.findViewById(var1);
         this._$_findViewCache.put(var1, var2);
      }

      return var2;
   }

   public void _$_clearFindViewByIdCache() {
      if (this._$_findViewCache != null) {
         this._$_findViewCache.clear();
      }

   }
}

Kto pracował z Java, to po pewnej chwili ogarnie kod. A jeżeli nie rozumiesz, to już spieszę z pomocą.

Na początku znajduje się prywatna zmienna _$_findViewCache, która wykorzystuje HashMap do budowania cache dla widoków. Oto pamięć podręczna widoku, o której wspomniałem wcześniej.

Gdy aplikacja będzie potrzebować widoku, spróbuje go znaleźć w pamięci podręcznej. Jeśli go tam nie ma, znajdzie go i doda do pamięci podręcznej. Całkiem proste.

Mamy także funkcję clearFindViewByIdCache() do czyszczenia pamięci podręcznej. Może przydać się, gdy musisz odbudować widoki, ponieważ stare widoki nie będą już ważne. Funkcję tą wykorzystuje się w czasie korzystania z fragmentów.

Kotlin Exstensions we fragmentach

Jeżeli korzystamy z fragmentów, może się zdarzyć sytuacja, gdzie widok ulegnie zmianie, a instancja obiektu fragmentu pozostanie nienaruszona. Oznacza to, że widoki w pamięci podręcznej nie będą już ważne.

Jeżeli stworzymy kod fragmentu w Kotlinie, a następnie zdekompilujemy go możemy zobaczyć różnice w stosunku do aktywności.

// $FF: synthetic method
public void onDestroyView() {
   super.onDestroyView();
   this._$_clearFindViewByIdCache();
}

W chwili usunięcia widoku następuje automatyczne wyczyszczenie cache. Dzieje się tak, gdyż, widoki znajdujące się w pamięci podręcznej i nie będą już potrzebne. Dlatego najlepiej będzie usunąć je z pamięci podręcznej i utworzyć je ponownie.

Edycja cache

Korzystając z adnotacji @ContainerOptions możemy określi, w jaki sposób będzie działać pamięć podręczna dla naszych widoków. Domyślną implementacją dla cache jest HashMap.

Jeśli wolisz używać SparseArray, możesz dodać adnotację w aktywności, fragmencie:

@ContainerOptions(CacheImplementation.SPARSE_ARRAY)

Podczas kompilacji cache zostanie ustawiony na SparseArray. Jest to wolniejszy typ, ale będzie potrzebował mniejszych zasobów pamięci.

Również możesz całkowicie zrezygnować z tej opcji, wstawiając NO_CACHE.

Istnieje także możliwość ustawienia strategii buforowania na poziomie modułu aplikacji. Ustawiając wartość defaultCacheImplementation w pliku build.gradle:

androidExtensions {
    defaultCacheImplementation = "SPARSE_ARRAY" // or HASH_MAP, NONE
}

LayoutContainer

Jak widzisz, łatwo jest uzyskać dostęp do widoków o właściwościach syntetycznych przy użyciu rozszerzenia Kotlin Extensions. Dotyczy to zarówno aktywności, jak i fragmentów.

W przypadku ViewHoldera (lub dowolnej klasy, która ma widok kontenera), możesz zaimplementować interfejs LayoutContainer, aby umieścić widoki w pamięci podręcznej.

Standardowo ViewHolder może wyglądać tak:

class MyViewHolder( val containerView: View) : RecyclerView.ViewHolder(containerView) {

        fun bind(text: String){
            containerView.rc_textview1.text = text
            containerView.rc_textview2.text = text
            containerView.rc_textview3.text = text
        }

}

Dekompilując kod bajtowy Kotlina na Javę, możesz zobaczyć, że findViewById() jest wywoływany. Teraz zróbmy to wydajniej:

W pliku build.gradle musisz ustawić odpowiednią flagę, która da nam dostęp do funkcji eksperymentalnych:

androidExtensions {
    experimental = true
}
class MyViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView), LayoutContainer {

        fun bind(text: String){
            rc_textview1.text = text
            rc_textview2.text = text
            rc_textview3.text = text
        }
}

Teraz odwróć kod i sprawdź. Co się stało? Widzisz różnice? Po zaimplementowaniu interfejsu LayoutContainer generowana jest zmienna findViewCache i odpowiadające jej metody findCachedViewById clearFindViewByIdCache. I oto nam chodziło 🙂

info:

Pamiętaj, że to jest funkcja eksperymentalna i może w niektórych warunkach zachować się nietypowo.

Implementacja @Parcelize

Parcelable to interfejs tylko dla Androida, który służy do serializacji klasy, dzięki czemu jej właściwości mogą być przenoszone z jednej aktywności do drugiej. Czyli jest to proces kopiowania danych między aktywnościami, a następnie ponownego tworzenia obiektu po drugiej stronie.

Aby użyć tej funkcjonalności, klasa powinna być implementowana za pomocą interfejsu Parcelable. Dodaje metody do klasy, którą chcesz przenieść. Dodawane funkcje wykonują pracę polegającą na konstruowaniu i dekonstruowaniu obiektu w aktywności. Parcelable składa się za następujących rzeczy:

  • konstruktora,
  • metody writeToParcel() — w tej metodzie dodajesz wszystkie właściwości klasy, które są potrzebne do przeniesienia,
  • metody describeContents() — ta metoda opisuje czy w klasie jest jakiś specjalny obiekt. W większości przypadków zwracamy 0. Inne wartości są używane w nietypowych przypadkach,
  • obiektu CREATOR — składa się z dwóch metod:
    • createFromParcel() — metoda odczytuje naszą klasę z obiektu typu Parcel.
    • newArray() — tworzy nową tablicę klasy Parcelable.

Brzmi dziwnie? Prawda. Mamy za sobą szybką powtórkę z Parcelable. Zobaczmy jak może wyglądać taka klasa.

class User2(val id: Int, val name: String?, val city: String?  ) : Parcelable {
    constructor(parcel: Parcel) : this(
        parcel.readInt(),
        parcel.readString(),
        parcel.readString()
    ) {
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeInt(id)
        parcel.writeString(name)
        parcel.writeString(city)
    }

    override fun describeContents(): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<User2> {
        override fun createFromParcel(parcel: Parcel): User2 {
            return User2(parcel)
        }

        override fun newArray(size: Int): Array<User2?> {
            return arrayOfNulls(size)
        }
    }

}

Pisanie Parcelable jest procesem czasochłonnym i podatnym na błędy. Gdybyśmy pisali w języku Java, musielibyśmy jeszcze dopisać gettery i settery. Oczywiście można skorzystać z rozszerzenia AutoValue, które pomoże w automatycznym generowaniu takich klas.

Lepszym pomysłem jest skorzystanie z Kotlin Extensions. Pomorze Ci zaimplementować Parcelable, używając adnotacji @Parcelize.

@Parcelize
class User(val id: Int, val name : String, val city : String  ) : Parcelable {}

A teraz plugin wykona za nas całą żmudą pracę 🙂

Adnotacja @Parcelize wymaga, aby wszystkie pola klasy przeznaczone do serializacji, zostały umieszczone w głównym konstruktorze.

Teraz przy tworzeniu Intenta wystarczy dodać obiekt.

val user = User(1, "Maciej", "Krakow")
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("data", user)
startActivity(intent)

Po drugiej stronie, odbieramy w następujący sposób:

val user = intent.getParcelableExtra<User>("data")
val id = user.id
val name = user.name
val city = user.city

Prosto i przyjemnie i mało kodu, prawda?

Jeśli Twoja klasa wymaga bardziej zaawansowanej logiki serializacji, możesz zrobić to tak:

@Parcelize
class User(var id: Int, val name : String, val city : String  ) : Parcelable {

    private companion object : Parceler<User> {
        override fun User.write(parcel: Parcel, flags: Int) {
            // Custom write implementation
        }
        
        override fun create(parcel: Parcel): User {
            // Custom read implementation
        }
    }
}

Podsumowanie

Dzięki prostej wtyczce Kotlin Extensions możesz przyspieszyć pisanie kodu oraz jego czytelność. Oczywiście to nie wszystkie jej możliwości. Opisałem najczęstsze przypadki, z których na pewno skorzystasz. W przyszłości mogą pojawić się nowe funckjonalności. Wtyczka również wspiera tworzenie różnych wersji aplikacji oraz pomoc w tworzeniu niestandardowych widoków. Te tematy na osobny wątek. Dlatego bądź pierwszą osobą, która się o tym dowiesz i zapisz się na [sc_signup_newsletter]newsletter MYENV [/sc_signup_newsletter].

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂