Reimpresso de http://blog.joanzapata.com/robust-architecture-for-an-android-app/
Arquitetura robusta e legível para um aplicativo Android
Desde os primeiros dias do Android, tenho procurado uma maneira robusta de construir aplicativos Android, manter as operações de IO fora do UI Thread, evitar chamadas de rede duplicadas, armazenar coisas relevantes em cache, atualizar o cache no momento certo, etc. .. com a sintaxe mais limpa possível.
Esta postagem do blog não fornecerá uma implementação precisa, mas uma maneira possível de estruturar um aplicativo com um bom equilíbrio entre flexibilidade, legibilidade e robustez.
Algumas soluções existentes
No início do Android, a maioria das pessoas confiava no AsyncTasks para processos de longa execução. Basicamente: foi uma droga, já tem muitos artigos sobre esse assunto. Mais tarde, o Honeycomb introduziu Loaders que eram melhores para suportar alterações de configuração. Em 2012, foi lançado o RobospiceService
, baseado em um Android rodando em segundo plano. Este infográfico mostra como funciona.
É ótimo comparado a AsyncTask
, mas ainda tenho alguns problemas com ele. Aqui está o código médio para fazer uma solicitação com o Robospice, em uma atividade. Não há necessidade de lê-lo com precisão, é apenas para se ter uma ideia:
FollowersRequest request = new FollowersRequest(user);
lastRequestCacheKey = request.createCacheKey();
spiceManager.execute(request, lastRequestCacheKey,
DurationInMillis.ONE_MINUTE,
new RequestListener<FollowerList> {
@Override
public void onRequestSuccess(FollowerList listFollowers) {
// On failure
}
@Override
public void onRequestFailure(SpiceException e) {
// On success
}
});
E a solicitação que o acompanha:
public class FollowersRequest extends SpringAndroidSpiceRequest<FollowerList> {
private String user;
public FollowersRequest(String user) {
super(FollowerList.class);
this.user = user;
}
@Override
public FollowerList loadDataFromNetwork() throws Exception {
String url = format("https://api.github.com/users/%s/followers", user);
return getRestTemplate().getForObject(url, FollowerList.class);
}
public String createCacheKey() {
return "followers." + user;
}
}
Problemas
- Este código parece horrível e você tem que fazer isso para cada solicitação!
- Você precisa criar uma
SpiceRequest
subclasse para cada tipo de solicitação - Você precisa criar um
RequestListener
para cada solicitação - Se o cache expirar em breve, o usuário terá que esperar a cada chamada
- Se o cache expirar após um longo período, o usuário poderá ver dados obsoletos
- Mantém
RequestListener
uma referência implícita à atividade, e quanto a vazamentos de memória?
Não tão bom…
Conciso e robusto em cinco etapas
Quando comecei a trabalhar na Candyshop , tentei outra coisa. Misturei diferentes bibliotecas com funcionalidades muito interessantes e tentei fazer algo conciso mas robusto.
- AndroidAnnotations , para @Background , @EBean , etc...
- Spring RestTemplate para chamadas de rede REST, bem integrado com AndroidAnnotations
- SnappyDB para armazenamento rápido em disco de objetos Java
- EventBus como um barramento de eventos
Aqui está um esquema global do que explicarei nas próximas partes.
Passo 1 — Um sistema de cache fácil de usar
Você precisará de um sistema de cache persistente. Mantenha simples.
@EBean
public class Cache {
public static enum CacheKey { USER, CONTACTS, ... }
public <T> T get(CacheKey key, Class<T> returnType) { ... }
public void put(CacheKey key, Object value) { ... }
}
Passo 2 — Um cliente REST
Dou isso como exemplo, apenas certifique-se de que a lógica da API REST que você está usando permaneça em um só lugar.
@Rest(rootUrl = "http://anything.com")
public interface CandyshopApi {
@Get("/api/contacts/")
ContactsWrapper fetchContacts();
@Get("/api/user/")
User fetchUser();
}
Passo 3 — Um barramento de eventos para todo o aplicativo
Instancie-o em um local estratégico, acessível de qualquer lugar do app, o Application
objeto é um bom candidato para isso.
public class CandyshopApplication extends Application {
public final static EventBus BUS = new EventBus();
...
}
Passo 4 — Uma atividade que precisa de alguns dados!
Minha solução é, como o Robospice, baseada em um serviço, mas não em Android. Um objeto singleton normal, compartilhado pelo aplicativo. Veremos o código desse serviço no passo 5. Mas agora vamos ver como fica o código da Activity, porque era isso que eu mais queria simplificar em primeiro lugar!
@EActivity(R.layout.activity_main)
public class MainActivity extends Activity {
// Inject the service
@Bean protected AppService appService;
// Once everything is loaded…
@AfterViews public void afterViews() {
// … request the user and his contacts (returns immediately)
appService.getUser();
appService.getContacts();
}
/*
The result of the previous calls will
come as events through the EventBus.
We'll probably update the UI, so we
need to use @UiThread.
*/
@UiThread public void onEvent(UserFetchedEvent e) {
...
}
@UiThread public void onEvent(ContactsFetchedEvent e) {
...
}
// Register the activity in the event bus when it starts
@Override protected void onStart() {
super.onStart();
BUS.register(this);
}
// Unregister it when it stops
@Override protected void onStop() {
super.onStop();
BUS.unregister(this);
}
}
Uma linha para solicitar ao usuário, uma linha para expressar o fato de que receberemos uma resposta para essa solicitação. A mesma coisa para contatos. Soa muito bem!
Passo 5 — Um serviço singleton
Como disse na etapa 4, o serviço que estou usando não é um serviço Android. Na verdade, comecei com um, mas mudei de ideia. A razão é a simplicidade . Os serviços devem ser usados quando você precisa ter algo em execução enquanto nenhuma atividade é exibida ou quando deseja disponibilizar algum código para outros aplicativos. Não era exatamente isso que eu queria. Usar um singleton simples me permite evitar o uso de ServiceConnection
, Binder
, etc ...
Há muitas coisas a dizer aqui. Vamos começar com um esquema para mostrar o que acontece quando chamamos getUser()
e getContacts()
da Activity. Então explicarei o código.
Você pode imaginar que cada série é um thread.
O que você vê aqui é o que eu realmente gosto neste modelo; a visualização é imediatamente preenchida com dados armazenados em cache, portanto, na maioria das vezes, o usuário não precisa esperar . Então, quando o resultado atualizado chega do servidor, as informações exibidas são substituídas. A contrapartida disso é que você precisa garantir que a atividade possa receber o mesmo tipo de resposta várias vezes. Tenha isso em mente ao criar a atividade e você ficará bem.
Ok, vamos ver algum código!
// As I said, a simple class, with a singleton scope
@EBean(scope = EBean.Scope.Singleton)
public class AppService {
// (Explained later)
public static final String NETWORK = "NETWORK";
public static final String CACHE = "CACHE";
// Inject the cache (step 1)
@Bean protected Cache cache;
// Inject the rest client (step 2)
@RestService protected CandyshopApi candyshopApi;
// This is what the activity calls, it's public
@Background(serial = CACHE)
public void getContacts() {
// Try to load the existing cache
ContactsFetchedEvent cachedResult =
cache.get(KEY_CONTACTS, ContactsFetchedEvent.class);
// If there's something in cache, send the event
if (cachedResult != null) BUS.post(cachedResult);
// Then load from server, asynchronously
getContactsAsync();
}
@Background(serial = NETWORK)
private void getContactsAsync() {
// Fetch the contacts (network access)
ContactsWrapper contacts = candyshopApi.fetchContacts();
// Create the resulting event
ContactsFetchedEvent event = new ContactsFetchedEvent(contacts);
// Store the event in cache (replace existing if any)
cache.put(KEY_CONTACTS, event);
// Post the event
BUS.post(event);
}
}
É muito código para uma única solicitação! Na verdade, eu explodi para torná-lo mais explicativo, mas é sempre o mesmo padrão para que você possa criar facilmente auxiliares para criar métodos de linha única. Por exemplo getUser()
ficaria assim:
@Background(serial = CACHE)
public void getUser() {
postIfPresent(KEY_USER, UserFetchedEvent.class);
getUserAsync();
}
@Background(serial = NETWORK)
private void getUserAsync() {
cacheThenPost(KEY_USER, new UserFetchedEvent(candyshopApi.fetchUser()));
}
Então e a serial
coisa? Aqui está o que o documento diz:
Por padrão, todos
@Background
os métodos anotados são executados em paralelo. É garantido que dois métodos que usam o mesmoserial
sejam executados no mesmo thread, sequencialmente (ou seja, um após o outro).
Executar chamadas de rede uma após a outra pode ter impactos no desempenho, mas é muito mais fácil lidar com coisas do tipo GET-after-POST, que estou pronto para sacrificar um pouco de desempenho. Além disso, você pode ajustar facilmente as séries posteriormente para melhorar o desempenho se notar alguma coisa. Atualmente na Candyshop, uso quatro seriais diferentes.
Concluir
A solução que descrevi aqui é um rascunho, é a ideia básica com a qual comecei há alguns meses. Até hoje, consegui resolver todos os casos específicos que encontrei e estou gostando muito de trabalhar com isso até agora. Há algumas outras coisas incríveis que gostaria de compartilhar sobre esse modelo, como gerenciamento de erros , expiração de cache , solicitações POST , cancelamento de operações inúteis , mas estou muito grato por você ter lido até agora, então não vou forçar!