Kotlin Android Extensions

Czyli automagiczna alternatywa dla findViewById().

Nie ma co ukrywać, programowanie na Androida często wymaga od nas sporej „ceremonii” – pisania kodu, który nie jest bezpośrednio związany z funkcjonalnością naszej apki, ale architektura systemu wymaga jego napisania.

Jednym z przykładów jest pozyskiwanie w kodzie referencji do widoków zdefiniowanych w plikach layout’u – czyli znienawidzone przez wszystkich findViewById().

Poniżej fragment typowego kodu w Javie:

//Java
public class MainActivity extends Activity {
    EditText titleEdit;
    EditText authorEdit;
    EditText lyricsEdit;
    Button scrollButton;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_main);

        titleEdit  = (EditText)findViewById(R.id.titleEdit);
        authorEdit = (EditText)findViewById(R.id.authorEdit);
        lyricsEdit = (EditText)findViewById(R.id.lyricsEdit);
        scrollButton = (Button)findViewById(R.id.scrollButton);

        titleEdit.setText("Shape of You");
        authorEdit.setText("Ed Sheeran");
        lyricsEdit.setText("...");
        scrollButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                System.out.println("It works!");
            }
        });
    }
}

Jak widać, dla każdego widoku musimy wykonać trzy kroki:

  • stworzyć pole,
  • uzyskać referencję do widoku (zwracaną z metody findViewById()),
  • zrzutować ją na konkretny typ widoku

Poza sporą ilością nadmiarowego kodu, problemem jest tutaj rzutowanie w dół (ang. downcasting), które jest potencjalnym źródłem trudnych do wykrycia błędów. Po to mamy w OOP dziedziczenie i polimorfizm, żeby nie musieć robić takich hacków, prawda?

Słyszałem nawet kiedyś taką zasadę dotyczącą programowania obiektowego, że:

Jeśli musisz rzutować w dół, to znaczy że Twoja architektura jest do d…

Także ten… Android już na starcie wymusza złe wzorce 😉

Oczywiście, z biegiem czasu powstały różne obejścia typu ButterKnife, w którym każdą referencję do widoku oznaczamy specjalną adnotacją. Jednak nadal musimy ręcznie stworzyć pole w klasie, i oznaczyć je adnotacją.

Kotlin to the rescue!

W Kotlinie – jak zwykle – da się to zrobić lepiej 🙂 W ramach zachęty pokażę efekt końcowy, który wygląda tak:

//Kotlin + Android Extensions
import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        titleEdit.setText("Shape of You")
        authorEdit.setText("Ed Sheeran")
        lyricsEdit.setText("...")
        scrollButton.setOnClickListener { 
            println("It works!")
        }
    }
}

O co chodzi? Co tu się dzieje?

Otóż korzystamy z pluginu o nazwie Kotlin Android Extensions. Wykorzystuje on bardzo sprytnie możliwości, jakie dają w Kotlinie rozszerzenia. Jego działanie polega na tym właśnie, że rozszerza naszą Aktywność o właściwości (properties), będące referencjami do widoków zdefiniowanych w pliku layout’u (tutaj: activity_main.xml). Dzięki temu nasze widoki są automatycznie „wstrzykiwane” do kodu, bez konieczności definiowania pól, oznaczania ich adnotacjami itp.

Konfiguracja projektu

Konfiguracja jest banalna, ponieważ ten plugin jest częścią pluginu „Kotlin”, który zainstalowaliśmy poprzednio. Więc wystarczy dodać poniższą linię do pliku build.gradle:

apply plugin: 'kotlin-android-extensions'

I to wszystko.

Jak tego używać?

Również bardzo prosto. Musimy tylko w klasie Aktywności zaimportować rozszerzenia widoków z odpowiedniego layoutu. Robimy to poniższą linią:

import kotlinx.android.synthetic.main.<layout>.*

…gdzie <layout> zastępujemy nazwą konkretnego pliku .xml.

Tak więc, jeśli mamy layout o nazwie activity_main, a w nim taki widok:

<!-- activity_main.xml -->
<EditText
    android:id="@+id/titleEdit"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

…to w pliku Aktywności robimy następujący import:

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

//...

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}

Oczywiście musimy pamiętać, aby importowane rozszerzenie dotyczyło tego samego layout’u, który ustawiamy jako content.

Dzięki temu, w kodzie otrzymujemy dostęp do właściwości o nazwie identycznej jak ID widoku:

println(titleEdit)
val title = titleEdit.text
titleEdit.setText("Shape of You")

Fragmenty

Działa to nie tylko dla Aktywności, ale i dla Fragmentów. W tym przypadku należy pamiętać, że nadal musimy ręcznie zrobić inflate(), czyli:

import kotlinx.android.synthetic.main.fragment_main.*

//...

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
	return inflater.inflate(R.layout.fragment_main, container, false)
}

Adaptery

Poza Aktywnościami i Fragmentami, wystarczy że mamy referencję do Widoku głównego. Przydaje się to w przypadku Adapterów. Możemy wtedy rozszerzyć typ View jak poniżej:

import kotlinx.android.synthetic.main.<layout>.view.*

//...
rootView.titleEdit.setText("Shape of You")

Zwróć uwagę na trochę inny import: ...view.*

Czy można jeszcze lepiej?

Pewnie ktoś powie: „Łee… Trzeba ręcznie wpisać import… Jakie lamerstwo…”. Zgadzam się. Dlatego można lepiej 😉

Wystarczy zacząć wpisywać ID dowolnego widoku, aby IntelliJ zaczął sam podpowiadać:

Wciskamy Enter, i import dodaje się sam 🙂

Co dalej?

Jeśli nie czytałeś poprzedniego wpisu – Android + Kotlin. Jak zacząć? – zapraszam do lektury 🙂

W innym wypadku polecam oficjalną dokumentację, oraz praktyczne ćwiczenia w interaktywnym IDE w przeglądarce.

To tyle na dziś. Do następnego wpisu!

Nie przegap kolejnych wpisów - subskrybuj!

2 komentarzy

  1. Czy w ten sposób można także użyć nazw widoków w adapterze do InfoWindow od Google Maps? GoogleMap.InfoWindowAdapter <- ten interfejs konkretnie mnie interesuje
    Trzeba w nim zaimplementować 2 metody, w tym jedna która zwraca widok. Sposób dla 'typowych' adapterów, tj. import kotlinx.android.synthetic.main..view.* nie działa w tym przypadku.

    1. Cześć, Adam 🙂 Nie wiem czy do końca rozumiem Twój problem, ale spróbuję… W przypadku adapterów robimy tak: 1) przekazujesz referencję do widoku głównego (rootView) do swojej implementacji adaptera (np. w konstruktorze), 2) robisz odpowiedni import `kotlinx…view.*`, 3) referencja do rootView jest teraz rozszerzona o odpowiednie widoki.

      To tak naprawdę działa dla dowolnej klasy – nie tylko adapterów. Ważne tylko, żeby przekazać sobie poprawną referencję do widoku (np. czy w momencie przekazania nie jest ona null’em, bo widok nie został jeszcze inflate’owany?).

Dodaj komentarz