Serviço API repousante

226

Estou procurando fazer um serviço que possa ser usado para fazer chamadas para uma API REST baseada na Web.

Basicamente, eu quero iniciar um serviço no init do aplicativo e desejo solicitar ao serviço que solicite um URL e retorne os resultados. Enquanto isso, quero poder exibir uma janela de progresso ou algo semelhante.

Atualmente, criei um serviço que usa IDL. Li em algum lugar que você realmente só precisa disso para comunicação entre aplicativos. Portanto, pense nelas, mas sem saber como fazer retornos de chamada sem ele. Além disso, quando acertei o post(Config.getURL("login"), values)aplicativo, ele parou por um tempo (parece estranho - a idéia por trás de um serviço era que ele roda em um segmento diferente!)

Atualmente, tenho um serviço com métodos post e http dentro, alguns arquivos AIDL (para comunicação bidirecional), um ServiceManager que lida com iniciar, parar, vincular etc ao serviço e estou criando dinamicamente um manipulador com código específico para os retornos de chamada, conforme necessário.

Não quero que ninguém me dê uma base de código completa para trabalhar, mas alguns indicadores serão muito apreciados.

Código (na maior parte) 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) {...}
};

Alguns arquivos AIDL:

package com.something.android

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

e

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

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

e o gerente de serviço:

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

Serviço init e bind:

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

chamada de função de serviço:

// 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
fonte
5
Isso pode ser muito útil para pessoas que aprendem a implementação do cliente REST Android. A apresentação de Dobjanschi transcrita para um PDF: drive.google.com/file/d/0B2dn_3573C3RdlVpU2JBWXdSb3c/…
Kay Zed
Como muitas pessoas recomendaram a apresentação de Virgil Dobjanschi e o link para o IO 2010 está quebrado agora, aqui está o link direto para o vídeo do YouTube: youtube.com/watch?v=xHXn3Kg2IQE
Jose_GD

Respostas:

283

Se o seu serviço fará parte do seu aplicativo, você estará tornando-o muito mais complexo do que precisa. Como você tem um caso de uso simples de obter alguns dados de um serviço da Web RESTful, deve procurar em ResultReceiver e IntentService .

Esse padrão Service + ResultReceiver funciona iniciando ou vinculando o serviço com startService () quando você deseja executar alguma ação. Você pode especificar a operação para executar e transmitir seu ResultReceiver (a atividade) pelos extras no Intent.

No serviço você implementa onHandleIntent para executar a operação especificada no Intent. Quando a operação é concluída, você utiliza o ResultReceiver transmitido para enviar uma mensagem de volta à Atividade, na qual o ponto emReceiveResult será chamado.

Por exemplo, você deseja extrair alguns dados do seu serviço da Web.

  1. Você cria a intenção e chama startService.
  2. A operação no serviço é iniciada e envia à atividade uma mensagem dizendo que foi iniciada
  3. A atividade processa a mensagem e mostra um progresso.
  4. O serviço termina a operação e envia alguns dados de volta para sua atividade.
  5. Sua atividade processa os dados e coloca em uma exibição de lista
  6. O serviço envia uma mensagem dizendo que está pronto e se mata.
  7. A atividade recebe a mensagem de conclusão e oculta a caixa de diálogo de progresso.

Sei que você mencionou que não queria uma base de código, mas o aplicativo Google I / O 2010 de código aberto usa um serviço dessa maneira que estou descrevendo.

Atualizado para adicionar código de exemplo:

A atividade.

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

O serviço:

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

Extensão 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);
        }
    }
}
Robby Pond
fonte
1
Excelente - obrigado! Acabei passando pelo aplicativo iosched do Google e uau ... é bem complexo, mas tenho algo que está funcionando, agora só preciso descobrir por que funciona! Mas sim, esse é o padrão básico que tenho trabalhando. Muito obrigado.
Martyn
29
Uma pequena adição à resposta: como você faz o mReceiver.setReceiver (null); no método onPause, você deve executar o mReceiver.setReceiver (this); no método onResume. Senão você pode não receber os eventos se sua atividade é retomada sem ser re-criado
Vincent Mimoun-Prat
7
Os documentos não dizem que você não precisa chamar stopSelf, pois o IntentService faz isso por você?
Mikael Ohlson
2
@MikaelOhlson Correto, você não deve ligar stopSelfse subclassificar IntentServiceporque, se o fizer, perderá solicitações pendentes para o mesmo IntentService.
quietmint
1
IntentServicese matará quando a tarefa estiver concluída, por isso this.stopSelf()é desnecessário.
Euporie
17

O desenvolvimento de aplicativos clientes REST Android tem sido um recurso incrível para mim. O alto-falante não mostra nenhum código, ele apenas repassa as considerações e técnicas de design para montar um Rest Api sólido no Android. Se você é um podcast ou não, recomendo ouvir este pelo menos uma vez, mas, pessoalmente, já o ouvi quatro ou cinco vezes até agora e provavelmente vou ouvi-lo novamente.

Desenvolvendo aplicativos cliente REST Android
Autor: Virgil Dobjanschi
Descrição:

Esta sessão apresentará considerações arquitetônicas para o desenvolvimento de aplicativos RESTful na plataforma Android. Ele se concentra nos padrões de design, integração de plataforma e problemas de desempenho específicos da plataforma Android.

E há tantas considerações que eu realmente não tinha feito na primeira versão da minha API que tive que refatorar

Terrance
fonte
4
+1 Contém os tipos de considerações em que você nunca pensaria quando começou.
Thomas Ahle
Sim, minha primeira incursão no desenvolvimento de um cliente Rest foi quase exatamente sua descrição do que exatamente não fazer (felizmente percebi que muito disso estava errado antes de assistir a isso). Eu disse isso um pouco.
Terrance
Eu já vi esse vídeo mais de uma vez e estou implementando o segundo padrão. Meu problema é que preciso usar transações no meu modelo de banco de dados complexo para atualizar dados locais de novos dados provenientes do servidor, e a interface ContentProvider não fornece uma maneira de fazê-lo. Você tem alguma sugestão, Terrance?
Flávio Faria
2
NB: Os comentários de Dobjanschi no HttpClient não são mais válidos. Veja stackoverflow.com/a/15524143/939250
Donal Lafferty
Sim, agora é preferível HttpURLConnection . Além disso, com o lançamento do Android 6.0, o suporte ao Apache HTTP Client foi oficialmente removido .
RonR
16

Além disso, quando acertei a postagem (Config.getURL ("login"), valores), o aplicativo parece pausar por um tempo (parece estranho - pensei que a idéia por trás de um serviço era que ele fosse executado em um segmento diferente!)

Não, você precisa criar um encadeamento, um serviço local é executado no encadeamento da interface do usuário por padrão.

Soumya Simanta
fonte
5

Só queria apontar todos vocês na direção de uma classe autônoma que eu rolei que incorpora toda a funcionalidade.

http://github.com/StlTenny/RestService

Ele executa a solicitação como não-bloqueante e retorna os resultados em um manipulador fácil de implementar. Mesmo vem com um exemplo de implementação.

StlTenny
fonte
4

Digamos que eu queira iniciar o serviço em um evento - onItemClicked () de um botão. O mecanismo Receiver não funcionaria nesse caso porque: -
a) Passei o Receiver para o serviço (como no Intent extra) de onItemClicked ()
b) A atividade se move em segundo plano. Em onPause (), defino a referência do receptor no ResultReceiver como null para evitar vazamento da atividade.
c) A atividade é destruída.
d) A atividade é criada novamente. No entanto, neste ponto, o Serviço não poderá fazer um retorno de chamada para a Atividade, pois a referência do receptor será perdida.
O mecanismo de uma transmissão limitada ou de um PendingIntent parece ser mais útil nesses cenários - consulte Notificar atividade do serviço

Nikhil_Katre
fonte
1
há um problema com o que você está dizendo. isto é, quando a atividade passa para o fundo, ela não é destruída ... então o receptor ainda existe e o contexto da atividade também.
DArkO
@DArkO Quando uma Atividade é pausada ou parada, ela pode ser eliminada pelo sistema Android em situações de pouca memória. Consulte Ciclo de vida da atividade .
Jk7
4

Observe que a solução da Robby Pond está faltando de alguma forma: dessa maneira, você só permite fazer uma chamada de API por vez, pois o IntentService lida apenas com uma intenção de cada vez. Muitas vezes, você deseja executar chamadas paralelas da API. Se você quiser fazer isso, precisará estender o Serviço em vez do IntentService e criar seu próprio encadeamento.

TjerkW
fonte
1
Você ainda pode fazer várias chamadas no IntentService, delegando as chamadas webservice API para um thread de serviço executor, presente como uma variável de membro de sua classe IntentService derivado
Viren
2

Além disso, quando acertei a postagem (Config.getURL ("login"), valores), o aplicativo parece pausar por um tempo (parece estranho - pensei que a idéia por trás de um serviço era que ele fosse executado em um segmento diferente!)

Nesse caso, é melhor usar o asynctask, que é executado em um thread diferente e retorna o resultado de volta ao thread da interface do usuário após a conclusão.

Aakash
fonte
2

Há outra abordagem aqui, que basicamente ajuda você a esquecer todo o gerenciamento das solicitações. Ele é baseado em um método de fila assíncrona e em uma resposta baseada em chamada / retorno de chamada. A principal vantagem é que, usando esse método, você poderá tornar todo o processo (solicitar, obter e analisar respostas, sabe para db) completamente transparente para você. Depois de obter o código de resposta, o trabalho já está concluído. Depois disso, você só precisa fazer uma chamada para o seu banco de dados e pronto. Também ajuda na problemática do que acontece quando sua atividade não está ativa. O que acontecerá aqui é que você terá todos os seus dados salvos no banco de dados local, mas a resposta não será processada pela sua atividade, é o caminho ideal.

Eu escrevi sobre uma abordagem geral aqui http://ugiagonzalez.com/2012/07/02/theres-life-after-asynctasks-in-android/

Colocarei um exemplo de código específico nas próximas postagens. Espero que ajude, sinta-se à vontade para entrar em contato comigo para compartilhar a abordagem e solucionar possíveis dúvidas ou problemas.

Jose L Ugia
fonte
Link morto. O domínio expirou.
Jk7
1

Robby fornece uma ótima resposta, embora eu possa ver você ainda procurando mais informações. Eu implementei api REST chama o caminho fácil, mas errado. Não foi até assistir a este vídeo de E / S do Google que eu entendi onde errei. Não é tão simples quanto montar um AsyncTask com uma chamada get / put HttpUrlConnection.

Andrew Halloran
fonte
Link morto. Aqui está actualizada - Google I / O 2010 - aplicativos cliente REST Android
zim