Android-Fragmente. Beibehalten einer AsyncTask während der Bildschirmdrehung oder Konfigurationsänderung


86

Ich arbeite an einer Smartphone / Tablet-App, verwende nur eine APK und lade je nach Bildschirmgröße die erforderlichen Ressourcen. Die beste Wahl für das Design schien die Verwendung von Fragmenten über die ACL zu sein.

Diese App hat bisher einwandfrei funktioniert und ist nur aktivitätsbasiert. Dies ist eine Scheinklasse, wie ich mit AsyncTasks und ProgressDialogs in den Aktivitäten umgehe, damit sie auch dann funktionieren, wenn der Bildschirm gedreht wird oder während der Kommunikation eine Konfigurationsänderung auftritt.

Ich werde das Manifest nicht ändern, um eine Neuerstellung der Aktivität zu vermeiden. Es gibt viele Gründe, warum ich es nicht tun möchte, aber hauptsächlich, weil die offiziellen Dokumente sagen, dass es nicht empfohlen wird und ich es bis jetzt ohne es geschafft habe, also empfehlen Sie es bitte nicht Route.

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

Dieser Code funktioniert einwandfrei, ich habe ungefähr 10.000 Benutzer ohne Beanstandung, daher schien es logisch, diese Logik einfach in das neue fragmentbasierte Design zu kopieren, aber natürlich funktioniert sie nicht.

Hier ist das 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
                }
            }
        }
    }
}

Ich kann es nicht verwenden, onRetainNonConfigurationInstance()da es von der Aktivität und nicht vom Fragment aufgerufen werden muss, das gleiche gilt für getLastNonConfigurationInstance(). Ich habe hier einige ähnliche Fragen ohne Antwort gelesen.

Ich verstehe, dass es einige Umarbeitungen erfordern könnte, um dieses Zeug richtig in Fragmenten zu organisieren. Abgesehen davon möchte ich die gleiche grundlegende Designlogik beibehalten.

Was wäre der richtige Weg, um die AsyncTask während einer Konfigurationsänderung beizubehalten und wenn sie noch ausgeführt wird, einen progressDialog anzuzeigen, wobei zu berücksichtigen ist, dass die AsyncTask eine innere Klasse des Fragments ist und das Fragment selbst die AsyncTask.execute aufruft ()?


1
Vielleicht kann der Thread zum Umgang mit Konfigurationsänderungen mit AsyncTask helfen
am

Verknüpfen Sie AsyncTask mit einem Anwendungslebenszyklus. Dies kann fortgesetzt werden, wenn die Aktivität neu erstellt wird
Fred Grott

Schauen Sie sich meinen Beitrag zu diesem Thema an: Umgang mit Konfigurationsänderungen mit Fragments
Alex Lockwood

Antworten:


75

Fragmente können dies tatsächlich viel einfacher machen. Verwenden Sie einfach die Methode Fragment.setRetainInstance (boolean) , um Ihre Fragmentinstanz über Konfigurationsänderungen hinweg beizubehalten. Beachten Sie, dass dies der empfohlene Ersatz für Activity.onRetainnonConfigurationInstance () in den Dokumenten ist.

Wenn Sie aus irgendeinem Grund wirklich kein beibehaltenes Fragment verwenden möchten, können Sie andere Ansätze wählen. Beachten Sie, dass jedes Fragment eine eindeutige Kennung hat, die von Fragment.getId () zurückgegeben wird . Über Fragment.getActivity (). IsChangingConfigurations () können Sie auch herausfinden, ob ein Fragment für eine Konfigurationsänderung abgerissen wird . An dem Punkt, an dem Sie Ihre AsyncTask stoppen möchten (höchstwahrscheinlich in onStop () oder onDestroy ()), können Sie beispielsweise überprüfen, ob sich die Konfiguration ändert, und sie in einem statischen SparseArray unter der Kennung des Fragments speichern. Überprüfen Sie dann in onCreate () oder onStart (), ob im spärlichen Array eine AsyncTask verfügbar ist.


Beachten Sie, dass setRetainInstance nur verwendet wird, wenn Sie keinen Backstack verwenden.
Neil

4
Ist es nicht möglich, dass die AsyncTask ihr Ergebnis zurücksendet, bevor die onCreateView des beibehaltenen Fragments ausgeführt wird?
Jakk

6
@jakk Die Lebenszyklusmethoden für Aktivitäten, Fragmente usw. werden nacheinander von der Nachrichtenwarteschlange des Haupt-GUI-Threads aufgerufen. Selbst wenn die Aufgabe gleichzeitig im Hintergrund beendet wurde, bevor diese Lebenszyklusmethoden abgeschlossen (oder sogar aufgerufen) wurden, muss die onPostExecuteMethode dies dennoch tun Warten Sie, bevor Sie schließlich von der Nachrichtenwarteschlange des Hauptthreads verarbeitet werden.
Alex Lockwood

Dieser Ansatz (RetainInstance = true) funktioniert nicht, wenn Sie für jede Ausrichtung unterschiedliche Layoutdateien laden möchten.
Justin

Das Starten der Asynctask innerhalb der onCreate-Methode von MainActivity scheint nur zu funktionieren, wenn die Asynctask - innerhalb des "Worker" -Fragments - durch explizite Benutzeraktion gestartet wird. Weil der Haupt-Thread und die Benutzeroberfläche verfügbar sind. Das Starten der Asynctask direkt nach dem Starten der App - jedoch ohne eine Benutzeraktion wie einen Klick auf eine Schaltfläche - gibt eine Ausnahme. In diesem Fall kann die Asynctask in der onStart-Methode in der MainActivity und nicht in der onCreate-Methode aufgerufen werden.
ᵔᴥᵔ ʔ

66

Ich denke, Sie werden mein äußerst umfassendes und funktionierendes Beispiel genießen, das unten aufgeführt ist.

  1. Die Drehung funktioniert und der Dialog bleibt erhalten.
  2. Sie können die Aufgabe und den Dialog abbrechen, indem Sie die Zurück-Taste drücken (wenn Sie dieses Verhalten wünschen).
  3. Es werden Fragmente verwendet.
  4. Das Layout des Fragments unter der Aktivität ändert sich ordnungsgemäß, wenn sich das Gerät dreht.
  5. Es gibt einen vollständigen Quellcode-Download und eine vorkompilierte APK, damit Sie sehen können, ob das Verhalten Ihren Wünschen entspricht .

Bearbeiten

Wie von Brad Larson angefordert, habe ich den größten Teil der unten aufgeführten verknüpften Lösung reproduziert. Auch seit ich es gepostet habe, wurde ich darauf hingewiesen AsyncTaskLoader. Ich bin mir nicht sicher, ob es für dieselben Probleme vollständig anwendbar ist, aber Sie sollten es trotzdem überprüfen.

Verwenden AsyncTaskmit Fortschrittsdialogen und Gerätedrehung.

Eine funktionierende Lösung!

Ich habe endlich alles zum Arbeiten. Mein Code hat folgende Funktionen:

  1. A, Fragmentdessen Layout sich mit der Ausrichtung ändert.
  2. Ein, AsyncTaskin dem Sie etwas arbeiten können.
  3. A, DialogFragmentdas den Fortschritt der Aufgabe in einem Fortschrittsbalken anzeigt (nicht nur ein unbestimmter Spinner).
  4. Die Drehung funktioniert, ohne die Aufgabe zu unterbrechen oder den Dialog zu schließen.
  5. Die Schaltfläche "Zurück" schließt den Dialog und bricht die Aufgabe ab (Sie können dieses Verhalten jedoch recht einfach ändern).

Ich glaube nicht, dass eine Kombination aus Arbeitsfähigkeit irgendwo anders zu finden ist.

Die Grundidee ist wie folgt. Es gibt eine MainActivityKlasse, die ein einzelnes Fragment enthält - MainFragment. MainFragmenthat unterschiedliche Layouts für die horizontale und vertikale Ausrichtung und setRetainInstance()ist falsch, damit sich das Layout ändern kann. Dies bedeutet , dass , wenn die Vorrichtung Orientierung geändert wird, die beide MainActivityund MainFragmentwerden vollständig zerstört und neu erstellt.

Separat haben wir MyTask(erweitert von AsyncTask) die die ganze Arbeit erledigt. Wir können es nicht speichern, MainFragmentda dies zerstört wird und Google mit so etwas wie veraltet ist setRetainNonInstanceConfiguration(). Das ist sowieso nicht immer verfügbar und bestenfalls ein hässlicher Hack. Stattdessen werden wir MyTaskin einem anderen Fragment speichern , einem DialogFragmentaufgerufenen TaskFragment. Dieses Fragment wird hat setRetainInstance()auf true gesetzt, so dass das Gerät dieses Fragment dreht nicht zerstört wird, und MyTaskwird beibehalten.

Schließlich müssen TaskFragmentwir demjenigen mitteilen , der informiert werden soll, wenn es fertig ist, und das tun wir, setTargetFragment(<the MainFragment>)wenn wir es erstellen. Wenn das Gerät gedreht und MainFragmentzerstört wird und eine neue Instanz erstellt wird, verwenden wir das FragmentManager, um den Dialog (basierend auf seinem Tag) zu finden und zu tun setTargetFragment(<the new MainFragment>). Das wars so ziemlich.

Ich musste noch zwei weitere Dinge tun: erstens die Aufgabe abbrechen, wenn der Dialog geschlossen wird, und zweitens die Entlassungsnachricht auf null setzen, andernfalls wird der Dialog seltsamerweise geschlossen, wenn das Gerät gedreht wird.

Der Code

Ich werde die Layouts nicht auflisten, sie sind ziemlich offensichtlich und Sie finden sie im Projekt-Download unten.

Hauptaktivität

Das ist ziemlich einfach. Ich habe dieser Aktivität einen Rückruf hinzugefügt, damit sie weiß, wann die Aufgabe abgeschlossen ist, aber das brauchen Sie möglicherweise nicht. Hauptsächlich wollte ich nur den Rückrufmechanismus für Fragmentaktivitäten zeigen, weil er ziemlich ordentlich ist und Sie ihn vielleicht vorher noch nicht gesehen haben.

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

Es ist lang, aber es lohnt sich!

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

Meine Aufgabe

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

Laden Sie das Beispielprojekt herunter

Hier ist der Quellcode und die APK . Entschuldigung, das ADT bestand darauf, die Support-Bibliothek hinzuzufügen, bevor ich ein Projekt erstellen konnte. Ich bin sicher, Sie können es entfernen.


4
Ich würde es vermeiden, den Fortschrittsbalken beizubehalten DialogFragment, da er UI-Elemente enthält, die Verweise auf den alten Kontext enthalten. Stattdessen würde ich AsyncTaskin einem anderen leeren Fragment speichern und DialogFragmentals Ziel festlegen .
SD

Werden diese Referenzen nicht gelöscht, wenn das Gerät gedreht und onCreateView()erneut aufgerufen wird? Das alte mProgressBarwird mindestens mit einem neuen überschrieben.
Timmmm

Nicht explizit, aber ich bin mir ziemlich sicher. Sie können hinzufügen , mProgressBar = null;in , onDestroyView()wenn Sie zusätzliche sicher sein wollen. Die Methode von Singularity mag eine gute Idee sein, aber sie erhöht die Codekomplexität noch mehr!
Timmmm

1
Die Referenz, die Sie in der Asynctask haben, ist das Progressdialog-Fragment, oder? Also 2 Fragen: 1- Was ist, wenn ich das reale Fragment ändern möchte, das den Fortschrittsdialog aufruft? 2- Was ist, wenn ich Parameter an die Asynctask übergeben möchte? Grüße,
Maxrunner

1
@Maxrunner, Für Parameter übergeben, die einfachste Sache ist wahrscheinlich zu bewegen mTask.execute()zu MainFragment.onClick(). Alternativ können Sie die Übergabe von Parametern zulassen setTask()oder diese sogar in MyTasksich selbst speichern . Ich bin mir nicht ganz sicher, was Ihre erste Frage bedeutet, aber vielleicht möchten Sie sie verwenden TaskFragment.getTargetFragment()? Ich bin mir ziemlich sicher, dass es mit a funktionieren wird ViewPager. Aber ViewPagersnicht sehr gut verstanden oder dokumentiert, also viel Glück! Denken Sie daran, dass Ihr Fragment erst erstellt wird, wenn es beim ersten Mal sichtbar ist.
Timmmm

16

Ich habe kürzlich einen Artikel veröffentlicht, in dem beschrieben wird, wie Konfigurationsänderungen mit beibehaltenen Fragments behandelt werden. Es löst das Problem, AsyncTaskeine Änderung über eine Rotation hinweg gut beizubehalten.

Der TL; DR ist die Verwendung Host Ihr AsyncTaskinnen ein Fragment, Anruf setRetainInstance(true)auf das Fragment, und berichtet über die AsyncTask‚Fortschritte / Ergebnisse geht es zurück Activity(oder das Ziel Fragment, wenn Sie den Ansatz von @Timmmm beschrieben verwenden) durch die beibehalten Fragment.


5
Wie würden Sie mit den verschachtelten Fragmenten umgehen? Wie eine AsyncTask, die von einem RetainedFragment in einem anderen Fragment (Tab) gestartet wurde.
Rekin

Wenn das Fragment bereits beibehalten wurde, warum nicht einfach die asynchrone Aufgabe innerhalb dieses beibehaltenen Fragments ausführen? Wenn es bereits beibehalten wurde, kann die asynchrone Task auch dann eine Rückmeldung an sie senden, wenn eine Konfigurationsänderung auftritt.
Alex Lockwood

@AlexLockwood Danke für den Blog. Anstatt damit umgehen zu müssen onAttachund onDetach, ist es besser, wenn TaskFragmentwir drinnen sind , rufen wir einfach an, getActivitywann immer wir einen Rückruf auslösen müssen. (Durch Überprüfung instaceof TaskCallbacks)
Cheok Yan Cheng

1
Sie können es so oder so tun. Ich habe es einfach gemacht onAttach()und onDetach()so konnte ich vermeiden, die Aktivität TaskCallbacksjedes Mal, wenn ich sie verwenden wollte, ständig zu übertragen.
Alex Lockwood

1
@AlexLockwood Wenn meine Anwendung einer einzelnen Aktivität folgt - Entwurf mehrerer Fragmente, sollte ich für jedes meiner UI-Fragmente ein separates Aufgabenfragment haben? Grundsätzlich wird der Lebenszyklus jedes Aufgabenfragments von seinem Zielfragment verwaltet, und es findet keine Kommunikation mit der Aktivität statt.
Manas Bajaj

13

Mein erster Vorschlag ist, innere AsyncTasks zu vermeiden . Sie können eine Frage lesen, die ich dazu gestellt habe, und die Antworten: Android: AsyncTask-Empfehlungen: Privatklasse oder öffentliche Klasse?

Danach fing ich an, nicht-inner zu verwenden und ... jetzt sehe ich VIELE Vorteile.

Die zweite Möglichkeit besteht darin, eine Referenz Ihrer ausgeführten AsyncTask in der ApplicationKlasse zu speichern - http://developer.android.com/reference/android/app/Application.html

Wenn Sie eine AsyncTask starten, setzen Sie sie in der Anwendung und setzen Sie sie nach Abschluss auf null.

Wenn ein Fragment / eine Aktivität gestartet wird, können Sie überprüfen, ob eine AsyncTask ausgeführt wird (indem Sie überprüfen, ob sie in der Anwendung null ist oder nicht), und dann die Referenz im Inneren auf das festlegen, was Sie möchten (Aktivität, Fragment usw., damit Sie Rückrufe durchführen können).

Dies löst Ihr Problem: Wenn zu einem bestimmten Zeitpunkt nur 1 AsyncTask ausgeführt wird, können Sie eine einfache Referenz hinzufügen:

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

Andernfalls haben Sie in der Anwendung eine HashMap mit Verweisen darauf.

Der Fortschrittsdialog kann genau dem gleichen Prinzip folgen.


2
Ich stimmte zu, solange Sie den Lebenszyklus von AsyncTask an das übergeordnete Element binden (indem Sie AsyncTask als innere Klasse von Aktivität / Fragment definieren), ist es ziemlich schwierig, Ihr AsyncTask aus der Lebenszykluswiederherstellung des Elternteils zu entkommen, aber ich mag Ihren nicht Lösung, es sieht einfach so hacky aus.
Yorkw

Die Frage ist ... haben Sie eine bessere Lösung?
Neteinstein

1
Ich muss hier @yorkw zustimmen, diese Lösung wurde mir vor einiger Zeit vorgestellt, als ich mich mit diesem Problem befasste, ohne Fragmente zu verwenden (aktivitätsbasierte App). Diese Frage: stackoverflow.com/questions/2620917/… hat dieselbe Antwort, und ich stimme einem der Kommentare zu, der besagt: "Die Anwendungsinstanz hat ihren eigenen Lebenszyklus - sie kann auch vom Betriebssystem beendet werden, sodass diese Lösung möglicherweise a schwer zu reproduzierender Fehler "
Blindstuff

1
Trotzdem sehe ich keinen anderen Weg, der weniger "hacky" ist, wie @yorkw sagte. Ich habe es in mehreren Apps verwendet und mit etwas Aufmerksamkeit für die möglichen Probleme funktioniert alles großartig.
Neteinstein

Vielleicht passt die @ hackbod-Lösung besser zu Ihnen.
Neteinstein

4

Ich habe mir eine Methode ausgedacht, um AsyncTaskLoaders dafür zu verwenden. Es ist ziemlich einfach zu bedienen und erfordert weniger Overhead IMO ..

Grundsätzlich erstellen Sie einen AsyncTaskLoader wie folgt:

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

Dann in Ihrer Aktivität, die den obigen AsyncTaskLoader verwendet, wenn auf eine Schaltfläche geklickt wird:

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

    }
}

Dies scheint Orientierungsänderungen gut zu handhaben und Ihre Hintergrundaufgabe wird während der Rotation fortgesetzt.

Ein paar Dinge zu beachten:

  1. Wenn Sie in onCreate erneut eine Verbindung zum asynctaskloader herstellen, werden Sie mit dem vorherigen Ergebnis in onLoadFinished () zurückgerufen (auch wenn Ihnen bereits mitgeteilt wurde, dass die Anforderung abgeschlossen ist). Dies ist eigentlich meistens ein gutes Verhalten, aber manchmal kann es schwierig sein, damit umzugehen. Obwohl ich mir vorstelle, dass es viele Möglichkeiten gibt, damit umzugehen, habe ich in onLoadFinished loader.abandon () genannt. Dann habe ich check in onCreate hinzugefügt, um den Loader nur dann wieder anzuschließen, wenn er nicht bereits verlassen wurde. Wenn Sie die resultierenden Daten erneut benötigen, möchten Sie dies nicht tun. In den meisten Fällen möchten Sie die Daten.

Ich habe weitere Details zur Verwendung für http-Anrufe hier


Sicher, dass dies getSupportLoaderManager().getLoader(0);nicht null zurückgibt (weil der Loader mit dieser ID 0 noch nicht existiert)?
EmmanuelMess

1
Ja, es wird null sein, es sei denn, eine Konfigurationsänderung hat dazu geführt, dass die Aktivität neu gestartet wurde, während der Loader ausgeführt wurde. Deshalb habe ich nach null gesucht.
Matt Wolfe

3

Ich habe eine sehr kleine Open-Source-Hintergrundaufgabenbibliothek erstellt, die stark auf dem Marshmallow basiert, AsyncTaskaber zusätzliche Funktionen wie:

  1. Automatische Beibehaltung von Aufgaben über Konfigurationsänderungen hinweg;
  2. UI-Rückruf (Listener);
  3. Startet oder bricht die Aufgabe nicht neu, wenn sich das Gerät dreht (wie es Loader tun würden).

Die Bibliothek verwendet intern eine FragmentBenutzeroberfläche ohne Benutzeroberfläche, die bei Konfigurationsänderungen beibehalten wird ( setRetainInstance(true)).

Sie finden es auf GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks

Das einfachste Beispiel (Version 0.2.0):

In diesem Beispiel wird die Aufgabe mit einer sehr begrenzten Menge an Code vollständig beibehalten.

Aufgabe:

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

Aktivität:

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

Mein Ansatz ist die Verwendung des Delegationsentwurfsmusters. Im Allgemeinen können wir die tatsächliche Geschäftslogik (Daten aus dem Internet oder der Datenbank oder was auch immer lesen) von AsyncTask (dem Delegator) bis BusinessDAO (dem Delegaten) in Ihrer AysncTask.doInBackground () -Methode isolieren Delegieren Sie die eigentliche Aufgabe an BusinessDAO und implementieren Sie dann einen Singleton-Prozessmechanismus in BusinessDAO, sodass bei mehreren Aufrufen von BusinessDAO.doSomething () nur eine tatsächliche Aufgabe ausgelöst wird, die jedes Mal ausgeführt wird und auf das Ergebnis der Aufgabe wartet. Die Idee ist, den Delegaten (dh BusinessDAO) während der Konfigurationsänderung anstelle des Delegators (dh AsyncTask) beizubehalten.

  1. Erstellen / Implementieren unserer eigenen Anwendung. Der Zweck besteht darin, BusinessDAO hier zu erstellen / zu initialisieren, sodass der Lebenszyklus unseres BusinessDAO anwendungsbezogen und nicht aktivitätsbezogen ist. Beachten Sie, dass Sie AndroidManifest.xml ändern müssen, um MyApplication verwenden zu können:

    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. Unsere vorhandenen Aktivitäten / Fragments sind größtenteils unverändert. Sie implementieren AsyncTask weiterhin als innere Klasse und beziehen AsyncTask.execute () aus Activity / Fragement ein. Der Unterschied besteht nun darin, dass AsyncTask die eigentliche Aufgabe an BusinessDAO delegiert. Während der Konfigurationsänderung wird also eine zweite AsyncTask ausgeführt wird initialisiert und ausgeführt und ruft BusinessDAO.doSomething () zum zweiten Mal auf. Der zweite Aufruf von BusinessDAO.doSomething () löst jedoch keine neue laufende Aufgabe aus, sondern wartet auf den Abschluss der aktuell ausgeführten Aufgabe:

    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. Implementieren Sie in BusinessDAO einen Singleton-Prozessmechanismus, zum Beispiel:

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

Ich bin nicht 100% sicher, ob dies funktionieren wird. Außerdem sollte das Beispielcode-Snippet als Pseudocode betrachtet werden. Ich versuche nur, Ihnen einen Hinweis von der Designebene zu geben. Feedback oder Vorschläge sind willkommen und willkommen.


Scheint sehr schöne Lösung. Seit du das vor ungefähr zweieinhalb Jahren beantwortet hast, hast du es seitdem getestet?! Sie sagen, ich bin nicht sicher, ob es funktioniert, die Sache ist auch nicht ich !! Ich suche nach einer gut getesteten Lösung für dieses Problem. Hast du irgendwelche Vorschläge?
Alireza A. Ahmadi

1

Sie können die AsyncTask zu einem statischen Feld machen. Wenn Sie einen Kontext benötigen, sollten Sie Ihren Anwendungskontext versenden. Dadurch werden Speicherverluste vermieden, da Sie sonst einen Verweis auf Ihre gesamte Aktivität behalten.


1

Wenn jemand den Weg zu diesem Thread findet, habe ich festgestellt, dass ein sauberer Ansatz darin besteht, die Async-Task von einem app.Service(mit START_STICKY gestartet) aus auszuführen und dann beim erneuten Erstellen über die ausgeführten Services zu iterieren, um herauszufinden, ob der Service (und damit die Async-Task) noch vorhanden ist Laufen;

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

Wenn dies der Fall ist, fügen Sie das DialogFragment(oder was auch immer) erneut hinzu, und wenn nicht sichergestellt ist, dass der Dialog geschlossen wurde.

Dies ist besonders relevant, wenn Sie die v4.support.*Bibliotheken verwenden, da sie (zum Zeitpunkt des Schreibens) Probleme mit der setRetainInstanceMethode und dem Anzeigen von Paging haben. Wenn Sie die Instanz nicht beibehalten, können Sie Ihre Aktivität mit anderen Ressourcen neu erstellen (dh mit einem anderen Ansichtslayout für die neue Ausrichtung).


Ist es nicht übertrieben, einen Dienst auszuführen, nur um eine AsyncTask beizubehalten? Ein Service wird in einem eigenen Prozess ausgeführt, und dies ist nicht ohne zusätzliche Kosten möglich.
WeNeigh

Interessantes Vinay. Ich habe nicht bemerkt, dass die App ressourcenintensiver ist (im Moment ist sie sowieso ziemlich leicht). Was hast du gefunden Ich betrachte einen Dienst als eine vorhersehbare Umgebung, in der das System unabhängig vom Status der Benutzeroberfläche mit schwerem Heben oder E / A betrieben werden kann. Die Kommunikation mit dem Dienst, um zu sehen, wann etwas abgeschlossen ist, schien „richtig“ zu sein. Die Dienste, mit denen ich anfange, ein wenig Arbeit auszuführen, werden nach Abschluss der Aufgabe gestoppt, sodass sie normalerweise etwa 10 bis 30 Sekunden überleben.
OceanLife

Die Antwort von Commonsware hier scheint darauf hinzudeuten , dass Services eine schlechte Idee sind. Ich denke jetzt über AsyncTaskLoaders nach, aber sie scheinen mit eigenen Problemen zu kommen (Inflexibilität, nur zum Laden von Daten usw.)
WeNeigh

1
Aha. Bemerkenswerterweise wurde dieser von Ihnen verknüpfte Dienst explizit so eingerichtet, dass er in einem eigenen Prozess ausgeführt wird. Dem Meister schien es nicht zu gefallen, dass dieses Muster oft verwendet wurde. Ich habe diese Eigenschaften "Jedes Mal in einem neuen Prozess ausführen" nicht explizit angegeben, so dass ich hoffentlich von diesem Teil der Kritik isoliert bin. Ich werde mich bemühen, die Auswirkungen zu quantifizieren. Services als Konzept sind natürlich keine „schlechten Ideen“ und von grundlegender Bedeutung für jede App, die etwas entfernt Interessantes tut, ohne dass ein Wortspiel beabsichtigt ist. Ihr JDoc bietet weitere Anleitungen zu ihrer Verwendung, wenn Sie sich immer noch nicht sicher sind.
OceanLife

0

Ich schreibe samepl Code, um dieses Problem zu lösen

Der erste Schritt besteht darin, die Anwendungsklasse zu erstellen:

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 Aktivität:

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

}

Wenn sich die Aktivitätsorientierung ändert, wird die Variable mTask aus dem App-Kontext gestartet. Wenn die Aufgabe beendet ist, wird die Variable auf null gesetzt und aus dem Speicher entfernt.

Für mich ist es genug.


0

Schauen Sie sich das folgende Beispiel an, wie Sie das beibehaltene Fragment verwenden, um die Hintergrundaufgabe beizubehalten:

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

Schauen Sie hier .

Es gibt eine Lösung, die auf der Lösung von Timmmm basiert .

Aber ich habe es verbessert:

  • Jetzt ist die Lösung erweiterbar - Sie müssen nur noch erweitern FragmentAbleToStartTask

  • Sie können mehrere Aufgaben gleichzeitig ausführen.

    Und meiner Meinung nach ist es so einfach wie startActivityForResult und Ergebnis zu erhalten

  • Sie können auch eine laufende Aufgabe stoppen und prüfen, ob eine bestimmte Aufgabe ausgeführt wird

Entschuldigung für mein Englisch


erste Verbindung ist unterbrochen
Tejzeratul
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.