Interfaccia utente dell'attività di aggiornamento Android dal servizio


102

Ho un servizio che controlla continuamente nuove attività. Se c'è una nuova attività, voglio aggiornare l'interfaccia utente dell'attività per mostrare quelle informazioni. Ho trovato https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ questo esempio. È un buon approccio? Altri esempi?

Grazie.


Vedi la mia risposta qui. Facile da definire un'interfaccia per comunicare tra classi utilizzando ascoltatori. stackoverflow.com/questions/14660671/…
Simon

Risposte:


228

Vedi sotto per la mia risposta originale - quel modello ha funzionato bene, ma recentemente ho iniziato a utilizzare un approccio diverso alla comunicazione di servizio / attività:

  • Utilizzare un servizio associato che consente all'attività di ottenere un riferimento diretto al servizio, consentendo così chiamate dirette su di esso, piuttosto che utilizzare Intents.
  • Usa RxJava per eseguire operazioni asincrone.

  • Se il servizio deve continuare le operazioni in background anche quando nessuna attività è in esecuzione, avviare anche il servizio dalla classe Application in modo che non venga arrestato quando non è associato.

I vantaggi che ho riscontrato in questo approccio rispetto alla tecnica startService () / LocalBroadcast sono

  • Non è necessario che gli oggetti dati implementino Parcelable: questo è particolarmente importante per me poiché ora condivido il codice tra Android e iOS (utilizzando RoboVM)
  • RxJava fornisce una pianificazione predefinita (e multipiattaforma) e una facile composizione di operazioni asincrone sequenziali.
  • Questo dovrebbe essere più efficiente rispetto all'utilizzo di un LocalBroadcast, sebbene il sovraccarico dell'utilizzo di RxJava potrebbe superare quello.

Alcuni esempi di codice. Prima il servizio:

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

E un'attività che si lega al servizio e riceve aggiornamenti sull'altitudine di pressione:

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

Il layout per questa attività è:

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

Se il servizio deve essere eseguito in background senza un'attività associata, può essere avviato anche dalla classe Application OnCreate()utilizzando Context#startService().


La mia risposta originale (dal 2013):

Nel tuo servizio: (usando COPA come servizio nell'esempio sotto).

Usa un LocalBroadCastManager. In onCreate del tuo servizio, configura l'emittente:

broadcaster = LocalBroadcastManager.getInstance(this);

Quando vuoi notificare qualcosa all'interfaccia utente:

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

Nella tua attività:

Crea un listener su 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.
        }
    };
}

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

4
@ user200658 Sì, onStart () e onStop () fanno parte del ciclo di vita dell'attività - vedere Ciclo di vita dell'attività
Clyde

8
Piccolo commento: ti manca la definizione di COPA_MESSAGE.
Lior

1
Grazie :) Coloro che chiedono COPA_RESULT, non è altro che una variabile statica generata dall'utente. "COPA" è il nome del suo servizio, quindi puoi sostituirlo completamente con il tuo. Nel mio caso, è la stringa pubblica finale statica MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil

1
L'interfaccia utente non si aggiornerà se l'utente esce dall'attività e vi ritorna al termine del servizio. Qual è il modo migliore per gestirlo?
SavageKing

1
@SavageKing Devi inviare qualsiasi richiesta al servizio sia appropriata nel tuo metodo onStart()o onResume(). In generale, se l'attività chiede al servizio di fare qualcosa, ma si chiude prima di ricevere il risultato, è ragionevole presumere che il risultato non sia più richiesto. Allo stesso modo, all'avvio di un'attività, si dovrebbe presumere che non ci siano richieste in sospeso in corso di elaborazione da parte del Servizio.
Clyde

32

per me la soluzione più semplice era inviare un broadcast, nell'attività oncreate ho registrato e definito il broadcast in questo modo (updateUIReciver è definito come istanza di classe):

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

E dal servizio invii l'intento in questo modo:

Intent local = new Intent();

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

this.sendBroadcast(local);

non dimenticare di annullare la registrazione del recupero nell'attività sulla distruzione:

unregisterReceiver(updateUIReciver);

1
Questa è una soluzione migliore, ma sarebbe meglio utilizzare LocalBroadcastManager se utilizzato all'interno dell'applicazione che sarebbe più efficiente.
Psypher

1
passaggi successivi dopo this.sendBroadcast (local); nel servizio?
angelo

@angel non c'è passaggio successivo, all'interno degli extra dell'intento aggiungi semplicemente gli aggiornamenti dell'interfaccia utente che desideri e questo è tutto
Eran Katsav

12

Userei un servizio associato per farlo e comunicare con esso implementando un ascoltatore nella mia attività. Quindi, se la tua app implementa myServiceListener, puoi registrarla come listener nel tuo servizio dopo esserti connesso, chiamare listener.onUpdateUI dal tuo servizio associato e aggiornare la tua interfaccia utente lì!


Questo sembra esattamente quello che sto cercando. Fammi provare. Grazie.
user200658

Fai attenzione a non far trapelare un riferimento alla tua attività. Perché la tua attività potrebbe essere distrutta e ricreata a rotazione.
Eric,

Ciao, per favore controlla questo link. Avevo condiviso un codice di esempio per questo. Inserendo il collegamento qui presumendo che qualcuno possa sentirsi utile in futuro. myownandroid.blogspot.in/2012/08/…
jrhamza

+1 Questa è una soluzione praticabile, ma nel mio caso, ho davvero bisogno che il servizio continui a funzionare anche se tutte le attività si svincolano (ad esempio, l'utente chiude l'applicazione, chiusura prematura del sistema operativo), non ho altra scelta che usare la trasmissione ricevitori.
Neon Warge

9

Consiglierei di dare un'occhiata a Otto , un EventBus su misura per Android. La tua attività / interfaccia utente può ascoltare gli eventi pubblicati sul bus dal servizio e separarsi dal back-end.


5

La soluzione di Clyde funziona, ma è una trasmissione, che sono abbastanza sicuro sarà meno efficiente rispetto alla chiamata diretta di un metodo. Potrei sbagliarmi, ma penso che le trasmissioni siano destinate più alla comunicazione tra le applicazioni.

Presumo che tu sappia già come associare un servizio a un'attività. Faccio qualcosa di simile al codice seguente per gestire questo tipo di problema:

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

Ho tralasciato i bit per la sicurezza dei thread, che è essenziale. Assicurati di utilizzare blocchi o qualcosa di simile quando controlli, utilizzi o modifichi i riferimenti ai frammenti nel servizio.


8
LocalBroadcastManager è progettato per la comunicazione all'interno di un'applicazione, quindi è abbastanza efficiente. L'approccio del servizio vincolato va bene quando si dispone di un numero limitato di client per il servizio e il servizio non deve essere eseguito in modo indipendente. L'approccio di trasmissione locale consente al servizio di essere disaccoppiato in modo più efficace dai suoi client e rende la sicurezza dei thread un non problema.
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);

1

La mia soluzione potrebbe non essere la più pulita ma dovrebbe funzionare senza problemi. La logica consiste semplicemente nel creare una variabile statica per memorizzare i dati sul Servicee aggiornare la visualizzazione ogni secondo sul tuo Activity.

Diciamo che hai un Stringsul tuo Serviceche vuoi inviarlo a un TextViewsul tuoActivity . Dovrebbe sembrare come questo

Il tuo servizio:

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

La tua attività:

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

2
Non fare mai cose con variabili statiche né in Servizio né in alcuna attività
Murtaza Khursheed Hussain

@MurtazaKhursheedHussain - puoi approfondire di più su questo?
SolidSnake

2
I membri statici sono la fonte di perdite di memoria nelle attività (ci sono molti articoli in giro) e mantenerli in servizio peggiora le cose. Un ricevitore di trasmissione è molto più appropriato nella situazione dell'OP o anche tenerlo in una memoria persistente è una soluzione.
Murtaza Khursheed Hussain,

@MurtazaKhursheedHussain - quindi diciamo che se ho una classe (non una classe di servizio solo una classe modello che uso per popolare i dati) con una Hashmap statica che viene ripopolata con alcuni dati (provenienti da un'API) ogni minuto è considerato come un perdita di memoria?
SolidSnake

Nel contesto di androidsì, se è un elenco, prova a creare un adattatore o a rendere persistenti i dati utilizzando sqllite / realm db.
Murtaza Khursheed Hussain
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.