Реализация ProgressDialog и AsyncTaskLoader на фрагментах

На Хабре уже опубликована хорошая статья в которой описано совместное использование ProgressDialog и AsyncTask, здесь я опишу, как добиться похожего функционала но на фрагментах, точнее используя DialogFragment и AsyncTaskLoader.

Итак, цель:
  • отображать ProgressDialog при выполнении длительной операции, текст сообщения которого может информировать о ходе выполнения задачи;
  • корректная поддержка смены ориентации приложением.

image

Реализация
1. Отправку сообщения из асинхронного процесса вынесем в абстрактный класс AbstractTaskLoader, наследник от AsyncTaskLoader:
public abstract class AbstractTaskLoader extends AsyncTaskLoader<Object> {
    
    //type of the published values
    public static int MSGCODE_PROGRESS = 1;
    public static int MSGCODE_MESSAGE = 2;

    private Handler handler;
   
    protected AbstractTaskLoader(Context context) {
        super(context);
       
    }
    protected void setHandler(Handler handler){
        this.handler = handler;
    }
    /**
     * Helper to publish string value
     * @param value
     */
    protected void publishMessage(String value){
       
        if(handler!=null){
       
            Bundle data = new Bundle();
            data.putString("message", value);
           
            /* Creating a message */
            Message msg = new Message();
            msg.setData(data);
            msg.what = MSGCODE_MESSAGE;
           
            /* Sending the message */
            handler.sendMessage(msg);
           
        }
       
    }
…

Далее, наш класс, выполняющий какую либо длительную операцию уже наследуется от этого абстрактного класса и может информировать о процессе выполнения задачи:
@Override
public Object loadInBackground() {
    …
    publishMessage(result);
    …
}

2. Следующий шаг это реализация ProgressDialog-а отображающего сообщение и отвечающего за корректный запуск/перезапуск нашего Loader-a при смене ориентации экрана. Класс включает также обработчики обратного вызова для Loader-a и диалога:
public abstract class AbstractTaskProgressDialogFragment extends DialogFragment
        implements LoaderCallbacks<Object>, OnCancelListener {

    private ProgressDialog progressDialog;
   
    private final AbstractTaskLoader loader;
    protected abstract void onLoadComplete(Object data);
    protected abstract void onCancelLoad();

    protected AbstractTaskProgressDialogFragment(AbstractTaskLoader loader,String message){
         loader.setHandler(handler);
         this.loader = loader;
       
        Bundle args = new Bundle();
        args.putString("message", message);
        setArguments(args);
    }

    @Override
    public ProgressDialog onCreateDialog(Bundle savedInstanceState) {
           
       String message = getArguments().getString("message");

       progressDialog = new ProgressDialog(getActivity());
       progressDialog.setMessage(message);
       progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
       progressDialog.setOnCancelListener(this);
       progressDialog.show();

       return progressDialog;
   }

    private final Handler handler = new Handler() {

       public void handleMessage(Message msg) {
           
           if(msg.what == AbstractTaskLoader.MSGCODE_MESSAGE){
               String message = AbstractTaskLoader.getMessageValue(msg);
               if(message!=null){
                   //update progress bar message
                   if(progressDialog!=null){
                       progressDialog.setMessage(message);
                   }
               }
         }
     }
};
...

Класс так же сделан абстрактным, впрочем это необязательно, просто один из вариантов реализации наименьшего связывания. Важны только результаты работы:
onLoadComplete(Object data);
onCancelLoad();
и реализация этих методов в наследнике (я сделал через интерфейс, см далее).

Здесь в конструктор передается Loader, который будет выполняться и тестовое сообщение, отображаемое сразу при запуске. Handler используется для передачи информационных сообщений от Loader-а о прогрессе. В onCreateDialog создаем и показываем наш диалог.

Далее нам надо указать, что при смене ориентации экрана этот фрагмент сохраняет свое состояние. Делается это c помощью метода setRetainInstance(true), в этом случае onDestroy при смене ориентации экрана вызываться не будет и диалог не закроется.
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
           
    setRetainInstance(true);
       
    // Prepare the loader.  Either re-connect with an existing one,
    // or start a new one.
    getLoaderManager().initLoader(0, loader.getArguments(), this);

}

Переопределяем метод onActivityCreated, в котором так же запускаем наш Loader при первом запуске или повторно соединяемся к существующему (при смене ориентации экрана).

Небольшое отступление:
Метод setRetainInstance не совсем корректно работает именно для DialogFragment в паре с “библиотекой совместимости” (compatibility-library), т.е метод onDestroy все-таки вызывается хотя не должен. Но есть обходное решение, в нашем диалоге добавляем:
@Override
public void onDestroyView() {
    if (getDialog() != null && getRetainInstance())
        getDialog().setOnDismissListener(null);
       
    super.onDestroyView();
}

Подробности здесь.

Обработка запуска и завершения работы Loader-a:
@Override
public Loader<Object> onCreateLoader(int id, Bundle args) {
    loader.forceLoad();
    return loader;
}

@Override
public void onLoadFinished(Loader<Object> loader, Object data) {
    onLoadComplete(data);
    ((AbstractTaskLoader) loader).setHandler(null);
       
    hideDialog();
}

Обработка отмены операции пользователем — нажатие кнопки “Back”:
@Override
public void onCancel(DialogInterface dialog) {
    super.onCancel(dialog);
       
    loader.cancelLoad();         //base metod
    loader.setCanseled(true);    //custom flag
       
    onCancelLoad();
    //do not invoke dismiss for this dialog       
}

3. Базовый каркас создан, теперь используем его.

Создаем интерфейс для уведомления о завершении работы:
public interface ITaskLoaderListener {
    void onLoadFinished(Object data);
    void onCancelLoad();
}

Создаем нашу реализацию ProgressDialog-а на базе созданного абстрактного класса:
public class TaskProgressDialogFragment extends
        AbstractTaskProgressDialogFragment {
       
    private ITaskLoaderListener taskLoaderListener;
    private final Handler handler = new Handler();

    protected TaskProgressDialogFragment(AbstractTaskLoader loader,    String message) {
        super(loader, message);
    }

    protected void setTaskLoaderListener(ITaskLoaderListener taskLoaderListener){
        this.taskLoaderListener = taskLoaderListener;
    }
   
    @Override
    protected void onLoadComplete(final Object data) {
        if(taskLoaderListener!=null){
            taskLoaderListener.onLoadFinished(data);
        }
    }

    @Override
    protected void onCancelLoad() {
        if (taskLoaderListener != null) {
            taskLoaderListener.onCancelLoad();
        }
    }
...

И Builder для удобного построения и запуска Loader-a:
public static class Builder {
       FragmentActivity fa;
       AbstractTaskLoader loader;
       ITaskLoaderListener taskLoaderListener;
       Boolean cancelable;
       String progressMsg;

       public Builder(FragmentActivity fa, AbstractTaskLoader loader,
               String progressMsg) {
           this.fa = fa;
           this.loader = loader;
           this.progressMsg = progressMsg;
       }

       public Builder setTaskLoaderListener(
               ITaskLoaderListener taskLoaderListener) {
           this.taskLoaderListener = taskLoaderListener;
           return this;
       }

       public Builder setCancelable(Boolean cancelable) {
           this.cancelable = cancelable;
           return this;
       }

       public void show() {

           String TAG_FRAGMENT = UUID.randomUUID().toString();

           // remove prev if exists
           FragmentManager fm = fa.getSupportFragmentManager();

           FragmentTransaction ft = fm.beginTransaction();
           Fragment prev = fm.findFragmentByTag(TAG_FRAGMENT);
           if (prev != null) {
               ft.remove(prev);
           }

           // create dlg fragment
           TaskProgressDialogFragment fragment = new TaskProgressDialogFragment(
                   loader, progressMsg);
           fragment.setTaskLoaderListener(taskLoaderListener);
           fragment.setCancelable(cancelable);

           Bundle args = new Bundle();
           args.putString("message", progressMsg);
           fragment.setArguments(args);

           // show the dialog.
           fragment.show(ft, TAG_FRAGMENT);

       }
   }

4. Последнее, это наш Loader. Для примера просто каждую секунду обновляет тест сообщения в ProgreesDialog-e:
public class SampleTask extends AbstractTaskLoader {
    @Override
    public Object loadInBackground() {
       
        String result = "";
        for(int i=0; i<10;i++){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            result = "Wait: " + String.valueOf(i);
            publishMessage(result);
           
            if(isCanselled()){
                break;
            }
           
            Log.d(TAG, result);
        }
        return result;
}

Добавим статитческий метод для удобного запуска:
public static void execute(FragmentActivity fa,
            ITaskLoaderListener taskLoaderListener) {

    SampleTask loader = new SampleTask(fa);

    new TaskProgressDialogFragment.Builder(fa, loader, "Wait...")
            .setCancelable(true)
            .setTaskLoaderListener(taskLoaderListener)
            .show();
}

5. Вызов
public class SampleActivity extends FragmentActivity implements ITaskLoaderListener{
…
   SampleTask.execute(this, this);

@Override
public void onCancelLoad() {
    Log.d(TAG, "task canceled");
}

@Override
public void onLoadFinished(Object data) {
    if(data!=null && data instanceof String){
        Log.d(TAG, "task result: " + data);
    }
}

Скачать исходный код примера, конструктивная критика и замечания приветствуются.

Источники:
1. Блог Evelina Vrabie, стьтья 'Android Fragments, Saving State and Screen Rotation'
2. Stack Overflow
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 6

    +2
    Как-то много действий для решения простой задачи.
      0
      Если быстро и на один раз, то можно все (Progress + Loader) реализовать в одном классе наследнике DialogFragment.
      В моем варианте Loader отделен от фрагмента, если потребуется добавить еще один Loader c другой логикой – не проблема (см. п.4). Да и диалог можно сделать другой (отображающий progress bar вместо тестового сообщения), в статье на это не акцентировано внимание, но в исходниках есть AbstractTaskLoader. publishProgress(int), останется только добавить отработку в AbstractTaskProgressDialogFragment. Handler полученного значения. В общем, я старался сделать универсальное решение.
      0
      Чем этот вариант лучше стандартного подхода?
        0
        Что подразумевается под фразой «стандартный подход», использование связки AsyncTasks & ProgressDialogs? Если да, то этот вариант не лучше, просто для для другой ситуации — когда вместо AsyncTasks надо использовать AsyncTaskLoader и информировать о прогрессе выполнения.
        0
        да, именно это я имел в виду
          0
          UPD. Промахнулся статьей(

          Only users with full accounts can post comments. Log in, please.