Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to do Android Data Binding a CustomAdapter inherited from BaseAdapter for Spinner?

I have a problem in implementing a CustomAdapter for a Spinner using Android DataBinding with BaseAdapter.

The data has two values. I want to use two TextViews. The CustomAdapter must be inherited from BaseAdapter, the simpler variant with ArrayAdapter supports only one TextView. In the long run the second TextView could be an ImageView, so merging the two values into one String to be able to use an ArrayAdapter would still not help.

What I tried also: To make sure the idea and the Spinner works as normal, I implemented a version without DataBinding And I based my data binding implementation on chrislizh's project at https://github.com/chrislizh/SpinnerTwoWayDataBindingDemo, which is using an ArrayAdapter. I also tried to call: binding.executePendingBindings(), and tried to not use the ViewHolder pattern.

The problem results in detail: The project is about planets. The Spinner allows the selection of a planet. Each planet has a name and a distance. Both values will be displayed. The result of my implementation of the CustomAdapter, called PlanetAdapter, with DataBinding displays the first item twice, after a selection. See screenshots. Selection of any other planet than the first item, 'switches' its position with the selection, and keeps being displayed twice. This way one planet is always missing in the displayed list.

Left drop down list with first planet listed twice. Right what is expected.

The code of the PlanetAdapter and the call to create it in the MainActivty's onCreate method:

PlanetAdapter

public class PlanetAdapter extends BaseAdapter
{
    private int itemLayoutResourceId;
    private final List<Planet> planets;
    private LayoutInflater inflater;

    public PlanetAdapter(@NonNull Context context, @LayoutRes int itemLayoutResourceId, List<Planet> planets)
    {
        inflater = LayoutInflater.from(context);
        this.itemLayoutResourceId = itemLayoutResourceId;
        this.planets = planets;
    }

    @Override
    public int getCount()
    {
        return planets.size();
    }

    @Override
    public Object getItem(int position)
    {
        return planets.get(position);
    }

    @Override
    public long getItemId(int position)
    {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
        PlanetViewHolder holder;

        if (convertView == null) {
            PlanetSpinnerItemBinding itemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.planet_spinner_item, parent, false);
            itemBinding.setPlanet(planets.get(position));

            holder = new PlanetViewHolder(itemBinding);
            holder.view = itemBinding.getRoot();
            holder.view.setTag(holder);
        }
        else {
            holder = (PlanetViewHolder) convertView.getTag();
        }
        return holder.view;
    }

    private static class PlanetViewHolder
    {
        private View view;

        PlanetViewHolder(PlanetSpinnerItemBinding binding)
        {
            this.view = binding.getRoot();
        }
    }}

Creation and setting the Adapter:

      public class MainActivity extends AppCompatActivity implements IDataChangeListener
        {
            private static final String BUNDLE_SELECTED_PLANET = "bundle_selected_planet";
            private ActivityMainBinding activityMainBinding_;
            private List<Planet> planets_;

        @Override
        protected void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);

            activityMainBinding_ = DataBindingUtil.setContentView(this, R.layout.activity_main);

            planets_ = loadPlanets(this, R.array.planetsInSolarSystem);
            if (planets_ != null) {
                 planets_.add(0, new Planet("", 0));  // insert a blank item on the top of the list

                PlanetAdapter planetAdapter = new PlanetAdapter(this, R.layout.planet_spinner_item, planets_);
                activityMainBinding_.setSpinAdapterPlanet(planetAdapter);
                Planet selectedPlanet = savedInstanceState != null ? savedInstanceState.<Planet>getParcelable(BUNDLE_SELECTED_PLANET) : planets_.get(3);//initial selected planet is Earth, 3 is the index of Earth after a blank item inserted
                activityMainBinding_.setBindingPlanet(new BindingPlanet(this, selectedPlanet));
            }
    } // loadPlanets skipped.
 }

The planet class:

public class Planet implements Parcelable {

    private String name_;
    private float distance_; //distance to Sun in AU(Astronomical Unit)

    public Planet(String name, float distance) {
        name_ = name;
        distance_ = distance;
    }

    protected Planet(Parcel in) {
        name_ = in.readString();
        distance_ = in.readFloat();
    }

    public static final Creator<Planet> CREATOR = new Creator<Planet>() {
        @Override
        public Planet createFromParcel(Parcel in) {
            return new Planet(in);
        }

        @Override
        public Planet[] newArray(int size) {
            return new Planet[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name_);
        dest.writeFloat(distance_);
    }

    @Override
    public String toString() {
        return name_ != null ? name_ : super.toString();
    }

    public String getName() {
        return name_;
    }

    public void setName(String name) {
        name_ = name;
    }

    public float getDistance() {
        return distance_;
    }

    public void setDistance(float distance) {
        distance_ = distance;
    }
}

The XML of the item layout: spinner_planet_item.xml `

<data>
    <variable
        name="planet"
        type="au.com.chrisli.spinnertwowaydatabindingdemo.Planet">
    </variable>
</data>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:orientation="horizontal"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

    <TextView
        android:id="@+id/planetName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{planet.name}"
        tools:text="Dummy Planet"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"/>

    <TextView
        android:id="@+id/planetDistance"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingLeft="8dp"
        android:text="@{String.valueOf(planet.distance)}"
        android:textAppearance="@style/TextAppearance.AppCompat.Medium"
        tools:text="2393.0 km"/>
</LinearLayout>

In the github code, I also included the classic case (without data binding) in the class PlanetAdapter, at the end of the file, but commented out. In case you want to see that it works - just switch getView und PlaneViewHolder implementations there.

The full modified project is on my github at: https://github.com/Minsky/SO_Question_SpinnerDataBindingBaseAdapter

What's wrong with my Binding implementation of the BaseAdapter?

like image 606
Minsky Avatar asked Dec 02 '22 13:12

Minsky


1 Answers

As Commonsware wrote in his comment, binding the views in the recycle case was missing. The working code for the method getView() in PlanetAdapter is something like this:

@Override
public View getView(int position, View convertView, ViewGroup parent)
{
    PlanetViewHolder holder;

    if (convertView == null) {
        PlanetSpinnerItemBinding itemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.planet_spinner_item, parent, false);

        holder = new PlanetViewHolder(itemBinding);
        holder.view = itemBinding.getRoot();
        holder.view.setTag(holder);
    }
    else {
        holder = (PlanetViewHolder) convertView.getTag();
    }
    holder.binding.setPlanet(planets.get(position));
    return holder.view;
}

And the PlanetViewHolder holds now the binding:

private static class PlanetViewHolder {
  private View view;
  private PlanetSpinnerItemBinding binding;

 PlanetViewHolder(PlanetSpinnerItemBinding binding) {
     this.view = binding.getRoot();
     this.binding = binding;
    }
}

final Github projet in branch "solved": https://github.com/Minsky/SO_Question_SpinnerDataBindingBaseAdapter/tree/solved

like image 134
Minsky Avatar answered Apr 19 '23 22:04

Minsky