0

I have a complex layout, that should include another layout only, if the viewmodel is of a specific subtype. (e.g. I have four types of entries and one of them has a special layout that others have not.) I cannot only toggle the visibility, as the layout requires the subtype of the viewmodel as a parameter, so some casting needs to happen if it gets included. The layout structure is simplified like that:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="de.example.ui.BaseItemViewModel" />
        <import type="android.view.View" />
    </data>
        <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                bind:contentPaddingTop="@dimen/item_container_vertical_margin"
                bind:contentPaddingBottom="@dimen/item_container_vertical_margin"
                android:background="?android:attr/selectableItemBackground"
                card_view:cardBackgroundColor="?attr/base_500"
                card_view:cardElevation="0dp"
                card_view:cardMaxElevation="1dp"
                card_view:cardPreventCornerOverlap="false"
                card_view:cardUseCompatPadding="true"
                >
                <androidx.constraintlayout.widget.ConstraintLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">
                    //Many UI elements

                    <include
                        android:id="@+id/dependent_layout"
                        layout="@layout/only_show_if_sub_class"
                        android:layout_width="0dp"
                        android:layout_height="@dimen/item_container_view_player_height"
                        bind:layout_constraintEnd_toStartOf="@id/primary_action"
                        bind:layout_constraintStart_toStartOf="parent"
                        bind:layout_constraintTop_toBottomOf="@+id/item_main"
                        android:layout_marginStart="@dimen/item_image_view_type_icon_margin_start"
                        android:layout_marginTop="@dimen/item_container_view_player_margin_top"
                        android:layout_marginEnd="@dimen/item_container_view_player_horizontal_margin"
                       />

                    //More UI
            </androidx.cardview.widget.CardView>
</layout>

The sublayout then requires to receive the subclass of the viewmodel:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="de.example.ui.SubtypeViewModel" />

    </data>

    <RelativeLayout
        android:id="@+id/item_player_holder"
        android:layout_width="match_parent"
        android:layout_height="@dimen/item_container_view_player_height"
        >
       //UI stuff
    </RelativeLayout>

</layout>

How can I achieve that?

1 Answers1

0

The trick is, that databinding does not require you to pass a viewmodel, as the viewmodel variable in layouts is nullable. However, a certain hindrance is that databinding seems to struggle with the instance of keyword in xml. A solution to that is to add a method in the parent viewmodel, that returns a boolean and tells you whether the ViewModel is of the given subtype, like that:

fun isSubtypeViewModel() : Boolean {
        return this is SubtypeViewModel
    }

If you want to use this pattern for many subclasses, you may use generics in the function, so that you can pass the class to check for in the xml.

Then in your xml you need to import the subclass within the data tags, so that it is aware of it:

<import type="de.example.ui.SubtypeViewModel"/>

Then you can add two lines in your include tag for the dependant layout. One will toggle the visibility, if the viewmodel is of the subtype and the other one will bind the (downcasted) viewmodel, if it is of subtype and will bind null, if it is not. (Since the layout is gone when the viewmodel is not of the subtype, binding null is unproblematic, as it only means no values will be bound. Includes are null-safe in that sense: When you do not bind a viewmodel or bind null, it will not crash, it will assume null/empty for all values provided by the viewmodel.) The code is the following within in the include tag:

android:visibility="@{viewModel.isSubtypeViewModel()? View.VISIBLE : View.GONE}"
bind:viewModel="@{viewModel.isSubtypeViewModel()? (SubtypeViewModel)viewModel : null}"

NOTE: It is important, that includes may not be cast-safe as they are null-safe. An invalid downcast may crash the app. Therefore it is important to make sure, that the provided function in the viewmodel really ensures the ViewModel is an instance of its Subtype and the ternary operator is written correctly, so it only downcasts if it is valid, to avoid crashes.

Whole layout:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:bind="http://schemas.android.com/apk/res-auto"
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="viewModel"
            type="de.example.ui.BaseItemViewModel" />
        <import type="android.view.View" />
        <import type="de.example.ui.SubtypeViewModel"/>
    </data>
        <androidx.cardview.widget.CardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                bind:contentPaddingTop="@dimen/item_container_vertical_margin"
                bind:contentPaddingBottom="@dimen/item_container_vertical_margin"
                android:background="?android:attr/selectableItemBackground"
                card_view:cardBackgroundColor="?attr/base_500"
                card_view:cardElevation="0dp"
                card_view:cardMaxElevation="1dp"
                card_view:cardPreventCornerOverlap="false"
                card_view:cardUseCompatPadding="true"
                >
                <androidx.constraintlayout.widget.ConstraintLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content">
                    //Many UI elements

                    <include
                        android:id="@+id/dependent_layout"
                        layout="@layout/only_show_if_sub_class"
                        android:layout_width="0dp"
                        android:layout_height="@dimen/item_container_view_player_height"
                        bind:layout_constraintEnd_toStartOf="@id/primary_action"
                        bind:layout_constraintStart_toStartOf="parent"
                        bind:layout_constraintTop_toBottomOf="@+id/item_main"
                        android:layout_marginStart="@dimen/item_image_view_type_icon_margin_start"
                        android:layout_marginTop="@dimen/item_container_view_player_margin_top"
                        android:layout_marginEnd="@dimen/item_container_view_player_horizontal_margin"
                        android:visibility="@{viewModel.isSubtypeViewModel()? View.VISIBLE : View.GONE}"
                        bind:viewModel="@{viewModel.isSubtypeViewModel()? (SubtypeViewModel)viewModel : null}"

                       />

                    //More UI
            </androidx.cardview.widget.CardView>
</layout>