Android, ListView IllegalStateException: «Содержимое адаптера изменилось, но ListView не получил уведомление» - PullRequest
184 голосов
/ 28 июня 2010

Что я хочу сделать : запустить фоновый поток, который вычисляет содержимое ListView и частично обновить ListView, пока рассчитываются результаты.

Чего я знаю, что мне следует избегать : я не могу связываться с содержимым ListAdapter из фонового потока, поэтому я унаследовал AsyncTask и опубликовал результат (добавление записей в адаптер) из onProgressUpdate.Мой адаптер использует ArrayList объектов результатов, все операции над этими массивами синхронизированы.

Исследования других людей : здесь есть очень ценные данные здесь .Я также страдал от почти ежедневных сбоев для группы из ~ 500 пользователей, и когда я добавил блок list.setVisibility(GONE)/trackList.setVisibility(VISIBLE) в onProgressUpdate, сбои уменьшились в 10 раз, но не исчезли.(это было предложено в ответ )

То, что я иногда получал : пожалуйста, обратите внимание, что это происходит очень редко (один раз в неделю для одного из пользователей 3.5k).Но я бы хотел полностью избавиться от этой ошибки.Вот частичная трассировка стека:

`java.lang.IllegalStateException:` The content of the adapter has changed but ListView  did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(2131296334, class android.widget.ListView) with Adapter(class com.transportoid.Tracks.TrackListAdapter)]
at android.widget.ListView.layoutChildren(ListView.java:1432)
at android.widget.AbsListView.onTouchEvent(AbsListView.java:2062)
at android.widget.ListView.onTouchEvent(ListView.java:3234)
at android.view.View.dispatchTouchEvent(View.java:3709)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:852)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
[...]

Справка? Больше не нужна, см. Ниже

ЗАКЛЮЧИТЕЛЬНЫЙ ОТВЕТ: Как оказалось, я былвызывая notifyDataSetChanged каждые 5 вставок, чтобы избежать мерцания и внезапных изменений списка.Это не может быть сделано таким образом, всегда уведомляйте адаптер при изменении базового списка.Эта ошибка давно исчезла для меня.

Ответы [ 24 ]

115 голосов
/ 15 июня 2011

У меня была такая же проблема.

Я добавлял элементы в ArrayList вне потока пользовательского интерфейса.

Решение: я выполнил оба действия, adding the items и вызвал notifyDataSetChanged() в потоке пользовательского интерфейса.

27 голосов
/ 01 сентября 2011

У меня была такая же проблема, но я исправил ее методом

requestLayout();

из класса ListView

20 голосов
/ 02 января 2014

Это Многопоточность Проблема и правильное использование Синхронизированные Блоки Это можно предотвратить. Не добавляя лишних вещей в UI Thread и не вызывая потери отзывчивости приложения.

Я тоже сталкивался с тем же. И поскольку наиболее приемлемый ответ предлагает внести изменения в данные адаптера из потока пользовательского интерфейса, можно решить эту проблему. Это будет работать, но это быстрое и простое решение, но не лучшее.

Как вы можете видеть для нормального случая. Обновление адаптера данных из фонового потока и вызов notifyDataSetChanged в потоке пользовательского интерфейса работает.

Это незаконное исключение состояния возникает, когда поток пользовательского интерфейса обновляет представление, а другой фоновый поток снова изменяет данные. Этот момент вызывает эту проблему.

Так что если вы будете синхронизировать весь код, который изменяет данные адаптера и делает вызов notifydatasetchange. Эта проблема должна исчезнуть. Как у меня, и я все еще обновляю данные из фонового потока.

Вот мой конкретный код для других пользователей.

Мой загрузчик на главном экране загружает контакты телефонной книги в мои источники данных в фоновом режиме.

    @Override
    public Void loadInBackground() {
        Log.v(TAG, "Init loadings contacts");
        synchronized (SingleTonProvider.getInstance()) {
            PhoneBookManager.preparePhoneBookContacts(getContext());
        }
    }

Этот PhoneBookManager.getPhoneBookContacts считывает контакты из телефонной книги и заполняет их в хэш-картах. Который непосредственно используется для списочных адаптеров для составления списка.

На моем экране есть кнопка. Это открывает деятельность, в которой перечислены эти номера телефонов. Если я непосредственно установил Adapter над списком до того, как предыдущий поток завершит свою работу, это происходит быстрее, так как случай с навигатором случается реже. Появляется исключение. Это название этого ТАКОГО вопроса. Поэтому я должен сделать что-то подобное во втором упражнении.

Мой загрузчик во втором задании ожидает завершения первого потока. Пока это показывает индикатор выполнения. Проверьте loadInBackground обоих загрузчиков.

Затем он создает адаптер и доставляет его деятельности, где в потоке пользовательского интерфейса я вызываю setAdapter.

Это решило мою проблему.

Этот код только фрагмент. Вам нужно изменить его, чтобы он хорошо скомпилировался.

@Override
public Loader<PhoneBookContactAdapter> onCreateLoader(int arg0, Bundle arg1) {
    return new PhoneBookContactLoader(this);
}

@Override
public void onLoadFinished(Loader<PhoneBookContactAdapter> arg0, PhoneBookContactAdapter arg1) {
    contactList.setAdapter(adapter = arg1);
}

/*
 * AsyncLoader to load phonebook and notify the list once done.
 */
private static class PhoneBookContactLoader extends AsyncTaskLoader<PhoneBookContactAdapter> {

    private PhoneBookContactAdapter adapter;

    public PhoneBookContactLoader(Context context) {
        super(context);
    }

    @Override
    public PhoneBookContactAdapter loadInBackground() {
        synchronized (SingleTonProvider.getInstance()) {
            return adapter = new PhoneBookContactAdapter(getContext());    
        }
    }

}

Надеюсь, это поможет

15 голосов
/ 13 ноября 2013

Я решил это с помощью 2 списков.Один список я использую только для адаптера, и я делаю все изменения / обновления данных в другом списке.Это позволяет мне обновлять один список в фоновом потоке, а затем обновлять список «адаптеров» в основном потоке / интерфейсе:

List<> data = new ArrayList<>();
List<> adapterData = new ArrayList();

...
adapter = new Adapter(adapterData);
listView.setAdapter(adapter);

// Whenever data needs to be updated, it can be done in a separate thread
void updateDataAsync()
{
    new Thread(new Runnable()
    {
        @Override
        public void run()
        {
            // Make updates the "data" list.
            ...

            // Update your adapter.
            refreshList();
        }
    }).start();
}

void refreshList()
{
    runOnUiThread(new Runnable()
    {
        @Override
        public void run()
        {
            adapterData.clear();
            adapterData.addAll(data);
            adapter.notifyDataSetChanged();
            listView.invalidateViews();
        }
    });
}
7 голосов
/ 30 июня 2010

Я написал этот код и запустил его в образе эмулятора 2.1 в течение ~ 12 часов, но не получил IllegalStateExceptionЯ собираюсь дать фреймворку для Android преимущество сомнения и сказать, что это, скорее всего, ошибка в вашем коде.Надеюсь, это поможет.Может быть, вы можете адаптировать его к вашему списку и данным.

public class ListViewStressTest extends ListActivity {
    ArrayAdapter<String> adapter;
    ListView list;
    AsyncTask<Void, String, Void> task;

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

        this.adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        this.list = this.getListView();

        this.list.setAdapter(this.adapter);

        this.task = new AsyncTask<Void, String, Void>() {
            Random r = new Random();
            int[] delete;
            volatile boolean scroll = false;

            @Override
            protected void onProgressUpdate(String... values) {
                if(scroll) {
                    scroll = false;
                    doScroll();
                    return;
                }

                if(values == null) {
                    doDelete();
                    return;
                }

                doUpdate(values);

                if(ListViewStressTest.this.adapter.getCount() > 5000) {
                    ListViewStressTest.this.adapter.clear();
                }
            }

            private void doScroll() {
                if(ListViewStressTest.this.adapter.getCount() == 0) {
                    return;
                }

                int n = r.nextInt(ListViewStressTest.this.adapter.getCount());
                ListViewStressTest.this.list.setSelection(n);
            }

            private void doDelete() {
                int[] d;
                synchronized(this) {
                    d = this.delete;
                }
                if(d == null) {
                    return;
                }
                for(int i = 0 ; i < d.length ; i++) {
                    int index = d[i];
                    if(index >= 0 && index < ListViewStressTest.this.adapter.getCount()) {
                        ListViewStressTest.this.adapter.remove(ListViewStressTest.this.adapter.getItem(index));
                    }
                }
            }

            private void doUpdate(String... values) {
                for(int i = 0 ; i < values.length ; i++) {
                    ListViewStressTest.this.adapter.add(values[i]);
                }
            }

            private void updateList() {
                int number = r.nextInt(30) + 1;
                String[] strings = new String[number];

                for(int i = 0 ; i < number ; i++) {
                    strings[i] = Long.toString(r.nextLong());
                }

                this.publishProgress(strings);
            }

            private void deleteFromList() {
                int number = r.nextInt(20) + 1;
                int[] toDelete = new int[number];

                for(int i = 0 ; i < number ; i++) {
                    int num = ListViewStressTest.this.adapter.getCount();
                    if(num < 2) {
                        break;
                    }
                    toDelete[i] = r.nextInt(num);
                }

                synchronized(this) {
                    this.delete = toDelete;
                }

                this.publishProgress(null);
            }

            private void scrollSomewhere() {
                this.scroll = true;
                this.publishProgress(null);
            }

            @Override
            protected Void doInBackground(Void... params) {
                while(true) {
                    int what = r.nextInt(3);

                    switch(what) {
                        case 0:
                            updateList();
                            break;
                        case 1:
                            deleteFromList();
                            break;
                        case 2:
                            scrollSomewhere();
                            break;
                    }

                    try {
                        Thread.sleep(0);
                    } catch(InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }

        };

        this.task.execute(null);
    }
}
3 голосов
/ 03 апреля 2014

Моя проблема была связана с использованием фильтра вместе с ListView.

При настройке или обновлении базовой модели данных ListView я былделая что-то вроде этого:

public void updateUnderlyingContacts(List<Contact> newContacts, String filter)
{
    this.allContacts = newContacts;
    this.filteredContacts = newContacts;
    getFilter().filter(filter);
}

Вызов filter() в последней строке вызовет (и должен) вызвать notifyDataSetChanged() в методе publishResults() фильтра.Иногда это может работать нормально, особенно в моем быстром Nexus 5. Но на самом деле, он скрывает ошибку, которую вы заметите на более медленных устройствах или в ресурсоемких условиях.

Проблема в том, что фильтрация выполняется асинхронно,и, таким образом, между концом оператора filter() и вызовом publishResults(), как в потоке пользовательского интерфейса, может выполняться какой-то другой код потока пользовательского интерфейса и изменяется содержимое адаптера.

Фактическое исправлениепросто, просто позвоните notifyDataSetChanged() также перед запросом на выполнение фильтрации:

public void updateUnderlyingContacts(List<Contact> newContacts, String filter)
{
    this.allContacts = newContacts;
    this.filteredContacts = newContacts;
    notifyDataSetChanged(); // Fix
    getFilter().filter(filter);
}
3 голосов
/ 11 марта 2017

Несколько дней назад я столкнулся с той же проблемой и вызывает несколько тысяч сбоев в день, примерно 0,1% пользователей встречают эту ситуацию.Я пробовал setVisibility(GONE/VISIBLE) и requestLayout(), но количество аварий только немного уменьшается.

И я наконец решил это.Ничего с setVisibility(GONE/VISIBLE).Ничего с requestLayout().

Наконец я нашел причину в том, что я использовал Handler для вызова notifyDataSetChanged() после обновления данных, что может привести к некоторому виду:

  1. Обновляет данные в объект модели (я называю это DataSource)
  2. Пользователь касается списка (который может вызвать checkForTap() / onTouchEvent() и, наконец, вызывает layoutChildren())
  3. Адаптер получает данные отобъект модели и вызов notifyDataSetChanged() и обновление представлений

И я допустил еще одну ошибку, что в getCount(), getItem() и getView() я непосредственно использую поля в DataSource, а не копирую их вадаптер.В конце концов происходит сбой, когда:

  1. Адаптер обновляет данные, которые дает последний ответ
  2. При следующем ответе DataSource обновляет данные, что вызывает изменение количества элементов
  3. Касания пользователяпросмотр списка, который может быть касанием, перемещением или переворотом
  4. getCount() и getView() вызывается, и просмотр списка обнаруживает, что данные не согласованы, и выдает исключения, такие как java.lang.IllegalStateException: The content of the adapter has changed but....Другое распространенное исключение - IndexOutOfBoundException, если вы используете верхний / нижний колонтитул в ListView.

Так что решение легко, я просто копирую данные в адаптер из моего источника данных, когда мой обработчик запускает адаптер для получения данныхи звонит notifyDataSetChanged().Авария больше никогда не повторяется.

3 голосов
/ 24 ноября 2014

У меня есть список объектов Feed. Он добавляется и усекается из потока без пользовательского интерфейса. Он отлично работает с адаптером ниже. В любом случае я вызываю FeedAdapter.notifyDataSetChanged в потоке пользовательского интерфейса, но чуть позже. Мне это нравится, потому что мои объекты Feed остаются в памяти в локальной службе, даже когда пользовательский интерфейс не работает.

public class FeedAdapter extends BaseAdapter {
    private int size = 0;
    private final List<Feed> objects;

    public FeedAdapter(Activity context, List<Feed> objects) {
        this.context = context;
        this.objects = objects;
        size = objects.size();
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        ...
    }

    @Override
    public void notifyDataSetChanged() {
        size = objects.size();

        super.notifyDataSetChanged();
    }

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

    @Override
    public Object getItem(int position) {
        try {
            return objects.get(position);
        } catch (Error e) {
            return Feed.emptyFeed;
        }
    }

    @Override
    public long getItemId(int position) {
        return position;
    }
}
3 голосов
/ 09 марта 2014

Если бы это происходило с перебоями, оказалось, что эта проблема возникла у меня только тогда, когда список прокручивался после того, как был нажат последний элемент «загрузить еще». Если список не прокручивался, все работало нормально.

После ОЧЕНЬ отладки это была ошибка с моей стороны, но также и несоответствие в коде Android.

Когда происходит проверка, этот код выполняется в ListView

        } else if (mItemCount != mAdapter.getCount()) {
            throw new IllegalStateException("The content of the adapter has changed but "
                    + "ListView did not receive a notification. Make sure the content of "

Но когда происходит onChange, он запускает этот код в AdapterView (родительский элемент ListView)

    @Override
    public void onChanged() {
        mDataChanged = true;
        mOldItemCount = mItemCount;
        mItemCount = getAdapter().getCount();

Обратите внимание на то, что адаптер НЕ гарантированно одинаков!

В моем случае, поскольку это был «LoadMoreAdapter», я возвращал WrappedAdapter в вызове getAdapter (для доступа к базовым объектам). Это привело к тому, что счетчики стали другими из-за дополнительного элемента «Загрузить еще» и выданного исключения.

Я сделал это только потому, что из документов видно, что все в порядке

ListView.getAdapter javadoc

Возвращает адаптер, который в данный момент используется в этом ListView. Возвращенный адаптер может не совпадать с адаптером setAdapter (ListAdapter), но может быть WrapperListAdapter.

2 голосов
/ 15 мая 2015

Это известная ошибка в Android 4–4,4 (KitKat), которая устраняется в «> 4.4»

См. Здесь: https://code.google.com/p/android/issues/detail?id=71936

...