Search code examples
androidandroid-viewbinding

How to use ViewBinding with an abstract base class


I started using ViewBinding. After searching for an example or some advice, I ended up posting this question here.

How do I use ViewBinding with an abstract base class that handles the same logic on views that are expected to be present in every child's layout?

Scenario:
I have a base class public abstract class BaseFragment. There are multiple Fragments that extend this base class. These Fragments have common views that are handled from the base class implementation (with the "old" findViewById()). For example, every fragment's layout is expected to contain a TextView with ID text_title. Here's how it's handled from the BaseFragment's onViewCreated():

TextView title = view.findViewById(R.id.text_title);
// Do something with the view from the base class

Now the ViewBinding API generates binding classes for each child Fragment. I can reference the views using the binding, but I can't use the concrete Bindings from the base class. Even if I introduced generics to the base class, there are too many types of fragment bindings So I discarded this solution for now.

What's the recommended way of handling the binding's views from the abstract base class? Are there any best practices? I didn't find a built-in mechanism in the API to handle this scenario in an elegant way.

When the child fragments are expected to contain common views, I could provide abstract methods that return the views from the concrete bindings of the Fragments and make them accessible from the base class. (For example protected abstract TextView getTitleView();). But is this an advantage rather than using findViewById()? Are there any other (better) solutions?


Solution

  • I found an applicable solution for my concrete scenario and I want to share it with you.

    Note that this is not an explanation on how ViewBinding works.

    I created some pseudocode below. (Migrated from my solution using DialogFragments that display an AlertDialog). I hope it's almost correctly adapted to Fragments (onCreateView() vs. onCreateDialog()). I got it to work that way.

    Imagine we have an abstract BaseFragment and two extending classes FragmentA and FragmentB.

    First have a look at all of our layouts. Note that I moved out the reusable parts of the layout into a separate file that will be included later from the concrete fragment's layouts. Specific views stay in their fragment's layouts. Using a common layout is important for this scenario.

    fragment_a.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        
        <!-- FragmentA-specific views -->
        <EditText
            android:id="@+id/edit_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="text" />
        
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/edit_name">
    
            <!-- Include the common layout -->
            <include
                layout="@layout/common_layout.xml"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </RelativeLayout>
    </RelativeLayout>
    

    fragment_b.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        
        <!-- FragmentB-specific, differs from FragmentA -->
        <TextView
            android:id="@+id/text_explain"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/explain" />
        
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_below="@+id/text_explain">
    
            <!-- Include the common layout -->
            <include
                layout="@layout/common_layout.xml"
                android:layout_width="match_parent"
                android:layout_height="wrap_content" />
        </RelativeLayout>
    </RelativeLayout>
    

    common_layout.xml

    <?xml version="1.0" encoding="utf-8"?>
    <merge xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        tools:parentTag="android.widget.RelativeLayout">
    
        <Button
            android:id="@+id/button_up"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/up"/>
    
        <Button
            android:id="@+id/button_down"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/button_up"
            android:text="@string/down" />
    </merge>
    

    Next the fragment classes. First our BaseFragment implementation.

    onCreateView() is the place where the bindings are inflated. We're able to bind the CommonLayoutBinding based on the fragment's bindings where the common_layout.xml is included. I defined an abstract method onCreateViewBinding() called on top of onCreateView() that returns the ViewBinding from FragmentA and FragmentB. That way I ensure that the fragment's binding is present when I need to create the CommonLayoutBinding.

    Next I am able to create an instance of CommonLayoutBinding by calling commonBinding = CommonLayoutBinding.bind(binding.getRoot());. Notice that the root-view from the concrete fragment's binding is passed to bind().

    getCommonBinding() allows to provide access to the CommonLayoutBinding from the extending fragments. We could be more strict: the BaseFragment should provide concrete methods that access that binding instead of make it public to it's child-classes.

    private CommonLayoutBinding commonBinding; // common_layout.xml
    
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 
            @Nullable Bundle savedInstanceState) {
        // Make sure to create the concrete binding while it's required to 
        // create the commonBinding from it
        ViewBinding binding = onCreateViewBinding(inflater);
        // We're using the concrete layout of the child class to create our 
        // commonly used binding 
        commonBinding = CommonLayoutBinding.bind(binding.getRoot());
        // ...
        return binding.getRoot();
    }
    
    // Makes sure to create the concrete binding class from child-classes before 
    // the commonBinding can be bound
    @NonNull
    protected abstract ViewBinding onCreateViewBinding(@NonNull LayoutInflater inflater, 
            @Nullable ViewGroup container);
    
    // Allows child-classes to access the commonBinding to access common 
    // used views
    protected CommonLayoutBinding getCommonBinding() {
        return commonBinding;
    }
    

    Now have a look at one of the the child-classes, FragmentA. From onCreateViewBinding() we create our binding like we would do from onCreateView(). In principle it's still called from onCreateVIew(). This binding is used from the base class as described above. I am using getCommonBinding() to be able to access views from common_layout.xml. Every child class of BaseFragment is now able to access these views from the ViewBinding.

    That way I can move up all logic based on common views to the base class.

    private FragmentABinding binding; // fragment_a.xml
    
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, 
            @Nullable Bundle savedInstanceState) {
        // Make sure commonBinding is present before calling super.onCreateView() 
        // (onCreateViewBinding() needs to deliver a result!)
        View view = super.onCreateView(inflater, container, savedInstanceState);
        binding.editName.setText("Test");
        // ...
        CommonLayoutBinding commonBinding = getCommonBinding();
        commonBinding.buttonUp.setOnClickListener(v -> {
            // Handle onClick-event...
        });
        // ...
        return view;
    }
    
    // This comes from the base class and makes sure we have the required 
    // binding-instance, see BaseFragment
    @Override
    protected ViewBinding onCreateViewBinding(@NonNull LayoutInflater inflater, 
            @Nullable ViewGroup container) {
        binding = FragmentABinding.inflate(inflater, container, false);
        return binding;
    }
    

    Pros:

    • Reduced duplicate code by moving it to the base class. Code in all fragments is now much clearer, and reduced to the essentials
    • Cleaner layout by moving reusable views into a layout that's included via <include />

    Cons:

    • Possibly not applicable where views can't be moved into a commonly used layout file
      • Views might need to be positioned differently between fragments/layouts
      • Many <included /> layouts would result in many Binding classes, nothing gained then
    • Requires another binding instance (CommonLayoutBinding). There is not only one binding class for each child (FragmentA, FragmentB) that provides access to all views in the view hierarchy

    What if views can't be moved into a common layout?
    I am strongly interested in how to solve this as best practice! Let's think about it: introduce a wrapper class around the concrete ViewBinding.

    We could introduce an interface that provides access to commonly used views. From the Fragments we wrap our bindings in these wrapper classes. On the other hand, this would result in many wrappers for each ViewBinding-type. But we can provide these wrappers to the BaseFragment using an abstract method (an generics). BaseFragment is then able to access the views, or work on them using the defined interface methods. What do you think?

    In conclusion:
    Maybe it's simply an limitation of ViewBinding that one layout needs to have its own Binding-class. If you found a good solution in cases the layout can't be shared and needs to be declared duplicated in each layout, let me know please.

    I don't know if this is best practice or if there are better solutions. But while this is the only known solution for my use case, it seems to be a good start!