Android, ListView IllegalStateException: "Содержимое адаптера изменилось, но ListView не получил уведомление"

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

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

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

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

`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 вставок, чтобы избежать мерцания и внезапных изменений списка. Это невозможно сделать таким образом, всегда уведомлять адаптер при изменении базового списка. Эта ошибка для меня уже давно исчезла.

Ответ 1

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

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

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

Ответ 2

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

requestLayout();

из класса ListView

Ответ 3

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

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

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

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

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

Вот мой конкретный код для других, на которые ссылаются другие.

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

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

Этот PhoneBookManager.getPhoneBookContacts читает контакт из телефонной книги и заполняет их в хэшмапах. Что непосредственно можно использовать для списков адаптеров для рисования списка.

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

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

Затем он создает адаптер и доставляет его в действие, в котором в потоке ui я вызываю 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());    
        }
    }

}

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

Ответ 4

Я решил это, имея 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();
        }
    });
}

Ответ 5

Я написал этот код и запустил его в изображении эмулятора 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);
    }
}

Ответ 6

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

После отладки MUCH это была ошибка с моей стороны, но также была несогласованность в коде 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 (для доступа к базовым объектам). Это привело к тому, что подсчеты отличались из-за дополнительного "Load More" и исключения.

Я сделал это только потому, что документы делают, как будто это нормально делать

ListView.getAdapter javadoc

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

Ответ 7

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

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

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

Вызов filter() в последней строке будет (и должен) вызывать notifyDataSetChanged() для вызова в методе Filter 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);
}

Ответ 8

У меня есть список, если объекты 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;
    }
}

Ответ 9

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

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

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

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

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

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

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

Ответ 10

Даже я столкнулся с той же проблемой в своем приложении уведомления XMPP, сообщение приемников должно быть добавлено обратно в представление списка (реализовано с помощью ArrayList). Когда я попытался добавить содержимое приемника через MessageListener (отдельный поток), приложение завершает работу с ошибкой выше. Я решил это, добавив контент в мой метод ArrayList и setListviewadapater через runOnUiThread, который является частью класса Activity. Это решило мою проблему.

Ответ 11

Я столкнулся с той же проблемой с точно таким же журналом ошибок. В моем случае onProgress() AsyncTask добавляет значения к адаптеру с помощью mAdapter.add(newEntry). Чтобы пользовательский интерфейс становился менее отзывчивым, я устанавливал mAdapter.setNotifyOnChange(false) и вызывал mAdapter.notifyDataSetChanged() 4 раза второй. Один раз в секунду массив сортируется.

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

Но, похоже, я нашел приемлемое обходное решение. Я предполагаю, что даже если вы просто работаете над нити ui, адаптер не принимает много изменений в данных без вызова notifyDataSetChanged(), из-за этого я создал очередь, в которой хранятся все новые элементы до тех пор, пока указанные 300 мс не будут завершены. Если этот момент будет достигнут, я добавлю все сохраненные предметы за один выстрел и вызову notifyDataSetChanged(). До сих пор я был не смог свернуть список больше.

Ответ 13

У меня была та же проблема, и я решил это. Моя проблема заключалась в том, что я использовал listview с адаптером массива и с фильтром. В методе performFiltering я возился с массивом, у которого есть данные, и это была проблема, поскольку этот метод не работает в потоке пользовательского интерфейса, и EVENTUALLY вызывает некоторые проблемы.

Ответ 14

Одной из причин этого сбоя является то, что объект ArrayList не может полностью измениться. Поэтому, когда я удаляю элемент, я должен это сделать:

mList.clear();
mList.addAll(newDataList);

Это фиксировало крушение для меня.

Ответ 15

В моем случае я вызвал метод GetFilter() на адаптере из метода TextWatcher() в основной операции, и я добавил данные в цикл For на GetFilter(). Решением было изменение цикла For на AfterTextChanged() sub method в основном Управлении и удаление вызова GetFilter()

Ответ 16

У меня возникла аналогичная проблема, вот как я решил в моем случае. Я проверяю, есть ли task уже RUNNING или FINISHED, потому что задача может выполняться только один раз. Ниже вы увидите частичный и адаптированный код из моего решения.

public class MyActivity... {
    private MyTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
       // your code
       task = new MyTask();
       setList();
    }

    private void setList() {
    if (task != null)
        if (task.getStatus().equals(AsyncTask.Status.RUNNING)){
            task.cancel(true);
            task = new MyTask();
            task.execute();         
        } else if (task.getStatus().equals(AsyncTask.Status.FINISHED)) {
            task = new MyTask();
            task.execute();
        } else 
            task.execute();
    }

    class MyTask extends AsyncTask<Void, Item, Void>{
       List<Item> Itens;

       @Override
       protected void onPreExecute() {

        //your code

        list.setVisibility(View.GONE);
        adapterItem= new MyListAdapter(MyActivity.this, R.layout.item, new ArrayList<Item>());
        list.setAdapter(adapterItem);

        adapterItem.notifyDataSetChanged();
    }

    @Override
    protected Void doInBackground(Void... params) {

        Itens = getItens();
        for (Item item : Itens) {
            publishProgress(item );
        }

        return null;
    }

    @Override
    protected void onProgressUpdate(Item ... item ) {           
        adapterItem.add(item[0]);
    }

    @Override
    protected void onPostExecute(Void result) {
        //your code
        adapterItem.notifyDataSetChanged();     
        list.setVisibility(View.VISIBLE);
    }

}

}

Ответ 17

Попробуйте одно из следующих решений:

  • Иногда, если вы добавляете новый объект в список данных в потоке (или doInBackground метод), эта ошибка возникает. Решением является создание временного списка и добавление данных в этот список в потоке (или doInBackground), а затем копирование всех данных из временного списка в список адаптеров в потоке пользовательского интерфейса (или onPostExcute)

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

Ответ 18

У меня была такая же проблема при добавлении новых данных в ленивый загрузчик изображений я просто положил

         adapter.notifyDataSetChanged();

в

       protected void onPostExecute(Void args) {
        adapter.notifyDataSetChanged();
        // Close the progressdialog
        mProgressDialog.dismiss();
         }

надеюсь, что это поможет вам

Ответ 19

Как @Mullins сказал: "
Я добавил элементы и назвал notifyDataSetChanged() в потоке пользовательского интерфейса, и я решил это. - Mullins ".

В моем случае у меня есть asynctask, и я вызвал notifyDataSetChanged() в методе doInBackground(), и проблема решена, когда я вызвал из onPostExecute(), я получил исключение.

Ответ 20

У меня был пользовательский ListAdapter и вызывал super.notifyDataSetChanged() в начале, а не в конце метода

@Override
public void notifyDataSetChanged() {
    recalculate();
    super.notifyDataSetChanged();
}

Ответ 21

Я также получал точно такую ​​же ошибку и использовал AsyncTask:

`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... etc

Я решил это, поставив adapter.notifyDataSetChanged(); в нижней части моего потока пользовательского интерфейса, то есть мой метод AsyncTask onPostExecute. Вот так:

 protected void onPostExecute(Void aVoid) {

 all my other stuff etc...
    all my other stuff etc...

           adapter.notifyDataSetChanged();

                }

            });
        }

Теперь мое приложение работает.

EDIT: на самом деле мое приложение по-прежнему разбилось примерно раз в 1 раз в 10 раз, давая ту же ошибку.

В конце концов я наткнулся на runOnUiThread на предыдущем посту, который, как я думал, может быть полезен. Поэтому я поместил его в свой метод doInBackground, например:

@Override
protected Void doInBackground(Void... voids) {

    runOnUiThread(new Runnable() {
                      public void run() { etc... etc...

И я удалил метод adapter.notifyDataSetChanged();. Теперь мое приложение никогда не падает.

Ответ 22

У меня было то же самое, у меня было много элементов buttongroup, которые содержали мой элемент в listview, и я менял некоторые логические значения внутри моего элемента, такие как holder.rbVar.setOnclik...

моя проблема возникла из-за того, что я вызывал метод внутри getView(); и сохранял объект внутри sharepreference, поэтому у меня была такая же ошибка выше

Как я решил это; Я удалил свой метод внутри getView(), чтобы notifyDataSetInvalidated() и проблема исчезла

   @Override
    public void notifyDataSetChanged() {
        saveCurrentTalebeOnShare(currentTalebe);
        super.notifyDataSetChanged();
    }

Ответ 23

У меня была та же проблема. наконец, я получил решение

перед обновлением списка, если мягкая клавиатура присутствует, сначала закройте его. после этого установите источник данных и вызовите notifydatasetchanged().

при закрытии внутренней панели клавиатуры будет обновлен ее ui. он продолжает звонить до закрытия клавиатуры. в этот раз, если источник данных изменит это, он будет выбрасывать это исключение. если данные обновляются в onActivityResult, есть вероятность для той же ошибки.

 InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
            imm.hideSoftInputFromWindow(v.getWindowToken(), 0);

        view.postDelayed(new Runnable() {
            @Override
            public void run() {
                refreshList();
            }
        },100L);

Ответ 24

Мое решение:

1) создать temp ArrayList.

2) Выполните тяжелую работу (извлечение строки sqlite,...) в методе doInBackground и добавьте элементы в временный массив.

3) добавить все элементы из временного архива в ваш список рассылки в методе onPostExecute.

note: Вы можете удалить некоторые элементы из списка, а также удалить из базы данных sqlite и, возможно, удалить некоторые файлы, связанные с элементами из sdcard, просто удалить элементы из базы данных и удалить связанные с ними файлы и добавить их во временный массив в background thread. затем в UI thread удалите элементы, существующие в временном массиве из списка рассылки.

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

Ответ 25

adapter.notifyDataSetChanged()