Android碎片。 在屏幕旋轉或配置更改期間保留AsyncTask



6 Answers

我想你會喜歡我下面詳細介紹的非常全面和實用的例子。

  1. 旋轉工作,並且對話生存。
  2. 您可以通過按下後退按鈕取消任務和對話框(如果您想要這種行為)。
  3. 它使用碎片。
  4. 設備旋轉時,活動下面的碎片佈局會正確更改。
  5. 有一個完整的源代碼下載和預編譯APK,所以你可以看到,如果行為是你想要的。

編輯

按照Brad Larson的要求,我已經復制了以下大多數鏈接解決方案。 另外,因為我發布它,我已被指向AsyncTaskLoader 。 我不確定它是否完全適用於相同的問題,但無論如何您都應該檢查一下。

使用帶進度對話框和設備旋轉的AsyncTask

工作解決方案!

我終於得到了一切工作。 我的代碼具有以下功能:

  1. 一個Fragment其佈局隨著方向而變化。
  2. 您可以在其中執行一些工作的AsyncTask
  3. 一個DialogFragment ,它顯示進度條中的任務進度(不僅僅是一個不確定的微調)。
  4. 旋轉在不中斷任務或解除對話的情況下工作。
  5. 後退按鈕關閉對話框並取消任務(儘管可以很容易地改變這種行為)。

我不認為在其他任何地方都可以找到工作的組合。

基本思路如下。 有一個MainActivity類包含一個片段 - MainFragmentMainFragment對於水平和垂直方向具有不同的佈局,並且setRetainInstance()為false,因此佈局可以更改。 這意味著當設備方向改變時, MainActivityMainFragment都被完全銷毀並重新創建。

另外我們有MyTask (從AsyncTask擴展)完成所有的工作。 我們無法將它存儲在MainFragment因為它會被銷毀,並且Google已經不贊成使用setRetainNonInstanceConfiguration()類的東西。 無論如何,這並不總是可用的,充其量也是一種醜陋的黑客。 相反,我們將MyTask存儲在另一個片段中,一個名為TaskFragment這個片段 setRetainInstance()設置為true,所以當設備旋轉時,這個片段不會被銷毀,並且MyTask被保留。

最後,我們需要告訴TaskFragment何時完成通知,並且在創建時使用setTargetFragment(<the MainFragment>)來完成。 當設備被旋轉並且MainFragment被銷毀並且一個新實例被創建時,我們使用FragmentManager來找到對話框(基於它的標籤)並且執行setTargetFragment(<the new MainFragment>) 。 這是非常多的。

還有其他兩件事情我需要做:首先取消任務,當對話框關閉時,第二次將取消消息設置為空,否則在旋轉設備時奇怪地忽略對話框。

代碼

我不會列出佈局,它們非常明顯,您可以在下面的項目下載中找到它們。

主要活動

這非常簡單。 我在這個活動中添加了一個回調,以便知道任務何時完成,但您可能不需要。 主要我只是想顯示片段活動回調機制,因為它非常整潔,你可能以前沒有看到它。

public class MainActivity extends Activity implements MainFragment.Callbacks
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onTaskFinished()
    {
        // Hooray. A toast to our success.
        Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();
        // NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*
        // the duration in milliseconds. ANDROID Y U NO ENUM? 
    }
}

MainFragment

這很長,但值得!

public class MainFragment extends Fragment implements OnClickListener
{
    // This code up to onDetach() is all to get easy callbacks to the Activity. 
    private Callbacks mCallbacks = sDummyCallbacks;

    public interface Callbacks
    {
        public void onTaskFinished();
    }
    private static Callbacks sDummyCallbacks = new Callbacks()
    {
        public void onTaskFinished() { }
    };

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
        if (!(activity instanceof Callbacks))
        {
            throw new IllegalStateException("Activity must implement fragment's callbacks.");
        }
        mCallbacks = (Callbacks) activity;
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
        mCallbacks = sDummyCallbacks;
    }

    // Save a reference to the fragment manager. This is initialised in onCreate().
    private FragmentManager mFM;

    // Code to identify the fragment that is calling onActivityResult(). We don't really need
    // this since we only have one fragment to deal with.
    static final int TASK_FRAGMENT = 0;

    // Tag so we can find the task fragment again, in another instance of this fragment after rotation.
    static final String TASK_FRAGMENT_TAG = "task";

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

        // At this point the fragment may have been recreated due to a rotation,
        // and there may be a TaskFragment lying around. So see if we can find it.
        mFM = getFragmentManager();
        // Check to see if we have retained the worker fragment.
        TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);

        if (taskFragment != null)
        {
            // Update the target fragment so it goes to this fragment instead of the old one.
            // This will also allow the GC to reclaim the old MainFragment, which the TaskFragment
            // keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't
            // use weak references. To be sure you aren't leaking, you may wish to make your own
            // setTargetFragment() which does.
            taskFragment.setTargetFragment(this, TASK_FRAGMENT);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState)
    {
        super.onViewCreated(view, savedInstanceState);

        // Callback for the "start task" button. I originally used the XML onClick()
        // but it goes to the Activity instead.
        view.findViewById(R.id.taskButton).setOnClickListener(this);
    }

    @Override
    public void onClick(View v)
    {
        // We only have one click listener so we know it is the "Start Task" button.

        // We will create a new TaskFragment.
        TaskFragment taskFragment = new TaskFragment();
        // And create a task for it to monitor. In this implementation the taskFragment
        // executes the task, but you could change it so that it is started here.
        taskFragment.setTask(new MyTask());
        // And tell it to call onActivityResult() on this fragment.
        taskFragment.setTargetFragment(this, TASK_FRAGMENT);

        // Show the fragment.
        // I'm not sure which of the following two lines is best to use but this one works well.
        taskFragment.show(mFM, TASK_FRAGMENT_TAG);
//      mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)
        {
            // Inform the activity. 
            mCallbacks.onTaskFinished();
        }
    }

TaskFragment

    // This and the other inner class can be in separate files if you like.
    // There's no reason they need to be inner classes other than keeping everything together.
    public static class TaskFragment extends DialogFragment
    {
        // The task we are running.
        MyTask mTask;
        ProgressBar mProgressBar;

        public void setTask(MyTask task)
        {
            mTask = task;

            // Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.
            mTask.setFragment(this);
        }

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

            // Retain this instance so it isn't destroyed when MainActivity and
            // MainFragment change configuration.
            setRetainInstance(true);

            // Start the task! You could move this outside this activity if you want.
            if (mTask != null)
                mTask.execute();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState)
        {
            View view = inflater.inflate(R.layout.fragment_task, container);
            mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);

            getDialog().setTitle("Progress Dialog");

            // If you're doing a long task, you probably don't want people to cancel
            // it just by tapping the screen!
            getDialog().setCanceledOnTouchOutside(false);

            return view;
        }

        // This is to work around what is apparently a bug. If you don't have it
        // here the dialog will be dismissed on rotation, so tell it not to dismiss.
        @Override
        public void onDestroyView()
        {
            if (getDialog() != null && getRetainInstance())
                getDialog().setDismissMessage(null);
            super.onDestroyView();
        }

        // Also when we are dismissed we need to cancel the task.
        @Override
        public void onDismiss(DialogInterface dialog)
        {
            super.onDismiss(dialog);
            // If true, the thread is interrupted immediately, which may do bad things.
            // If false, it guarantees a result is never returned (onPostExecute() isn't called)
            // but you have to repeatedly call isCancelled() in your doInBackground()
            // function to check if it should exit. For some tasks that might not be feasible.
            if (mTask != null) {
                mTask.cancel(false);
            }

            // You don't really need this if you don't want.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);
        }

        @Override
        public void onResume()
        {
            super.onResume();
            // This is a little hacky, but we will see if the task has finished while we weren't
            // in this activity, and then we can dismiss ourselves.
            if (mTask == null)
                dismiss();
        }

        // This is called by the AsyncTask.
        public void updateProgress(int percent)
        {
            mProgressBar.setProgress(percent);
        }

        // This is also called by the AsyncTask.
        public void taskFinished()
        {
            // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
            // after the user has switched to another app.
            if (isResumed())
                dismiss();

            // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
            // onResume().
            mTask = null;

            // Tell the fragment that we are done.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);
        }
    }

MyTask

    // This is a fairly standard AsyncTask that does some dummy work.
    public static class MyTask extends AsyncTask<Void, Void, Void>
    {
        TaskFragment mFragment;
        int mProgress = 0;

        void setFragment(TaskFragment fragment)
        {
            mFragment = fragment;
        }

        @Override
        protected Void doInBackground(Void... params)
        {
            // Do some longish task. This should be a task that we don't really
            // care about continuing
            // if the user exits the app.
            // Examples of these things:
            // * Logging in to an app.
            // * Downloading something for the user to view.
            // * Calculating something for the user to view.
            // Examples of where you should probably use a service instead:
            // * Downloading files for the user to save (like the browser does).
            // * Sending messages to people.
            // * Uploading data to a server.
            for (int i = 0; i < 10; i++)
            {
                // Check if this has been cancelled, e.g. when the dialog is dismissed.
                if (isCancelled())
                    return null;

                SystemClock.sleep(500);
                mProgress = i * 10;
                publishProgress();
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Void... unused)
        {
            if (mFragment == null)
                return;
            mFragment.updateProgress(mProgress);
        }

        @Override
        protected void onPostExecute(Void unused)
        {
            if (mFragment == null)
                return;
            mFragment.taskFinished();
        }
    }
}

下載示例項目

這裡是源代碼APK 。 對不起,ADT堅持在讓我做項目之前添加支持庫。 我相信你可以刪除它。

Question

我正在使用智能手機/平板電腦應用程序,只使用一個APK,並根據屏幕大小加載資源,最好的設計選擇似乎是通過ACL使用碎片。

這個應用程序一直工作正常,直到現在只是基於活動。 這是我如何在活動中處理AsyncTasks和ProgressDialogs的模擬類,以便即使在屏幕旋轉或通信中發生配置更改時也能使其工作。

我不會為了避免娛樂活動而改變清單,我有很多理由不想這樣做,但主要是因為官方文檔說這不是推薦的,而且我沒有做到這一點,所以請不要推薦路線。

public class Login extends Activity {

    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.login);
        //SETUP UI OBJECTS
        restoreAsyncTask();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (pd != null) pd.dismiss();
        if (asyncLoginThread != null) return (asyncLoginThread);
        return super.onRetainNonConfigurationInstance();
    }

    private void restoreAsyncTask();() {
        pd = new ProgressDialog(Login.this);
        if (getLastNonConfigurationInstance() != null) {
            asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();
            if (asyncLoginThread != null) {
                if (!(asyncLoginThread.getStatus()
                        .equals(AsyncTask.Status.FINISHED))) {
                    showProgressDialog();
                }
            }
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
        @Override
        protected Boolean doInBackground(String... args) {
            try {
                //Connect to WS, recieve a JSON/XML Response
                //Place it somewhere I can use it.
            } catch (Exception e) {
                return true;
            }
            return true;
        }

        protected void onPostExecute(Boolean result) {
            if (result) {
                pd.dismiss();
                //Handle the response. Either deny entry or launch new Login Succesful Activity
            }
        }
    }
}

這段代碼工作正常,我有大約10,000個用戶沒有投訴,所以把這個邏輯複製到新的基於片段的設計中似乎是合乎邏輯的,但是,當然,它並不工作。

這裡是LoginFragment:

public class LoginFragment extends Fragment {

    FragmentActivity parentActivity;
    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    public interface OnLoginSuccessfulListener {
        public void onLoginSuccessful(GlobalContainer globalContainer);
    }

    public void onSaveInstanceState(Bundle outState){
        super.onSaveInstanceState(outState);
        //Save some stuff for the UI State
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setRetainInstance(true);
        //If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.
        parentActivity = getActivity();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            loginSuccessfulListener = (OnLoginSuccessfulListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);
        return loginLayout;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //SETUP UI OBJECTS
        if(savedInstanceState != null){
            //Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
            @Override
            protected Boolean doInBackground(String... args) {
                try {
                    //Connect to WS, recieve a JSON/XML Response
                    //Place it somewhere I can use it.
                } catch (Exception e) {
                    return true;
                }
                return true;
            }

            protected void onPostExecute(Boolean result) {
                if (result) {
                    pd.dismiss();
                    //Handle the response. Either deny entry or launch new Login Succesful Activity
                }
            }
        }
    }
}

我不能使用onRetainNonConfigurationInstance()因為它必須從Activity而不是Fragment中調用,與getLastNonConfigurationInstance() 。 我在這裡讀了一些類似的問題,沒有回答。

我知道這可能需要一些解決方法才能讓這些東西在碎片中得到妥善組織,也就是說,我想保持相同的基本設計邏輯。

在配置更改過程中保留AsyncTask的正確方法是什麼,如果它仍在運行,則顯示progressDialog,同時考慮到AsyncTask是Fragment的內部類,並且它是調用AsyncTask.execute的Fragment本身()?




我的第一個建議是避免內部的AsyncTasks ,你可以閱讀一個關於這個問題和答案的問題: Android:AsyncTask建議:私有類還是公共類?

之後,我開始使用非內部和...現在我看到很多好處。

第二個是,在Application類中保留一個正在運行的AsyncTask的引用 - http://developer.android.com/reference/android/app/Application.html

每次啟動一個AsyncTask,將其設置在應用程序上,當它完成時將其設置為空。

當一個片段/活動開始時,您可以檢查是否有任何AsyncTask正在運行(通過檢查應用程序中是否為null),然後將引用設置為任何您想要的(活動,片段等,以便您可以執行回調)。

這將解決您的問題:如果您只有1個AsyncTask在任何確定的時間運行,您可以添加一個簡單的參考:

AsyncTask<?,?,?> asyncTask = null;

否則,在應用程序中有一個HashMap並引用它們。

進度對話框可以遵循完全相同的原則。




我創建了一個非常小的開源後台任務庫,它基於Marshmallow AsyncTask但具有其他功能,如:

  1. 通過配置更改自動保留任務;
  2. UI回調(偵聽器);
  3. 設備旋轉時不會重新啟動或取消任務(就像Loaders所做的那樣);

該庫在內部使用一個沒有任何用戶界面的Fragment ,該界面在配置更改( setRetainInstance(true) )中保留。

你可以在GitHub上找到它: https://github.com/NeoTech-Software/Android-Retainable-Taskshttps://github.com/NeoTech-Software/Android-Retainable-Tasks

最基本的例子(版本0.2.0):

這個例子完全保留了這個任務,使用的代碼量非常有限。

任務:

private class ExampleTask extends Task<Integer, String> {

    public ExampleTask(String tag){
        super(tag);
    }

    protected String doInBackground() {
        for(int i = 0; i < 100; i++) {
            if(isCancelled()){
                break;
            }
            SystemClock.sleep(50);
            publishProgress(i);
        }
        return "Result";
    }
}

活動:

public class Main extends TaskActivityCompat implements Task.Callback {

    @Override
    public void onClick(View view){
        ExampleTask task = new ExampleTask("activity-unique-tag");
        getTaskManager().execute(task, this);
    }

    @Override
    public Task.Callback onPreAttach(Task<?, ?> task) {
        //Restore the user-interface based on the tasks state
        return this; //This Activity implements Task.Callback
    }

    @Override
    public void onPreExecute(Task<?, ?> task) {
        //Task started
    }

    @Override
    public void onPostExecute(Task<?, ?> task) {
        //Task finished
        Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show();
    }
}



如果有人發現他們的方式來到這個線程,那麼我發現一個乾淨的方法是從app.Service運行異步任務(以START_STICKY開始),然後重新運行正在運行的服務的迭代以找出服務(以及異步任務)仍在運行;

    public boolean isServiceRunning(String serviceClassName) {
    final ActivityManager activityManager = (ActivityManager) Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
    final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

    for (RunningServiceInfo runningServiceInfo : services) {
        if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
            return true;
        }
    }
    return false;
 }

如果是,重新添加DialogFragment (或其他),如果不是確保對話已被解除。

如果你使用v4.support.*庫,這是特別貼切的,因為(在撰寫本文時)他們已經知道setRetainInstance方法和查看分頁的問題。 此外,通過不保留實例,您可以使用不同的資源集重新創建活動(即新方向的不同視圖佈局)




I write samepl code to solve this problem

First step is make Application class:

public class TheApp extends Application {

private static TheApp sTheApp;
private HashMap<String, AsyncTask<?,?,?>> tasks = new HashMap<String, AsyncTask<?,?,?>>();

@Override
public void onCreate() {
    super.onCreate();
    sTheApp = this;
}

public static TheApp get() {
    return sTheApp;
}

public void registerTask(String tag, AsyncTask<?,?,?> task) {
    tasks.put(tag, task);
}

public void unregisterTask(String tag) {
    tasks.remove(tag);
}

public AsyncTask<?,?,?> getTask(String tag) {
    return tasks.get(tag);
}
}

In AndroidManifest.xml

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="com.example.tasktest.TheApp">

Code in activity:

public class MainActivity extends Activity {

private Task1 mTask1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTask1 = (Task1)TheApp.get().getTask("task1");

}

/*
 * start task is not running jet
 */
public void handletask1(View v) {
    if (mTask1 == null) {
        mTask1 = new Task1();
        TheApp.get().registerTask("task1", mTask1);
        mTask1.execute();
    } else
        Toast.makeText(this, "Task is running...", Toast.LENGTH_SHORT).show();

}

/*
 * cancel task if is not finished
 */
public void handelCancel(View v) {
    if (mTask1 != null)
        mTask1.cancel(false);
}

public class Task1 extends AsyncTask<Void, Void, Void>{

    @Override
    protected Void doInBackground(Void... params) {
        try {
            for(int i=0; i<120; i++) {
                Thread.sleep(1000);
                Log.i("tests", "loop=" + i);
                if (this.isCancelled()) {
                    Log.e("tests", "tssk cancelled");
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onCancelled(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }

    @Override
    protected void onPostExecute(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }
}

}

When activity orientation changes variable mTask is inited from app context. When task is finished variable is set to null and remove from memory.

For me its enough.




看看here

There is a solution based on Timmmm's solution.

但我改進了它:

  • 現在解決方案是可擴展的 - 你只需要擴展 FragmentAbleToStartTask

  • 您可以同時繼續運行多個任務。

    在我看來,它就像startActivityForResult一樣簡單並接收結果

  • 您還可以停止正在運行的任務並檢查特定任務是否正在運行

對不起我的英語不好




Related