AsyncTask et gestion des erreurs sur Android

147

Je convertis mon code de l'utilisation Handleren AsyncTask. Ce dernier est excellent dans ce qu'il fait - les mises à jour asynchrones et la gestion des résultats dans le thread principal de l'interface utilisateur. Ce qui ne me semble pas clair, c'est comment gérer les exceptions si quelque chose se détraque AsyncTask#doInBackground.

La façon dont je le fais est d'avoir un gestionnaire d'erreur et de lui envoyer des messages. Cela fonctionne bien, mais est-ce la «bonne» approche ou y a-t-il une meilleure alternative?

Je comprends également que si je définis le gestionnaire d'erreurs en tant que champ d'activité, il doit s'exécuter dans le thread d'interface utilisateur. Cependant, parfois (de manière très imprévisible) j'obtiendrai une exception indiquant que le code déclenché à partir de Handler#handleMessages'exécute sur le mauvais thread. Dois-je initialiser le gestionnaire d'erreurs à la Activity#onCreateplace? Le placement runOnUiThreaddans Handler#handleMessagesemble redondant mais il s'exécute de manière très fiable.

Bostone
la source
Pourquoi avez-vous voulu convertir votre code? Y avait-il une bonne raison?
HGPB
4
@Haraldo c'est une meilleure pratique de codage, du moins c'est ce que je ressens
Bostone

Réponses:

178

Cela fonctionne bien, mais est-ce la «bonne» approche et y a-t-il une meilleure alternative?

Je tiens le Throwableou Exceptiondans l' AsyncTaskinstance elle-même, puis je fais quelque chose avec onPostExecute(), donc ma gestion des erreurs a la possibilité d'afficher une boîte de dialogue à l'écran.

CommonsWare
la source
8
Brillant! Plus besoin de singe avec Handlers
Bostone
5
Est-ce ainsi que je devrais conserver le jetable ou l'exception? "Ajoutez une variable d'instance à votre propre sous-classe AsyncTask qui contiendra le résultat de votre traitement en arrière-plan." Lorsque vous obtenez une exception, stockez l'exception (ou une autre chaîne / code d'erreur) dans cette variable. Lorsque onPostExecute est appelé, voyez si cette variable d'instance est définie sur une erreur. Si tel est le cas, affichez un message d'erreur. "(De l'utilisateur" Streets of Boston " groups.google.com/group/android-developers/browse_thread/thread/… )
OneWorld
1
@OneWorld: Oui, ça devrait aller.
CommonsWare
2
Salut CW, pourriez-vous s'il vous plaît expliquer votre façon de faire cela plus en détail s'il vous plaît - peut-être avec un bref exemple de code? Merci beaucoup!!
Bruiser
18
@Bruiser: github.com/commonsguy/cw-lunchlist/tree/master/15-Internet/... a un AsyncTasksuivant le modèle que je décris.
CommonsWare
140

Créez un objet AsyncResult (que vous pouvez également utiliser dans d'autres projets)

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

Renvoyez cet objet à partir de vos méthodes AsyncTask doInBackground et vérifiez-le dans postExecute. (Vous pouvez utiliser cette classe comme classe de base pour vos autres tâches asynchrones)

Vous trouverez ci-dessous une maquette d'une tâche qui obtient une réponse JSON du serveur Web.

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
la source
1
Je l'aime. Belle encapsulation. Puisqu'il s'agit d'une paraphrase de la réponse originale, la réponse reste mais cela mérite vraiment un point
Bostone
C'est une jolie démonstration de l'utilité des génériques. Cela jette une odeur étrange en termes de complexité, mais pas d'une manière que je peux vraiment articuler.
num1
4
Belle idée, juste une question: pourquoi appelez-vous super()dans AsyncTaskResultlorsque la classe ne couvre rien?
donturner
7
"aucun mal" - le code redondant est toujours nuisible à la lisibilité et à la maintenance. Sortez de là! :)
donturner
2
J'ai vraiment aimé la solution ... en y réfléchissant - les gars C # ont utilisé exactement la même méthode dans l'implémentation native de BackgroundTask correspondante en C # ...
Vova
11

Lorsque je ressens le besoin de gérer AsyncTaskcorrectement les exceptions , j'utilise ceci comme super classe:

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

}

Comme d'habitude, vous remplacez doInBackgroundvotre sous-classe pour effectuer un travail en arrière-plan, en lançant volontiers des exceptions si nécessaire. Vous êtes alors obligé de mettre en œuvre onPostExecute(car c'est abstrait) et cela vous rappelle doucement de gérer tous les types de Exception, qui sont passés en paramètre. Dans la plupart des cas, les exceptions conduisent à un type de sortie d'interface utilisateur, c'est donc onPostExecuteun endroit parfait pour le faire.

sulai
la source
1
Whoa, pourquoi ne pas simplement passer l' paramsavant, pour qu'il soit plus similaire à l'original et plus facile à migrer?
TWiStErRob
@TWiStErRob rien de mal avec cette idée. C'est une question de préférence personnelle, je suppose, car j'ai tendance à ne pas utiliser de paramètres. Je préfère new Task("Param").execute()plus new Task().execute("Param").
sulai
5

Si vous souhaitez utiliser le framework RoboGuice qui vous apporte d'autres avantages, vous pouvez essayer le RoboAsyncTask qui a un Callback onException () supplémentaire. Fonctionne vraiment bien et je l'utilise. http://code.google.com/p/roboguice/wiki/RoboAsyncTask

Ludwigm
la source
quelle est votre expérience avec cela? assez stable?
nickaknudson
Est-ce RoboGuicetoujours vivant? Semble n'a pas été mis à jour depuis 2012?
Dimitry K
Non, RoboGuice est mort et obsolète. Dagger2 est le remplacement recommandé, mais il s'agit uniquement d'une bibliothèque DI simple.
Avi Cherry
3

J'ai créé ma propre sous-classe AsyncTask avec une interface qui définit les rappels pour le succès et l'échec. Donc, si une exception est levée dans votre AsyncTask, la fonction onFailure obtient l'exception, sinon le rappel onSuccess obtient votre résultat. Pourquoi Android n'a pas quelque chose de mieux disponible me dépasse.

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
la source
3

Une solution plus complète à Cagatay Kalan est présentée ci-dessous:

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

Exemple de tâche

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
la source
J'ai eu la méthode getResources () comme dans myActivity.getApplicationContext (). GetResources ()
Stephane
2

Ce cours simple peut vous aider

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
la source
2

Une autre façon qui ne dépend pas du partage de membres variables consiste à utiliser cancel.

Cela provient de la documentation Android:

public final boolean cancel (booléen mayInterruptIfRunning)

Tente d'annuler l'exécution de cette tâche. Cette tentative échouera si la tâche est déjà terminée, a déjà été annulée ou n'a pas pu être annulée pour une autre raison. En cas de succès et que cette tâche n'a pas démarré lorsque l'annulation est appelée, cette tâche ne doit jamais s'exécuter. Si la tâche a déjà démarré, le paramètre mayInterruptIfRunning détermine si le thread exécutant cette tâche doit être interrompu pour tenter d'arrêter la tâche.

L'appel de cette méthode entraînera l'appel de onCancelled (Object) sur le thread d'interface utilisateur après le retour de doInBackground (Object []). L'appel de cette méthode garantit que onPostExecute (Object) n'est jamais appelé. Après avoir appelé cette méthode, vous devez vérifier périodiquement la valeur renvoyée par isCancelled () depuis doInBackground (Object []) pour terminer la tâche le plus tôt possible.

Vous pouvez donc appeler cancel dans l'instruction catch et vous assurer que onPostExcute n'est jamais appelé, mais à la place onCancelled est appelé sur le thread d'interface utilisateur. Ainsi, vous pouvez afficher le message d'erreur.

Ali
la source
Vous ne pouvez pas afficher le message d'erreur correctement, car vous ne connaissez pas le problème (exception), vous devez toujours intercepter et renvoyer un AsyncTaskResult. De plus, l'annulation d'un utilisateur n'est pas une erreur, c'est une interaction attendue: comment les différencier?
TWiStErRob
cancel(boolean)résultant en un appel à onCancelled()existait depuis le début, mais a onCancelled(Result)été ajouté dans l'API 11 .
TWiStErRob
0

En fait, AsyncTask utilise FutureTask & Executor, FutureTask prend en charge la chaîne d'exceptions Tout d'abord définissons une classe d'assistance

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

Deuxièmement, utilisons

    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();
    }
Yessy
la source
-2

Personnellement, j'utiliserai cette approche. Vous pouvez simplement attraper les exceptions et imprimer la trace de la pile si vous avez besoin des informations.

faire en sorte que votre tâche en arrière-plan renvoie une valeur booléenne.

C'est comme ça:

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

        @Override
                protected void onPostExecute(Boolean result) {

              if(result){
              // no error
               }
              else{
                // error handling
               }
}
Harry
la source
-2

Une autre possibilité serait d'utiliser Objectcomme type de retour, et en onPostExecute()contrôle pour le type d'objet. C'est court.

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
la source
1
complètement hors de propos
Dinu
-2

Si vous connaissez l'exception correcte, vous pouvez appeler le

Exception e = null;

publishProgress(int ...);

par exemple:

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

et allez dans "onProgressUpdate" et faites ce qui suit

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

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

Cela ne sera utile que dans certains cas. Vous pouvez également conserver une Global Exceptionvariable et accéder à l'exception.

Ajmal Muhammad P
la source
1
S'il vous plaît, ne faites pas ça. C'est vraiment, vraiment, mauvais style!
JimmyB