AsyncTask i obsługa błędów w systemie Android

147

Konwertuję mój kod z używania Handlerna AsyncTask. Ten ostatni jest świetny w tym, co robi - asynchronicznych aktualizacjach i obsłudze wyników w głównym wątku interfejsu użytkownika. Nie jest dla mnie jasne, jak radzić sobie z wyjątkami, jeśli coś pójdzie nie tak AsyncTask#doInBackground.

Sposób, w jaki to robię, to mieć obsługę błędów i wysyłać do niego komunikaty. Działa dobrze, ale czy jest to „właściwe” podejście, czy też jest lepsza alternatywa?

Rozumiem również, że jeśli zdefiniuję obsługę błędu jako pole działania, powinien on zostać wykonany w wątku interfejsu użytkownika. Jednak czasami (bardzo nieprzewidywalnie) otrzymuję wyjątek informujący, że kod wyzwalany z Handler#handleMessagejest wykonywany w niewłaściwym wątku. Czy Activity#onCreatezamiast tego powinienem zainicjować procedurę obsługi błędów ? Umieszczanie runOnUiThreadw programie Handler#handleMessagewydaje się zbędne, ale działa bardzo niezawodnie.

Bostone
źródło
Dlaczego chciałeś przekonwertować swój kod? Czy był dobry powód?
HGPB
4
@Haraldo to lepsza praktyka kodowania, przynajmniej tak się czuję
Bostone

Odpowiedzi:

178

Działa dobrze, ale czy jest to „właściwe” podejście i czy istnieje lepsza alternatywa?

Trzymam się Throwablelub Exceptionw AsyncTasksamej instancji, a następnie coś z nim robię onPostExecute(), więc moja obsługa błędów ma opcję wyświetlenia okna dialogowego na ekranie.

CommonsWare
źródło
8
Znakomity! Nie ma już
potrzeby małpowania z Handlerami
5
Czy w ten sposób powinienem trzymać się Rzutu lub Wyjątku? „Dodaj zmienną instancji do własnej podklasy AsyncTask, która będzie przechowywać wyniki przetwarzania w tle”. Kiedy otrzymasz wyjątek, zapisz wyjątek (lub inny ciąg / kod błędu) w tej zmiennej. Po wywołaniu onPostExecute sprawdź, czy ta zmienna instancji jest ustawiona na jakiś błąd. Jeśli tak, pokaż komunikat o błędzie. ”(Od użytkownika„ Streets of Boston ” groups.google.com/group/android-developers/browse_thread/thread/… )
OneWorld
1
@OneWorld: Tak, powinno wystarczyć.
CommonsWare
2
Cześć CW, czy mógłbyś wyjaśnić, jak to robisz, bardziej szczegółowo - może za pomocą krótkiego przykładu kodu? Wielkie dzięki!!
Bruiser
18
@Bruiser: github.com/commonsguy/cw-lunchlist/tree/master/15-Internet/ ... ma AsyncTasknastępujący wzór, który opisuję.
CommonsWare,
140

Utwórz obiekt AsyncResult (którego możesz również użyć w innych projektach)

public class AsyncTaskResult<T> {
    private T result;
    private Exception error;

    public T getResult() {
        return result;
    }

    public Exception getError() {
        return error;
    }

    public AsyncTaskResult(T result) {
        super();
        this.result = result;
    }

    public AsyncTaskResult(Exception error) {
        super();
        this.error = error;
    }
}

Zwróć ten obiekt z metod AsyncTask doInBackground i sprawdź go w postExecute. (Możesz użyć tej klasy jako klasy bazowej dla innych zadań asynchronicznych)

Poniżej znajduje się makieta zadania, które pobiera odpowiedź JSON z serwera WWW.

AsyncTask<Object,String,AsyncTaskResult<JSONObject>> jsonLoader = new AsyncTask<Object, String, AsyncTaskResult<JSONObject>>() {

        @Override
        protected AsyncTaskResult<JSONObject> doInBackground(
                Object... params) {
            try {
                // get your JSONObject from the server
                return new AsyncTaskResult<JSONObject>(your json object);
            } catch ( Exception anyError) {
                return new AsyncTaskResult<JSONObject>(anyError);
            }
        }

        protected void onPostExecute(AsyncTaskResult<JSONObject> result) {
            if ( result.getError() != null ) {
                // error handling here
            }  else if ( isCancelled()) {
                // cancel handling here
            } else {

                JSONObject realResult = result.getResult();
                // result handling here
            }
        };

    }
Cagatay Kalan
źródło
1
Lubię to. Niezła hermetyzacja. Ponieważ jest to parafraza oryginalnej odpowiedzi, odpowiedź pozostaje, ale to zdecydowanie zasługuje na punkt
Bostone
To całkiem niezła demonstracja użyteczności generyków. Rzuca dziwny zapach pod względem złożoności, ale nie w sposób, który naprawdę mogę wyrazić.
num1
4
Fajny pomysł, tylko jedno pytanie: dlaczego nazywacie super()się AsyncTaskResult, gdy klasa nic nie przedłużyć?
donturner
7
„bez szkody” - nadmiarowy kod jest zawsze szkodliwy dla czytelności i konserwacji. Zabierz to stamtąd! :)
donturner
2
Naprawdę podobało mi się to rozwiązanie ... kiedy się nad tym zastanowić - faceci z C # używali dokładnie tej samej metody w natywnej implementacji BackgroundTask w C # ...
Vova
11

Kiedy czuję potrzebę odpowiedniego obsłużenia wyjątków AsyncTask, używam tego jako superklasy:

public abstract class ExceptionAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {

    private Exception exception=null;
    private Params[] params;

    @Override
    final protected Result doInBackground(Params... params) {
        try {
            this.params = params; 
            return doInBackground();
        }
        catch (Exception e) {
            exception = e;
            return null;
        }
    }

    abstract protected Result doInBackground() throws Exception;

    @Override
    final protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        onPostExecute(exception, result);
    }

    abstract protected void onPostExecute(Exception exception, Result result);

    public Params[] getParams() {
        return params;
    }

}

Jak zwykle, nadpisujesz doInBackgroundswoją podklasę, aby wykonać pracę w tle, szczęśliwie rzucając Wyjątki w razie potrzeby. Jesteś wtedy zmuszony do implementacji onPostExecute(ponieważ jest abstrakcyjny), a to delikatnie przypomina ci o obsłudze wszystkich typów Exception, które są przekazywane jako parametry. W większości przypadków Wyjątki prowadzą do pewnego typu wyjścia interfejsu użytkownika, więc onPostExecutejest to idealne miejsce do zrobienia tego.

sulai
źródło
1
Whoa, dlaczego nie przejść paramsdalej, aby był bardziej podobny do oryginału i łatwiejszy do migracji?
TWiStErRob
@TWiStErRob nie ma nic złego w tym pomyśle. Myślę, że to kwestia osobistych preferencji, ponieważ zwykle nie używam parametrów. Wolę new Task("Param").execute()więcej new Task().execute("Param").
sulai
5

Jeśli chcesz używać frameworka RoboGuice, który zapewnia inne korzyści, możesz wypróbować RoboAsyncTask, które ma dodatkowe wywołanie zwrotne onException (). Działa naprawdę dobrze i używam go. http://code.google.com/p/roboguice/wiki/RoboAsyncTask

ludwigm
źródło
jakie masz z tym doświadczenia? całkiem stabilny?
nickaknudson,
Czy RoboGuicejeszcze żyje? Wygląda na to, że nie był aktualizowany od 2012 roku?
Dimitry K
Nie, RoboGuice jest martwy i przestarzały. Dagger2 jest zalecanym zamiennikiem, ale jest to tylko biblioteka DI typu bare-bones.
Avi Cherry,
3

Stworzyłem własną podklasę AsyncTask z interfejsem, który definiuje wywołania zwrotne dla sukcesu i niepowodzenia. Więc jeśli wyjątek zostanie zgłoszony w Twoim AsyncTask, funkcja onFailure otrzymuje wyjątek, w przeciwnym razie wywołanie zwrotne onSuccess dostaje przekazany wynik. Dlaczego android nie ma czegoś lepszego, jest poza mną.

public class SafeAsyncTask<inBackgroundType, progressType, resultType>
extends AsyncTask<inBackgroundType, progressType, resultType>  {
    protected Exception cancelledForEx = null;
    protected SafeAsyncTaskInterface callbackInterface;

    public interface SafeAsyncTaskInterface <cbInBackgroundType, cbResultType> {
        public Object backgroundTask(cbInBackgroundType[] params) throws Exception;
        public void onCancel(cbResultType result);
        public void onFailure(Exception ex);
        public void onSuccess(cbResultType result);
    }

    @Override
    protected void onPreExecute() {
        this.callbackInterface = (SafeAsyncTaskInterface) this;
    }

    @Override
    protected resultType doInBackground(inBackgroundType... params) {
        try {
            return (resultType) this.callbackInterface.backgroundTask(params);
        } catch (Exception ex) {
            this.cancelledForEx = ex;
            this.cancel(false);
            return null;
        }
    }

    @Override
    protected void onCancelled(resultType result) {
        if(this.cancelledForEx != null) {
            this.callbackInterface.onFailure(this.cancelledForEx);
        } else {
            this.callbackInterface.onCancel(result);
        }
    }

    @Override
    protected void onPostExecute(resultType result) {
        this.callbackInterface.onSuccess(result);
    }
}
ErlVolton
źródło
3

Bardziej kompleksowe rozwiązanie rozwiązania Cagatay Kalan pokazano poniżej:

AsyncTaskResult

public class AsyncTaskResult<T> 
{
    private T result;
    private Exception error;

    public T getResult() 
    {
        return result;
    }

    public Exception getError() 
    {
        return error;
    }

    public AsyncTaskResult(T result) 
    {
        super();
        this.result = result;
    }

    public AsyncTaskResult(Exception error) {
        super();
        this.error = error;
    }
}

ExceptionHandlingAsyncTask

public abstract class ExceptionHandlingAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, AsyncTaskResult<Result>>
{
    private Context context;

    public ExceptionHandlingAsyncTask(Context context)
    {
        this.context = context;
    }

    public Context getContext()
    {
        return context;
    }

    @Override
    protected AsyncTaskResult<Result> doInBackground(Params... params)
    {
        try
        {
            return new AsyncTaskResult<Result>(doInBackground2(params));
        }
        catch (Exception e)
        {
            return new AsyncTaskResult<Result>(e);
        }
    }

    @Override
    protected void onPostExecute(AsyncTaskResult<Result> result)
    {
        if (result.getError() != null)
        {
            onPostException(result.getError());
        }
        else
        {
            onPostExecute2(result.getResult());
        }
        super.onPostExecute(result);
    }

    protected abstract Result doInBackground2(Params... params);

    protected abstract void onPostExecute2(Result result);

    protected void onPostException(Exception exception)
    {
                        new AlertDialog.Builder(context).setTitle(R.string.dialog_title_generic_error).setMessage(exception.getMessage())
                .setIcon(android.R.drawable.ic_dialog_alert).setPositiveButton(R.string.alert_dialog_ok, new DialogInterface.OnClickListener()
                {
                    public void onClick(DialogInterface dialog, int which)
                    {
                        //Nothing to do
                    }
                }).show();
    }
}

Przykładowe zadanie

public class ExampleTask extends ExceptionHandlingAsyncTask<String, Void, Result>
{
    private ProgressDialog  dialog;

    public ExampleTask(Context ctx)
    {
        super(ctx);
        dialog = new ProgressDialog(ctx);
    }

    @Override
    protected void onPreExecute()
    {
        dialog.setMessage(getResources().getString(R.string.dialog_logging_in));
        dialog.show();
    }

    @Override
    protected Result doInBackground2(String... params)
    {
        return new Result();
    }

    @Override
    protected void onPostExecute2(Result result)
    {
        if (dialog.isShowing())
            dialog.dismiss();
        //handle result
    }

    @Override
    protected void onPostException(Exception exception)
    {
        if (dialog.isShowing())
            dialog.dismiss();
        super.onPostException(exception);
    }
}
vahapt
źródło
Otrzymałem metodę getResources () jak w myActivity.getApplicationContext (). GetResources ()
Stephane,
2

Ta prosta klasa może ci pomóc

public abstract class ExceptionAsyncTask<Param, Progress, Result, Except extends Throwable> extends AsyncTask<Param, Progress, Result> {
    private Except thrown;

    @SuppressWarnings("unchecked")
    @Override
    /**
     * Do not override this method, override doInBackgroundWithException instead
     */
    protected Result doInBackground(Param... params) {
        Result res = null;
        try {
            res = doInBackgroundWithException(params);
        } catch (Throwable e) {
            thrown = (Except) e;
        }
        return res;
    }

    protected abstract Result doInBackgroundWithException(Param... params) throws Except;

    @Override
    /**
     * Don not override this method, override void onPostExecute(Result result, Except exception) instead
     */
    protected void onPostExecute(Result result) {
        onPostExecute(result, thrown);
        super.onPostExecute(result);
    }

    protected abstract void onPostExecute(Result result, Except exception);
}
Denis
źródło
2

Innym sposobem, który nie zależy od udostępniania zmiennych składowych, jest użycie anulowania.

To pochodzi z dokumentów Androida:

public final boolean Cancel (boolean mayInterruptIfRunning)

Próbuje anulować wykonanie tego zadania. Ta próba zakończy się niepowodzeniem, jeśli zadanie zostało już ukończone, zostało już anulowane lub nie można go było anulować z innego powodu. Jeśli się powiedzie, a to zadanie nie zostało uruchomione po wywołaniu anulowania, to zadanie nie powinno nigdy być uruchamiane. Jeśli zadanie już się rozpoczęło, parametr mayInterruptIfRunning określa, czy wątek wykonujący to zadanie powinien zostać przerwany przy próbie zatrzymania zadania.

Wywołanie tej metody spowoduje wywołanie onCancelled (Object) w wątku interfejsu użytkownika po powrocie doInBackground (Object []). Wywołanie tej metody gwarantuje, że onPostExecute (Object) nigdy nie zostanie wywołany. Po wywołaniu tej metody należy okresowo sprawdzać wartość zwracaną przez isCancelled () z doInBackground (Object []), aby jak najszybciej zakończyć zadanie.

Możesz więc wywołać anulowanie w instrukcji catch i upewnić się, że onPostExcute nigdy nie jest wywoływany, ale zamiast tego onCancelled jest wywoływany w wątku interfejsu użytkownika. Możesz więc wyświetlić komunikat o błędzie.

Ali
źródło
Nie możesz poprawnie wyświetlić komunikatu o błędzie, ponieważ nie znasz problemu (wyjątek), nadal musisz przechwycić i zwrócić AsyncTaskResult. Anulowanie przez użytkownika również nie jest błędem, to oczekiwana interakcja: Jak je rozróżniasz?
TWiStErRob,
cancel(boolean)w wyniku czego wezwanie do onCancelled()istniało od początku, ale onCancelled(Result)zostało dodane w API 11 .
TWiStErRob
0

Właściwie AsyncTask używa FutureTask & Executor, FutureTask obsługuje łańcuch wyjątków Najpierw zdefiniujmy klasę pomocniczą

public static class AsyncFutureTask<T> extends FutureTask<T> {

    public AsyncFutureTask(@NonNull Callable<T> callable) {
        super(callable);
    }

    public AsyncFutureTask<T> execute(@NonNull Executor executor) {
        executor.execute(this);
        return this;
    }

    public AsyncFutureTask<T> execute() {
        return execute(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    @Override
    protected void done() {
        super.done();
        //work done, complete or abort or any exception happen
    }
}

Po drugie, użyjmy

    try {
        Log.d(TAG, new AsyncFutureTask<String>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                //throw Exception in worker thread
                throw new Exception("TEST");
            }
        }).execute().get());
    } catch (InterruptedException e) {
        e.printStackTrace();
    } catch (ExecutionException e) {
        //catch the exception throw by worker thread in main thread
        e.printStackTrace();
    }
Tak
źródło
-2

Osobiście skorzystam z tego podejścia. Możesz po prostu złapać wyjątki i wydrukować ślad stosu, jeśli potrzebujesz informacji.

spraw, aby twoje zadanie w tle zwróciło wartość logiczną.

to jest tak:

    @Override
                protected Boolean doInBackground(String... params) {
                    return readXmlFromWeb(params[0]);
         }

        @Override
                protected void onPostExecute(Boolean result) {

              if(result){
              // no error
               }
              else{
                // error handling
               }
}
Złupić
źródło
-2

Inną możliwością byłoby użycie Objectjako typu zwracanego i onPostExecute()sprawdzania typu obiektu. To jest krótkie.

class MyAsyncTask extends AsyncTask<MyInObject, Void, Object> {

    @Override
    protected AsyncTaskResult<JSONObject> doInBackground(MyInObject... myInObjects) {
        try {
            MyOutObject result;
            // ... do something that produces the result
            return result;
        } catch (Exception e) {
            return e;
        }
    }

    protected void onPostExecute(AsyncTaskResult<JSONObject> outcome) {
        if (outcome instanceof MyOutObject) {
            MyOutObject result = (MyOutObject) outcome;
            // use the result
        } else if (outcome instanceof Exception) {
            Exception e = (Exception) outcome;
            // show error message
        } else throw new IllegalStateException();
    }
}
Matthias Ronge
źródło
1
całkowicie nieistotne
Dinu
-2

Jeśli znasz poprawny wyjątek, możesz wywołać

Exception e = null;

publishProgress(int ...);

na przykład:

@Override
protected Object doInBackground(final String... params) {

    // TODO Auto-generated method stub
    try {
        return mClient.call(params[0], params[1]);
    } catch(final XMLRPCException e) {

        // TODO Auto-generated catch block
        this.e = e;
        publishProgress(0);
        return null;
    }
}

i przejdź do „onProgressUpdate” i wykonaj następujące czynności

@Override
protected void onProgressUpdate(final Integer... values) {

    // TODO Auto-generated method stub
    super.onProgressUpdate(values);
    mDialog.dismiss();
    OptionPane.showMessage(mActivity, "Connection error", e.getMessage());
}

Będzie to pomocne tylko w niektórych przypadkach. Możesz także zachować Global Exceptionzmienną i uzyskać dostęp do wyjątku.

Ajmal Muhammad P
źródło
1
Proszę, nie rób tego. To naprawdę, naprawdę zły styl!
JimmyB,