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

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

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

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

что у меня иногда: обратите внимание, это происходит очень редко (один раз в неделю для одного из пользователей 3.5 k). Но я бы хотел избавьтесь от этой ошибки полностью. Вот частичный 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 вставок, чтобы избежать мерцания и внезапных изменений списка. Это невозможно сделать таким образом, всегда уведомляйте адаптер при изменении базового списка. Этот жук давно ушел для меня.

24 ответов


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

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

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


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

requestLayout();

класс ListView


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

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

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

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

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

вот мой код конкретного случая для других, чтобы ссылаться.

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

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

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

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

мой загрузчик во втором действии ожидает завершения первого потока. Пока он не покажет индикатор выполнения. Проверьте 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());    
        }
    }

}

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


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

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();
        }
    });
}

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

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);
    }
}

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

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

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

ListView.getAdapter javadoc

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


моя проблема была связана с использованием фильтр вместе с 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);
}

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

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;
    }
}

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

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

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

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

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

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

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


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

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

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


Это известная ошибка в Android 4 до 4.4 (KitKat) и разрешается в ">4.4"

смотрите здесь: https://code.google.com/p/android/issues/detail?id=71936


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


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


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

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

это исправило аварию для меня.


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


Я также получал точно такую же ошибку и использовал 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(); метод. Теперь мое приложение никогда не падает.


я столкнулся с аналогичной проблемой, вот как я решил в моем случае. Я проверяю, если 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);
    }

}

}

пожалуйста, попробуйте одно из этих решений :

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

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


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

         adapter.notifyDataSetChanged();

на

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

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


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

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


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

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

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

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

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

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

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

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

при закрытии клавиатуры внутренне listview обновит свой пользовательский интерфейс. он продолжает звонить до закрытия клавиатуры. в это время, если источник данных изменится, он будет вызывать это исключение. если данные обновляются в 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);

мое решение:

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

2) Сделайте свои тяжелые работы (SQLite row fetch , ...) in doInBackground метод и добавление элементов в Временный arraylist.

3) Добавьте все элементы из temp araylist в arraylist вашего listview в onPostExecute метод.

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

надеюсь, что это помогает.