Service API reposant

226

Je cherche à créer un service que je peux utiliser pour passer des appels à une API REST basée sur le Web.

Fondamentalement, je veux démarrer un service sur l'application init puis je veux pouvoir demander à ce service de demander une URL et de renvoyer les résultats. En attendant, je veux pouvoir afficher une fenêtre de progression ou quelque chose de similaire.

J'ai actuellement créé un service qui utilise IDL, j'ai lu quelque part que vous n'en avez vraiment besoin que pour la communication inter-applications, alors pensez que ces besoins doivent être supprimés, mais ne savez pas comment faire des rappels sans. De plus, lorsque je clique sur l' post(Config.getURL("login"), values)application, l'application semble faire une pause pendant un certain temps (cela semble étrange - je pensais que l'idée derrière un service était qu'il fonctionnait sur un thread différent!)

Actuellement, j'ai un service avec des méthodes post et get http à l'intérieur, quelques fichiers AIDL (pour la communication bidirectionnelle), un ServiceManager qui traite du démarrage, de l'arrêt, de la liaison, etc. au service et je crée dynamiquement un gestionnaire avec du code spécifique pour les rappels au besoin.

Je ne veux pas que quelqu'un me donne une base de code complète sur laquelle travailler, mais certains pointeurs seraient grandement appréciés.

Code complet (principalement):

public class RestfulAPIService extends Service  {

final RemoteCallbackList<IRemoteServiceCallback> mCallbacks = new RemoteCallbackList<IRemoteServiceCallback>();

public void onStart(Intent intent, int startId) {
    super.onStart(intent, startId);
}
public IBinder onBind(Intent intent) {
    return binder;
}
public void onCreate() {
    super.onCreate();
}
public void onDestroy() {
    super.onDestroy();
    mCallbacks.kill();
}
private final IRestfulService.Stub binder = new IRestfulService.Stub() {
    public void doLogin(String username, String password) {

        Message msg = new Message();
        Bundle data = new Bundle();
        HashMap<String, String> values = new HashMap<String, String>();
        values.put("username", username);
        values.put("password", password);
        String result = post(Config.getURL("login"), values);
        data.putString("response", result);
        msg.setData(data);
        msg.what = Config.ACTION_LOGIN;
        mHandler.sendMessage(msg);
    }

    public void registerCallback(IRemoteServiceCallback cb) {
        if (cb != null)
            mCallbacks.register(cb);
    }
};

private final Handler mHandler = new Handler() {
    public void handleMessage(Message msg) {

        // Broadcast to all clients the new value.
        final int N = mCallbacks.beginBroadcast();
        for (int i = 0; i < N; i++) {
            try {
                switch (msg.what) {
                case Config.ACTION_LOGIN:
                    mCallbacks.getBroadcastItem(i).userLogIn( msg.getData().getString("response"));
                    break;
                default:
                    super.handleMessage(msg);
                    return;

                }
            } catch (RemoteException e) {
            }
        }
        mCallbacks.finishBroadcast();
    }
    public String post(String url, HashMap<String, String> namePairs) {...}
    public String get(String url) {...}
};

Quelques fichiers AIDL:

package com.something.android

oneway interface IRemoteServiceCallback {
    void userLogIn(String result);
}

et

package com.something.android
import com.something.android.IRemoteServiceCallback;

interface IRestfulService {
    void doLogin(in String username, in String password);
    void registerCallback(IRemoteServiceCallback cb);
}

et le chef de service:

public class ServiceManager {

    final RemoteCallbackList<IRemoteServiceCallback> mCallbacks = new RemoteCallbackList<IRemoteServiceCallback>();
    public IRestfulService restfulService;
    private RestfulServiceConnection conn;
    private boolean started = false;
    private Context context;

    public ServiceManager(Context context) {
        this.context = context;
    }

    public void startService() {
        if (started) {
            Toast.makeText(context, "Service already started", Toast.LENGTH_SHORT).show();
        } else {
            Intent i = new Intent();
            i.setClassName("com.something.android", "com.something.android.RestfulAPIService");
            context.startService(i);
            started = true;
        }
    }

    public void stopService() {
        if (!started) {
            Toast.makeText(context, "Service not yet started", Toast.LENGTH_SHORT).show();
        } else {
            Intent i = new Intent();
            i.setClassName("com.something.android", "com.something.android.RestfulAPIService");
            context.stopService(i);
            started = false;
        }
    }

    public void bindService() {
        if (conn == null) {
            conn = new RestfulServiceConnection();
            Intent i = new Intent();
            i.setClassName("com.something.android", "com.something.android.RestfulAPIService");
            context.bindService(i, conn, Context.BIND_AUTO_CREATE);
        } else {
            Toast.makeText(context, "Cannot bind - service already bound", Toast.LENGTH_SHORT).show();
        }
    }

    protected void destroy() {
        releaseService();
    }

    private void releaseService() {
        if (conn != null) {
            context.unbindService(conn);
            conn = null;
            Log.d(LOG_TAG, "unbindService()");
        } else {
            Toast.makeText(context, "Cannot unbind - service not bound", Toast.LENGTH_SHORT).show();
        }
    }

    class RestfulServiceConnection implements ServiceConnection {
        public void onServiceConnected(ComponentName className, IBinder boundService) {
            restfulService = IRestfulService.Stub.asInterface((IBinder) boundService);
            try {
            restfulService.registerCallback(mCallback);
            } catch (RemoteException e) {}
        }

        public void onServiceDisconnected(ComponentName className) {
            restfulService = null;
        }
    };

    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        public void userLogIn(String result) throws RemoteException {
            mHandler.sendMessage(mHandler.obtainMessage(Config.ACTION_LOGIN, result));

        }
    };

    private Handler mHandler;

    public void setHandler(Handler handler) {
        mHandler = handler;
    }
}

Service init et bind:

// this I'm calling on app onCreate
servicemanager = new ServiceManager(this);
servicemanager.startService();
servicemanager.bindService();
application = (ApplicationState)this.getApplication();
application.setServiceManager(servicemanager);

appel de fonction de service:

// this lot i'm calling as required - in this example for login
progressDialog = new ProgressDialog(Login.this);
progressDialog.setMessage("Logging you in...");
progressDialog.show();

application = (ApplicationState) getApplication();
servicemanager = application.getServiceManager();
servicemanager.setHandler(mHandler);

try {
    servicemanager.restfulService.doLogin(args[0], args[1]);
} catch (RemoteException e) {
    e.printStackTrace();
}

...later in the same file...

Handler mHandler = new Handler() {
    public void handleMessage(Message msg) {

        switch (msg.what) {
        case Config.ACTION_LOGIN:

            if (progressDialog.isShowing()) {
                progressDialog.dismiss();
            }

            try {
                ...process login results...
                }
            } catch (JSONException e) {
                Log.e("JSON", "There was an error parsing the JSON", e);
            }
            break;
        default:
            super.handleMessage(msg);
        }

    }

};
Martyn
la source
5
Cela pourrait être très utile pour les personnes apprenant la mise en œuvre du client Android REST. Présentation de Dobjanschi transcrite en PDF: drive.google.com/file/d/0B2dn_3573C3RdlVpU2JBWXdSb3c/…
Kay Zed
Étant donné que de nombreuses personnes ont recommandé la présentation de Virgil Dobjanschi et que le lien vers IO 2010 est rompu maintenant, voici le lien direct vers la vidéo YT: youtube.com/watch?v=xHXn3Kg2IQE
Jose_GD

Réponses:

283

Si votre service va faire partie de votre application, vous le rendez beaucoup plus complexe qu'il ne devrait l'être. Étant donné que vous avez un cas d'utilisation simple pour obtenir des données à partir d'un service Web RESTful, vous devez examiner ResultReceiver et IntentService .

Ce modèle Service + ResultReceiver fonctionne en démarrant ou en se liant au service avec startService () lorsque vous souhaitez effectuer une action. Vous pouvez spécifier l'opération à effectuer et transmettre votre ResultReceiver (l'activité) via les extras de l'intention.

Dans le service, vous implémentez onHandleIntent pour effectuer l'opération spécifiée dans l'intention. Lorsque l'opération est terminée, vous utilisez le passé dans ResultReceiver pour renvoyer un message à l'activité à quel point onReceiveResult sera appelé.

Ainsi, par exemple, vous souhaitez extraire certaines données de votre service Web.

  1. Vous créez l'intention et appelez startService.
  2. L'opération dans le service démarre et il envoie à l'activité un message disant qu'elle a démarré
  3. L'activité traite le message et montre une progression.
  4. Le service termine l'opération et renvoie des données à votre activité.
  5. Votre activité traite les données et les met en liste
  6. Le service vous envoie un message disant que c'est fait et qu'il se tue.
  7. L'activité reçoit le message de fin et masque la boîte de dialogue de progression.

Je sais que vous avez mentionné que vous ne vouliez pas de base de code, mais l'application open source Google I / O 2010 utilise un service de la manière que je décris.

Mis à jour pour ajouter un exemple de code:

L'activité.

public class HomeActivity extends Activity implements MyResultReceiver.Receiver {

    public MyResultReceiver mReceiver;

    public void onCreate(Bundle savedInstanceState) {
        mReceiver = new MyResultReceiver(new Handler());
        mReceiver.setReceiver(this);
        ...
        final Intent intent = new Intent(Intent.ACTION_SYNC, null, this, QueryService.class);
        intent.putExtra("receiver", mReceiver);
        intent.putExtra("command", "query");
        startService(intent);
    }

    public void onPause() {
        mReceiver.setReceiver(null); // clear receiver so no leaks.
    }

    public void onReceiveResult(int resultCode, Bundle resultData) {
        switch (resultCode) {
        case RUNNING:
            //show progress
            break;
        case FINISHED:
            List results = resultData.getParcelableList("results");
            // do something interesting
            // hide progress
            break;
        case ERROR:
            // handle the error;
            break;
    }
}

Le service:

public class QueryService extends IntentService {
    protected void onHandleIntent(Intent intent) {
        final ResultReceiver receiver = intent.getParcelableExtra("receiver");
        String command = intent.getStringExtra("command");
        Bundle b = new Bundle();
        if(command.equals("query") {
            receiver.send(STATUS_RUNNING, Bundle.EMPTY);
            try {
                // get some data or something           
                b.putParcelableArrayList("results", results);
                receiver.send(STATUS_FINISHED, b)
            } catch(Exception e) {
                b.putString(Intent.EXTRA_TEXT, e.toString());
                receiver.send(STATUS_ERROR, b);
            }    
        }
    }
}

Extension ResultReceiver - modifiée sur le point d'implémenter MyResultReceiver.Receiver

public class MyResultReceiver implements ResultReceiver {
    private Receiver mReceiver;

    public MyResultReceiver(Handler handler) {
        super(handler);
    }

    public void setReceiver(Receiver receiver) {
        mReceiver = receiver;
    }

    public interface Receiver {
        public void onReceiveResult(int resultCode, Bundle resultData);
    }

    @Override
    protected void onReceiveResult(int resultCode, Bundle resultData) {
        if (mReceiver != null) {
            mReceiver.onReceiveResult(resultCode, resultData);
        }
    }
}
Robby Pond
la source
1
Excellent merci! J'ai fini par passer par l'application iosched de Google et wow ... c'est assez complexe mais j'ai quelque chose qui fonctionne, maintenant j'ai juste besoin de comprendre pourquoi ça marche! Mais oui, c'est le schéma de base que je travaille. Merci beaucoup.
Martyn
29
Un petit ajout à la réponse: comme vous le faites le mReceiver.setReceiver (null); dans la méthode onPause, vous devez faire le mReceiver.setReceiver (this); dans la méthode onResume. Sinon, vous risquez de ne pas recevoir les événements si votre activité reprend sans être recréée
Vincent Mimoun-Prat
7
Les documents ne disent-ils pas que vous n'avez pas besoin d'appeler stopSelf, puisque IntentService le fait pour vous?
Mikael Ohlson
2
@MikaelOhlson Correct, vous ne devez pas appeler stopSelfsi vous sous IntentService- classe car si vous le faites, vous perdrez toutes les demandes en attente à la même IntentService.
quietmint
1
IntentServicese tuera une fois la tâche terminée, donc this.stopSelf()inutile.
Euporie
17

Le développement d'applications clientes Android REST a été une ressource formidable pour moi. Le haut-parleur ne montre aucun code, il passe simplement en revue les considérations et les techniques de conception pour assembler un Rest Api solide comme le roc dans Android. Si vous êtes un podcast un peu ou non, je recommanderais de donner à celui-ci au moins une écoute mais, personnellement, je l'ai écouté quatre ou cinq fois jusqu'à présent et je vais probablement l'écouter à nouveau.

Développement d'applications client Android REST
Auteur: Virgil Dobjanschi
Description:

Cette session présentera des considérations architecturales pour le développement d'applications RESTful sur la plate-forme Android. Il se concentre sur les modèles de conception, l'intégration de la plate-forme et les problèmes de performances spécifiques à la plate-forme Android.

Et il y a tellement de considérations que je n'avais vraiment pas faites dans la première version de mon api que j'ai dû refactoriser

Terrance
la source
4
+1 Ceci contient le genre de considérations auxquelles vous ne pensez jamais lorsque vous commencez.
Thomas Ahle
Oui, ma première incursion dans le développement d'un client Rest était presque exactement sa description de ce qu'il ne fallait pas faire (heureusement, je me suis rendu compte que cela était mal avant de regarder cela). Je m'y attarde un peu.
Terrance
J'ai vu cette vidéo plus d'une fois et j'implémente le deuxième modèle. Mon problème est que je dois utiliser des transactions dans mon modèle de base de données complexe pour mettre à jour les données locales à partir de nouvelles données provenant du serveur, et l'interface ContentProvider ne me fournit pas un moyen de le faire. Avez-vous une suggestion, Terrance?
Flávio Faria
2
NB: Les commentaires de Dobjanschi sur HttpClient ne tiennent plus. Voir stackoverflow.com/a/15524143/939250
Donal Lafferty
Oui, HttpURLConnection est désormais préféré. De plus, avec la sortie d'Android 6.0, la prise en charge du client HTTP Apache a officiellement été supprimée .
RonR
16

De plus, lorsque je clique sur le message (Config.getURL ("login"), valeurs), l'application semble faire une pause pendant un certain temps (semble étrange - l'idée derrière un service était qu'il fonctionnait sur un thread différent!)

Non, vous devez créer un thread vous-même, un service local s'exécute dans le thread d'interface utilisateur par défaut.

Soumya Simanta
la source
5

Je voulais juste vous indiquer tous dans la direction d'une classe autonome que j'ai roulée qui intègre toutes les fonctionnalités.

http://github.com/StlTenny/RestService

Il exécute la demande comme non bloquante et renvoie les résultats dans un gestionnaire facile à implémenter. Vient même avec un exemple d'implémentation.

StlTenny
la source
4

Disons que je veux démarrer le service sur un événement - onItemClicked () d'un bouton. Le mécanisme du récepteur ne fonctionnerait pas dans ce cas car: -
a) j'ai transmis le récepteur au service (comme dans Intent extra) à partir de onItemClicked ()
b) l'activité se déplace en arrière-plan. Dans onPause (), je définis la référence du récepteur dans le ResultReceiver sur null pour éviter de fuir l'activité.
c) L'activité est détruite.
d) L'activité est à nouveau créée. Cependant, à ce stade, le service ne sera pas en mesure d'effectuer un rappel de l'activité car cette référence de récepteur est perdue.
Le mécanisme d'une diffusion limitée ou d'un PendingIntent semble être plus utile dans de tels scénarios - se référer à Notifier l'activité du service

Nikhil_Katre
la source
1
il y a un problème avec ce que vous dites. c'est-à-dire que lorsque l'activité se déplace en arrière-plan, elle n'est pas détruite ... donc le récepteur existe toujours et le contexte de l'activité aussi.
DArkO
@DArkO Lorsqu'une activité est suspendue ou arrêtée, elle peut être supprimée par le système Android dans des situations de mémoire insuffisante. Reportez-vous à Cycle de vie d'activité .
jk7
4

Notez que la solution de Robby Pond fait en quelque sorte défaut: de cette façon, vous n'autorisez qu'un seul appel api à la fois car l'IntentService ne gère qu'une seule intention à la fois. Souvent, vous souhaitez effectuer des appels API parallèles. Si vous voulez faire cela, vous devez étendre le service au lieu de IntentService et créer votre propre thread.

TjerkW
la source
1
Vous pouvez toujours effectuer plusieurs appels sur IntentService en déléguant les appels de l'API Webservice à un exécuteur-thread-service, présent en tant que variable membre de votre classe dérivée d'IntentService
Viren
2

De plus, lorsque je clique sur le message (Config.getURL ("login"), valeurs), l'application semble faire une pause pendant un certain temps (semble étrange - l'idée derrière un service était qu'il fonctionnait sur un thread différent!)

Dans ce cas, il vaut mieux utiliser asynctask, qui s'exécute sur un thread différent et renvoie le résultat au thread ui à la fin.

Aakash
la source
2

Il existe une autre approche ici qui vous aide essentiellement à oublier toute la gestion des demandes. Il est basé sur une méthode de file d'attente asynchrone et une réponse basée sur l'appel / le rappel. Le principal avantage est qu'en utilisant cette méthode, vous serez en mesure de rendre l'ensemble du processus (demande, obtenir et analyser la réponse, sabe à db) complètement transparent pour vous. Une fois que vous obtenez le code de réponse, le travail est déjà terminé. Après cela, il vous suffit d'appeler votre base de données et vous avez terminé. Cela aide également à résoudre la problématique de ce qui se passe lorsque votre activité n'est pas active. Ce qui se passera ici, c'est que vous aurez toutes vos données enregistrées dans votre base de données locale, mais la réponse ne sera pas traitée par votre activité, c'est le moyen idéal.

J'ai écrit ici sur une approche générale http://ugiagonzalez.com/2012/07/02/theres-life-after-asynctasks-in-android/

Je mettrai un exemple de code spécifique dans les prochains articles. J'espère que cela vous aide, n'hésitez pas à me contacter pour partager l'approche et résoudre les doutes ou problèmes potentiels.

Jose L Ugia
la source
Lien mort. Le domaine a expiré.
jk7
1

Robby fournit une excellente réponse, bien que je puisse vous voir toujours à la recherche de plus d'informations. J'ai implémenté les appels api REST de la manière simple MAIS dans le mauvais sens. Ce n'est qu'en regardant cette vidéo Google I / O que j'ai compris où je me suis trompé. Ce n'est pas aussi simple que d'assembler une AsyncTask avec un appel get / put HttpUrlConnection.

Andrew Halloran
la source
Lien mort. Voici celui mis à jour - Google I / O 2010 - Applications client Android REST
zim