W jaki sposób RxKotlin i Android Data Binding pomogły mi rozwiązać problem wieloklików?

Podczas pisania aplikacji mobilnych często spotykałem się z problemem obsługi kilku szybkich kliknięć przez użytkownika. Jeżeli przycisk ten prowadził do nowego widoku to bez obsługi tej sytuacji, pojawiała się kolejka tych samych żądań. W efekcie ekran docelowy pojawiał się kilka razy. Często radziłem sobie z tym po prostu blokując przycisk po pierwszym kliknięciu. Jednak ostatnio zastanawiałem się jak wykorzystać programowanie reaktywne i jednocześnie korzystać z benefitów data bindingu na Androidzie. O tym jest ten wpis.

Pokażę Ci jak połączyć te dwie rzeczy i uzyskać zamierzony efekt. A to wszystko krok po kroku.

Android Data Binding

Stwórz nowy interfejs CustomClicks oraz implementację CustomClicksImpl. Będzie on zarządzała naszym custom bindingiem.

interface ClickBindings
{
    @BindingAdapter("rxClick")
    fun bindClick(view: View, listener: ClickListener)

    interface ClickListener
    {
        fun onClick()
    }
}

class ClickBindingsImpl(private val lifecycle: Lifecycle) : ClickBindings, LifecycleObserver
{
    private var disposables = CompositeDisposable()

    init
    {
        lifecycle.addObserver(this)
    }

    override fun bindClick(view: View, listener: ClickBindings.ClickListener)
    {
        RxView.clicks(view)
                .throttleFirst(2, TimeUnit.SECONDS)
                .subscribe { view ->
                    listener.onClick()
                }.addTo(disposables)

    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun dispose()
    {
        lifecycle.removeObserver(this)
        disposables.clear()
    }
}

Zwróć uwagę na interfejs. Zawiera on w sobie definicję drugiego, którego nazywa się ClickListener. To on będzie używany do ‘przekazania’ kliknięcia do modelu widoku (najczęściej to z nim będziesz tworzył wiązanie). 

Poniżej dwie zasady, którymy się kieruję podczas tworzenia własnego bindingu:

  1. Tworzę interfejs, w którym dodaje metody, które okraszam atrybutem. Jego parametr decyduje o nazwie właściwości, z której skorzystamy w widoku. 
  2. Dodaję implementację tego interfejsu, a w nim dbam o zarządzanie subskrypcjami. Dlatego korzystam z LifecycleObserver. Obiekt Lifecycle, który jest przekazywany w konstruktorze, będzie tym samym, który jest we fragmencie, w którym będziemy korzystać z bindingu (wkrótce Ci to pokażę). Dzięki temu, kiedy fragment zostanie zniszczony, subskrypcje dotyczące klików zostaną także usunięte. A stanie się to dlatego, że w metodzie init dodajemy obserwowanie tegoż właśnie lifecycle. Metoda dispose (nazwa dowolna) zostanie wywołana przy zniszczeniu fragmentu, a to dzięki atrybutowi OnLifecycleEvent z parametrem ON_DESTROY.

Na pierwszy rzut oka może wydawać się to zawiłe, ale przeanalizuj kod na spokojnie. Pamiętaj, że zawsze możesz napisać do mnie  🤗

W metodzie bindClick subskrybujemy się na kliknięcia widoku (zwróć uwagę, że nie musimy ograniczać się tylko do przycisków). Dzięki użyciu throttleFirst i przekazania czasu blokujemy kliknięcia, jeżeli poprzednie nastąpiło mniej niż 2 sekundy temu.

Krótki kod a jaki skuteczny! Zastanów się co musiałbyś zrobić jeżeli chciałbyś napisać własną implementację takiego zachowania.

Czekałoby Cię piekło timerów!

LifecycleComponent

Czas na dodanie czegoś co będzie zarządzało naszym custom bindingiem. Przecież musimy przekazać obiekt opisujący cykl życia fragmentu, w którym skorzystamy z bindingu. Proszę państwa, oto LifecycleComponent

class LifecycleComponent(private val lifecycle: Lifecycle) : DataBindingComponent, LifecycleObserver
{
    init
    {
        lifecycle.addObserver(this)
    }

    override fun getClickBindings(): ClickBindings
    {
        return ClickBindingsImpl(lifecycle)
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun dispose()
    {
        lifecycle.removeObserver(this)
    }
}

Rozszerzamy interfejs DataBindingComponent, który jest wygenerowany przez środowisko. Jeżeli IDE Ci go nie znajduje koniecznie przebuduj projekt. Powinno Ci go wygenerować na podstawie naszego wcześniejszego interfejsu ClickBinding(a tak naprawdę na podstawie atrybutu, którym go zwieńczyliśmy).

Sprawa taka sama jak wcześniej. Kontruktor z parametrem typu Lifecycle oraz obsługa cyklu życia.

Widok

Czas na użycie własnego bindingu. Środowisko powinno wygenerować Ci właściwość rxClick. Dodaj własny model widoku (powinien dziedziczyć po klasie ViewModel), a w nim publiczną metodę o dowolnej nazwie. 

Zwróć uwagę na użycie elementu layout w definicji widoku. Dzięki niemu możemy korzystać z dobrodziejstw databindingu. Oprócz tego należy w elemencie data zdefiniować dostęp do modelu widoku. W tym celu tworzymy nową zmienną i nadajemy jej nazwę viewModel odpowiedniego typu. Zwróć uwagę, że dzięki wsparciu IDE podczas tworzenia bindingu we właściwości rxClick, możemy przeglądać dostępne metody (o ile istnieją) w modelu widoku.

.<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewModel"
            type="yournamespace.viewmodels.ManagementViewModel" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/root_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/dark_color">

        <com.google.android.material.button.MaterialButton
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_margin="8dp"
            android:text="Click me!"
            rxClick="@{()->viewModel.boomAction()}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Zwieńczeniem jest fragment

Ostatnim krokiem będzie przekazanie obiektu lifecycle fragmentu do instancji LifecycleComponent. W tym celu skorzystaj z poniższego kodu:

override fun onCreateView(inflater: LayoutInflater,
                          container: ViewGroup?,
                          savedInstanceState: Bundle?): View?
{
    val binding = DataBindingUtil.inflate<FragmentManagementBinding>(inflater, R.layout.fragment_management, container, false, LifecycleComponent(lifecycle)).apply {
        viewModel = managementViewModel
        lifecycleOwner = this@ManagementFragment.viewLifecycleOwner
    }

    return binding.root
}

Korzystamy z DataBindingUtil, aby stworzyć kontekst data bindingu. Metoda inflate przyjmuje generyczny typ, w tym przypadku FragmentManagementBinding. Jest to klasa utworzona przez IDE. Jej nazwa jest utworzona na podstawie nazwy definicji widoku, w tym przypadku fragment_management. Możemy odwołać się do utworzonej zmiennej viewModel przekazując odpowiedni obiekt (w moim przypadku jest to obiekt modelu widoku). 

Zwróć uwagę na ostatni parametr metody inflate. Jest to nasz LifecycleComponent.

Baza do rozwoju

Pokazałem Ci, w jaki sposób stworzyć własny binding. Od teraz nie musisz martwić się o to, że użytkownik wielokrotnie kliknie jakiś widok i utworzy kolejkę akcji. 

Programowanie reaktywne jest świetne. Pozwala na uproszczenie wielu zadań, a samo podejście jest stosunkowe łatwe w zrozumieniu. Można je wykorzystać w wielu miejscach. Dlatego jestem pewny, że to dopiero początek wpisów odnośnie tego. 

Stay tuned!

Dzięki! 👋

Mam coś dla Ciebie

Zapisz się do mojego newslettera, a ja prześlę Ci zbiór kilkunastu praktycznych wskazówek dla programisty aplikacji mobilnych.

Menu