Benutzeroberfläche für Android-Aktualisierungsaktivitäten vom Dienst

102

Ich habe einen Dienst, der ständig nach neuen Aufgaben sucht. Wenn es eine neue Aufgabe gibt, möchte ich die Benutzeroberfläche der Aktivität aktualisieren, um diese Informationen anzuzeigen. Ich habe https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ in diesem Beispiel gefunden. Ist das ein guter Ansatz? Irgendwelche anderen Beispiele?

Vielen Dank.

user200658
quelle
Siehe meine Antwort hier. Einfach zu definierende Schnittstelle für die Kommunikation zwischen Klassen mithilfe von Listenern. stackoverflow.com/questions/14660671/…
Simon

Antworten:

228

Unten finden Sie meine ursprüngliche Antwort - dieses Muster hat gut funktioniert, aber seit kurzem verwende ich einen anderen Ansatz für die Service- / Aktivitätskommunikation:

  • Verwenden Sie einen gebundenen Dienst der es der Aktivität ermöglicht, einen direkten Verweis auf den Dienst abzurufen, sodass direkte Anrufe auf ihn möglich sind, anstatt Absichten zu verwenden.
  • Verwenden Sie RxJava, um asynchrone Operationen auszuführen.

  • Wenn der Dienst Hintergrundvorgänge fortsetzen muss, auch wenn keine Aktivität ausgeführt wird, starten Sie den Dienst auch über die Anwendungsklasse, damit er nicht gestoppt wird, wenn er nicht gebunden ist.

Die Vorteile, die ich bei diesem Ansatz gegenüber der startService () / LocalBroadcast-Technik gefunden habe, sind:

  • Für die Implementierung von Parcelable sind keine Datenobjekte erforderlich. Dies ist für mich besonders wichtig, da ich jetzt Code zwischen Android und iOS (mithilfe von RoboVM) freigebe.
  • RxJava bietet eine vordefinierte (und plattformübergreifende) Planung sowie eine einfache Zusammenstellung von sequentiellen asynchronen Vorgängen.
  • Dies sollte effizienter sein als die Verwendung eines LocalBroadcasts, obwohl der Aufwand für die Verwendung von RxJava diesen überwiegen kann.

Ein Beispielcode. Zuerst der Service:

public class AndroidBmService extends Service implements BmService {

    private static final int PRESSURE_RATE = 500000;   // microseconds between pressure updates
    private SensorManager sensorManager;
    private SensorEventListener pressureListener;
    private ObservableEmitter<Float> pressureObserver;
    private Observable<Float> pressureObservable;

    public class LocalBinder extends Binder {
        public AndroidBmService getService() {
            return AndroidBmService.this;
        }
    }

    private IBinder binder = new LocalBinder();

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        logMsg("Service bound");
        return binder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return START_NOT_STICKY;
    }

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

        sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
        Sensor pressureSensor = sensorManager.getDefaultSensor(Sensor.TYPE_PRESSURE);
        if(pressureSensor != null)
            sensorManager.registerListener(pressureListener = new SensorEventListener() {
                @Override
                public void onSensorChanged(SensorEvent event) {
                    if(pressureObserver != null) {
                        float lastPressure = event.values[0];
                        float lastPressureAltitude = (float)((1 - Math.pow(lastPressure / 1013.25, 0.190284)) * 145366.45);
                        pressureObserver.onNext(lastPressureAltitude);
                    }
                }

                @Override
                public void onAccuracyChanged(Sensor sensor, int accuracy) {

                }
            }, pressureSensor, PRESSURE_RATE);
    }

    @Override
    public Observable<Float> observePressure() {
        if(pressureObservable == null) {
            pressureObservable = Observable.create(emitter -> pressureObserver = emitter);
            pressureObservable = pressureObservable.share();
        }
         return pressureObservable;
    }

    @Override
    public void onDestroy() {
        if(pressureListener != null)
            sensorManager.unregisterListener(pressureListener);
    }
} 

Und eine Aktivität, die an den Dienst gebunden ist und Aktualisierungen der Druckhöhe erhält:

public class TestActivity extends AppCompatActivity {

    private ContentTestBinding binding;
    private ServiceConnection serviceConnection;
    private AndroidBmService service;
    private Disposable disposable;

    @Override
    protected void onDestroy() {
        if(disposable != null)
            disposable.dispose();
        unbindService(serviceConnection);
        super.onDestroy();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = DataBindingUtil.setContentView(this, R.layout.content_test);
        serviceConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                logMsg("BlueMAX service bound");
                service = ((AndroidBmService.LocalBinder)iBinder).getService();
                disposable = service.observePressure()
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(altitude ->
                        binding.altitude.setText(
                            String.format(Locale.US,
                                "Pressure Altitude %d feet",
                                altitude.intValue())));
            }

            @Override
            public void onServiceDisconnected(ComponentName componentName) {
                logMsg("Service disconnected");
            }
        };
        bindService(new Intent(
            this, AndroidBmService.class),
            serviceConnection, BIND_AUTO_CREATE);
    }
}

Das Layout für diese Aktivität lautet:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="com.controlj.mfgtest.TestActivity">

        <TextView
            tools:text="Pressure"
            android:id="@+id/altitude"
            android:gravity="center_horizontal"
            android:layout_gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

    </LinearLayout>
</layout>

Wenn der Dienst im Hintergrund ohne gebundene Aktivität ausgeführt werden muss, kann er auch über die Anwendungsklasse gestartet OnCreate()werden Context#startService().


Meine ursprüngliche Antwort (von 2013):

In Ihrem Dienst: (Verwenden von COPA als Dienst im folgenden Beispiel).

Verwenden Sie einen LocalBroadCastManager. Richten Sie in onCreate Ihres Dienstes den Sender ein:

broadcaster = LocalBroadcastManager.getInstance(this);

Wenn Sie die Benutzeroberfläche über etwas informieren möchten:

static final public String COPA_RESULT = "com.controlj.copame.backend.COPAService.REQUEST_PROCESSED";

static final public String COPA_MESSAGE = "com.controlj.copame.backend.COPAService.COPA_MSG";

public void sendResult(String message) {
    Intent intent = new Intent(COPA_RESULT);
    if(message != null)
        intent.putExtra(COPA_MESSAGE, message);
    broadcaster.sendBroadcast(intent);
}

In Ihrer Aktivität:

Erstellen Sie einen Listener auf onCreate:

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    super.setContentView(R.layout.copa);
    receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String s = intent.getStringExtra(COPAService.COPA_MESSAGE);
            // do something here.
        }
    };
}

und registrieren Sie es in onStart:

@Override
protected void onStart() {
    super.onStart();
    LocalBroadcastManager.getInstance(this).registerReceiver((receiver), 
        new IntentFilter(COPAService.COPA_RESULT)
    );
}

@Override
protected void onStop() {
    LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver);
    super.onStop();
}
Clyde
quelle
4
@ user200658 Ja, onStart () und onStop () sind Teil des Aktivitätslebenszyklus - siehe Aktivitätslebenszyklus
Clyde
8
Winziger Kommentar - Ihnen fehlt die Definition von COPA_MESSAGE.
Lior
1
Vielen Dank :) Diejenigen, die nach COPA_RESULT fragen, es ist nichts anderes als eine vom Benutzer generierte statische Variable. "COPA" ist sein Servicename, sodass Sie ihn vollständig durch Ihren ersetzen können. In meinem Fall ist es statisch final public String MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil
1
Die Benutzeroberfläche wird nicht aktualisiert, wenn der Benutzer von der Aktivität weg navigiert und nach Beendigung des Dienstes wieder darauf zurückgreift. Was ist der beste Weg, um damit umzugehen?
SavageKing
1
@SavageKing Sie müssen jede Anfrage an den Dienst senden, die für Ihre onStart()oder Ihre onResume()Methode geeignet ist . Im Allgemeinen ist es vernünftig anzunehmen, dass das Ergebnis nicht mehr benötigt wird, wenn die Aktivität den Dienst auffordert, etwas zu tun, aber vor Erhalt des Ergebnisses beendet wird. Ebenso sollte beim Starten einer Aktivität davon ausgegangen werden, dass keine ausstehenden Anforderungen vom Service verarbeitet werden.
Clyde
32

Für mich war die einfachste Lösung, eine Sendung zu senden. In der Aktivität oncreate habe ich die Sendung wie folgt registriert und definiert (updateUIReciver ist als Klasseninstanz definiert):

 IntentFilter filter = new IntentFilter();

 filter.addAction("com.hello.action"); 

 updateUIReciver = new BroadcastReceiver() {

            @Override
            public void onReceive(Context context, Intent intent) {
                //UI update here

            }
        };
 registerReceiver(updateUIReciver,filter);

Und von dem Dienst senden Sie die Absicht wie folgt:

Intent local = new Intent();

local.setAction("com.hello.action");

this.sendBroadcast(local);

Vergessen Sie nicht, die Wiederherstellung in der Aktivität "Zerstören" aufzuheben:

unregisterReceiver(updateUIReciver);
Eran Katsav
quelle
1
Dies ist eine bessere Lösung, aber es wäre besser, LocalBroadcastManager zu verwenden, wenn es in der Anwendung verwendet wird, was effizienter wäre.
Psypher
1
nächste Schritte nach this.sendBroadcast (lokal); im Dienste?
Engel
@angel gibt es keinen nächsten Schritt, in den Extras der Absicht fügen Sie einfach die gewünschten UI-Updates hinzu und das wars
Eran Katsav
12

Ich würde einen gebundenen Dienst verwenden, um dies zu tun und mit ihm zu kommunizieren, indem ich einen Listener in meiner Aktivität implementiere. Wenn Ihre App myServiceListener implementiert, können Sie sie als Listener in Ihrem Dienst registrieren, nachdem Sie sie gebunden haben. Rufen Sie listener.onUpdateUI von Ihrem gebundenen Dienst aus auf und aktualisieren Sie dort Ihre Benutzeroberfläche!

Psykhi
quelle
Das sieht genau so aus, wie ich es suche. Lass es mich ausprobieren. Vielen Dank.
user200658
Achten Sie darauf, keinen Hinweis auf Ihre Aktivität zu verlieren. Weil Ihre Aktivität bei Rotation möglicherweise zerstört und neu erstellt wird.
Eric
Hallo, bitte überprüfen Sie diesen Link. Ich hatte einen Beispielcode dafür geteilt. Setzen Sie den Link hier unter der Annahme, dass sich jemand in Zukunft hilfreich fühlen könnte. myownandroid.blogspot.in/2012/08/…
jrhamza
+1 Dies ist eine praktikable Lösung, aber in meinem Fall muss der Dienst wirklich weiter ausgeführt werden, obwohl alle Aktivitäten nicht an ihn gebunden sind (z. B. Benutzer schließen die Anwendung, vorzeitige Beendigung des Betriebssystems). Ich habe keine andere Wahl, als Broadcast zu verwenden Empfänger.
Neon Warge
9

Ich würde empfehlen, Otto zu testen, einen EventBus, der speziell auf Android zugeschnitten ist. Ihre Aktivität / Benutzeroberfläche kann Ereignisse abhören, die vom Dienst auf dem Bus veröffentlicht wurden, und sich vom Backend entkoppeln.

SeanPONeil
quelle
5

Die Lösung von Clyde funktioniert, aber es handelt sich um eine Sendung, von der ich mir ziemlich sicher bin, dass sie weniger effizient ist als das direkte Aufrufen einer Methode. Ich könnte mich irren, aber ich denke, die Sendungen sind eher für die Kommunikation zwischen Anwendungen gedacht.

Ich gehe davon aus, dass Sie bereits wissen, wie Sie einen Dienst an eine Aktivität binden. Ich mache so etwas wie den folgenden Code, um diese Art von Problem zu lösen:

class MyService extends Service {
    MyFragment mMyFragment = null;
    MyFragment mMyOtherFragment = null;

    private void networkLoop() {
        ...

        //received new data for list.
        if(myFragment != null)
            myFragment.updateList();
        }

        ...

        //received new data for textView
        if(myFragment !=null)
            myFragment.updateText();

        ...

        //received new data for textView
        if(myOtherFragment !=null)
            myOtherFragment.updateSomething();

        ...
    }
}


class MyFragment extends Fragment {

    public void onResume() {
        super.onResume()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyFragment=this;
    }

    public void onPause() {
        super.onPause()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyFragment=null;
    }

    public void updateList() {
        runOnUiThread(new Runnable() {
            public void run() {
                //Update the list.
            }
        });
    }

    public void updateText() {
       //as above
    }
}

class MyOtherFragment extends Fragment {
             public void onResume() {
        super.onResume()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyOtherFragment=this;
    }

    public void onPause() {
        super.onPause()
        //Assuming your activity bound to your service
        getActivity().mMyService.mMyOtherFragment=null;
    }

    public void updateSomething() {//etc... }
}

Ich habe Bits für die Thread-Sicherheit weggelassen, was wichtig ist. Stellen Sie sicher, dass Sie Sperren oder ähnliches verwenden, wenn Sie die Fragmentreferenzen für den Dienst überprüfen und verwenden oder ändern.

Reynard
quelle
8
LocalBroadcastManager wurde für die Kommunikation innerhalb einer Anwendung entwickelt und ist daher sehr effizient. Der gebundene Service-Ansatz ist in Ordnung, wenn Sie eine begrenzte Anzahl von Clients für den Service haben und der Service nicht unabhängig ausgeführt werden muss. Der lokale Broadcast-Ansatz ermöglicht eine effektivere Entkopplung des Dienstes von seinen Clients und macht die Thread-Sicherheit zu einem Problem.
Clyde
5
Callback from service to activity to update UI.
ResultReceiver receiver = new ResultReceiver(new Handler()) {
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        //process results or update UI
    }
}

Intent instructionServiceIntent = new Intent(context, InstructionService.class);
instructionServiceIntent.putExtra("receiver", receiver);
context.startService(instructionServiceIntent);
Mohammed Shoeb
quelle
1

Meine Lösung ist möglicherweise nicht die sauberste, sollte aber ohne Probleme funktionieren. Die Logik besteht einfach darin, eine statische Variable zu erstellen, um Ihre Daten auf dem zu speichern Serviceund Ihre Ansicht jede Sekunde auf Ihrem zu aktualisieren Activity.

Nehmen wir an, Sie haben eine Stringauf Ihrem Service, die Sie an eine TextViewauf Ihrem senden möchten Activity. Es sollte so aussehen

Dein Dienst:

public class TestService extends Service {
    public static String myString = "";
    // Do some stuff with myString

Ihre Aktivität:

public class TestActivity extends Activity {
    TextView tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        tv = new TextView(this);
        setContentView(tv);
        update();
        Thread t = new Thread() {
            @Override
            public void run() {
                try {
                    while (!isInterrupted()) {
                        Thread.sleep(1000);
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                update();
                            }
                        });
                    }
                } catch (InterruptedException ignored) {}
            }
        };
        t.start();
        startService(new Intent(this, TestService.class));
    }
    private void update() {
        // update your interface here
        tv.setText(TestService.myString);
    }
}
Naheel
quelle
2
Machen Sie
@MurtazaKhursheedHussain - können Sie das näher erläutern?
SolidSnake
2
Statische Mitglieder sind die Ursache für Speicherverluste bei Aktivitäten (es gibt viele Artikel), und wenn sie in Betrieb bleiben, wird dies noch schlimmer. Ein Rundfunkempfänger ist in der Situation von OP viel besser geeignet, oder es ist auch eine Lösung, ihn dauerhaft zu speichern.
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - Nehmen wir also an, ich habe eine Klasse (keine Serviceklasse, nur eine Modellklasse, die ich zum Auffüllen von Daten verwende) mit einer statischen Hashmap, die jede Minute mit einigen Daten (die von einer API stammen) neu aufgefüllt wird Speicherleck?
SolidSnake
androidVersuchen Sie im Kontext von yes, wenn es sich um eine Liste handelt, einen Adapter zu erstellen oder Daten mit sqllite / Realm DB beizubehalten.
Murtaza Khursheed Hussain