Android - изменить адаптер из потока пользовательского интерфейса, а не из фонового потока

У меня есть ListView, заполненный из json и использующий собственный адаптер.

Это мой укороченный код в ZoznamActivity:

 public class Zoznam extends AppCompatActivity{

    private ArrayList<Actors> actorsList;
    private ActorAdapter adapter;

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

   setContentView(R.layout.search_filter);
        final ListView lv = findViewById(R.id.listView1);

        actorsList = new ArrayList<>();
        adapter = new ActorAdapter(this, "Zoznam", actorsList);

        lv.setAdapter(adapter);adapter.notifyDataSetChanged();

        new GetContacts(Zoznam.this).execute("all","all");

   private static class GetContacts extends AsyncTask<String, Void, String> {
        ProgressDialog dialog;
        private final WeakReference<Zoznam> activityReference;

        GetContacts(Zoznam context) {
            activityReference = new WeakReference<>(context);
        }

        @Override
        protected void onPreExecute() {
            Zoznam activity = activityReference.get();
            if (activity == null || activity.isFinishing()) return;
            super.onPreExecute();
            dialog = new ProgressDialog(activity);
            dialog.setMessage(activity.getResources().getString(R.string.Loading));
            dialog.setTitle(activity.getResources().getString(R.string.connecting));
            dialog.show();
            dialog.setCancelable(false);
        }

        @Override
        protected String doInBackground(String... sText1) {final Zoznam activity = activityReference.get();


            HttpHandler sh = new HttpHandler();
            String url = "URL";
            String jsonStr = sh.makeServiceCall(url);

            if (jsonStr != null) {
                try {JSONObject jsonObj = new JSONObject(jsonStr);
                    JSONArray actors = jsonObj.getJSONArray("result");

                    for (int i = 0; i < actors.length(); i++) {
                        JSONObject c = actors.getJSONObject(i);

                        Actors actor = new Actors();

                        actor.setLetter(c.getString("letter"));
                        actor.setNazov(c.getString("nazov"));
                        actor.setThumb(c.getString("thumb"));


                        activityReference.get().actorsList.add(actor);

                    }

                }  catch (final JSONException e) {

                    activity.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(activity,
                                    R.string.Nodata,
                                    Toast.LENGTH_LONG).show();
                        }
                    }); }

return jsonStr;

            } else {
                activity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(activity,
                                R.string.Network,
                                Toast.LENGTH_LONG).show();
                    }
                });
                return null;
            }
        }

        protected void onPostExecute(String result) {
            Zoznam activity = activityReference.get();
            if (activity == null || activity.isFinishing()) return;
            dialog.dismiss();
            activity.adapter.notifyDataSetChanged();
            super.onPostExecute(result);

        }

    }

    protected void onResume() {
        super.onResume();
        adapter.notifyDataSetChanged();
    }

Проблема в том, что в отчетах о сбоях я нашел эту ошибку:

Содержимое адаптера изменилось, но ListView не получил уведомления. Убедитесь, что содержимое вашего адаптера не изменяется из фонового потока, а только из потока пользовательского интерфейса. Убедитесь, что ваш адаптер вызывает notifyDataSetChanged() при изменении его содержимого.

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

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


person Darksymphony    schedule 10.11.2019    source источник
comment
Не уверен, что это решит проблему, но вы вызываете activity.adapter.notifyDataSetChanged(); перед вызовом super.onPostExecute(result); Также вам не нужны вызовы UIThread activity.runOnUiThread внутри doInBackground, вы можете сделать все это внутри onPostExecute   -  person Yupi    schedule 10.11.2019
comment
да, я переключил порядок в onPostExecute, чтобы проверить его, но он работает так же, как и сейчас. Я не могу воспроизвести проблему, на моем устройстве она всегда работает. Исключение появилось только один раз на одном устройстве. Итак, что еще мне делать в onPostExecute?   -  person Darksymphony    schedule 10.11.2019


Ответы (2)


Измените свой AsyncTask так, чтобы вы вычисляли список внутри doInBackground(), а затем возвращали его в onPostExecute(), а затем выполняли все свои обновления там:

// change the type parameters to return a List
private static class GetContacts extends AsyncTask<Void, Void, List<Actors>> {

    @Override
    protected List<Actors> doInBackground(Void... ignored) {
        // add this at the top
        List<Actors> actors = new ArrayList<>();

                    // inside your loop, replace this line:
                    // activityReference.get().actorsList.add(actor);
                    // with this instead
                    actors.add(actor);

        // and at the end, return the list
        return actors;
    }

    // the list you returned from doInBackground() is passed here
    protected void onPostExecute(List<Actors> result) {
        // update your activity all at once in this method
        activity.actorsList.clear();
        activity.actorsList.addAll(result);
        activity.adapter.notifyDataSetChanged();
    }
}

Я вырезал кучу кода, чтобы его было легче читать, но общая идея осталась.

person Ben P.    schedule 17.11.2019
comment
спасибо, я забыл изменить последний параметр на List‹Actors› в Getcontacts раньше. Я думаю, что в вашем коде мы также должны изменить защищенную строку doInBackground на защищенный список‹Actors› doInBackground, и таким образом это работает. Я проверю в будущем выпуске, появится ли проблема после этого изменения, но, надеюсь, нет. - person Darksymphony; 17.11.2019
comment
Упс, да, вы абсолютно правы, doInBackground() должен вернуть List<Actors> - person Ben P.; 18.11.2019

Вы, вероятно, получили исключение, потому что вы обновили список адаптеров в фоновом потоке.

activityReference.get().actorsList.add(actor); Эта строка в методе doInBackground обновляет фактический список, который вы передали адаптеру. Все эти вычисления в doInBackground должны происходить довольно быстро, и вызывается notifyDataSetChanged из onPostExecute. Я не знаю как, но в этот раз ваш onPostExecute не был вызван вовремя, поэтому ваш список был обновлен, но ваш ListView не получил уведомление.

Вы можете создать локальный список в своем AsyncTask и обновить список действий в onPostExecute. Образец кода -

private static class GetContacts extends AsyncTask<String, Void, String> {
    ProgressDialog dialog;
    ArrayList<Actors> actors = new ArrayList<>();
    ...
    @Override
    protected String doInBackground(String... sText1) {
        ...
        for (int i = 0; i < actors.length(); i++) {
            ...
            // activityReference.get().actorsList.add(actor); <-- remove this.
            actors.add(actor);
        }
        ...
    }

    protected void onPostExecute(String result) {
        super.onPostExecute(result);
        Zoznam activity = activityReference.get();
        if (activity == null || activity.isFinishing()) return;
        dialog.dismiss();
        activityReference.get().actorsList.add(actors); <-- add this
        activity.adapter.notifyDataSetChanged();
    }
}

Это должно работать, но вы можете улучшить свой код, используя обратные вызовы. Таким образом, вам не нужно будет хранить ссылку на активность внутри asyncTask, и вы сможете перенести всю логику из asyncTask. Этот ответ должен быть полезен https://stackoverflow.com/a/15693380/6168272.

person Ranjan    schedule 10.11.2019
comment
Я не уверен, как создать там собственный список, но что, если я сделаю это внутри цикла for следующим образом: добавить (актер); activity.adapter.notifyDataSetChanged(); } }); - person Darksymphony; 10.11.2019
comment
Не делайте этого, это будет неоптимизированный код. Под локальным списком я имел в виду создание нового ArrayList внутри вашей AsyncTask. Я добавлю пример кода. - person Ranjan; 10.11.2019
comment
когда я добавил в цикл for это:actors.add(actor); затем он говорит, что не может разрешить добавление метода. И в postexecute я добавил activityReference.get().actorsList.add(actors); и он говорит, что не может разрешить актеров. - person Darksymphony; 16.11.2019