DialogFragment poprzez zmiany konfiguracji i śmierć procesu.

Nov 29 2022
Cześć wszystkim, Używam medium od dłuższego czasu, aby uczyć się nowych rzeczy, a teraz nadszedł czas, abym napisał swój pierwszy artykuł. Prawdopodobnie początek wielu innych.

Cześć wszystkim,

Od dłuższego czasu używam medium do uczenia się nowych rzeczy, a teraz nadszedł czas, abym napisał swój pierwszy artykuł. Prawdopodobnie początek wielu innych.

Wstęp

Dlaczego mam mówić o DialogFragment związanym ze zmianami konfiguracji i śmiercią procesu? Ponieważ zależy nam na doświadczeniach użytkowników i nie lubimy raportów o awariach lub dziwnych zachowaniach, gdy coś pójdzie nie tak.

Ten artykuł dotyczy DialogFragment, ale refleksja jest taka sama dla każdego fragmentu.

Aby wykonać zmiany konfiguracji

Łatwym sposobem na „zmiany konfiguracji” w aplikacji jest przełączanie między trybem jasnym i ciemnym. Krótko mówiąc, Fragmenty/Działania zostaną odtworzone z zapisanymi argumentami i zapisanym stanem instancji, ale ViewModels nadal będą istnieć. W procesie śmierci, modele widoków, działania i fragmenty zostaną odtworzone.

Aby sprowokować śmierć procesu

- Opcja programisty (prosty sposób): Opcje programisty -> Nie zatrzymuj działań -> włącz.
- Opcja rzeczywistej sytuacji: uruchom kilka zachłannych aplikacji lub stwórz własną aplikację, która pochłonie dużo pamięci (na przykład za pomocą pętli), dopóki urządzenie nie oszacuje, że wcześniej otwarte aplikacje powinny zostać zwolnione.

Za każdym razem, gdy wrócisz do dowolnej aplikacji, która została zwolniona, zostanie ona ponownie uruchomiona z zapisanymi argumentami / saveInstanceState. Tezy powinny być jedynym źródłem prawdziwych danych, o które naprawdę powinieneś dbać.

Dobrą praktyką jest zawsze testowanie wszystkich ekranów w aplikacji przy użyciu zmian konfiguracji / śmierci procesu, aby upewnić się, że wszystko działa zgodnie z oczekiwaniami. Jeśli masz awarie lub dziwne zachowania z tym związane, nie ignoruj ​​​​tego, ponieważ jest to „rzadkie”…
Co więcej, w przypadku niektórych urządzeń, które osiągają mniej niż 15% żywotności baterii, urządzenie automatycznie przełączy się w tryb ciemny, aby oszczędzać baterię co oznacza, że ​​użytkownik może wykonać jakieś losowe akcje w Twojej aplikacji, a Twoja aplikacja może nagle zostać odtworzona z tego powodu.

Cóż, wprowadziłem złe wieści, proszę o nie opuszczanie artykułu :(

Jeśli poświęcam czas na wyjaśnienie tych problemów, to dlatego, że jest tu wiele błyszczących artykułów, które wyjaśniają, jak napisać piękny kod w kotlinie za pomocą DSL z parametrami funkcji wysyłanymi bezpośrednio do okna dialogowego.
Nie jestem nowicjuszem w programowaniu na Androida i pomimo faktu, że Kotlin to świetny język, na pewno go uwielbiam, niektóre artykuły powinny zajmować się zmianami konfiguracji lub śmiercią procesu, w przeciwnym razie nie służą młodszym programistom.

Mówi się rzeczy

1°) Fragment (i oczywiście DialogFragment) powinien zawsze mieć pusty konstruktor. Spróbuj przełączyć się na tryb ciemny/jasny, gdy fragment ma konstruktora z parametrami; to nie kończy się dobrze.

Ponadto, przechodząc do elementu DialogFragment, nie przekazuj do niego funkcji za pośrednictwem funkcji zdefiniowanych we fragmencie DialogFragment, który zapisuje go w zmiennej lokalnej, ponieważ przy zmianie konfiguracji fragment zostanie przywrócony, ale bez wywoływania tych funkcji; więc twoje zmienne lokalne będą niezainicjowane. Wywołane zostaną tylko puste konstruktory i przesłonięte funkcje (takie jak onCreate, onCreateDialog itp.) z DialogFragment. Argumenty i saveInstanceState powinny być tylko twoim zmartwieniem.

2°) Nie przekazuj funkcji jako argumentu do swojego fragmentu przez pakiet. Funkcja nie może być spakowana/serializowana, aplikacja ulegnie awarii po zmianie konfiguracji.

Przekazywanie funkcji do dowolnego fragmentu to bardzo zła praktyka; Ponadto może to prowadzić do wycieków pamięci, więc nie rób tego. Wiem, łatwo powiedzieć: „ Hej, jak klikniesz sukces, przejdź do ekranu X ” z lambdą. Zrobiłem to też na początku rozwoju Androida, ale uwierz mi, nie chcesz dostarczać użytkownikowi niestabilnego produktu w produkcji.

3°) Używając udostępnionego ViewModelu? Do diabła nie! Po pierwsze, z powodu śmierci procesu, a następnie dlatego, że nie chcesz mieć bólu głowy podczas debugowania, dlaczego w niektórych przypadkach nie działa. Współdzielone modele widoków to koncepcja najbardziej podatna na błędy. Może to być przydatne w kilku przypadkach, ale co najwyżej nie używaj go.

Rozwiązanie

Wykonaj żądanie do DialogFragment, poczekaj na wynik, a następnie wykonaj coś w zależności od otrzymanego wyniku.

Prosty przypadek użycia:

Chcesz otrzymać od swojego użytkownika odpowiedź dotyczącą zatwierdzenia określonej akcji. Akcja „OK” oznacza, że ​​przejdziesz do następnego ekranu z określonymi danymi związanymi z danymi wprowadzonymi przez użytkownika.
W naszym prostym przykładzie użycia akcja użytkownika może wyglądać następująco: „Ok”, „Anuluj” (2 przyciski interfejsu użytkownika) i „Odrzuć” (ikona wstecz w interfejsie użytkownika lub naciśnięcie wstecz w systemie).
Ale dla uproszczenia uznamy, że „Odrzuć” jest traktowane jako akcja „Anuluj”. Tak więc interakcja użytkownika jest ograniczona do dwóch rodzajów interakcji: „OK” lub „Anuluj”.

Możemy potencjalnie wielokrotnie zażądać tego samego typu DialogFragment w jednym lub kilku fragmentach, w zależności od wielu kontekstów. Dlatego udostępnimy „requestKey”, który w większości przypadków jest unikalny dla określonego fragmentu. Jedyne, czego chcemy, to odpowiedź na to, o co prosiliśmy: „Ok” lub „Anuluj”.

Chodzi o to, aby wyemitować „requestKey” do DialogFragment i czekać na akcję użytkownika: „Ok” lub „Anuluj” dla tego „requestKey”.

Istnieje wiele sposobów komunikacji między fragmentem okna dialogowego a fragmentem wykonującym żądanie.

osobiście używam:

- setFragmentResult z DialogFragment
- setFragmentResultListener w onCreate() z twojego Fragmentu wykonującego żądanie

Funkcja setFragmentResult ma dwa parametry: „requestKey: String” i „result: Bundle”. Ta funkcja propaguje dane przez parentFragmentManager, co oznacza, że ​​każdy fragment żądający z tym samym requestKey jest podatny na przechwycenie wyniku.

Poniżej znajduje się trochę „uproszczonego” kodu przy użyciu rozszerzeń funkcji (w ten sposób skupiamy się tylko na tym, co naprawdę ważne i możemy łatwo zrozumieć, kto co wywołuje. W praktyce oczywiście kopiuj/wklej funkcję ciała istniejącej funkcji fragmentu/dialogfragmentu):

enum class UserAction {
    OK, CANCEL
}

/*******************
 * DIALOG FRAGMENT *
 *******************/

/**
 * In your DialogFragment, the contract mentions that REQUEST_KEY_ARG_KEY is a mandatory arg.
 * You will notice that it works for Fragment too, not only DialogFragment
 * In our case, a device back press or UI back press will invoke onDismiss(), just override it and call this function
 * @param userAction 'Yes' or 'Cancel'
 */
fun DialogFragment.deliverUserAction(userAction: UserAction) {
    setFragmentResult(requireArguments().getString(REQUEST_KEY_ARG_KEY)!!,
                      bundleOf(USER_ACTION_RESULT_KEY to userAction,
                               REQUEST_DATA_BUNDLE_KEY to requireArguments().getBundle(REQUEST_DATA_BUNDLE_KEY)))
}

/**
 * Defined in public in your DialogFragment
 */
const val REQUEST_KEY_ARG_KEY = "request_key" // Mandatory in arg
const val REQUEST_DATA_BUNDLE_KEY = "request_data" // Optional in arg and result, it is just a way to forward contextual data
const val USER_ACTION_RESULT_KEY = "user_action" // Mandatory in result

/***********************************
 * FRAGMENT REQUESTING USER ACTION *
 ***********************************/

/**
 * Listen to a user action
 * Of cource, use it in real override onCreate function
 */
fun Fragment.fakeOverrideOnCreate() {
    setFragmentResultListener(REQUEST_KEY_TO_GO_TO_SCREEN_B) { _, bundle: Bundle ->
        // Called once in Lifecycle.State.ON_START
        val userAction: UserAction = bundle.getSerializable(USER_ACTION_RESULT_KEY) as UserAction // We know at this point that we will get an user action
        val requestData: Bundle? = bundle.getBundle(REQUEST_DATA_BUNDLE_KEY) // Optional because we don't necessary need data at this point for some usecase
        when (userAction) {
            UserAction.OK -> {
                TODO("Do something with ${requestData} if you need it to navigate to next screen")
            }
            UserAction.CANCEL -> TODO()
        }
    }
}

/**
 * Request a user action
 * @see fakeOverrideOnCreate directly related to it
 * @param requestKey requestKey is mandatory
 * @param requestData optional, some data you may need to forward to dialog fragment and get back in setFragmentResultListener
 */
fun Fragment.navigateToMyDialogFragmentX(requestKey: String, requestData: Bundle?) {
    bundleOf(
        REQUEST_KEY_ARG_KEY to requestKey,
        REQUEST_DATA_BUNDLE_KEY to requestData
    )
    // etc.
    // depending on if you are using Navigation component or not
}

fun Fragment.calledAnywhereInFragmentRequestingUserAction1() {
    navigateToMyDialogFragmentX(requestKey = REQUEST_KEY_TO_GO_TO_SCREEN_B,
                                requestData = bundleOf("input1" to "Here", "input2" to "we", "input3" to "go!"))
}

/**
 * Defined in private in your fragment requesting user action
 */
private const val REQUEST_KEY_TO_GO_TO_SCREEN_B = "request_key_to_go_to_screen_B"

Poniżej przedstawiono kilka ulepszeń wykorzystujących przepływy

/***************************************
 * YOURFRAGMENT REQUESTING USER ACTION *
 ***************************************/

private val _userActionResultMutableStateFlow: MutableStateFlow<Pair<String, Bundle>?> = MutableStateFlow(null)
private val userActionResultStateFlow: StateFlow<Pair<String, Bundle>?> get() = _userActionResultMutableStateFlow

/**
 * Listen to a user action
 * Of cource, use it in real override onCreate function
 */
fun Fragment.fakeOverrideOnCreate() {
    setFragmentResultListener(REQUEST_KEY_TO_GO_TO_SCREEN_B) { requestKey, bundle: Bundle ->
        _userActionResultMutableStateFlow.value = requestKey to bundle
    }
}

/**
 * Listen to a user action and consume it only when fragment is the currentBackStackEntry
 * Of cource, use it in real override onViewCreated function
 */
fun Fragment.fakeOverrideOnViewCreated() {
    viewLifecycleOwner.lifecycleScope.launch {
        combine((findNavController().currentBackStackEntryFlow), userActionResultStateFlow) { currentBackStackEntry, userActionResult ->
            if (currentBackStackEntry.destination.id in R.id.YOURFRAGMENT_DEFINED_IN_NAV_GRAPH) {
                userActionResult
            } else {
                null
            }
        }.filterNotNull().collect { (requestKey, bundle) ->
            consumeUserActionResult(requestKey, bundle)
            _userActionResultMutableStateFlow.value = null // don't forget to notify that user action has been consumed
        }
    }
}

/**
 * consume user action result
 */
fun consumeUserActionResult(requestKey: String,
                            bundle: Bundle) {
    when (requestKey) {
        REQUEST_KEY_TO_GO_TO_SCREEN_B -> {
            val userAction: UserAction = bundle.getSerializable(USER_ACTION_RESULT_KEY) as UserAction // We know at this point that we will get an user action
            val requestData: Bundle? = bundle.getBundle(REQUEST_DATA_BUNDLE_KEY) // Optional because we don't necessary need data at this point for some usecase
            when (userAction) {
                UserAction.OK -> {
                    TODO("Do something with ${requestData} if you need it to navigate to next screen")
                }
                UserAction.CANCEL -> TODO()
            }
        }
    }
}

Zauważysz, że jest dużo kodu za niewiele. Co więcej, możesz pomyśleć, że będziesz musiał zduplikować ten kod szablonowy na każdym fragmencie, używając fragmentu okna dialogowego. Tak i nie. Delegat to potężne narzędzie, które pozwoli Ci napisać ten kod tylko raz. Daj mi znać, jeśli jesteś zainteresowany takim ulepszeniem, aby dodać w tym artykule, czy nie.

Jeśli czegoś się dowiesz lub jeśli już jesteś zaznajomiony, ale czujesz, że inni powinni o tym wiedzieć, nie krępuj się klaskać :).

Lubię dzielić się wiedzą i jeśli czegoś nie rozumiesz lub piszę coś nie tak, nie wahaj się skomentować poniżej. Odpowiem i w razie błędów wprowadzę zmiany.

Tylko dla podzielenia się, kilku interesujących facetów, z którymi polecam ci się uczyć:

Jake Wharton
– Twitter:https://twitter.com/JakeWharton
- Średni:https://medium.com/@techyourchance
Wasilij Zukanow
– Twitter:https://twitter.com/VasiliyZukanov
- Średni :https://medium.com/@techyourchance
Christophe Beyls
– Twitter:https://twitter.com/BladeCoder
- Średni:https://medium.com/@bladecoder
Gabor Varadi
- Twitter:https://twitter.com/Zhuinden
- Średni:https://medium.com/@zhuinden

Uwaga: nie znam osobiście tych gości, śledzę ich tylko dlatego, że są dobrzy i aktywni na Twitterze.