Search code examples
androidandroid-recyclerviewandroid-databinding

Cannot find the setter for attribute 'app:fastScrollEnabled' with parameter type boolean on androidx.recyclerview.widget.RecyclerView


I've tried setting an ObservableField or a String value, still doesn't work. If I just set a static true or false value instead of the viewModel reference, it works.

Layout file:

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

    <data>

        <variable
                name="viewModel"
                type="app.viewmodel.ViewModel"/>
    </data>
 <androidx.recyclerview.widget.RecyclerView
                        android:id="@+id/recyclerview"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        android:scrollbars="vertical"
                        app:fastScrollEnabled="@{viewModel.isUserAdmin}"
                        app:fastScrollHorizontalThumbDrawable="@drawable/thumb_drawable"
                        app:fastScrollHorizontalTrackDrawable="@drawable/line_drawable"
                        app:fastScrollVerticalThumbDrawable="@drawable/thumb_drawable"
                        app:fastScrollVerticalTrackDrawable="@drawable/line_drawable"
                        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
</layout>

ViewModel:

class AppointmentsViewModel()
{
     val isUserAdmin: Boolean = sharedPreferencesRepo.isUserAdmin
}

Solution

  • RecyclerView currently has no public method for setting the fast-scrolling attributes from code. As of this writing, the only way to enable fast-scrolling is by setting the relevant attributes in the layout XML. RecyclerView then calls a private method from its constructor to set that up.

    The data binding library, while generating its adapter code, can't find a public method for the fastScrollEnabled attribute, which is why you're getting that error. It's just not possible yet.

    There is a request in the Issue Tracker to add the relevant functionality to RecyclerView, and even a comment there that points out the data binding limitation, but it doesn't seem to be a high priority, at the moment.


    If you don't mind a little extra work, we can actually accomplish this manually, since RecyclerView's FastScroller implementation is simply an ItemDecoration and OnItemTouchListener. We can copy that class from the library source and set it up externally, allowing us to enable and disable fast-scrolling programmatically, and thusly handle it through data binding.

    Directly copying the current FastScroller class should require only a few minor edits that will be obvious (e.g., changing the package, some annotations, etc.), and the following example assumes that you have it copied under its original name, FastScroller.

    There is one vital addition necessary to allow detaching a FastScroller from its RecyclerView without error. (Indeed, it seems possibly oversight that it's not there already.) In FastScroller's destroyCallbacks() method, add a call to mShowHideAnimator.cancel():

    private void destroyCallbacks() {
        mRecyclerView.removeItemDecoration(this);
        mRecyclerView.removeOnItemTouchListener(this);
        mRecyclerView.removeOnScrollListener(mOnScrollListener);
        cancelHide();
    
        // ADD THIS
        mShowHideAnimator.cancel();
    }
    

    Fortunately, a FastScroller basically requires no further attention after instantiation, and since this is replacing a RecyclerView-internal functionality anyway, it makes sense to attach each FastScroller instance to its RecyclerView as a tag, so we don't have to track those instances anywhere else. We only need to be able to retrieve them once later to detach, and this arrangement works well with the data binding setup.

    We need to define a unique resource ID as our tag key. It can be placed in any resource file under the res/values/ folder; /res/values/ids.xml would be appropriate, if you like things neat.

    <resources>
        <id name="recycler_view_fast_scroller_tag_key" />
    </resources>
    

    Our FastScroller initialization code1 is essentially a reordering of the initializations in RecyclerView and its constructor, consolidated in a static utility method:

    static void initFastScroller(RecyclerView recyclerView) {
        Resources resources = recyclerView.getContext().getResources();
        Resources.Theme theme = recyclerView.getContext().getTheme();
    
        StateListDrawable verticalThumbDrawable = (StateListDrawable)
                ResourcesCompat.getDrawable(resources, R.drawable.thumb_drawable, theme);
        Drawable verticalTrackDrawable =
                ResourcesCompat.getDrawable(resources, R.drawable.line_drawable, theme);
        StateListDrawable horizontalThumbDrawable = (StateListDrawable)
                ResourcesCompat.getDrawable(resources, R.drawable.thumb_drawable, theme);
        Drawable horizontalTrackDrawable =
                ResourcesCompat.getDrawable(resources, R.drawable.line_drawable, theme);
    
        if (verticalThumbDrawable == null || verticalTrackDrawable == null
                || horizontalThumbDrawable == null || horizontalTrackDrawable == null) {
            throw new IllegalArgumentException(
                    "Trying to set fast scroller without all required drawables.");
        }
    
        FastScroller fastScroller =
                new FastScroller(recyclerView, verticalThumbDrawable, verticalTrackDrawable,
                        horizontalThumbDrawable, horizontalTrackDrawable,
                        resources.getDimensionPixelSize(R.dimen.fastscroll_default_thickness),
                        resources.getDimensionPixelSize(R.dimen.fastscroll_minimum_range),
                        resources.getDimensionPixelOffset(R.dimen.fastscroll_margin));
    
        recyclerView.setTag(R.id.recycler_view_fast_scroller_tag_key, fastScroller);
    }
    

    This example moves all of the specific resource selections into the initFastScroller() method for simplicity, but this all can certainly be modified and rearranged however is needed; e.g., into a custom RecyclerView subclass. Additionally, you can specify whatever dimension arguments you like here, though the example is using the default resource values from the RecyclerView package.2

    The drawables are being handled dynamically now, so their corresponding attribute settings need to be removed from the layout XML:3

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scrollbars="vertical"
        app:fastScrollEnabled="@{viewModel.isUserAdmin}"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
    

    Lastly, to tie all of this together, the setFastScrollEnabled() method shows how we can enable and disable fast-scrolling programmatically, and we annotate it as a BindingAdapter to expose that functionality to data binding through the fastScrollEnabled attribute:

    @BindingAdapter("fastScrollEnabled")
    public static void setFastScrollEnabled(RecyclerView recyclerView, boolean setEnabled) {
        Object fastScroller = recyclerView.getTag(R.id.recycler_view_fast_scroller_tag_key);
        boolean isEnabled = fastScroller instanceof FastScroller;
        if (setEnabled != isEnabled) {
            if (setEnabled) {
                initFastScroller(recyclerView);
            } else {
                // Detach the FastScroller from the RecyclerView
                ((FastScroller) fastScroller).attachToRecyclerView(null);
                // Nullify our only reference to it
                recyclerView.setTag(R.id.recycler_view_fast_scroller_tag_key, null);
            }
        }
    }
    

    This method first checks for our FastScroller tag on the RecyclerView at R.id.recycler_view_fast_scroller_tag_key. If it's there, then fast-scrolling is currently enabled, and disabled otherwise. If that state is different than what we've been called to set, continue either to initialize and setTag() the FastScroller (which handles attaching itself to the RecyclerView internally upon instantiation), or to detach it and nullify the tag.


    1 These methods are provided in Java solely because that is the language of the code borrowed from the original sources. If you would rather have it in Kotlin, Android Studio can auto-convert it rather adequately as a first step, though you may wish to make further syntax/structural/etc. adjustments.

    2 The given three R.dimens all resolved and built without issue in my tests, somewhat unexpectedly. If you do want to use those default values but have problems doing so, you can copy the corresponding <dimen>s from the library source as well.

    3 This should work with or without the app namespace prefix on the fastScrollEnabled attribute, since it's apparently ignored/stripped by the data binding framework. It is left in the XML here only to show the minimal changes necessary.


    Of course, this all works independently of data binding, as well, and it can be adapted into more suitable utility methods, or Kotlin extensions, for example:

    var RecyclerView.isFastScrollEnabled: Boolean
        get() = getTag(R.id.recycler_view_fast_scroller_tag_key) is FastScroller
        set(value) {
            if (value != isFastScrollEnabled) {
                if (value) {
                    // Convert initFastScroller() from above as you like
                    initFastScroller(this)
                } else {
                    // Detach the FastScroller from the RecyclerView
                    (getTag(R.id.recycler_view_fast_scroller_tag_key) as FastScroller)
                        .attachToRecyclerView(null)
                    // Nullify our only reference to it
                    setTag(R.id.recycler_view_fast_scroller_tag_key, null)
                }
            }
        }