Android Parçaları. Ekran döndürme veya yapılandırma değişikliği sırasında AsyncTask'ı tutma


86

Bir Akıllı Telefon / Tablet uygulaması üzerinde çalışıyorum, yalnızca bir APK kullanıyorum ve ekran boyutuna bağlı olarak kaynakları gerektiği gibi yüklüyorum, en iyi tasarım seçeneği ACL aracılığıyla Fragments kullanıyor gibiydi.

Bu uygulama şu ana kadar sadece aktivite tabanlı olarak iyi çalışıyordu. Bu, ekran döndürüldüğünde veya iletişimin ortasında bir yapılandırma değişikliği meydana geldiğinde bile çalışmalarını sağlamak için Etkinliklerdeki AsyncTasks ve ProgressDialogs'u nasıl işlediğimin sahte bir sınıfıdır.

Faaliyetin yeniden yaratılmasını önlemek için manifestosu değiştirmeyeceğim, bunu yapmak istemememin birçok nedeni var, ancak esas olarak resmi belgeler tavsiye edilmediğini söylediği ve şimdiye kadar onsuz başardım, bu yüzden lütfen bunu rota.

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

Bu kod iyi çalışıyor, şikayet etmeden yaklaşık 10.000 kullanıcım var, bu yüzden bu mantığı yeni Parça Tabanlı Tasarım'a kopyalamak mantıklı görünüyordu, ama elbette çalışmıyor.

İşte 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
                }
            }
        }
    }
}

Parçadan onRetainNonConfigurationInstance()değil, Etkinlikten çağrılması gerektiğinden kullanamıyorum, aynı şey geçerli getLastNonConfigurationInstance(). Burada cevapsız benzer sorular okudum.

Bu şeyleri parçalar halinde düzgün bir şekilde organize etmek için biraz çalışma gerektirebileceğini anlıyorum, aynı temel tasarım mantığını korumak istiyorum.

Bir yapılandırma değişikliği sırasında AsyncTask'ı korumanın uygun yolu nedir ve hala çalışıyorsa, AsyncTask'ın Fragment için bir iç sınıf olduğunu ve AsyncTask.execute'u çağıran Fragment'in kendisi olduğunu dikkate alarak bir progressDialog gösterin. ()?



AsyncTask'i bir uygulama yaşam döngüsüyle ilişkilendirin..thus etkinlik yeniden oluştuğunda devam edebilir
Fred

Bu konudaki gönderime göz atın: s ile Yapılandırma DeğişiklikleriniFragment
Yönetmek

Yanıtlar:


75

Parçalar aslında bunu çok daha kolay hale getirebilir. Parça örneğinizin yapılandırma değişikliklerinde tutulması için Fragment.setRetainInstance (boolean) yöntemini kullanın . Bunun, belgelerdeki Activity.onRetainnonConfigurationInstance () için önerilen yedek olduğunu unutmayın .

Herhangi bir nedenle tutulan bir parçayı gerçekten kullanmak istemiyorsanız, uygulayabileceğiniz başka yaklaşımlar da vardır. Her parçanın Fragment.getId () tarafından döndürülen benzersiz bir tanımlayıcıya sahip olduğunu unutmayın . Ayrıca Fragment.getActivity (). İsChangingConfigurations () aracılığıyla bir yapılandırma değişikliği için bir parçanın parçalanıp parçalanmadığını da öğrenebilirsiniz . Dolayısıyla, AsyncTask'ınızı (büyük olasılıkla onStop () veya onDestroy ()) durdurmaya karar vereceğiniz noktada, örneğin yapılandırmanın değişip değişmediğini kontrol edebilir ve eğer öyleyse, parçanın tanımlayıcısının altındaki statik bir SparseArray'e yapıştırabilirsiniz. ve sonra onCreate () veya onStart () içinde seyrek dizide bir AsyncTask'in olup olmadığını kontrol edin.


SetRetainInstance'ın yalnızca back stack kullanmıyorsanız olduğunu unutmayın.
Neil

4
AsyncTask'ın, tutulan Fragment'in onCreateView çalışmasından önce sonucunu geri göndermesi mümkün değil mi?
jakk

6
@jakk Aktiviteler, Parçalar, vb. için yaşam döngüsü yöntemleri, ana GUI iş parçacığının mesaj kuyruğu tarafından sırayla çağrılır, bu nedenle görev, bu yaşam döngüsü yöntemleri tamamlanmadan (veya hatta çağrılmadan) önce arka planda eşzamanlı olarak bitirilse bile, onPostExecuteyöntemin yine de sonunda ana iş parçacığının ileti kuyruğu tarafından işlenmeden önce bekleyin.
Alex Lockwood

Bu yaklaşım (RetainInstance = true), her yön için farklı mizanpaj dosyaları yüklemek istiyorsanız işe yaramaz.
Justin

Eşzamansız görevin MainActivity'nin onCreate yöntemi içinde başlatılması, yalnızca eşzamansız görev - "çalışan" parçasının içindeki - açık kullanıcı eylemiyle başlatıldığında işe yarıyor gibi görünüyor. Çünkü ana iş parçacığı ve kullanıcı arayüzü mevcut. Ancak, uygulamayı başlattıktan hemen sonra asenkron görevi başlatmak - düğme tıklaması gibi bir kullanıcı eylemi olmadan - bir istisna sağlar. Bu durumda, asynctask onCreate yönteminde değil MainActivity üzerindeki onStart yönteminde çağrılabilir.
ʕ ᵔᴥᵔ ʔ

66

Aşağıda ayrıntılı olarak açıklanan son derece kapsamlı ve çalışma örneğimi beğeneceğinizi düşünüyorum.

  1. Rotasyon çalışır ve diyalog devam eder.
  2. Geri düğmesine basarak görevi ve iletişim kutusunu iptal edebilirsiniz (bu davranışı istiyorsanız).
  3. Parçaları kullanır.
  4. Aktivitenin altındaki parçanın düzeni, cihaz döndüğünde düzgün şekilde değişir.
  5. Tam bir kaynak kodu indirme ve önceden derlenmiş bir APK var, böylece davranışın istediğiniz gibi olup olmadığını görebilirsiniz.

Düzenle

Brad Larson'un talep ettiği gibi, aşağıdaki bağlantılı çözümün çoğunu yeniden oluşturdum. Ayrıca yayınladığımdan beri işaret edildim AsyncTaskLoader. Aynı sorunlara tamamen uygulanabilir olduğundan emin değilim, ancak yine de kontrol etmelisiniz.

AsyncTaskİlerleme iletişim kutuları ve cihaz döndürme ile kullanma .

Çalışan bir çözüm!

Sonunda çalışacak her şeyi aldım. Kodum aşağıdaki özelliklere sahiptir:

  1. A Fragmentdüzeni yönelimle değişen.
  2. İçinde AsyncTaskbiraz iş yapabileceğiniz bir.
  3. Bir DialogFragmentilerleme çubuğu görevin ilerlemesini (sadece belirsiz bir spinner) gösteren.
  4. Döndürme, görevi kesintiye uğratmadan veya iletişim kutusunu kapatmadan çalışır.
  5. Geri düğmesi iletişim kutusunu kapatır ve görevi iptal eder (bu davranışı oldukça kolay bir şekilde değiştirebilirsiniz).

İşleyiş kombinasyonunun başka hiçbir yerde bulunabileceğini sanmıyorum.

Basit fikir aşağıdakiler gibidir. MainActivityTek bir parça içeren bir sınıf var - MainFragment. MainFragmentyatay ve dikey yönlendirme için farklı düzenlere sahiptir setRetainInstance()ve düzenin değişebilmesi için yanlıştır. Cihaz yönü değiştiğinde, her ikisi de, bu araçlar MainActivityve MainFragmenttamamen tahrip olmuş ve yeniden oluşturulur.

Ayrı olarak , tüm işi yapan MyTask(uzattık AsyncTask) var. Onu saklayamayız MainFragmentçünkü bu imha edilecek ve Google buna benzer herhangi bir şeyi kullanımdan kaldırmıştır setRetainNonInstanceConfiguration(). Bu zaten her zaman mevcut değildir ve en iyi ihtimalle çirkin bir hack'tir. Bunun yerine MyTask, DialogFragmentadı verilen başka bir parçada saklayacağız TaskFragment. Bu fragman olacak olan setRetainInstance()cihaz, bu fragman, yok edilmez döner ve böylece, doğru olarak ayarlanır MyTaskmuhafaza edilir.

Son olarak TaskFragment, bittiğinde kime haber vereceğimizi söylememiz gerekiyor ve bunu, setTargetFragment(<the MainFragment>)onu oluştururken kullanarak yapıyoruz. Aygıt döndürüldüğünde ve MainFragmentyok edildiğinde ve yeni bir örnek oluşturulduğunda, FragmentManageriletişim kutusunu (etiketine göre) bulmak için kullanırız ve yaparız setTargetFragment(<the new MainFragment>). Hepsi bukadar.

Yapmam gereken iki şey daha vardı: birincisi, diyalog kapatıldığında görevi iptal et ve ikincisi, kapat mesajını null olarak ayarla, aksi takdirde cihaz döndürüldüğünde diyalog garip bir şekilde kapatılır.

Kod

Düzenleri listelemeyeceğim, oldukça açıklar ve bunları aşağıdaki proje indirmesinde bulabilirsiniz.

Ana aktivite

Bu oldukça basit. Bu etkinliğe bir geri arama ekledim, böylece görevin ne zaman bittiğini bilebilir, ancak buna ihtiyacınız olmayabilir. Temelde, parça etkinliği geri arama mekanizmasını göstermek istedim çünkü oldukça temiz ve daha önce görmemiş olabilirsiniz.

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

Uzun ama buna değer!

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

Benim görevim

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

Örnek projeyi indirin

İşte kaynak kodu ve APK . Üzgünüm, ADT bir proje yapmama izin vermeden önce destek kitaplığını eklemekte ısrar etti. Eminim kaldırabilirsin.


4
İlerleme çubuğunu korumaktan kaçınırdım DialogFragment, çünkü eski bağlama referansları tutan UI öğelerine sahip. Bunun yerine, AsyncTaskbaşka bir boş parçada saklayıp DialogFragmenthedef olarak belirledim .
SD

Cihaz döndürülüp onCreateView()tekrar arandığında bu referanslar silinmeyecek mi? En mProgressBarazından yenisiyle eskisinin üzerine yazılacaktır.
Timmmm

Açıkça değil, ama bundan oldukça eminim. Ekleyebilir mProgressBar = null;içinde onDestroyView()ekstra emin olmak istiyorsanız. Tekillik yöntemi iyi bir fikir olabilir, ancak kod karmaşıklığını daha da artıracaktır!
Timmmm

1
Asynctask üzerinde tuttuğunuz referans, progresif diyalog parçası, değil mi? Öyleyse 2 soru: 1- İlerleme diyalogunu çağıran gerçek parçayı değiştirmek istersem ne olur; 2- Params'ı asynctask'a geçirmek istersem ne olur? Saygılarımızla,
Maxrunner

1
@Maxrunner, parametreleri başarılı sayılabilmek için en kolay şey hareket etmek muhtemelen mTask.execute()için MainFragment.onClick(). Alternatif olarak, parametrelerin aktarılmasına izin verebilir setTask()veya hatta MyTaskkendi içinde depolayabilirsiniz . İlk sorunuzun ne anlama geldiğinden tam olarak emin değilim, ama belki kullanmak istersiniz TaskFragment.getTargetFragment()? A kullanarak çalışacağından oldukça eminim ViewPager. Ancak ViewPagersçok iyi anlaşılmamış veya belgelenmemiş, bu yüzden iyi şanslar! Parçanızın ilk kez görünene kadar oluşturulmadığını unutmayın.
Timmmm

16

Yakın zamanda , tutulan e- postalar kullanılarak yapılandırma değişikliklerinin nasıl ele alınacağını açıklayan bir makale yayınladımFragment . AsyncTaskBir rotasyon değişimini güzelce tutma sorununu çözer .

TL; DR kullanım barındırmak için olan AsyncTaskiçinde bir Fragmentçağrı setRetainInstance(true)üzerine Fragmentve rapor AsyncTask'ın ilerleme / sonuçlar' s için geri Activity(ya da hedefi Fragmentkorudu aracılığıyla size @Timmmm tarafından açıklanan yaklaşım kullanmayı seçerseniz,) Fragment.


5
İç içe geçmiş Parçalar hakkında nasıl hareket edersiniz? Bir AsyncTask gibi, başka bir Fragment (Sekme) içindeki bir RetainedFragment'ten başlatıldı.
Rekin

Parça zaten tutulmuşsa, o zaman neden eşzamansız görevi tutulan parçanın içinden yürütmeyesiniz? Zaten tutulmuşsa, zaman uyumsuz görev, bir yapılandırma değişikliği meydana gelse bile ona geri rapor verebilir.
Alex Lockwood

@AlexLockwood Blog için teşekkürler. Halletmek zorunda kalmak yerine onAttachve onDetachiçeride olması daha iyi olacak mı TaskFragment, getActivityne zaman bir geri aramayı başlatmamız gerektiğinde ararız . (İnstaceof'u kontrol ederek TaskCallbacks)
Cheok Yan Cheng

1
Her iki şekilde de yapabilirsiniz. Bunu yeni yaptım onAttach()ve onDetach()böylece TaskCallbacksher kullanmak istediğimde aktiviteyi sürekli olarak yayınlamaktan kaçınabilirdim .
Alex Lockwood

1
@AlexLockwood Uygulamam tek bir etkinliği (birden çok parça tasarımı) izliyorsa, UI parçalarımın her biri için ayrı bir görev parçası olmalı mıyım? Dolayısıyla, temelde her bir görev parçasının yaşam döngüsü, hedef parçası tarafından yönetilecek ve etkinlikle hiçbir iletişim olmayacaktır.
Manas Bajaj

13

İlk önerim, iç AsyncTasks'ten kaçınmaktır , bu konuda sorduğum bir soruyu ve cevapları okuyabilirsiniz : Android: AsyncTask önerileri: özel sınıf mı yoksa genel sınıf mı?

Bundan sonra içsel olmayanları kullanmaya başladım ve ... şimdi çok fazla fayda görüyorum.

İkincisi, Sınıfta çalışan AsyncTask'in bir referansını saklayın Application- http://developer.android.com/reference/android/app/Application.html

Bir AsyncTask'ı her başlattığınızda, Uygulamada ayarlayın ve bittiğinde null olarak ayarlayın.

Bir parça / etkinlik başladığında, herhangi bir AsyncTask'ın çalışıp çalışmadığını kontrol edebilir (Uygulamada boş olup olmadığını kontrol ederek) ve ardından içindeki referansı istediğiniz her şeye ayarlayabilirsiniz (etkinlik, parça vb. Böylece geri arama yapabilirsiniz).

Bu, sorununuzu çözecektir: Belirli herhangi bir zamanda yalnızca 1 AsyncTask çalışıyorsanız, basit bir referans ekleyebilirsiniz:

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

Aksi takdirde, Aplikasyonda bunlara referans olan bir HashMap bulundurun.

İlerleme iletişim kutusu tamamen aynı prensibi izleyebilir.


2
AsyncTask'ın yaşam döngüsünü Ebeveynine bağladığınız sürece (AsyncTask'ı Activity / Fragment'in iç sınıfı olarak tanımlayarak), AsyncTask'ınızı ebeveyninin yaşam döngüsü rekreasyonundan kurtarmanın oldukça zor olduğunu kabul ettim, ancak çözüm, çok zor görünüyor.
yorkw

Soru şu ki .. daha iyi bir çözümün var mı?
neteinstein

1
Burada @yorkw ile hemfikir olmak zorundayım, bu çözüm aws bana bir süre önce parçaları kullanmadan bu sorunla uğraşırken sundu (Aktivite tabanlı uygulama). Şu soru: stackoverflow.com/questions/2620917/… aynı yanıtı veriyor ve "Uygulama örneğinin kendi yaşam döngüsü vardır - işletim sistemi tarafından da kapatılabilir, bu nedenle bu çözüm bir yeniden üretilmesi zor hata "
blindstuff

1
Yine de @yorkw'un dediği gibi daha az "hacky" olan başka bir yol görmüyorum. Birkaç uygulamada kullanıyorum ve olası sorunlara biraz dikkat ederek hepsi harika çalışıyor.
neteinstein

Belki @hackbod çözümü size daha çok uyuyor.
neteinstein

4

Bunun için AsyncTaskLoaders kullanma yöntemini buldum. Kullanımı oldukça kolaydır ve daha az ek IMO gerektirir ..

Temel olarak şu şekilde bir AsyncTaskLoader oluşturursunuz:

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

Ardından, bir düğmeye tıklandığında yukarıdaki AsyncTaskLoader'ı kullanan etkinliğinizde:

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

    }
}

Bu, yönlendirme değişikliklerini iyi hallediyor gibi görünüyor ve arka plan göreviniz dönüş sırasında devam edecek.

Dikkat edilmesi gereken birkaç nokta:

  1. OnCreate içinde asynctaskloader'a yeniden bağlanırsanız, önceki sonuçla birlikte onLoadFinished () içinde geri çağrılırsınız (isteğin tamamlandığı zaten size söylenmiş olsa bile). Bu aslında çoğu zaman iyi bir davranıştır, ancak bazen başa çıkmak zor olabilir. Bunu halletmenin birçok yolu olduğunu hayal etsem de, onLoadFinished'de loader.abandon () adını verdim. Ardından, yalnızca önceden terk edilmemişse yükleyiciye yeniden bağlanmak için check-in onCreate'i ekledim. Elde edilen verilere tekrar ihtiyacınız olursa, bunu yapmak istemezsiniz. Çoğu durumda verileri istersiniz.

Burada http çağrıları için bunu kullanmakla ilgili daha fazla ayrıntı var


Tabii o getSupportLoaderManager().getLoader(0);(yani id, 0 ile yükleyici, henüz var olmadığından) null adlı döndürmez?
EmmanuelMess

1
Evet, bir yapılandırma değişikliği, etkinliğin yükleyici devam ederken yeniden başlamasına neden olmadıkça boş olacaktır .. Bu yüzden boş kontrolü yaptım.
Matt Wolfe

3

Ağırlıklı olarak Marshmallow'a dayanan, AsyncTaskancak aşağıdaki gibi ek işlevlere sahip çok küçük bir açık kaynaklı arka plan görev kitaplığı oluşturdum :

  1. Yapılandırma değişikliklerinde görevleri otomatik olarak tutma;
  2. UI geri çağırma (dinleyiciler);
  3. Cihaz döndüğünde görevi yeniden başlatmaz veya iptal etmez (Yükleyicilerin yaptığı gibi);

Kütüphane dahili olarak Fragmentherhangi bir kullanıcı arabirimi olmadan kullanır ve bu, konfigürasyon değişiklikleri ( setRetainInstance(true)) ile korunur .

Bunu GitHub'da bulabilirsiniz: https://github.com/NeoTech-Software/Android-Retainable-Tasks

En temel örnek (sürüm 0.2.0):

Bu örnek, çok sınırlı miktarda kod kullanarak görevi tam olarak korur.

Görev:

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

Aktivite:

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

Benim yaklaşımım, delegasyon tasarım modelini kullanmaktır, genel olarak, AsyncTask'tan (delege eden) BusinessDAO'ya (temsilci) gerçek iş mantığını AysncTask.doInBackground () yönteminizde izole edebiliriz (internetten veya veritabanından veya herhangi bir şekilde veri okuyun) , fiili görevi BusinessDAO'ya devredin, ardından BusinessDAO'da tekil işlem mekanizmasını uygulayın, böylece BusinessDAO.doSomething () 'e yapılan çoklu çağrı her seferinde çalışan ve görev sonucunu bekleyen bir gerçek görevi tetikler. Fikir, temsilci (yani AsyncTask) yerine yapılandırma değişikliği sırasında temsilciyi (yani BusinessDAO) korumaktır.

  1. Kendi Uygulamamızı Oluşturun / Uygulayın, amaç burada BusinessDAO'yu oluşturmak / başlatmaktır, böylece BusinessDAO'muzun yaşam döngüsü etkinlik kapsamlı değil uygulama kapsamlıdır, MyApplication'ı kullanmak için AndroidManifest.xml'yi değiştirmeniz gerektiğini unutmayın:

    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. Mevcut Activity / Fragement çoğunlukla değişmedi, hala AsyncTask'i bir iç sınıf olarak uyguluyor ve Activity / Fragement'tan AsyncTask.execute () 'yi içeriyor, artık fark AsyncTask'ın asıl görevi BusinessDAO'ya devredeceği, bu nedenle yapılandırma değişikliği sırasında ikinci bir AsyncTask başlatılacak ve çalıştırılacak ve BusinessDAO.doSomething () 'i ikinci kez çağıracak, ancak BusinessDAO.doSomething ()' e yapılan ikinci çağrı yeni bir çalışan görevi tetiklemeyecek, bunun yerine mevcut çalışan görevin bitmesini bekleyecektir:

    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 içinde, tekli süreç mekanizmasını uygulayın, örneğin:

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

Bunun işe yarayıp yaramayacağından% 100 emin değilim, ayrıca örnek kod pasajı sözde kod olarak düşünülmelidir. Size sadece tasarım seviyesinden bir ipucu vermeye çalışıyorum. Herhangi bir geri bildirim veya öneri memnuniyetle karşılanır ve takdir edilir.


Çok güzel bir çözüm gibi görünüyor. Bunu yaklaşık 2 buçuk yıl önce yanıtladığınızdan beri test ettiniz mi ?! Çalıştığından emin olmadığımı söylüyorsun, sorun ben de öyle değil !! Bu sorun için iyi test edilmiş bir çözüm için kilitliyorum. Önerin var mı?
Alireza A. Ahmadi

1

AsyncTask'i statik bir alan yapabilirsiniz. Bir bağlama ihtiyacınız varsa, uygulama bağlamınızı göndermelisiniz. Bu, bellek sızıntılarını önleyecektir, aksi takdirde tüm faaliyetinize bir referans tutarsınız.


1

Herhangi biri bu iş parçacığına giden yolu bulursa, temiz bir yaklaşım buldum, Async görevini bir app.Service(START_STICKY ile başlayan) çalıştırıp ardından hizmetin (ve dolayısıyla eşzamansız görevin) hala olup olmadığını öğrenmek için çalışan hizmetler üzerinde yineleme yapmaktı. koşuyor;

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

Eğer öyleyse, DialogFragment(veya her neyse) tekrar ekleyin ve değilse iletişim kutusunun kapatıldığından emin olun.

Bu özellikle, v4.support.*kütüphaneleri kullanıyorsanız (yazarken) setRetainInstanceyöntemle ilgili sorunları bildiklerinden ve sayfalamayı görüntülediklerinden önemlidir. Ayrıca, örneği alıkoymayarak farklı bir kaynak kümesi kullanarak (yani yeni yön için farklı bir görünüm düzeni) etkinliğinizi yeniden oluşturabilirsiniz.


Bir AsyncTask'ı korumak için bir Hizmeti çalıştırmak fazla değil mi? Bir Hizmet kendi sürecinde çalışır ve bu ek maliyet olmadan gelmez.
WeNeigh

İlginç Vinay. Uygulamanın daha fazla kaynak ağırlıklı olduğunu fark etmedim (şu anda oldukça hafif). Ne buldun Bir hizmeti, UI durumundan bağımsız olarak sistemin bazı ağır kaldırma veya G / Ç işlemlerine devam etmesine izin vermek için öngörülebilir bir ortam olarak görüyorum. Bir şeyin ne zaman tamamlandığını görmek için servisle iletişim kurmak 'doğru' göründü. Yürütmeye başladığım hizmetler, görevin tamamlanmasında biraz iş durur, bu nedenle genellikle 10-30'larda hayatta kalır.
BrantApps

Commonsware'in buradaki cevabı , Hizmetlerin kötü bir fikir olduğunu öne sürüyor. Şimdi AsyncTaskLoaders'ı düşünüyorum, ancak kendi sorunlarıyla geliyor gibi görünüyorlar (esneklik, sadece veri yükleme vb. İçin)
WeNeigh

1
Anlıyorum. Dikkat çekici bir şekilde, bağlandığınız bu hizmet açıkça kendi sürecinde çalışmak üzere ayarlanıyordu. Usta, bu kalıbın sıklıkla kullanılmasından hoşlanmadı. Açıkça bu "her seferinde yeni bir işlemde çalıştırma" özelliklerini sağlamadım, bu yüzden umarım eleştirinin bu kısmından yalıtılmışım. Etkileri ölçmeye çalışacağım. Bir kavram olarak hizmetler elbette 'kötü fikirler' değildir ve her uygulamanın uzaktan ilginç şeyler yapması için temeldir, hiçbir şekilde amaçlanmamıştır. Hala emin değilseniz, JDoc'leri kullanımları hakkında daha fazla rehberlik sağlar.
BrantApps

0

Bu sorunu çözmek için aynıepl kodu yazıyorum

İlk adım, uygulama sınıfı yapmaktır:

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'de

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

Etkinlikteki kod:

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

}

Etkinlik yönü değiştiğinde, değişken mTask uygulama bağlamından başlatılır. Görev bittiğinde değişken null olarak ayarlanır ve bellekten kaldırılır.

Benim için yeterli.


0

Arka plan görevini korumak için tutulan parçanın nasıl kullanılacağıyla ilgili aşağıdaki örneğe bakın:

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

Buraya bir göz atın .

Timmmm'in çözümüne dayalı bir çözüm var .

Ama ben onu geliştirdim:

  • Artık çözüm genişletilebilir - yalnızca genişletmeniz gerekir FragmentAbleToStartTask

  • Aynı anda birkaç görevi çalıştırmaya devam edebilirsiniz.

    Ve bence startActivityForResult ve sonuç almak kadar kolay

  • Ayrıca çalışan bir görevi durdurabilir ve belirli bir görevin çalışıp çalışmadığını kontrol edebilirsiniz.

İngilizcem için üzgünüm


ilk bağlantı koptu
Tejzeratul
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.