2

It's Xamarin.Android via C#.

The problem is custom adapter shows too many items in ListView if adapter was used from Fragment code. If I'll use it from MainActivity it will work fine.

I Need to show dynamic custom info inside ListView element which is located in Tab (Fragment). Each row of ListView should contain dynamic quantity of text boxes one by one in one row. For this purpose custom adapter generates GridLayout with TextView elements. Quantity of rows also is dynamic, that's why List<> was used. I tried to simplify my code as much as possible. In real life Data class is more complicated, a lot of Tabs used, etc.

Input: 1. Simple Class for some data

public class MyData
{
    internal int ID;
    internal string Nick;

    public MyData(int _id, string _nick)
    {
        ID = _id;
        Nick = _nick;
    }
}     

2. Tab1.axml is layout for our Tab we need to show. It contains only ListView element where Adapter should place data

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
    android:minWidth="25px"
    android:minHeight="25px"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/listView1" />

3. Grid.axml is layout which will represent every single line for ListView. It will contain dynamic quantity of TextView elements inside of GridView

<GridLayout xmlns:p1="http://schemas.android.com/apk/res/android"
p1:minWidth="25px"
p1:minHeight="25px"
p1:layout_width="match_parent"
p1:layout_height="match_parent"
p1:id="@+id/gridLayout1"
p1:columnCount="2" />

4. Main.axml is main layout which contains Tabs (only 1 for this example). fragmentContainer is our container for Tab. listViewMain is listview to check if Adapter works properly.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/linearLayout1">
<FrameLayout
    android:minWidth="25px"
    android:minHeight="25px"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/fragmentContainer" />
<ListView
    android:minWidth="25px"
    android:minHeight="25px"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/listViewMain" />

5. MainActivity.cs contains all the code

 protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);
        SetContentView(Resource.Layout.Main);

        //creating one main tab
        ActionBar.NavigationMode = ActionBarNavigationMode.Tabs;
        AddTab("Tab 1", new SampleTabFragment1());

        /* //to test if Adapter works properly From MainActivtity instead of Fragment
        List<MyData> listOfData = new List<MyData>();
        MyData item = new MyData(1, "Nick#1");
        listOfData.Add(item);

        item = new MyData(2, "Nick#2");
        listOfData.Add(item);

        ListView table = this.FindViewById<ListView>(Resource.Id.listViewMain);
        table.Adapter = new MyDataAdapter(this, listOfData);*/
    }

Standard function for creating Tab:

void AddTab(string tabText, Fragment view)
    {
        var tab = this.ActionBar.NewTab();
        tab.SetText(tabText);

        // must set event handler before adding Tab
        tab.TabSelected += delegate (object sender, ActionBar.TabEventArgs e)
        {
            var fragment = this.FragmentManager.FindFragmentById(Resource.Id.fragmentContainer);
            if (fragment != null)
                e.FragmentTransaction.Remove(fragment);
            e.FragmentTransaction.Add(Resource.Id.fragmentContainer, view);
        };
        tab.TabUnselected += delegate (object sender, ActionBar.TabEventArgs e)
        {
            e.FragmentTransaction.Remove(view);
        };

        this.ActionBar.AddTab(tab);
    }

Fragment for Tab where we bind custom adapter to listview:

class SampleTabFragment1 : Fragment
    {
        public override void OnDestroyView()
        {
            base.OnDestroyView();
        }            
        public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            base.OnCreateView(inflater, container, savedInstanceState);
            var view = inflater.Inflate(Resource.Layout.Tab1, container, false);

            //Creating our data
            List <MyData> listOfData = new List<MyData>();                
            MyData item = new MyData(1, "Nick#1");
            listOfData.Add(item);

            item = new MyData(2, "Nick#2");
            listOfData.Add(item);

            //Find listview from Tab1.axml which is actually our Tab Fragment
            ListView table = view.FindViewById<ListView>(Resource.Id.listView1);
            //Binding listview with custom adapter
            table.Adapter = new MyDataAdapter(this.Activity, listOfData);

            return view;
        }
    }

Finally, adapter for our custom data:

public class MyDataAdapter : BaseAdapter<MyData>
    {
        Activity context;
        List<MyData> list;
        public MyDataAdapter(Activity _context, List<MyData> _list)
        {
            context = _context;
            list = _list;
        }
        public override View GetView(int position, View convertView, ViewGroup parent)
        {
            View view = convertView;

            // re-use an existing view, if one is available
            // otherwise create a new one
            if (view == null)
                view = context.LayoutInflater.Inflate(Resource.Layout.Grid, parent, false);

            //find GridView from Grid.axml and set columns count to 2 (only ID and Nick)
            GridLayout tableView = view.FindViewById<GridLayout>(Resource.Id.gridLayout1);
            tableView.ColumnCount = 2;

            //Creating TextView to show ID, place position is 0 - first
            TextView textCaption = new TextView(view.Context);
            textCaption.SetText(list[position].ID.ToString(), TextView.BufferType.Normal);
            tableView.AddView(textCaption, 0);

            //Creating TextView to show ID, place position is 1 - second in a row of Grid
            textCaption = new TextView(view.Context);
            textCaption.SetText(list[position].Nick, TextView.BufferType.Normal);
            tableView.AddView(textCaption, 1);

            return view;
        }
        public override int Count
        {
            get { return list.Count; }
        }
        public override long GetItemId(int position)
        {
            return position;
        }

        public override MyData this[int index]
        {
            get { return list[index]; }
        }            
    }

All of this should show to end-user two rows of ListView. First row contains GridView with two TextView elements "1" and "Nick#1" in one row. Second row should contains GridView with "2" and "Nick#2" in one row like this: calling from MainActivity

...but instead each of 2 ListView rows contains a lot of GridView rows with data: calling from Fragment

If you will disable Tab by commenting

        //ActionBar.NavigationMode = ActionBarNavigationMode.Tabs;
        //AddTab("Tab 1", new SampleTabFragment1());

and uncommenting area of code from MainActivity.cs

//to test if Adapter works properly without Fragment

it will show correct result using exactly the same Custom Adapter and Layouts structure as it was used from Fragment SampleTabFragment1() call. Look like there are some tricks about using Fragments with custom Adapter and\or layouts, what do you think?

SerhiiK
  • 781
  • 8
  • 12
  • 1
    I think you might want to take another look at the design philosophy of fragments - https://developer.android.com/guide/components/fragments.html It seems like you might be "nesting" layouts in which your expected list is being added multiple times. Simplify your layouts to ensure that you have a simple `FrameLayout` that you are plugging your fragment into. You should then set breakpoints on your `list.Count` and ensuring it only has 2 items. Then check to see if other controls are being populated as well. – Jon Douglas Feb 06 '17 at 17:47
  • But it's already simplified as much as possible: Main Layout contains FragmentContainer for Fragment. Layout for Fragment is very simple - just ListView. That's it. Grid Layout is only using for custom list adapter which works pretty well. The problem is list.Count property is ok and ==2. But it's calling few times from Fragment for some reason and it's not correct. And it's calling correct if use from MainActivity instead of Fragment. – SerhiiK Feb 07 '17 at 15:58
  • I prefer using `FragmentTransaction.Replace()` as adding and removing fragments can sometimes be error prone unless you understand the lifecycle. Try to refactor that code to a `Replace()` instead! – Jon Douglas Feb 07 '17 at 16:40
  • Looks like problem is how `ListView` recycling mechanism works. Here [some info](http://stackoverflow.com/questions/11945563/how-listviews-recycling-mechanism-works) But in my case I can't even use `match_parent` for `layout_height` of `ListView`. Instead should use fixed value. Anyway, it's not a solution to use fixed size since all my data is dynamic and It's not good idea to use fixed size at all. Look like need to take a look at ArrayAdapter Android implementation and try to understand "magic" why it works fine for Fragment (if size isn't fixed) and why all custom Adapters doesn't... – SerhiiK Feb 07 '17 at 17:10

1 Answers1

1

After facing this problem I decided to switch to Java Android instead of using C#. In a few weeks after learning Java (and Android development) I tried to rewrite my program and faced the same problem! This time I've tried to dynamically create TableRows with TextViews inside it, via my custom adapter for ListView.

Still got duplicated elements in first row! But solution is very easy. You should create new Views during runtime only if current view for adapter wasn't created (equals null). (Of course, if you don't create any Views dynamically, instead use from XML layout it already works fine) This solution works just fine for my Java program and it should be like this for my old C# code:

public override View GetView(int position, View convertView, ViewGroup parent)
{
    View view = convertView;

    // re-use an existing view, if one is available
    // otherwise create a new one
    if (view == null)
    {
        view = context.LayoutInflater.Inflate(Resource.Layout.Grid, parent, false);
        //find GridView from Grid.axml and set columns count to 2 (only ID and Nick)
        GridLayout tableView = view.FindViewById<GridLayout>(Resource.Id.gridLayout1);
        tableView.ColumnCount = 2;

        //Creating TextView to show ID, place position is 0 - first
        TextView textCaption = new TextView(view.Context);
        textCaption.SetText(list[position].ID.ToString(), TextView.BufferType.Normal);
        tableView.AddView(textCaption, 0);

        //Creating TextView to show ID, place position is 1 - second in a row of Grid
        textCaption = new TextView(view.Context);
        textCaption.SetText(list[position].Nick, TextView.BufferType.Normal);
        tableView.AddView(textCaption, 1);
    }

    return view;
}
SerhiiK
  • 781
  • 8
  • 12