Interface utilisateur de l'activité de mise à jour Android du service

102

J'ai un service qui vérifie en permanence les nouvelles tâches. S'il y a une nouvelle tâche, je souhaite actualiser l'interface utilisateur de l'activité pour afficher ces informations. J'ai trouvé https://github.com/commonsguy/cw-andtutorials/tree/master/18-LocalService/ cet exemple. Est-ce une bonne approche? D'autres exemples?

Merci.

user200658
la source
Voir ma réponse ici. Facile à définir une interface pour communiquer entre les classes en utilisant des écouteurs. stackoverflow.com/questions/14660671/…
Simon

Réponses:

228

Voir ci-dessous ma réponse originale - ce modèle a bien fonctionné, mais récemment, j'ai commencé à utiliser une approche différente de la communication Service / Activité:

  • Utilisez un service lié qui permet à l'activité d'obtenir une référence directe au service, permettant ainsi des appels directs dessus, plutôt que d'utiliser des intentions.
  • Utilisez RxJava pour exécuter des opérations asynchrones.

  • Si le service doit continuer les opérations en arrière-plan même lorsqu'aucune activité n'est en cours d'exécution, démarrez également le service à partir de la classe Application afin qu'il ne soit pas arrêté lorsqu'il n'est pas lié.

Les avantages que j'ai trouvés dans cette approche par rapport à la technique startService () / LocalBroadcast sont

  • Pas besoin d'objets de données pour implémenter Parcelable - cela est particulièrement important pour moi car je partage maintenant du code entre Android et iOS (en utilisant RoboVM)
  • RxJava fournit une planification prédéfinie (et multiplateforme) et une composition facile des opérations asynchrones séquentielles.
  • Cela devrait être plus efficace que d'utiliser un LocalBroadcast, bien que la surcharge liée à l'utilisation de RxJava puisse l'emporter sur cela.

Quelques exemples de code. D'abord le 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);
    }
} 

Et une activité qui se lie au service et reçoit des mises à jour d'altitude pression:

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

La disposition de cette activité est:

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

Si le service doit s'exécuter en arrière-plan sans activité liée, il peut également être démarré à partir de la classe Application en OnCreate()utilisant Context#startService().


Ma réponse originale (à partir de 2013):

Dans votre service: (en utilisant COPA comme service dans l'exemple ci-dessous).

Utilisez un LocalBroadCastManager. Dans onCreate de votre service, configurez le diffuseur:

broadcaster = LocalBroadcastManager.getInstance(this);

Lorsque vous souhaitez informer l'interface utilisateur de quelque chose:

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

Dans votre activité:

Créez un auditeur sur 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.
        }
    };
}

et enregistrez-le dans 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
la source
4
@ user200658 Oui, onStart () et onStop () font partie du cycle de vie des activités - voir Cycle de vie des activités
Clyde
8
Petit commentaire - il vous manque la définition de COPA_MESSAGE.
Lior
1
Merci :) Ceux qui posent des questions sur COPA_RESULT, ce n'est rien d'autre qu'une variable statique générée par l'utilisateur. "COPA" est son nom de service, vous pouvez donc le remplacer complètement par le vôtre. Dans mon cas, c'est statique final public String MP_Result = "com.widefide.musicplayer.MusicService.REQUEST_PROCESSED";
TheOnlyAnil
1
L'interface utilisateur ne sera pas mise à jour si l'utilisateur quitte l'activité, puis y revient une fois le service terminé. Quelle est la meilleure façon de gérer cela?
SavageKing
1
@SavageKing Vous devez envoyer toute demande au service appropriée dans votre méthode onStart()ou onResume(). En général, si l'activité demande au service de faire quelque chose, mais se ferme avant de recevoir le résultat, il est raisonnable de supposer que le résultat n'est plus nécessaire. De même, lors du démarrage d'une activité, il doit supposer qu'il n'y a pas de demandes en cours de traitement par le service.
Clyde
32

pour moi la solution la plus simple était d'envoyer une diffusion, dans l'activité oncreate, j'ai enregistré et défini la diffusion comme ceci (updateUIReciver est défini comme une instance de 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);

Et à partir du service, vous envoyez l'intention comme ceci:

Intent local = new Intent();

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

this.sendBroadcast(local);

n'oubliez pas de désenregistrer la récupération dans l'activité sur destroy:

unregisterReceiver(updateUIReciver);
Eran Katsav
la source
1
C'est une meilleure solution mais il serait préférable d'utiliser LocalBroadcastManager s'il est utilisé dans l'application, ce qui serait plus efficace.
Psypher
1
prochaines étapes après this.sendBroadcast (local); dans le service?
ange
@angel il n'y a pas de prochaine étape, à l'intérieur des extras de l'intention, ajoutez simplement les mises à jour de l'interface utilisateur que vous voulez et c'est tout
Eran Katsav
12

J'utiliserais un service lié pour le faire et communiquerais avec lui en implémentant un auditeur dans mon activité. Donc, si votre application implémente myServiceListener, vous pouvez l'enregistrer en tant qu'auditeur dans votre service après vous être lié avec elle, appelez listener.onUpdateUI à partir de votre service lié et mettez à jour votre interface utilisateur!

psykhi
la source
Cela ressemble exactement à ce que je recherche. Laissez-moi l'essayer. Merci.
user200658
Veillez à ne pas divulguer une référence à votre activité. Parce que votre activité peut être détruite et recréée en rotation.
Eric
Bonjour, veuillez consulter ce lien. J'avais partagé un exemple de code pour cela. Mettre le lien ici en supposant que quelqu'un peut se sentir utile à l'avenir. myownandroid.blogspot.in/2012/08/…
jrhamza
+1 C'est une solution viable, mais dans mon cas, j'ai vraiment besoin du service pour continuer à fonctionner même si toute l'activité se dissocie (par exemple, l'utilisateur ferme l'application, l'arrêt prématuré du système d'exploitation), je n'ai pas d'autre choix que d'utiliser la diffusion récepteurs.
Neon Warge
9

Je recommanderais de consulter Otto , un EventBus spécialement conçu pour Android. Votre activité / interface utilisateur peut écouter les événements publiés sur le bus à partir du service et se dissocier du backend.

SeanPONeil
la source
5

La solution de Clyde fonctionne, mais c'est une diffusion, ce qui, j'en suis sûr, sera moins efficace que d'appeler directement une méthode. Je peux me tromper, mais je pense que les diffusions sont davantage destinées à la communication inter-applications.

Je suppose que vous savez déjà comment lier un service à une activité. Je fais quelque chose comme le code ci-dessous pour gérer ce genre de problème:

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

J'ai omis des bits pour la sécurité des threads, ce qui est essentiel. Assurez-vous d'utiliser des verrous ou quelque chose du genre lors de la vérification et de l'utilisation ou de la modification des références de fragment sur le service.

Reynard
la source
8
LocalBroadcastManager est conçu pour la communication au sein d'une application, il est donc assez efficace. L'approche de service lié est correcte lorsque vous avez un nombre limité de clients pour le service et que le service n'a pas besoin de s'exécuter indépendamment. L'approche de diffusion locale permet au service d'être plus efficacement découplé de ses clients et fait de la sécurité des threads un problème.
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
la source
1

Ma solution n'est peut-être pas la plus propre, mais elle devrait fonctionner sans problème. La logique consiste simplement à créer une variable statique pour stocker vos données sur le Serviceet mettre à jour votre vue chaque seconde sur votre Activity.

Disons que vous avez un Stringsur votre Serviceque vous souhaitez envoyer à un TextViewsur votre Activity. ça devrait ressembler à ça

Ton service:

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

Votre activité:

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
la source
2
Ne jamais faire de chose statique variable ni dans le service ni dans aucune activité
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - pouvez-vous en dire plus à ce sujet?
SolidSnake
2
Les membres statiques sont la source de fuites de mémoire dans les activités (il y a beaucoup d'articles autour) et leur maintien en service aggrave la situation. Un récepteur de diffusion est beaucoup plus approprié dans la situation d'OP ou le maintien en stockage persistant est également une solution.
Murtaza Khursheed Hussain
@MurtazaKhursheedHussain - disons donc que si j'ai une classe (pas une classe de service, juste une classe de modèle que j'utilise pour remplir des données) avec un Hashmap statique qui est re-rempli avec des données (provenant d'une API) chaque minute est considérée comme un fuite de mémoire?
SolidSnake
Dans le contexte de androidoui, si sa liste, essayez de créer un adaptateur ou persistent données à l' aide SqlLite / domaine db.
Murtaza Khursheed Hussain