ListView in widget adds randomly items on scrolling and resizing (nested remoteviews)
Asked Answered
F

3

14

Update: I created a repository with less code to make it a bit easier to understand.

I'm trying to create a widget. I made it like described here: https://mcmap.net/q/606008/-implement-listview-in-android-widgets

It works partially, but I have a really strange bug. I made a screencast, so it's easier to understand what I mean: http://c.maysi.de/c6H9

Screenshot: enter image description here

As you can see there are some items which were added randomly. (RemoteViews which were added to another RemoteViews object) The same happens when I resize the widget.

The things I printed out in the log are like expected. there is no wrong data. Also there are no new log entries when I scroll.

This is my code:

RemoteViewsFactory:

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class MyWidgetViewsFactory implements RemoteViewsService.RemoteViewsFactory {
    private static ArrayList<Item> items = new ArrayList<>();
    private static int itemnr = 0;
    private static int subitemnr = 0;
    private int appWidgetId;
    private Context context;

    public MyWidgetViewsFactory(Context context, Intent intent) {
        this.context = context;
        appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);


        //Some random data to display
        for (int i = 0; i < 10; i++) {
            Item item = new Item(String.valueOf(itemnr++));

            for (int j = 0; j < 3; j++) {
                String[] subitem = {String.valueOf(subitemnr++), String.valueOf(subitemnr++), String.valueOf(subitemnr++)};
                item.addSubitem(subitem);
            }

            items.add(item);
        }
    }

    @Override
    public void onCreate() {
        // no-op
    }

    @Override
    public void onDestroy() {
        // no-op
    }

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

    @Override
    public RemoteViews getViewAt(int position) {
        Log.d("MyWidgetViewsFactory", "getViewAt(" + position + "):" + items.get(position));
        Item item = items.get(position);

        RemoteViews itemView = new RemoteViews(context.getPackageName(), R.layout.widget_listview_item);

        itemView.setTextViewText(R.id.textView_itemnr, item.getItemNr());

        for (String[] s : item.getSubitems()) {
            Log.d("MyWidgetViewsFactory", "subitem:" + s[0] + "|" + s[1] + "|" + s[2]);
            RemoteViews subitem = new RemoteViews(context.getPackageName(), R.layout.widget_listview_subitem);

            subitem.setTextViewText(R.id.textView_1, s[0]);
            subitem.setTextViewText(R.id.textView_2, s[1]);
            subitem.setTextViewText(R.id.textView_3, s[2]);

            itemView.addView(R.id.linearLayout_item_body, subitem);
        }
        return itemView;
    }

    @Override
    public RemoteViews getLoadingView() {
        return (null);
    }

    @Override
    public int getViewTypeCount() {
        return (1);
    }

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

    @Override
    public boolean hasStableIds() {
        return (true);
    }

    @Override
    public void onDataSetChanged() {
        // no-op
    }

    class Item {
        private ArrayList<String[]> subitems = new ArrayList<>();
        private String itemnr = "";

        Item(String itemnr) {
            this.itemnr = itemnr;
        }

        Item() {
        }

        public void addSubitem(String[] subitem) {
            this.subitems.add(subitem);
        }

        public ArrayList<String[]> getSubitems() {
            return subitems;
        }

        public String getItemNr() {
            return itemnr;
        }

        public void setItemNr(String itemnr) {
            this.itemnr = itemnr;
        }
    }
}

AppWidgetProvider

    public class MyWidgetProvider extends AppWidgetProvider {

    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        Log.d("MyWidgetProvider", "appWidgetIds.lenght:" + appWidgetIds.length);
        for (int appWidgetId : appWidgetIds) {
            Intent svcIntent = new Intent(context, MyWidgetService.class);
            svcIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);

            RemoteViews widget = new RemoteViews(context.getPackageName(), R.layout.widget_root);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
                widget.setRemoteAdapter(R.id.listView_widget, svcIntent);
            else
                widget.setRemoteAdapter(appWidgetId, R.id.listView_widget, svcIntent);

            /*
            Intent clickIntent = new Intent(context, MainActivity.class);
            PendingIntent clickPI = PendingIntent.getActivity(context, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT);
            widget.setPendingIntentTemplate(R.id.listView_widget, clickPI);*/

            appWidgetManager.updateAppWidget(appWidgetId, widget);
        }
    }

    @Override
    public void onEnabled(Context context) {
        // Enter relevant functionality for when the first widget is created
    }

    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
    }
}

RemoteViewsService

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class MyWidgetService extends RemoteViewsService {
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return (new MyWidgetViewsFactory(this.getApplicationContext(), intent));
    }
}

all other resources can you find in the repo at GitHub.


Logcat output:

08-08 02:11:10.858  32427-32444/? D/MyWidgetViewsFactory﹕ getViewAt(0):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@3e7179c9
08-08 02:11:10.860  32427-32444/? D/MyWidgetViewsFactory﹕ subitem:0|1|2
08-08 02:11:10.864  32427-32444/? D/MyWidgetViewsFactory﹕ subitem:3|4|5
08-08 02:11:10.866  32427-32444/? D/MyWidgetViewsFactory﹕ subitem:6|7|8
08-08 02:11:10.927  32427-32443/? D/MyWidgetViewsFactory﹕ getViewAt(0):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@3e7179c9
08-08 02:11:10.927  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:0|1|2
08-08 02:11:10.927  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:3|4|5
08-08 02:11:10.927  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:6|7|8
08-08 02:11:10.931  32427-32443/? D/MyWidgetViewsFactory﹕ getViewAt(1):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@23e248ce
08-08 02:11:10.931  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:9|10|11
08-08 02:11:10.931  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:12|13|14
08-08 02:11:10.931  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:15|16|17
08-08 02:11:10.933  32427-32443/? D/MyWidgetViewsFactory﹕ getViewAt(2):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@16dbf3ef
08-08 02:11:10.933  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:18|19|20
08-08 02:11:10.933  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:21|22|23
08-08 02:11:10.933  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:24|25|26
08-08 02:11:10.936  32427-32443/? D/MyWidgetViewsFactory﹕ getViewAt(3):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@19d3defc
08-08 02:11:10.936  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:27|28|29
08-08 02:11:10.936  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:30|31|32
08-08 02:11:10.936  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:33|34|35
08-08 02:11:10.938  32427-32443/? D/MyWidgetViewsFactory﹕ getViewAt(4):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@ee985
08-08 02:11:10.938  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:36|37|38
08-08 02:11:10.938  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:39|40|41
08-08 02:11:10.938  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:42|43|44
08-08 02:11:10.941  32427-32443/? D/MyWidgetViewsFactory﹕ getViewAt(8):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@335e23da
08-08 02:11:10.941  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:72|73|74
08-08 02:11:10.941  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:75|76|77
08-08 02:11:10.941  32427-32443/? D/MyWidgetViewsFactory﹕ subitem:78|79|80
08-08 02:11:10.943  32427-32447/? D/MyWidgetViewsFactory﹕ getViewAt(9):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@229de00b
08-08 02:11:10.943  32427-32447/? D/MyWidgetViewsFactory﹕ subitem:81|82|83
08-08 02:11:10.943  32427-32447/? D/MyWidgetViewsFactory﹕ subitem:84|85|86
08-08 02:11:10.943  32427-32447/? D/MyWidgetViewsFactory﹕ subitem:87|88|89
08-08 02:11:10.945  32427-32444/? D/MyWidgetViewsFactory﹕ getViewAt(5):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@2afdeee8
08-08 02:11:10.945  32427-32444/? D/MyWidgetViewsFactory﹕ subitem:45|46|47
08-08 02:11:10.945  32427-32444/? D/MyWidgetViewsFactory﹕ subitem:48|49|50
08-08 02:11:10.945  32427-32444/? D/MyWidgetViewsFactory﹕ subitem:51|52|53
08-08 02:11:10.948  32427-32444/? D/MyWidgetViewsFactory﹕ getViewAt(7):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@1c599901
08-08 02:11:10.948  32427-32444/? D/MyWidgetViewsFactory﹕ subitem:63|64|65
08-08 02:11:10.948  32427-32444/? D/MyWidgetViewsFactory﹕ subitem:66|67|68
08-08 02:11:10.948  32427-32444/? D/MyWidgetViewsFactory﹕ subitem:69|70|71
08-08 02:11:10.951  32427-32447/? D/MyWidgetViewsFactory﹕ getViewAt(6):de.mayerhofersimon.listviewproblem.MyWidgetViewsFactory$Item@368aa3a6
08-08 02:11:10.951  32427-32447/? D/MyWidgetViewsFactory﹕ subitem:54|55|56
08-08 02:11:10.951  32427-32447/? D/MyWidgetViewsFactory﹕ subitem:57|58|59
08-08 02:11:10.951  32427-32447/? D/MyWidgetViewsFactory﹕ subitem:60|61|62

So the data gets passed correct. it just doesn't get correct displayed...

BTW: this is what it should look like: http://c.maysi.de/cB8K

Could it be, that the problem is because of nested remoteviews? because all the outer remoteviews are displayed correct...

Fungistat answered 30/7, 2015 at 0:28 Comment(3)
I added a screenshot. you can also see it in the screencast. I just scroll down the list and up again and then these extra items were added.Fungistat
and these items aren't always the same. the change. but they are part of the widget inside another list-item. the items which were added are different in the screencast and the screenshot.Fungistat
I don't call it somewhere. This is done by android I think. After the time I set in the manifest.Fungistat
F
4

I found the answer myself.

To fix the problem with the weird adding of views on scrolling and resizing you have to call removeAllViews on the Layout where the subviews were added:

@Override
    public RemoteViews getViewAt(int position) {
        ...
        RemoteViews itemView = new RemoteViews(context.getPackageName(), R.layout.widget_listview_item);
        itemView.removeAllViews(R.id.linearLayout_item_body);
        ...
        return itemView;
    }

And the problem that the views are not displayed is because of the color: After adding

    subitem.setTextColor(R.id.textView_1, context.getResources().getColor(R.color.abc_primary_text_material_light));
    subitem.setTextColor(R.id.textView_2, context.getResources().getColor(R.color.abc_primary_text_material_light));
    subitem.setTextColor(R.id.textView_3, context.getResources().getColor(R.color.abc_primary_text_material_light));

all views are displayed:

enter image description here

Fungistat answered 8/8, 2015 at 1:30 Comment(5)
It's great that you solved it. However calling removeAllViews() seems like a workaround fix. You could call removeAllViews in any other logical override method. And getViewAt(int position) is meant to return a certain view from a row position in a ListView. So I suspect the fix has a drawback in reducing performance.Wolenik
I like your solution because it works :) but I have to admit I don't understand why it works: why should one have to remove anything from a perfectly new, empty LinearLayout? BTW out of curiosity I put your ListView inside an activity using the same row layout and got the same scrolling problem. Your solution worked there as well :)Gimlet
@0X0nosugar why should one have to remove anything from a perfectly new, empty LinearLayout? . I don't think it is empty. I think what you get is a reference to the existing view; the one currently in the widget display.Halitosis
@iturki - yes, I think now that the RemoteViews object is indeed created "new", but for performance reasons it works just like an adapter by recycling a ViewGroup (Layout) you have already seen on screen. Do you know of something like a ViewHolder pattern for Homescreen Widgets/ RemoteViews?Gimlet
"However calling removeAllViews() seems like a workaround fix." - well, it's the solution used in a popular book from CommonsWare. Researching the issue further - I found that's exactly what was recommended. Reference:RemoteViewsHostActivity.java line 109 github.com/commonsguy/cw-advandroid/blob/master/RemoteViews/…Walsingham
F
4

Your thought behind ListView recycling issue is correct. You need to understand how things works at ground level.

MVC Pattern

The Android graphics works on the MVC pattern i.e. Model-View and Controller pattern. Model is your data, database in your case, View is your layout or graphical portion,such as ListView or RecyclerView or RemoteView. Controller changes your view after data update, parent View or ViewGroup in your case the RemoteViewsService.RemoteViewsFactory is the controller. I suggest to read further by googling the MVC model.

How Pattern is Implemented?

Any time the data changes, the view should be updated by the controller. The Android framework gives you opportunity to display your view at the given position with your data by overriding the getViewAt(int position).The controller calls the getViewAt(int position) to get the view at the given position in the ListView or RecyclerView. ListView or RecyclerView renders only visible rows on the screen. For example, If you have 100 items in the ListView and only 7 is visible on the screen than it will call getItemAt(int) 7 times. Every time you scroll the getItemAt(int) is called for the visible rows. The ListView and RemoteView take liberty to recycle/reuse the previously passed View returned by the getItemAt(int position). It ensures that memory consumed by graphical portion of your application is limited

Why there is strange behavior?

First of all every visible thing on the screen is a View such as TextView, ImageView and ListView etc. If not it can not be displayed on the screen. RemoteView is not a View. You pass the layout and data to be displayed with the RemoteView (View + Data).

Here the I am referring to your Screencast for the explanation.

1) Initialization: The ListView in your case, initially creates say 6 rows based on the visible space on screen and getViewAt(int position) is called once if getCount() returns 1. I request to check the return value of the getCount() of the List adapter.
2) You scrolled down: Nothing happens to the ListView and rendering of new Rows.
3) You Scrolled Up: getPositionAt(int position) is called again and RemoteView is passed back.Two rows are visible now. I request to check the getCount() return value. It should be 2 if not than the reason could be caching of rows by ListView.
4) You scrolled down: Nothing happens to the ListView and rendering of new Rows.
5) You Scrolled Up: Refer 3. The getCount() should be 3 and so on.

What you should do?

As per your implementation, you created the RemoteView only once and tried reusing the same view at the getItemAt(int) , may be to save on the layout inflation time.
To fix the issue, you MUST provide the FRESH RemoteView every time getItemAt(int) is called.

@Override
public RemoteViews getViewAt(int position) 
{
    Log.d("VplanWidgetViewsFactory", "getViewAt("+position+"):"+stunden.get(position));
    //TODO: Store context when constructor is called.
    RemoteView rv = new RemoteViews(context.getPackageName(), R.layout.fragment_stunde_widget);
    rv.setTextViewText(R.id.textView_lesson_nr, "" + (position + 1) + "."); <=  I have not tested this.

    return rv;
    //return stunden.get(position); <=COMMENT THIS
}
Frisch answered 7/8, 2015 at 0:48 Comment(12)
I managed to reproduce the bug in Android Studio and your approach with a fresh RemoteView produces the same weird behaviour. Logging showed that getViewAt() was called twice for the first list item, once for the others. The first rendering of the data is correct but when you are scrolling the random addition of RemoteViews (faecher) happens in a kind of black box. I suppose android cannot cope with nesting layouts in a widget listview :(Gimlet
First: Thanks! I updated my question. please have a look at it.Fungistat
Thanks. Could you please provide the getCount() output as well? As per my understanding the getCount() is called to know how many maximum views is supposed to be displayed. If getCount() returns one, your initial count, it should call the getItemAt(int) only once. Please check the same. One last thing I could suggest to reduce the height of list view to "wrap_content" or "some small integer, 48dp" or so. and see it makes any sense or not.Frisch
the getCount always returns 0 (if there is no data in my database) or the size of my ArrayList of my Stunde objects. so its always the same value.Fungistat
You logcat shows that the getViewAt(int) is called for the various positions in your ListView. At the time of rendering layouts, the caller of getViewAt(int) thinks that all the Layout/rows it tries to display has some data with it. Check the passed position with the size of the Stunde count if it is greater than return the null view. It will not display the duplicate view after that.Frisch
I think there is a fallacy. Correct me if I'm wrong. I think this is the way the listview gets filled: 1. call to getCount() 2. for getCount times: call getViewAt. e.g. I have 3 items in my stunden ArrayList getCount returns 3. getView(0) returns the data which is stored in stunden.get(0) and so on. Just to clarify: the data for the sub-remoteviews(but not the remoteviews itself) like Alb|S1/1|KFH is stored in an object in stunden. And this information is used in getViewAt to create a new RemoteViews object which gets added to another remoteview (the think which looks like a card)Fungistat
getViewAt() is called only for the visible rows in the ListView. The getCount() is used internally to know how many rows should be inflated. Example: List.size() = 10 items, Visibility based on screen size = 5 items, Initially getViewAt(int position) 5 times: getViewAt(0..4). If you scroll than getViewAt(int) is called for the new visible positions. ListView initially creates only 5 visible rows/views and reuses them with new data returned from the getViewAt() call.Frisch
If you wish pass me your complete project, I will get back to you after playing with your code.Frisch
I'm working on it. to make it a lot easier to understand. I think in about 15 minutes you can get it on GitHub. I'll notice youFungistat
I created a repo so you can try it: github.com/SimonMayerhofer/android-listview-problem the only thing: the text of the subitems is not shown (similar to my app)Fungistat
The reason why I gave you the bounty though I found the answer myself is the following: You helped me a lot to find the right answer. without your explanation I wouldn't have find it. Because of this: a big thanks to you for your help!Fungistat
Thank you. I am happy that you could solve the issue.Frisch
F
4

I found the answer myself.

To fix the problem with the weird adding of views on scrolling and resizing you have to call removeAllViews on the Layout where the subviews were added:

@Override
    public RemoteViews getViewAt(int position) {
        ...
        RemoteViews itemView = new RemoteViews(context.getPackageName(), R.layout.widget_listview_item);
        itemView.removeAllViews(R.id.linearLayout_item_body);
        ...
        return itemView;
    }

And the problem that the views are not displayed is because of the color: After adding

    subitem.setTextColor(R.id.textView_1, context.getResources().getColor(R.color.abc_primary_text_material_light));
    subitem.setTextColor(R.id.textView_2, context.getResources().getColor(R.color.abc_primary_text_material_light));
    subitem.setTextColor(R.id.textView_3, context.getResources().getColor(R.color.abc_primary_text_material_light));

all views are displayed:

enter image description here

Fungistat answered 8/8, 2015 at 1:30 Comment(5)
It's great that you solved it. However calling removeAllViews() seems like a workaround fix. You could call removeAllViews in any other logical override method. And getViewAt(int position) is meant to return a certain view from a row position in a ListView. So I suspect the fix has a drawback in reducing performance.Wolenik
I like your solution because it works :) but I have to admit I don't understand why it works: why should one have to remove anything from a perfectly new, empty LinearLayout? BTW out of curiosity I put your ListView inside an activity using the same row layout and got the same scrolling problem. Your solution worked there as well :)Gimlet
@0X0nosugar why should one have to remove anything from a perfectly new, empty LinearLayout? . I don't think it is empty. I think what you get is a reference to the existing view; the one currently in the widget display.Halitosis
@iturki - yes, I think now that the RemoteViews object is indeed created "new", but for performance reasons it works just like an adapter by recycling a ViewGroup (Layout) you have already seen on screen. Do you know of something like a ViewHolder pattern for Homescreen Widgets/ RemoteViews?Gimlet
"However calling removeAllViews() seems like a workaround fix." - well, it's the solution used in a popular book from CommonsWare. Researching the issue further - I found that's exactly what was recommended. Reference:RemoteViewsHostActivity.java line 109 github.com/commonsguy/cw-advandroid/blob/master/RemoteViews/…Walsingham
F
1

Your problem seems to be in:

if(stundenContainer[j]!=null)
    Log.d("VplanWidgetViewsFactory", "stundenContainer["+j+"]:" + stundenContainer[j].toString());
else
    Log.d("VplanWidgetViewsFactory", "stundenContainer[" + j + "]:null");

if (stundenContainer[j] == null) {
    //Freistunde
    Log.d("VplanWidgetViewsFactory", "Freistunde");
    // HERE -----
    stunden.add(new RemoteViews(context.getPackageName(), R.layout.fragment_stunde_widget));
    faecher.add(new RemoteViews(context.getPackageName(), R.layout.fragment_fach));
    stunden.get(stunden.size() - 1).setTextViewText(R.id.textView_lesson_nr, "" + (j + 1) + ".");
 } else if (!stundenContainer[j].get(0).getSubject().equals("ignore")) {
     Log.d("VplanWidgetViewsFactory", "stundenContainer[j].get(0).getSubject(): " + stundenContainer[j].get(0).getSubject());
     // HERE -----
     stunden.add(new RemoteViews(context.getPackageName(), R.layout.fragment_stunde_widget));

You are adding it twice.. , but only when the first item is not ignored, so it appears random.

stunden.add(new RemoteViews(context.getPackageName(), R.layout.fragment_stunde_widget));
Fuge answered 1/8, 2015 at 19:40 Comment(8)
in the database all data is correct, but I'll try thisFungistat
I tried it and this is not the problem. I changed every variable of database (my manager class) to static and I use only the application context. but this did not workFungistat
Also on resizing the widgetFungistat
no problem. thanks for your help anyway :) is the code for the activity or fragment necessary? they don't get in touch with the widget in any way.Fungistat
Oh, my fault! Sry! I updated my code according to Devendra Vaja answer and forgot to add it to the question. Here is the new RemoteViewsFactory: c.maysi.de/cB74 - everything in the constructor is initilisation for my ArrayList<Stunde> stunden. So I think it is not necessary to debug this, because the result is ok. I moved the creation of the RemoteViews objects to the getViewAt(int position) method.Fungistat
Still the same behaviour. have you read the "UPDATE" section in my question?Fungistat
It's nearly the same. the only difference is, that there is only 1 sub-remoteview in the widget now (Alb|S1/1|KFH) but when I scroll (or resize) there were items added with no content and the (Alb|S1/1|KFH) is duplicated some times. so its nearly the same. there are just not as many sub-remoteviews as before. have you understood what I mean?Fungistat
I created a repo so you can try it: github.com/SimonMayerhofer/android-listview-problemFungistat

© 2022 - 2024 — McMap. All rights reserved.