Androidのフラグメント。画面の回転または構成の変更中にAsyncTaskを保持する


86

私はスマートフォン/タブレットアプリで作業しており、APKを1つだけ使用し、画面サイズに応じて必要に応じてリソースを読み込んでいます。最適な設計の選択は、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()フラグメントではなくアクティビティから呼び出す必要があるため、使用できませんgetLastNonConfigurationInstance()。同じことが。にも当てはまります。私はここでいくつかの同様の質問を読みましたが、答えはありません。

このようなものをフラグメントに適切に編成するには、回避策が必要になる可能性があることを理解しています。とはいえ、同じ基本的な設計ロジックを維持したいと思います。

構成の変更中にAsyncTaskを保持し、それがまだ実行されている場合は、AsyncTaskがFragmentの内部クラスであり、AsyncTask.executeを呼び出すのはFragment自体であることを考慮して、progressDialogを表示する適切な方法は何でしょうか。 ()?


1
たぶん上のスレッドAsyncTaskで設定変更をどのように扱うかは助けることができる
RDS

AsyncTaskをアプリケーションのライフサイクルに関連付けます。したがって、アクティビティが再作成されたときに再開できます
Fred

このトピックに関する私の投稿をチェックしてください:sを使用した構成変更の処理Fragment
Alex Lockwood 2013

回答:


75

フラグメントは実際にこれをはるかに簡単にすることができます。Fragment.setRetainInstance(boolean)メソッドを使用するだけで、構成の変更後もフラグメントインスタンスを保持できます。これは、ドキュメントのActivity.onRetainnonConfigurationInstance()の推奨される置換であることに注意してください。

何らかの理由で保持されたフラグメントを本当に使用したくない場合は、他の方法をとることができます。各フラグメントには、Fragment.getId()によって返される一意の識別子があることに注意してください。Fragment.getActivity()。isChangingConfigurations()を使用して、構成変更のためにフラグメントが破棄されているかどうかを確認することもできます。したがって、AsyncTaskを停止することを決定した時点で(おそらくonStop()またはonDestroy()で)、たとえば、構成が変更されているかどうかを確認し、変更されている場合は、フラグメントの識別子の下の静的SparseArrayに貼り付けることができます。次に、onCreate()またはonStart()で、使用可能なスパース配列にAsyncTaskがあるかどうかを確認します。


setRetainInstanceは、バックスタックを使用していない場合のみであることに注意してください。
ニール

4
保持されたフラグメントのonCreateViewが実行される前に、AsyncTaskが結果を送り返すことは可能ではありませんか?
jakk 2012年

6
@jakkアクティビティ、フラグメントなどのライフサイクルメソッドは、メインGUIスレッドのメッセージキューによって順番に呼び出されるため、これらのライフサイクルメソッドが完了する(または呼び出される)前にタスクがバックグラウンドで同時に終了した場合でも、onPostExecuteメソッドは次のことを行う必要があります。最終的にメインスレッドのメッセージキューによって処理される前に待機します。
アレックスロックウッド

このアプローチ(RetainInstance = true)は、方向ごとに異なるレイアウトファイルをロードする場合は機能しません。
ジャスティン

MainActivityのonCreateメソッド内でasynctaskを開始することは、「worker」フラグメント内のasynctaskが明示的なユーザーアクションによって開始された場合にのみ機能するようです。メインスレッドとユーザーインターフェイスが利用できるためです。ただし、アプリの起動直後に非同期タスクを開始すると(ボタンクリックなどのユーザーアクションなしで)、例外が発生します。その場合、asynctaskは、onCreateメソッドではなく、MainActivityのonStartメソッドで呼び出すことができます。
ʕᵔᴥᵔʔ

66

以下に詳述する私の非常に包括的で実用的な例をお楽しみいただけると思います。

  1. 回転が機能し、ダイアログは存続します。
  2. 戻るボタンを押すと、タスクとダイアログをキャンセルできます(この動作が必要な場合)。
  3. フラグメントを使用します。
  4. デバイスが回転すると、アクティビティの下のフラグメントのレイアウトが適切に変更されます。
  5. 完全なソースコードのダウンロードとプリコンパイルされたAPKがあるので、動作が希望どおりかどうかを確認できます。

編集

Brad Larsonの要求に応じて、以下のリンクされたソリューションのほとんどを再現しました。また、投稿してから指摘されていAsyncTaskLoaderます。同じ問題に完全に適用できるかどうかはわかりませんが、とにかくチェックする必要があります。

使用する AsyncTask進行状況ダイアログとデバイスの回転でします。

実用的なソリューション!

私はついにすべてが機能するようになりました。私のコードには次の機能があります。

  1. A Fragment向きによってレイアウトが変わる
  2. アン AsyncTaskこれであなたは、いくつかの作業を行うことができます。
  3. A DialogFragmentタスクの進行状況を進行状況バーに表示する(不確定なスピナーだけではありません)。
  4. 回転は、タスクを中断したり、ダイアログを閉じたりすることなく機能します。
  5. 戻るボタンはダイアログを閉じてタスクをキャンセルします(ただし、この動作はかなり簡単に変更できます)。

働きの組み合わせは他のどこにも見られないと思います。

基本的な考え方は次のとおりです。あるMainActivity単一のフラグメントが含まれているクラスが- MainFragmentMainFragment水平方向と垂直方向のレイアウトが異なりsetRetainInstance()、レイアウトが変更される可能性があるためfalseです。これは、デバイスの向きが変更されるMainActivityと、MainFragment完全に破壊され、再作成されています。

これとは別に、すべての作業を行うMyTask(から拡張AsyncTask)があります。MainFragment破棄されるため、保存できません。Googleはのようなものの使用を廃止しましたsetRetainNonInstanceConfiguration()。それはとにかく常に利用できるわけではなく、せいぜい醜いハックです。代わりMyTaskに、DialogFragmentと呼ばれる別のフラグメントに格納しTaskFragmentます。この断片は、あろうsetRetainInstance()デバイスは、この断片が破壊されない回転し、そしてように、trueに設定MyTask保持されます。

最後にTaskFragment、それが終了したときに誰に通知するかを伝える必要があり、それsetTargetFragment(<the MainFragment>)を作成するときに使用します。デバイスが回転し、MainFragmentが破棄されて新しいインスタンスが作成さFragmentManagerれたら、を使用して(タグに基づいて)ダイアログを検索し、を実行しますsetTargetFragment(<the new MainFragment>)。それはほとんどそれです。

他に2つのことを行う必要がありました。最初にダイアログが閉じられたときにタスクをキャンセルし、次に閉じメッセージをnullに設定します。そうしないと、デバイスが回転したときにダイアログが奇妙に閉じられます。

コード

レイアウトはリストしません。それらは非常に明白であり、以下のプロジェクトのダウンロードで見つけることができます。

主な活動

これは非常に簡単です。このアクティビティにコールバックを追加して、タスクがいつ終了したかがわかるようにしましたが、必要ない場合もあります。主に、フラグメントアクティビティコールバックメカニズムを表示したかったのは、それが非常にすっきりしていて、これまで見たことがないかもしれないからです。

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は、プロジェクトを作成する前に、サポートライブラリを追加することを主張しました。あなたはそれを取り除くことができると確信しています。


4
DialogFragment古いコンテキストへの参照を保持するUI要素があるため、プログレスバーを保持することは避けます。代わりに、AsyncTask別の空のフラグメントに格納しDialogFragment、そのターゲットとして設定します。
SD

デバイスが回転してonCreateView()再度呼び出されたときに、これらの参照はクリアされませんか?古いmProgressBarものは少なくとも新しいもので上書きされます。
Timmmm 2012年

明示的ではありませんが、私はそれについてかなり確信しています。あなたは追加することができますmProgressBar = null;onDestroyView()あなたが余分なことを確認したい場合。特異点の方法は良い考えかもしれませんが、それはコードの複雑さをさらに増します!
Timmmm 2012

1
asynctaskで保持している参照は、progressdialogフラグメントですよね?したがって、2つの質問:1-progressdialogを呼び出す実際のフラグメントを変更したい場合はどうなりますか。2-パラメータを非同期タスクに渡したい場合はどうなりますか?よろしく、
Maxrunner 2012

1
@Maxrunner、パラメータを渡すための最も簡単なことは、おそらくに移動するmTask.execute()ことMainFragment.onClick()です。または、パラメータの受け渡しを許可しsetTask()たり、パラメータをMyTaskそれ自体に保存したりすることもできます。あなたの最初の質問が何を意味するのか正確にはわかりませんが、多分あなたは使いたいTaskFragment.getTargetFragment()ですか?を使用して動作すると確信していViewPagerます。しかしViewPagers、あまりよく理解されておらず、文書化されていないので、頑張ってください!フラグメントは、最初に表示されるまで作成されないことに注意してください。
Timmmm 2012

16

最近保持されたを使用して構成の変更を処理する方法を説明する記事投稿しましたFragment。それは保持の問題を解決しますAsyncTask、回転全体の変化をうまく。

TL; DRは、使用ホストにあなたのあるAsyncTask内部のA Fragment、コールsetRetainInstance(true)Fragment、およびレポートAsyncTaskの進捗状況/結果はそれのにバックアップActivity(またはそれのターゲットFragment保持を通してあなたが@Timmmmで説明したアプローチを使用することを選択した場合、) Fragment


5
ネストされたフラグメントについてどのように処理しますか?別のフラグメント(タブ)内のRetainedFragmentから開始されたAsyncTaskのように。
Rekin 2014年

フラグメントがすでに保持されている場合は、保持されているフラグメント内から非同期タスクを実行しないのはなぜですか?すでに保持されている場合は、構成が変更された場合でも、非同期タスクがレポートを返すことができます。
Alex Lockwood 2014

@AlexLockwoodブログをありがとう。代わりに処理するために持つのonAttachonDetach、内部があれば、それは良くなるTaskFragment、私達はちょうど呼び出しgetActivity、我々はコールバックを発射する必要があるとき。(instaceofをチェックすることによりTaskCallbacks
Yan Cheng

1
どちらの方法でも実行できます。私はそれをやっただけなのでonAttach()onDetach()それTaskCallbacksを使いたいと思うたびに常にアクティビティをキャストすることを避けることができました。
Alex Lockwood 2014年

1
@AlexLockwoodアプリケーションが単一のアクティビティ(複数のフラグメントの設計)に従っている場合、UIフラグメントごとに個別のタスクフラグメントを用意する必要がありますか?したがって、基本的に、各タスクフラグメントのライフサイクルは、そのターゲットフラグメントによって管理され、アクティビティとの通信はありません。
Manas Bajaj 2015

13

私の最初の提案は、内部のAsyncTasks回避することです。これについて尋ねた質問と、その回答を読むことができます。Android:AsyncTaskの推奨事項:プライベートクラスまたはパブリッククラス?

その後、私は非インナーを使い始めました...今ではたくさんの利点があります。

2つ目は、実行中のAsyncTaskの参照をApplicationクラスに保持することです-http://developer.android.com/reference/android/app/Application.html

AsyncTaskを開始するたびに、アプリケーションに設定し、終了したらnullに設定します。

フラグメント/アクティビティが開始したら、AsyncTaskが実行されているかどうかを確認し(アプリケーションでnullかどうかを確認することで)、内部の参照を必要なもの(アクティビティ、フラグメントなど)に設定してコールバックを実行できます。

これにより、問題が解決します。特定の時間に1つのAsyncTaskしか実行していない場合は、簡単な参照を追加できます。

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

それ以外の場合は、アプリケーションにそれらへの参照を含むHashMapを含めます。

進行状況ダイアログは、まったく同じ原則に従うことができます。


2
AsyncTaskのライフサイクルをその親にバインドする限り(AsyncTaskをActivity / Fragmentの内部クラスとして定義することによって)、AsyncTaskをその親のライフサイクルレクリエーションから脱出させるのはかなり難しいことに同意しましたが、私はあなたが好きではありません解決策、それはとてもハッキーに見えます。
yorkw 2011

問題は..あなたはより良い解決策を持っていますか?
neteinstein 2011

1
ここで@yorkwに同意する必要があります。このソリューションは、フラグメントを使用せずにこの問題を処理していたときに、しばらく前に提示されました(アクティビティベースのアプリ)。この質問:stackoverflow.com/questions/2620917/…も同じ答えです。「アプリケーションインスタンスには独自のライフサイクルがあります。OSによっても強制終了される可能性があるため、このソリューションによって次の問題が発生する可能性があります。再現が難しいバグ」
ブラインド2011

1
それでも、@ yorkwが言ったように、「ハッキー」でない方法は他にありません。私はいくつかのアプリでそれを使用していますが、起こりうる問題に注意を払えば、すべてうまく機能します。
neteinstein 2011

たぶん、@ hackbodソリューションはあなたにもっと適しています。
neteinstein 2011

4

これにAsyncTaskLoadersを使用する方法を思いつきました。使い方はとても簡単で、必要なオーバーヘッドIMOも少なくて済みます。

基本的に、次のようにAsyncTaskLoaderを作成します。

public class MyAsyncTaskLoader extends AsyncTaskLoader {
    Result mResult;
    public HttpAsyncTaskLoader(Context context) {
        super(context);
    }

    protected void onStartLoading() {
        super.onStartLoading();
        if (mResult != null) {
            deliverResult(mResult);
        }
        if (takeContentChanged() ||  mResult == null) {
            forceLoad();
        }
    }

    @Override
    public Result loadInBackground() {
        SystemClock.sleep(500);
        mResult = new Result();
        return mResult;
    }
}

次に、ボタンがクリックされたときに上記のAsyncTaskLoaderを使用するアクティビティで:

public class MyActivityWithBackgroundWork extends FragmentActivity implements LoaderManager.LoaderCallbacks<Result> {

    private String username,password;       
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mylayout);
        //this is only used to reconnect to the loader if it already started
        //before the orientation changed
        Loader loader = getSupportLoaderManager().getLoader(0);
        if (loader != null) {
            getSupportLoaderManager().initLoader(0, null, this);
        }
    }

    public void doBackgroundWorkOnClick(View button) {
        //might want to disable the button while you are doing work
        //to prevent user from pressing it again.

        //Call resetLoader because calling initLoader will return
        //the previous result if there was one and we may want to do new work
        //each time
        getSupportLoaderManager().resetLoader(0, null, this);
    }   


    @Override
    public Loader<Result> onCreateLoader(int i, Bundle bundle) {
        //might want to start a progress bar
        return new MyAsyncTaskLoader(this);
    }


    @Override
    public void onLoadFinished(Loader<LoginResponse> loginLoader,
                               LoginResponse loginResponse)
    {
        //handle result
    }

    @Override
    public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
    {
        //remove references to previous loader resources

    }
}

これは向きの変更をうまく処理しているようで、ローテーション中もバックグラウンドタスクが続行されます。

注意すべきいくつかの事柄:

  1. onCreateでasynctaskloaderに再接続すると、onLoadFinished()で前の結果でコールバックされます(要求が完了したと既に通知されている場合でも)。これは実際にはほとんどの場合良好な動作ですが、処理が難しい場合があります。これを処理する方法はたくさんあると思いますが、onLoadFinishedでloader.abandon()を呼び出しました。次に、チェックインonCreateを追加して、ローダーがまだ破棄されていない場合にのみローダーに再接続します。結果のデータが再び必要な場合は、それを行いたくないでしょう。ほとんどの場合、データが必要です。

これをhttp呼び出しに使用する方法の詳細については、こちらをご覧ください


getSupportLoaderManager().getLoader(0);nullを返さないのは確かですか(そのIDが0のローダーはまだ存在しないため)?
emmanuelMess

1
ローダーの進行中に構成の変更によってアクティビティが再起動しない限り、nullになります。そのため、nullをチェックしました。
Matt Wolfe 2017

3

私は非常に小さなオープンソースのバックグラウンドタスクライブラリを作成しました。これはマシュマロに大きく基づいていAsyncTaskますが、次のような追加機能を備えています。

  1. 構成の変更後もタスクを自動的に保持します。
  2. UIコールバック(リスナー);
  3. デバイスが回転したときにタスクを再開またはキャンセルしません(ローダーのように)。

ライブラリは内部的にFragmentユーザーインターフェイスなしでを使用します。これは構成の変更後も保持されます(setRetainInstance(true))。

GitHubで見つけることができます:https//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();
    }
}

1

私のアプローチは、委任デザインパターンを使用することです。一般に、AysncTask.doInBackground()メソッドで、実際のビジネスロジック(インターネットやデータベースなどからデータを読み取る)をAsyncTask(委任者)からBusinessDAO(委任者)に分離できます。 、実際のタスクをBusinessDAOに委任してから、BusinessDAOにシングルトンプロセスメカニズムを実装します。これにより、BusinessDAO.doSomething()を複数回呼び出すと、毎回1つの実際のタスクが実行され、タスクの結果を待機します。アイデアは、デリゲーター(つまり、AsyncTask)ではなく、構成の変更中にデリゲート(つまり、BusinessDAO)を保持することです。

  1. 独自のアプリケーションを作成/実装します。目的はここでBusinessDAOを作成/初期化することです。これにより、BusinessDAOのライフサイクルはアクティビティスコープではなくアプリケーションスコープになります。MyApplicationを使用するにはAndroidManifest.xmlを変更する必要があることに注意してください。

    public class MyApplication extends android.app.Application {
      private BusinessDAO businessDAO;
    
      @Override
      public void onCreate() {
        super.onCreate();
        businessDAO = new BusinessDAO();
      }
    
      pubilc BusinessDAO getBusinessDAO() {
        return businessDAO;
      }
    
    }
    
  2. 既存のActivity / Fragementはほとんど変更されておらず、内部クラスとしてAsyncTaskを実装し、Activity / FragementからAsyncTask.execute()を含みます。違いは、AsyncTaskが実際のタスクをBusinessDAOに委任するため、構成の変更中に2番目のAsyncTaskがは初期化されて実行され、BusinessDAO.doSomething()を2回呼び出しますが、BusinessDAO.doSomething()を2回呼び出すと、新しい実行中のタスクはトリガーされず、現在実行中のタスクが終了するのを待ちます。

    public class LoginFragment extends Fragment {
      ... ...
    
      public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> {
        // get a reference of BusinessDAO from application scope.
        BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO();
    
        @Override
        protected Boolean doInBackground(String... args) {
            businessDAO.doSomething();
            return true;
        }
    
        protected void onPostExecute(Boolean result) {
          //Handle task result and update UI stuff.
        }
      }
    
      ... ...
    }
    
  3. BusinessDAO内で、次のようなシングルトンプロセスメカニズムを実装します。

    public class BusinessDAO {
      ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1));
      Future<MyTask> myFutureTask = null;
    
      public void doSomething() {
        if (myFutureTask == null) {
          // nothing running at the moment, submit a new callable task to run.
          MyTask myTask = new MyTask();
          myFutureTask = completionExecutor.submit(myTask);
        }
        // Task already submitted and running, waiting for the running task to finish.
        myFutureTask.get();
      }
    
      // If you've never used this before, Callable is similar with Runnable, with ability to return result and throw exception.
      private class MyTask extends Callable<MyTask> {
        public MyAsyncTask call() {
          // do your job here.
          return this;
        }
      }
    
    }
    

これが機能するかどうかは100%わかりません。さらに、サンプルコードスニペットは擬似コードと見なす必要があります。デザインレベルからの手がかりを提供しようとしています。フィードバックや提案は大歓迎です。


とてもいい解決策のようです。約2年半前に答えてからテストしましたか?!あなたはそれが機能しているかどうかわからないと言っています、それは私もそうではありません!! 私はこの問題に対して十分にテストされた解決策を求めています。何か提案はありますか?
Alireza A. Ahmadi 2014

1

AsyncTaskを静的フィールドにすることができます。コンテキストが必要な場合は、アプリケーションコンテキストを出荷する必要があります。これにより、メモリリークが回避されます。そうしないと、アクティビティ全体への参照が保持されます。


1

誰かがこのスレッドへの道を見つけた場合、クリーンなアプローチは、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メソッドとビューのページングに関する問題を知っているためです。さらに、インスタンスを保持しないことで、異なるリソースのセット(つまり、新しい方向の異なるビューレイアウト)を使用してアクティビティを再作成できます。


AsyncTaskを保持するためだけにサービスを実行するのはやり過ぎではありませんか?サービスは独自のプロセスで実行されますが、それは追加コストなしでは実現できません。
WeNeigh 2012年

興味深いVinay。アプリのリソースがこれ以上重いことに気づいていません(とにかく現時点ではかなり軽量です)。何を見つけましたか?私は、サービスを、UIの状態に関係なく、システムに手間のかかる作業やI / Oを実行させるための予測可能な環境と見なしています。何かがいつ完了したかを確認するためにサービスと通信することは、「正しい」ように思われました。私が実行を開始するサービスは、タスクの完了時に停止するため、通常は10〜30秒程度存続します。
BrantApps 2012年

ここでのCommonswareの回答は、サービスが悪い考えであることを示唆しているようです。私は現在AsyncTaskLoadersを検討していますが、独自の問題が発生しているようです(柔軟性がない、データの読み込みのみ)
WeNeigh 2012年

1
そうですか。注目すべきことに、リンクしたこのサービスは、独自のプロセスで実行するように明示的に設定されていました。マスターは、このパターンが頻繁に使用されることを好まなかったようです。「毎回新しいプロセスで実行する」プロパティを明示的に提供していないので、批判のその部分から隔離されていることを願っています。効果の定量化に努めます。もちろん、概念としてのサービスは「悪い考え」ではなく、すべてのアプリがリモートで興味深いことを行うための基本であり、しゃれは意図されていません。それでもわからない場合は、JDocでそれらの使用に関する詳細なガイダンスが提供されます。
BrantApps 2012年

0

私はこの問題を解決するためにsameplコードを書きます

最初のステップは、Applicationクラスを作成することです。

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

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

活動中のコード:

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

}

アクティビティの方向が変わると、変数mTaskがアプリコンテキストから初期化されます。タスクが終了すると、変数はnullに設定され、メモリから削除されます。

私にとってはそれで十分です。


0

以下の例、保持されたフラグメントを使用してバックグラウンドタスクを保持する方法をご覧ください。

public class NetworkRequestFragment extends Fragment {

    // Declare some sort of interface that your AsyncTask will use to communicate with the Activity
    public interface NetworkRequestListener {
        void onRequestStarted();
        void onRequestProgressUpdate(int progress);
        void onRequestFinished(SomeObject result);
    }

    private NetworkTask mTask;
    private NetworkRequestListener mListener;

    private SomeObject mResult;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // Try to use the Activity as a listener
        if (activity instanceof NetworkRequestListener) {
            mListener = (NetworkRequestListener) activity;
        } else {
            // You can decide if you want to mandate that the Activity implements your callback interface
            // in which case you should throw an exception if it doesn't:
            throw new IllegalStateException("Parent activity must implement NetworkRequestListener");
            // or you could just swallow it and allow a state where nobody is listening
        }
    }

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

        // Retain this Fragment so that it will not be destroyed when an orientation
        // change happens and we can keep our AsyncTask running
        setRetainInstance(true);
    }

    /**
     * The Activity can call this when it wants to start the task
     */
    public void startTask(String url) {
        mTask = new NetworkTask(url);
        mTask.execute();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // If the AsyncTask finished when we didn't have a listener we can
        // deliver the result here
        if ((mResult != null) && (mListener != null)) {
            mListener.onRequestFinished(mResult);
            mResult = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // We still have to cancel the task in onDestroy because if the user exits the app or
        // finishes the Activity, we don't want the task to keep running
        // Since we are retaining the Fragment, onDestroy won't be called for an orientation change
        // so this won't affect our ability to keep the task running when the user rotates the device
        if ((mTask != null) && (mTask.getStatus == AsyncTask.Status.RUNNING)) {
            mTask.cancel(true);
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();

        // This is VERY important to avoid a memory leak (because mListener is really a reference to an Activity)
        // When the orientation change occurs, onDetach will be called and since the Activity is being destroyed
        // we don't want to keep any references to it
        // When the Activity is being re-created, onAttach will be called and we will get our listener back
        mListener = null;
    }

    private class NetworkTask extends AsyncTask<String, Integer, SomeObject> {

        @Override
        protected void onPreExecute() {
            if (mListener != null) {
                mListener.onRequestStarted();
            }
        }

        @Override
        protected SomeObject doInBackground(String... urls) {
           // Make the network request
           ...
           // Whenever we want to update our progress:
           publishProgress(progress);
           ...
           return result;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mListener != null) {
                mListener.onRequestProgressUpdate(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(SomeObject result) {
            if (mListener != null) {
                mListener.onRequestFinished(result);
            } else {
                // If the task finishes while the orientation change is happening and while
                // the Fragment is not attached to an Activity, our mListener might be null
                // If you need to make sure that the result eventually gets to the Activity
                // you could save the result here, then in onActivityCreated you can pass it back
                // to the Activity
                mResult = result;
            }
        }

    }
}

-1

見ていこちらを

Timmmmのソリューションに基づくソリューションがあります。

しかし、私はそれを改善しました:

  • これでソリューションは拡張可能になりました-拡張する必要があるのは FragmentAbleToStartTask

  • 複数のタスクを同時に実行し続けることができます。

    そして私の意見では、startActivityForResultと同じくらい簡単で結果を受け取ることができます

  • 実行中のタスクを停止して、特定のタスクが実行されているかどうかを確認することもできます

私の英語でごめんなさい


最初のリンクが壊れています
Tejzeratul 2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.