Servicio API tranquilo

226

Estoy buscando hacer un servicio que pueda usar para hacer llamadas a una API REST basada en la web.

Básicamente, quiero iniciar un servicio en la aplicación init, luego quiero poder pedirle a ese servicio que solicite una URL y devuelva los resultados. Mientras tanto, quiero poder mostrar una ventana de progreso o algo similar.

Actualmente he creado un servicio que usa IDL, he leído en alguna parte que solo necesitas esto para la comunicación entre aplicaciones, así que piensa que estas necesidades se eliminan pero no estoy seguro de cómo hacer devoluciones de llamada sin él. Además, cuando llego a la post(Config.getURL("login"), values)aplicación, la aplicación parece detenerse por un tiempo (parece extraño, ¡pensé que la idea detrás de un servicio era que se ejecuta en un hilo diferente!)

Actualmente tengo un servicio con publicación y obtengo métodos http dentro, un par de archivos AIDL (para comunicación bidireccional), un ServiceManager que se encarga de iniciar, detener, vincular, etc. al servicio y estoy creando dinámicamente un controlador con código específico para las devoluciones de llamada según sea necesario.

No quiero que nadie me dé una base de código completa para trabajar, pero algunos punteros serían muy apreciados.

Código en (en su mayoría) completo:

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

Un par de archivos AIDL:

package com.something.android

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

y

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

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

y el gerente de servicio:

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

Servicio init y bind:

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

llamada a la función de servicio:

// 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
fuente
55
Esto podría ser muy útil para las personas que están aprendiendo la implementación del cliente REST de Android. La presentación de Dobjanschi se transcribió a un PDF: drive.google.com/file/d/0B2dn_3573C3RdlVpU2JBWXdSb3c/…
Kay Zed
Como muchas personas recomendaron la presentación de Virgil Dobjanschi y el enlace a IO 2010 está roto ahora, aquí está el enlace directo al video de
YouTube

Respuestas:

283

Si su servicio va a ser parte de su aplicación, entonces lo está haciendo mucho más complejo de lo que debe ser. Como tiene un caso de uso simple de obtener algunos datos de un servicio web RESTful, debe buscar ResultReceiver e IntentService .

Este patrón de Service + ResultReceiver funciona iniciando o vinculando el servicio con startService () cuando desea realizar alguna acción. Puede especificar la operación a realizar y pasar su ResultReceiver (la actividad) a través de los extras en la Intención.

En el servicio, implemente onHandleIntent para realizar la operación que se especifica en la intención. Cuando se completa la operación, utiliza el pasado en ResultReceiver para enviar un mensaje de regreso a la Actividad, en cuyo punto se llamará a OnReceiveResult .

Entonces, por ejemplo, desea extraer algunos datos de su servicio web.

  1. Crea la intención y llama a startService.
  2. La operación en el servicio comienza y envía a la actividad un mensaje que dice que comenzó
  3. La actividad procesa el mensaje y muestra un progreso.
  4. El servicio finaliza la operación y envía algunos datos a su actividad.
  5. Su actividad procesa los datos y los coloca en una vista de lista
  6. El servicio le envía un mensaje diciendo que está hecho y se suicida.
  7. La actividad obtiene el mensaje de finalización y oculta el diálogo de progreso.

Sé que mencionaste que no querías una base de código, pero la aplicación de código abierto Google I / O 2010 utiliza un servicio de la manera que describo.

Actualizado para agregar código de muestra:

La actividad.

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

El servicio:

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

Extensión ResultReceiver: editada para implementar 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);
        }
    }
}
Estanque Robby
fuente
1
¡Excelente gracias! Terminé revisando la aplicación iosched de Google y wow ... es bastante complejo, pero tengo algo que funciona, ¡ahora solo necesito averiguar por qué funciona! Pero sí, este es el patrón básico que tengo trabajando. muchas gracias.
Martyn
29
Una pequeña adición a la respuesta: como lo hace el mReceiver.setReceiver (nulo); en el método onPause, debe hacer mReceiver.setReceiver (this); en el método onResume. De lo contrario, es posible que no reciba los eventos si su actividad se reanuda sin ser recreado
Vincent Mimoun-Prat
77
¿No dicen los documentos que no tiene que llamar a stopSelf, ya que IntentService lo hace por usted?
Mikael Ohlson
2
@MikaelOhlson correcta, usted debe no llamar stopSelfsi subclase IntentServiceporque si lo hace, perderá todas las solicitudes pendientes a la misma IntentService.
quietmint
1
IntentServicese suicidará cuando se complete la tarea, por lo que this.stopSelf()es innecesario.
Euporie
17

El desarrollo de aplicaciones cliente REST de Android ha sido un recurso increíble para mí. El orador no muestra ningún código, solo repasa las consideraciones de diseño y las técnicas para armar una Rest Api sólida en Android. Si eres un podcast un poco o no, te recomiendo que escuches este al menos una vez, pero personalmente lo he escuchado 4 o cinco veces hasta ahora y probablemente lo volveré a escuchar.

Desarrollo de aplicaciones cliente REST de Android
Autor: Virgil Dobjanschi
Descripción:

Esta sesión presentará consideraciones arquitectónicas para desarrollar aplicaciones RESTful en la plataforma Android. Se centra en patrones de diseño, integración de plataformas y problemas de rendimiento específicos de la plataforma Android.

Y hay tantas consideraciones que realmente no había hecho en la primera versión de mi API que tuve que refactorizar

Terrance
fuente
44
+1 Contiene el tipo de consideraciones en las que nunca pensarías cuando comienzas.
Thomas Ahle
Sí, mi primera incursión en el desarrollo de un cliente Rest fue casi exactamente su descripción de lo que no debía hacer (Afortunadamente me di cuenta de que mucho de esto estaba mal antes de ver esto). Me rio un poco por eso.
Terrance
He visto este video más de una vez y estoy implementando el segundo patrón. Mi problema es que necesito usar transacciones en mi modelo de base de datos complejo para actualizar datos locales a partir de datos nuevos que provienen del servidor, y la interfaz ContentProvider no me proporciona una forma de hacerlo. ¿Tienes alguna sugerencia, Terrance?
Flávio Faria
2
NB: Los comentarios de Dobjanschi sobre HttpClient ya no se mantienen. Ver stackoverflow.com/a/15524143/939250
Donal Lafferty
Sí, ahora se prefiere HttpURLConnection . Además, con el lanzamiento de Android 6.0, el soporte para Apache HTTP Client se ha eliminado oficialmente .
RonR
16

Además, cuando llego a la publicación (Config.getURL ("inicio de sesión"), valores), la aplicación parece detenerse por un tiempo (parece extraño: ¡la idea detrás de un servicio es que se ejecuta en un hilo diferente!)

No, tiene que crear un subproceso usted mismo, un servicio local se ejecuta en el subproceso de la interfaz de usuario de forma predeterminada.

Soumya Simanta
fuente
5

Solo quería señalarles a todos en la dirección de una clase independiente que hice rodar que incorpora toda la funcionalidad.

http://github.com/StlTenny/RestService

Ejecuta la solicitud como no bloqueante y devuelve los resultados en un controlador fácil de implementar. Incluso viene con un ejemplo de implementación.

StlTenny
fuente
4

Digamos que quiero iniciar el servicio en un evento: onItemClicked () de un botón. El mecanismo del receptor no funcionaría en ese caso porque: -
a) pasé el receptor al servicio (como en Intent extra) desde onItemClicked ()
b) La actividad se mueve al fondo. En onPause () configuré la referencia del receptor dentro del ResultReceiver como nula para evitar filtrar la Actividad.
c) La actividad se destruye.
d) La actividad se vuelve a crear. Sin embargo, en este punto, el Servicio no podrá realizar una devolución de llamada a la Actividad ya que se pierde esa referencia del receptor.
El mecanismo de una transmisión limitada o un PendingIntent parece ser más útil en tales escenarios; consulte Notificar actividad del servicio

Nikhil_Katre
fuente
1
Hay un problema con lo que estás diciendo. es decir, cuando la actividad se mueve al fondo no se destruye ... por lo que el receptor todavía existe y el contexto de la actividad también.
DArkO
@DArkO Cuando una actividad está en pausa o detenida, el sistema Android la puede matar en situaciones de poca memoria. Consulte Ciclo de vida de la actividad .
jk7
4

Tenga en cuenta que la solución de Robby Pond es de alguna manera faltante: de esta manera solo permite hacer una llamada API a la vez, ya que IntentService solo maneja una intención a la vez. A menudo, desea realizar llamadas de API paralelas. Si desea hacer esto, debe extender el Servicio en lugar de IntentService y crear su propio hilo.

TjerkW
fuente
1
Todavía puede hacer varias llamadas en el IntentService delegando las llamadas API del servicio web a un ejecutor-thread-service, presente como una variable miembro de su clase derivada de IntentService
Viren
2

Además, cuando llego a la publicación (Config.getURL ("inicio de sesión"), valores), la aplicación parece detenerse por un tiempo (parece extraño: ¡la idea detrás de un servicio es que se ejecuta en un hilo diferente!)

En este caso, es mejor usar asynctask, que se ejecuta en un subproceso diferente y devuelve el resultado al subproceso ui al finalizar.

Aakash
fuente
2

Aquí hay otro enfoque que básicamente le ayuda a olvidarse de toda la gestión de las solicitudes. Se basa en un método de cola asíncrona y una respuesta basada en llamada / devolución de llamada. La principal ventaja es que al usar este método podrá hacer que todo el proceso (solicitud, obtener y analizar la respuesta, sabe a db) sea completamente transparente para usted. Una vez que obtenga el código de respuesta, el trabajo ya está hecho. Después de eso, solo necesita hacer una llamada a su base de datos y listo. También ayuda con la problemática de lo que sucede cuando su actividad no está activa. Lo que sucederá aquí es que tendrá todos sus datos guardados en su base de datos local, pero la respuesta no será procesada por su actividad, esa es la forma ideal.

Escribí sobre un enfoque general aquí http://ugiagonzalez.com/2012/07/02/theres-life-after-asynctasks-in-android/

Pondré un código de muestra específico en las próximas publicaciones. Espero que ayude, no dude en ponerse en contacto conmigo para compartir el enfoque y resolver posibles dudas o problemas.

Jose L Ugia
fuente
Enlace muerto El dominio ha expirado.
jk7
1

Robby ofrece una excelente respuesta, aunque puedo ver que aún buscas más información. Implementé llamadas REST api de manera fácil PERO incorrecta. No fue hasta que vi este video de Google I / O que entendí dónde me equivoqué. No es tan simple como armar una AsyncTask con una llamada get / put HttpUrlConnection.

Andrew Halloran
fuente
Enlace muerto Aquí está el actualizado - Google I / O 2010 - Aplicaciones de cliente REST de Android
zim