Bądź leniwy z argumentami fragmentów

May 07 2023
Lubię usuwać niektóre kody szablonów, kiedy tylko mogę. Mniej kodu oznacza mniej podatności na błędy.

Lubię usuwać niektóre kody szablonów, kiedy tylko mogę. Mniej kodu oznacza mniej podatności na błędy.

Dzisiaj opowiem o przekazywaniu argumentów do fragmentu i o tym, jak bezpiecznie go uzyskać.

Uwaga: Świat „komponowania” nie jest częścią tego artykułu. Co więcej, „komponowanie nawigacji” jest niewątpliwie wadą z nietypowanymi argumentami , cofaniem się, które prowadzi do zachowania podobnego do javascript .

Jeśli tak jak ja używasz Komponentu nawigacyjnego, który jest świetnym narzędziem do bezpiecznej nawigacji z fragmentu do drugiego, może się zdarzyć, że z jakichś powodów będziesz musiał sam utworzyć instancję fragmentu. Na przykład, możesz użyć ViewPager(2) z FragmentStateAdapter do tworzenia instancji wielu fragmentów. W tym przypadku NavGraph z Komponentu Nawigacyjnego jest bez sensu. Zarządzanie tym nie należy do obowiązków tej biblioteki.

Komponent nawigacyjny zawiera wiele rozszerzeń, takich jak „by navArgs()”, które pozwalają nam pobierać argumenty z fragmentu w leniwy, bezpieczny sposób.

Na końcu tego artykułu, dzięki mocy Kotlina, będziemy mieli leniwą funkcję bezpieczeństwa „by args()”, aby uzyskać wpisane argumenty z naszego własnego fragmentu.

Ale najpierw spójrzmy na stary sposób. W tym artykule będziemy stopniowo ulepszać nasz kod, aby był bezpieczniejszy.

Pakiet

Nic dziwnego, przekazywanie danych jest restrykcyjne, niektóre dane trzeba przekazać do wiązki. Nie ma innego sposobu na przekazanie danych do fragmentu.

Rzućmy na to okiem, oto funkcja bundleOf z pakietu androidx.core.os

public fun bundleOf(vararg pairs: Pair<String, Any?>): Bundle = Bundle(pairs.size).apply {
    for ((key, value) in pairs) {
        when (value) {
            null -> putString(key, null) // Any nullable type will suffice.

            // Scalars
            is Boolean -> putBoolean(key, value)
            is Byte -> putByte(key, value)
            is Char -> putChar(key, value)
            is Double -> putDouble(key, value)
            is Float -> putFloat(key, value)
            is Int -> putInt(key, value)
            is Long -> putLong(key, value)
            is Short -> putShort(key, value)

            // References
            is Bundle -> putBundle(key, value)
            is CharSequence -> putCharSequence(key, value)
            is Parcelable -> putParcelable(key, value)

            // Scalar arrays
            is BooleanArray -> putBooleanArray(key, value)
            is ByteArray -> putByteArray(key, value)
            is CharArray -> putCharArray(key, value)
            is DoubleArray -> putDoubleArray(key, value)
            is FloatArray -> putFloatArray(key, value)
            is IntArray -> putIntArray(key, value)
            is LongArray -> putLongArray(key, value)
            is ShortArray -> putShortArray(key, value)

            // Reference arrays
            is Array<*> -> {
                val componentType = value::class.java.componentType!!
                @Suppress("UNCHECKED_CAST") // Checked by reflection.
                when {
                    Parcelable::class.java.isAssignableFrom(componentType) -> {
                        putParcelableArray(key, value as Array<Parcelable>)
                    }
                    String::class.java.isAssignableFrom(componentType) -> {
                        putStringArray(key, value as Array<String>)
                    }
                    CharSequence::class.java.isAssignableFrom(componentType) -> {
                        putCharSequenceArray(key, value as Array<CharSequence>)
                    }
                    Serializable::class.java.isAssignableFrom(componentType) -> {
                        putSerializable(key, value)
                    }
                    else -> {
                        val valueType = componentType.canonicalName
                        throw IllegalArgumentException(
                            "Illegal value array type $valueType for key \"$key\""
                        )
                    }
                }
            }

            // Last resort. Also we must check this after Array<*> as all arrays are serializable.
            is Serializable -> putSerializable(key, value)

            else -> {
                if (Build.VERSION.SDK_INT >= 18 && value is IBinder) {
                    BundleApi18ImplKt.putBinder(this, key, value)
                } else if (Build.VERSION.SDK_INT >= 21 && value is Size) {
                    BundleApi21ImplKt.putSize(this, key, value)
                } else if (Build.VERSION.SDK_INT >= 21 && value is SizeF) {
                    BundleApi21ImplKt.putSizeF(this, key, value)
                } else {
                    val valueType = value.javaClass.canonicalName
                    throw IllegalArgumentException("Illegal value type $valueType for key \"$key\"")
                }
            }
        }
    }
}

class MyFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val contentId: Long = requireArguments().getLong(BUNDLE_ARGUMENT_KEY_CONTENT_ID)
        val otherData: String = requireArguments().getString(BUNDLE_ARGUMENT_KEY_OTHER_DATA)!!
    }

    companion object {
        private const val BUNDLE_ARGUMENT_KEY_CONTENT_ID = "content_id"
        private const val BUNDLE_ARGUMENT_KEY_OTHER_DATA = "other_data"
        fun newInstance(
            contentId: Long,
            otherData: String
        ): MyFragment {
            return MyFragment().apply {
                arguments = Bundle().apply {
                    putLong(BUNDLE_ARGUMENT_KEY_CONTENT_ID, contentId)
                    putString(BUNDLE_ARGUMENT_KEY_OTHER_DATA, otherData)
                }
            }
        }

        // Or
        fun newInstance(
            contentId: Long,
            otherData: String
        ): MyFragment {
            return MyFragment().apply {
                arguments = bundleOf(
                    BUNDLE_ARGUMENT_KEY_CONTENT_ID to contentId, 
                    BUNDLE_ARGUMENT_KEY_OTHER_DATA to otherData
                )
            }
        }
    }
}

1°) Za każdym razem, gdy chcemy dodać nowy argument, mamy kilka zmian , aby uzyskać i ustawić nowy.

2°) Prymitywy w świecie Java (inne niż String) nie mogą mieć wartości NULL w pakiecie z powodu ograniczeń Java.

3°) Z przyzwyczajenia możemy kopiować wpisy i zapominać o przypisaniu odpowiedniego klucza pakietu do nowo dodanego argumentu.

4°) w funkcji newInstance możemy któregoś dnia zdecydować o zmianie parametru z not nullable na nullable. W takim przypadku nie zostanie popełniony żaden błąd w IDE/czasie kompilacji. Potencjalnie wynik w czasie wykonywania do wyjątku wskaźnika o wartości null, jeśli spróbujemy uzyskać wartość inną niż null na obiekcie dopuszczającym wartość null.

Wniosek dotyczący tego starego sposobu :

Taki kod zrobiłem dawno temu. Ten kod jest podatny na błędy, należy pamiętać o wielu rzeczach, aby uniknąć głupich błędów podczas dodawania/usuwania/zmiany argumentów. W dzisiejszych czasach jako dobry programista i z mocą Kotlina chcemy być bezpieczni niż kiedyś, pisać mniej kodu i być leniwym.

Kapsułkowanie

Cóż, wysłanie wielu prymitywów bezpośrednio w pakiecie w celu fragmentacji argumentów było prawdopodobnie dobrym wyborem dawno temu ze względu na słabą wydajność. Tworzenie obiektów ma swoją cenę.

Hermetyzacja prymitywów/obiektów w unikalny obiekt argumentu ma główną zaletę: „Zachowaj modelową umowę” i zezwala na „zerowe prymitywy”.

Uwaga: Komponent nawigacyjny pozwala nam pisać osobne argumenty dla fragmentu w plikach xml. Jest to bezpieczne, ponieważ generują one w czasie kompilacji automatycznie generowaną klasę z sufiksem „NavigationArgs”, który transkrybuje do niej argumenty xml. Dzięki „by navArgs” otrzymujemy tę automatycznie generowaną klasę.

@Parcelize
data class MyFragmentArgs(
    val contentId: Long,
    val otherData: String
) : Parcelable

class MyFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val args: MyFragmentArgs = requireArguments().getParcelable(BUNDLE_ARGUMENT_KEY)!!
        val (contentId: Long, otherData: String) = args
    }

    companion object {
        private const val BUNDLE_ARGUMENT_KEY = "argument"
        fun newInstance(
            contentId: Long,
            otherData: String
        ): MyFragment {
            return MyFragment().apply {
                arguments = Bundle().apply {
                    putParcelable(BUNDLE_ARGUMENT_KEY, MyFragmentArgs(contentId = contentId, otherData = otherData))
                }
            }
        }
    }
}

Aby model działał, musi rozszerzyć klasę Parcelable, a dzięki adnotacji „@Parcelize” unika kodu standardowego.

Dodatkowo, jeśli w naszym modelu MyFragmentArgs zmienimy „val otherData: String” na „val otherData: String?”, Android Studio ostrzeże nas o „val (contentId: Long, otherData: String) = args”

Wielokrotnego użytku

Byłoby fajnie, gdybyśmy mogli zachować ten sam kontrakt argumentów set/get dla dowolnego fragmentu, aby uniknąć powielania.

Zróbmy to

@Parcelize
data class MyFragmentArgs(
    val contentId: Long,
    val otherData: String
) : Parcelable

class MyFragment : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val args: MyFragmentArgs = args()
        val (contentId: Long, otherData: String) = args
    }

    companion object {
        private fun newInstance(
            contentId: Long,
            otherData: String
        ): MyFragment {
            return MyFragment().setArgs(MyFragmentArgs(contentId = contentId, otherData = otherData))
        }
    }
}

private const val BUNDLE_ARGUMENTS_KEY = "arguments"

fun <FRAGMENT : Fragment> FRAGMENT.setArgs(args: Parcelable): FRAGMENT {
    return apply {
        arguments = Bundle().apply {
            putParcelable(BUNDLE_ARGUMENTS_KEY, args)
        }
    }
}

fun <FRAGMENT : Fragment, ARGS : Parcelable> FRAGMENT.args(): ARGS {
    return requireArguments().getParcelable(BUNDLE_ARGUMENTS_KEY)!!
}

Bezpieczeństwo

Byłoby fajnie, gdybyśmy mogli połączyć setter i getter na argumentach fragmentów, aby uniknąć błędnego wpisania właściwej klasy.

Zróbmy to !

@Parcelize
data class MyFragmentArgs(
    val contentId: Long,
    val otherData: String
) : IArgs

class MyFragment : Fragment(),
                   IFragmentArgs<MyFragmentArgs> {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val (contentId: Long, otherData: String) = args()
    }

    companion object {
        private fun newInstance(
            contentId: Long,
            otherData: String
        ): MyFragment {
            return MyFragment().setArgs(MyFragmentArgs(contentId = contentId, otherData = otherData))
        }
    }
}

interface IArgs : Parcelable
interface IFragmentArgs<ARGS : IArgs>

private const val BUNDLE_ARGUMENTS_KEY = "arguments"

fun <FRAGMENT, ARGS : IArgs> FRAGMENT.setArgs(args: ARGS): FRAGMENT where FRAGMENT : Fragment, FRAGMENT : IFragmentArgs<ARGS> {
    return apply {
        arguments = Bundle().apply {
            putParcelable(BUNDLE_ARGUMENTS_KEY, args)
        }
    }
}

fun <FRAGMENT, ARGS : IArgs> FRAGMENT.args(): ARGS where FRAGMENT : Fragment, FRAGMENT : IFragmentArgs<ARGS> {
    return requireArguments().getParcelable(BUNDLE_ARGUMENTS_KEY)!!
}

1°) Argumenty Modele muszą rozszerzać interfejs IArgs

2°) Fragmenty muszą rozszerzać interfejs IFragmentArgs

Być leniwym

Wracam do „by navArgs()” z „Navigation Component”.
Dlaczego jest to przydatne?

Uwaga: Nie powinniśmy zmieniać argumentów po pierwszym ustawieniu. Tak więc, odzyskując argumenty zwrotne, moglibyśmy, dzięki optymalizacji, przechowywać je wtedy, gdy potrzebujemy ich za pierwszym razem. Taki jest cel „leniwego” obiektu z jego systemem pamięci podręcznej.

Teraz misją jest posiadanie własnego „przez args()”!

@Parcelize
data class MyFragmentArgs(
    val contentId: Long,
    val otherData: String
) : IArgs

class MyFragment : Fragment(),
                   IFragmentArgs<MyFragmentArgs> {

    val args by args()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val (contentId: Long, otherData: String) = args
    }

    companion object {
        private fun newInstance(
            contentId: Long,
            otherData: String
        ): MyFragment {
            return MyFragment().setArgs(MyFragmentArgs(contentId = contentId, otherData = otherData))
        }
    }
}

interface IArgs : Parcelable
interface IFragmentArgs<ARGS : IArgs>

private const val BUNDLE_ARGUMENTS_KEY = "argument"

fun <FRAGMENT, ARGS : IArgs> FRAGMENT.setArgs(args: ARGS): FRAGMENT where FRAGMENT : Fragment, FRAGMENT : IFragmentArgs<ARGS> {
    return apply {
        arguments = Bundle().apply {
            putParcelable(BUNDLE_ARGUMENTS_KEY, args)
        }
    }
}

@MainThread
inline fun <FRAGMENT, reified ARGS : IArgs> FRAGMENT.args(): ArgsLazy<ARGS> where FRAGMENT : Fragment, FRAGMENT : IFragmentArgs<ARGS> = ArgsLazy {
    arguments ?: throw IllegalStateException("Fragment $this has null arguments")
}

class ArgsLazy<ARGS : IArgs>(
    private val argumentsProducer: () -> Bundle
) : Lazy<ARGS> {
    private var cached: ARGS? = null

    override val value: ARGS
        get() {
            var args = cached
            if (args == null) {
                val arguments = argumentsProducer()

                args = arguments.getParcelable(BUNDLE_ARGUMENTS_KEY)!!
                cached = args
            }
            return args
        }

    override fun isInitialized(): Boolean = cached != null
}

Zduplikuj MyFragmentArgs do MyFragmentArgs2 I spróbuj wywołać setArgs() z nowym modelem. Android Studio ostrzeże Cię o błędzie, który zamierzasz zrobić. Ta umowa nie może zostać naruszona

Uwaga: w funkcji „args()” podano adnotację „@MainThread” z powodu impasu. Uniemożliwia dostęp do tego z wielu wątków w tle. Powinien być dostępny tylko z głównego wątku.

Bonus — Fragment argumentów z ViewModel

class MyFragmentViewModel(override val savedStateHandle: SavedStateHandle) : ViewModel(),
                                                                             IViewModelArgs<MyFragmentArgs> {
    val args = args()

    init {
        val (contentId: Long, otherData: String) = args
    }
}

interface IViewModelArgs<ARGS : IArgs> {
    val savedStateHandle: SavedStateHandle
}


fun <VIEWMODEL, ARGS : IArgs> VIEWMODEL.args(): ARGS where VIEWMODEL : ViewModel, VIEWMODEL : IViewModelArgs<ARGS> {
    return savedStateHandle.get(BUNDLE_ARGUMENTS_KEY) as? ARGS ?: throw IllegalStateException("ViewModel $this has null arguments")
}

@Parcelize
data class MyFragmentArgs(
    val contentId: Long,
    val otherData: String
) : IArgs

class MyFragment : Fragment(),
                   IFragmentArgs<MyFragmentArgs> {

    val args by args()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val (contentId: Long, otherData: String) = args
    }

    companion object {
        private fun newInstance(
            contentId: Long,
            otherData: String
        ): MyFragment {
            return MyFragment().setArgs(MyFragmentArgs(contentId = contentId, otherData = otherData))
        }
    }
}

class MyFragmentViewModel(override val savedStateHandle: SavedStateHandle) : ViewModel(),
                                                                             IViewModelArgs<MyFragmentArgs> {
    val args = args()

    init {
        val (contentId: Long, otherData: String) = args
    }
}

interface IArgs : Parcelable
interface IFragmentArgs<ARGS : IArgs>
interface IViewModelArgs<ARGS : IArgs> {
    val savedStateHandle: SavedStateHandle
}

private const val BUNDLE_ARGUMENTS_KEY = "arguments"

fun <FRAGMENT, ARGS : IArgs> FRAGMENT.setArgs(args: ARGS): FRAGMENT where FRAGMENT : Fragment, FRAGMENT : IFragmentArgs<ARGS> {
    return apply {
        arguments = Bundle().apply {
            putParcelable(BUNDLE_ARGUMENTS_KEY, args)
        }
    }
}

@MainThread
inline fun <FRAGMENT, reified ARGS : IArgs> FRAGMENT.args(): ArgsLazy<ARGS> where FRAGMENT : Fragment, FRAGMENT : IFragmentArgs<ARGS> = ArgsLazy {
    arguments ?: throw IllegalStateException("Fragment $this has null arguments")
}

class ArgsLazy<ARGS : IArgs>(
    private val argumentsProducer: () -> Bundle
) : Lazy<ARGS> {
    private var cached: ARGS? = null

    override val value: ARGS
        get() {
            var args = cached
            if (args == null) {
                val arguments = argumentsProducer()

                args = arguments.getParcelable(BUNDLE_ARGUMENTS_KEY)!!
                cached = args
            }
            return args
        }

    override fun isInitialized(): Boolean = cached != null
}

fun <VIEWMODEL, ARGS : IArgs> VIEWMODEL.args(): ARGS where VIEWMODEL : ViewModel, VIEWMODEL : IViewModelArgs<ARGS> {
    return savedStateHandle.get(BUNDLE_ARGUMENTS_KEY) as? ARGS ?: throw IllegalStateException("ViewModel $this has null arguments")
}

Wykorzystaj moc Kotlina do pisania bezpiecznego kodu i pisz mniej kodu, kiedy tylko możesz :D

To mój drugi artykuł. Lubię pisać przydatne wskazówki.
Jeśli podobają Ci się tego typu artykuły, nie wahaj się i klaszcz