it-swarm.com.ru

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

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

177
tomash

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

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

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

111
Mullins

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

requestLayout();

из класса ListView

26
gian1200

Это MultiThreading Issue and Using должным образом Synchronized Blocks Это может быть предотвращено . Не добавляя дополнительные вещи в поток пользовательского интерфейса и вызывая потерю отзывчивости приложения.

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

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

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

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

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

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

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

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

На моем экране есть кнопка. Это открывает действие, в котором перечислены эти телефонные номера Если я непосредственно установил Adapter над списком до того, как предыдущий поток завершит свою работу, что является быстрым случаем навигации, который случается реже. Появляется исключение. Это название этого 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());    
        }
    }

}

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

20
Javanator

Я решил это с помощью 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();
        }
    });
}
15
triad

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

Несколько дней назад я столкнулся с той же самой проблемой и вызывает несколько тысяч сбоев в день, примерно 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() вызывается, и listview обнаруживает, что данные не согласованы, и выдает исключения, такие как Java.lang.IllegalStateException: The content of the adapter has changed but.... Другое распространенное исключение - IndexOutOfBoundException, если вы используете верхний/нижний колонтитул в ListView.

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

3
HJWAJ

Моя проблема была связана с использованием Filter вместе с 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
cprcrack

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

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
ilya

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

После ОЧЕНЬ большой отладки это была ошибка с моей стороны, но также и несоответствие в коде 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.

3
aaronvargas

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

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

2
Ram Prakash Bhat

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

2
Balaji

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

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

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

2
Lars K.

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

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

1
sek13300

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

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

Это исправило крах для меня.

1
andude

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

1
Gusthema

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

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

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

0
Phuc Tran

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

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

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

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

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

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

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

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

0
Ciro Mine

Мое решение:

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

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

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

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

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

0
Nobody8

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

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

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

         adapter.notifyDataSetChanged();

в 

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

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

0
Hobii Sgonf

Я также получаю точно такую ​​же ошибку и использую 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();

                }

            });
        }

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

Правка: На самом деле, мое приложение все еще зависало примерно 1 раз в 10 раз, выдавая ту же ошибку.

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

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

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

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

0
CHarris