W tym temacie omawiamy niektóre z najbardziej przydatnych aspektów języka Kotlin podczas tworzenia aplikacji na Androida.
Praca z fragmentami
W kolejnych sekcjach znajdziesz Fragment przykłady, które pokazują niektóre z najlepszych funkcji języka Kotlin.
Dziedziczenie
Klasę w języku Kotlin możesz zadeklarować za pomocą słowa kluczowego class. W tym przykładzie LoginFragment jest podklasą Fragment. Dziedziczenie możesz wskazać, używając operatora : między podklasą a jej klasą nadrzędną:
class LoginFragment : Fragment()
W tej deklaracji klasy LoginFragment odpowiada za wywołanie konstruktora swojej klasy nadrzędnej Fragment.
W LoginFragment możesz zastąpić kilka wywołań zwrotnych cyklu życia, aby reagować na zmiany stanu w Fragment. Aby zastąpić funkcję, użyj słowa kluczowego override, jak pokazano w tym przykładzie:
override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.login_fragment, container, false)
}
Aby odwołać się do funkcji w klasie nadrzędnej, użyj słowa kluczowego super, jak pokazano w tym przykładzie:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
}
Dopuszczalność wartości null i inicjowanie
W poprzednich przykładach niektóre parametry w zastąpionych metodach mają typy z sufiksem w postaci znaku zapytania ?. Oznacza to, że argumenty przekazywane dla tych parametrów mogą mieć wartość null. Pamiętaj, aby bezpiecznie obsługiwać wartości null.
W Kotlinie musisz zainicjować właściwości obiektu podczas jego deklarowania.
Oznacza to, że gdy uzyskasz instancję klasy, możesz od razu odwołać się do dowolnej z jej dostępnych właściwości. Obiekty View w Fragment nie są jednak gotowe do rozpakowania, dopóki nie zostanie wywołana funkcja Fragment#onCreateView, więc musisz mieć możliwość odroczenia inicjowania właściwości dla View.
Właściwość lateinit umożliwia odroczenie inicjowania właściwości. Jeśli używasz lateinit, jak najszybciej zainicjuj usługę.
Ten przykład pokazuje, jak używać lateinit do przypisywania obiektów View w onViewCreated:
class LoginFragment : Fragment() {
    private lateinit var usernameEditText: EditText
    private lateinit var passwordEditText: EditText
    private lateinit var loginButton: Button
    private lateinit var statusTextView: TextView
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        usernameEditText = view.findViewById(R.id.username_edit_text)
        passwordEditText = view.findViewById(R.id.password_edit_text)
        loginButton = view.findViewById(R.id.login_button)
        statusTextView = view.findViewById(R.id.status_text_view)
    }
    ...
}
Konwersja SAM
Aby nasłuchiwać zdarzeń kliknięcia w Androidzie, zaimplementuj interfejs OnClickListener. Obiekty Button zawierają funkcję setOnClickListener(), która przyjmuje implementację interfejsu OnClickListener.
OnClickListener ma jedną metodę abstrakcyjną, onClick(), którą musisz zaimplementować. Ponieważ setOnClickListener() zawsze przyjmuje OnClickListener jako argument, a OnClickListener zawsze ma tę samą pojedynczą metodę abstrakcyjną, tę implementację można przedstawić za pomocą funkcji anonimowej w języku Kotlin. Ten proces jest nazywany konwersją do pojedynczej metody abstrakcyjnej lub konwersją SAM.
Konwersja SAM może znacznie uprościć kod. W przykładzie poniżej pokazujemy, jak za pomocą konwersji SAM wdrożyć OnClickListener w przypadku Button:
loginButton.setOnClickListener {
    val authSuccessful: Boolean = viewModel.authenticate(
            usernameEditText.text.toString(),
            passwordEditText.text.toString()
    )
    if (authSuccessful) {
        // Navigate to next screen
    } else {
        statusTextView.text = requireContext().getString(R.string.auth_failed)
    }
}
Kod w funkcji anonimowej przekazanej do setOnClickListener() jest wykonywany, gdy użytkownik kliknie loginButton.
Obiekty towarzyszące
Obiekty towarzyszące umożliwiają definiowanie zmiennych lub funkcji, które są koncepcyjnie powiązane z typem, ale nie są powiązane z konkretnym obiektem. Obiekty towarzyszące są podobne do używania słowa kluczowego static w przypadku zmiennych i metod w języku Java.
W tym przykładzie TAG jest stałą String. Nie potrzebujesz unikalnego wystąpienia String dla każdego wystąpienia LoginFragment, więc zdefiniuj je w obiekcie towarzyszącym:
class LoginFragment : Fragment() {
    ...
    companion object {
        private const val TAG = "LoginFragment"
    }
}
Możesz zdefiniować TAG na najwyższym poziomie pliku, ale plik może też zawierać dużą liczbę zmiennych, funkcji i klas, które również są zdefiniowane na najwyższym poziomie. Obiekty towarzyszące pomagają łączyć zmienne, funkcje i definicję klasy bez odwoływania się do konkretnej instancji tej klasy.
Przekazywanie dostępu do usługi
Podczas inicjowania właściwości możesz powtarzać niektóre z najczęstszych wzorców Androida, np. uzyskiwanie dostępu do ViewModel w ramach Fragment. Aby uniknąć nadmiaru zduplikowanego kodu, możesz użyć składni przekazywania właściwości w Kotlinie.
private val viewModel: LoginViewModel by viewModels()
Delegowanie właściwości zapewnia wspólną implementację, której możesz używać w całej aplikacji. Android KTX udostępnia kilka delegatów właściwości.
Na przykład viewModels pobiera ViewModel, który jest ograniczony do bieżącego Fragment.
Delegowanie właściwości korzysta z odbicia, co wiąże się z pewnym obciążeniem wydajności. W zamian otrzymujesz zwięzłą składnię, która pozwala zaoszczędzić czas programowania.
Dopuszczalność wartości null
Kotlin zapewnia ścisłe reguły dopuszczania wartości null, które utrzymują bezpieczeństwo typów w całej aplikacji. W Kotlinie odwołania do obiektów domyślnie nie mogą zawierać wartości null. Aby przypisać do zmiennej wartość null, musisz zadeklarować typ zmiennej dopuszczającej wartość null, dodając znak ? na końcu typu podstawowego.
Na przykład to wyrażenie jest w Kotlinie nieprawidłowe. name jest typu String i nie może mieć wartości null:
val name: String = null
Aby zezwolić na wartość null, musisz użyć typu String, String?, który dopuszcza wartość null, jak pokazano w tym przykładzie:
val name: String? = null
Interoperacyjność
Rygorystyczne zasady języka Kotlin sprawiają, że kod jest bezpieczniejszy i zwięźlejszy. Te reguły zmniejszają ryzyko wystąpienia NullPointerException, które mogłoby spowodować awarię aplikacji. Ponadto zmniejszają one liczbę sprawdzeń wartości null, które musisz wykonać w kodzie.
Podczas pisania aplikacji na Androida często musisz też wywoływać kod inny niż Kotlin, ponieważ większość interfejsów API Androida jest napisana w języku programowania Java.
Nullability to kluczowy obszar, w którym Java i Kotlin różnią się w działaniu. Java jest mniej rygorystyczna w zakresie składni dopuszczalności wartości null.
Na przykład klasa Account ma kilka właściwości, w tym właściwość String o nazwie name. Java nie ma reguł Kotlin dotyczących możliwości przyjmowania wartości null, zamiast tego opiera się na opcjonalnych adnotacjach o możliwości przyjmowania wartości null, aby wyraźnie zadeklarować, czy można przypisać wartość null.
Platforma Androida jest napisana głównie w języku Java, więc możesz napotkać tę sytuację podczas wywoływania interfejsów API bez adnotacji o możliwości wystąpienia wartości null.
Typy platform
Jeśli używasz języka Kotlin do odwoływania się do nieoznaczonego elementu name zdefiniowanego w klasie Account w języku Java, kompilator nie wie, czy element String jest mapowany na String czy String? w języku Kotlin. Ta niejednoznaczność jest reprezentowana przez typ platformy, String!.
String! nie ma specjalnego znaczenia dla kompilatora języka Kotlin. String! może reprezentować String lub String?, a kompilator umożliwia przypisanie wartości dowolnego z tych typów. Pamiętaj, że jeśli przedstawisz typ jako String i przypiszesz wartość null, możesz spowodować błąd NullPointerException.
Aby rozwiązać ten problem, podczas pisania kodu w Javie używaj adnotacji o możliwości przyjmowania wartości null. Te adnotacje pomagają deweloperom korzystającym z języków Java i Kotlin.
Oto na przykład klasa Account zdefiniowana w języku Java:
public class Account implements Parcelable {
    public final String name;
    public final String type;
    private final @Nullable String accessId;
    ...
}
Jedna ze zmiennych składowych, accessId, jest oznaczona adnotacją @Nullable, co oznacza, że może zawierać wartość null. Kotlin traktuje wtedy accessId jako String?.
Aby wskazać, że zmienna nigdy nie może mieć wartości null, użyj adnotacji @NonNull:
public class Account implements Parcelable {
    public final @NonNull String name;
    ...
}
W tym przypadku name jest w Kotlinie traktowane jako String, które nie może mieć wartości null.
Adnotacje dotyczące możliwości przyjmowania wartości null są uwzględnione we wszystkich nowych interfejsach API Androida i wielu istniejących interfejsach API Androida. Wiele bibliotek Java ma teraz adnotacje o możliwości przyjmowania wartości null, aby lepiej obsługiwać deweloperów Kotlin i Java.
Obsługa wartości null
Jeśli nie masz pewności co do typu Java, uznaj, że może on przyjmować wartość null.
Na przykład element name klasy Account nie jest oznaczony, więc należy założyć, że jest to dopuszczający wartość null String?.
Jeśli chcesz przyciąć wartość name, aby nie zawierała spacji na początku ani na końcu, możesz użyć funkcji trim w języku Kotlin. Możesz bezpiecznie przyciąć film na kilka sposobów.String? Jednym z nich jest użycie operatora potwierdzenia, że wartość nie jest null, !!, jak pokazano w tym przykładzie:
val account = Account("name", "type")
val accountName = account.name!!.trim()
Operator !! traktuje wszystko po lewej stronie jako wartość niezerową, więc w tym przypadku traktujesz name jako niezerową wartość String. Jeśli wynik wyrażenia po lewej stronie jest wartością null, aplikacja zgłasza wyjątek NullPointerException.
Ten operator jest szybki i łatwy w użyciu, ale należy go stosować oszczędnie, ponieważ może on ponownie wprowadzić do kodu instancje NullPointerException.
Bezpieczniejszym wyborem jest użycie operatora bezpiecznego wywołania ?., jak pokazano w tym przykładzie:
val account = Account("name", "type")
val accountName = account.name?.trim()
Jeśli name nie jest wartością null, wynik name?.trim() to wartość nazwy bez spacji na początku i na końcu. Jeśli wartość name to null, wynikiem działania funkcji name?.trim() jest null. Oznacza to, że podczas wykonywania tego polecenia aplikacja nigdy nie zgłosi wyjątku NullPointerException.
Operator bezpiecznego wywołania chroni przed potencjalnym błędem NullPointerException, ale przekazuje wartość null do następnej instrukcji. Zamiast tego możesz od razu obsługiwać przypadki wartości null, używając operatora Elvisa (?:), jak pokazano w tym przykładzie:
val account = Account("name", "type")
val accountName = account.name?.trim() ?: "Default name"
Jeśli wynikiem wyrażenia po lewej stronie operatora Elvisa jest wartość null, wartość po prawej stronie jest przypisywana do accountName. Ta technika przydaje się do podawania wartości domyślnej, która w przeciwnym razie byłaby zerowa.
Możesz też użyć operatora Elvisa, aby wcześniej zakończyć działanie funkcji, jak pokazano w tym przykładzie:
fun validateAccount(account: Account?) {
    val accountName = account?.name?.trim() ?: "Default name"
    // account cannot be null beyond this point
    account ?: return
    ...
}
Zmiany w interfejsie API Androida
Interfejsy API Androida są coraz bardziej przyjazne dla języka Kotlin. Wiele najpopularniejszych interfejsów API Androida, w tym AppCompatActivity i Fragment, zawiera adnotacje o możliwości wystąpienia wartości null, a niektóre wywołania, np. Fragment#getContext, mają bardziej przyjazne dla Kotlina alternatywy.
Na przykład dostęp do Context elementu Fragment prawie zawsze zwraca wartość inną niż null, ponieważ większość wywołań wykonywanych w Fragment ma miejsce, gdy Fragment jest dołączony do Activity (podklasy Context). Mimo to Fragment#getContext nie zawsze zwraca wartość inną niż null, ponieważ w niektórych przypadkach Fragment nie jest dołączony do Activity. Dlatego typ zwracany funkcji Fragment#getContext może mieć wartość null.
Ponieważ wartość Context zwracana przez Fragment#getContext może mieć wartość null (i jest oznaczona adnotacją @Nullable), w kodzie Kotlin musisz traktować ją jako Context?.
Oznacza to zastosowanie jednego z wymienionych wcześniej operatorów do adresu, który może mieć wartość null, przed uzyskaniem dostępu do jego właściwości i funkcji. W niektórych z tych przypadków Android zawiera alternatywne interfejsy API, które zapewniają tę wygodę.
Fragment#requireContext zwraca na przykład wartość niezerową Context i zgłasza wyjątek IllegalStateException, jeśli zostanie wywołana w sytuacji, w której Context miałaby wartość null. Dzięki temu możesz traktować wynikowy typ Context jako niepusty bez konieczności używania operatorów bezpiecznego wywołania lub obejść.
Inicjowanie właściwości
Właściwości w języku Kotlin nie są domyślnie inicjowane. Muszą być inicjowane w momencie inicjowania klasy, w której się znajdują.
Właściwości można inicjować na kilka sposobów. Poniższy przykład pokazuje, jak zainicjować zmienną index, przypisując jej wartość w deklaracji klasy:
class LoginFragment : Fragment() {
    val index: Int = 12
}
Inicjowanie można też zdefiniować w bloku inicjatora:
class LoginFragment : Fragment() {
    val index: Int
    init {
        index = 12
    }
}
W przykładach powyżej zmienna index jest inicjowana podczas tworzenia zmiennej LoginFragment.
Może się jednak zdarzyć, że niektóre właściwości nie będą mogły zostać zainicjowane podczas tworzenia obiektu. Możesz na przykład odwołać się do View w Fragment, co oznacza, że układ musi zostać najpierw rozszerzony. Inflacja nie występuje, gdy tworzony jest Fragment. Zamiast tego jest on zawyżany podczas połączeń na numer Fragment#onCreateView.
Jednym ze sposobów rozwiązania tego problemu jest zadeklarowanie widoku jako dopuszczającego wartość null i jak najszybsze zainicjowanie go, jak pokazano w tym przykładzie:
class LoginFragment : Fragment() {
    private var statusTextView: TextView? = null
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView?.setText(R.string.auth_failed)
    }
}
Działa to zgodnie z oczekiwaniami, ale od teraz musisz zarządzać możliwością przyjmowania wartości null w przypadku każdego odwołania do View. Lepszym rozwiązaniem jest użycie lateinit do Viewinicjowania, jak w tym przykładzie:
class LoginFragment : Fragment() {
    private lateinit var statusTextView: TextView
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            statusTextView = view.findViewById(R.id.status_text_view)
            statusTextView.setText(R.string.auth_failed)
    }
}
Słowo kluczowe lateinit pozwala uniknąć inicjowania właściwości podczas tworzenia obiektu. Jeśli odwołasz się do właściwości przed jej zainicjowaniem, Kotlin zgłosi wyjątek UninitializedPropertyAccessException. Dlatego jak najszybciej zainicjuj właściwość.
