Conception d'un framework de requête réseau performant pour Android (Retrofit + RxJava + Glide)
Ce chapitre détaille la conception et l'implémentation d'un framework de réseau robuste et performant pour les applications Android, en tirant parti de bibliothèques éprouvées telles que Retrofit, RxJava et Glide.
- Intégration de Retrofit pour les requêtes réseau
1.1 Présentation de Retrofit
Retrofit est une bibliothèque de conversion de type pour Android et Java, développée par Square, qui simplifie grandement les appels réseau RESTful. Elle transforme les appels API REST en interfaces Java, rendant le code plus propre et plus facile à maintenir.
1.2 Principes RESTful
Une compréhension des principes RESTful est essentielle pour concevoir une API web efficace. Il est recommandé de consulter des ressources telles que l'article de ruanyifeng.com sur les API RESTful.
1.3 Mise en place des dépendances
L'intégration de Retrofit et de ses dépendances peut se faire en ajoutant les lignes suivantes au fichier build.gradle de votre module d'application :
implementation 'com.squareup.okio:okio:1.13.0'
implementation 'com.squareup.okhttp3:okhttp:3.8.1'
implementation 'com.squareup.retrofit2:retrofit:2.3.0'
// Convertisseur pour les réponses scalaires (ex: String)
implementation 'com.squareup.retrofit2:converter-scalars:2.3.0'
// Pour gérer les réponses JSON, RxJava, etc. (non détaillé ici)
// implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
// implementation 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
1.4 Conception de l'interface de service réseau
Le pattern Builder est idéal pour la construction d'objets complexes comme notre client réseau. Nous définissons une interface RestService qui encapsule les différentes méthodes HTTP supportées par Retrofit.
L'interface RestService se présente comme suit :
package com.flj.latte.net;
import java.util.WeakHashMap;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.DELETE;
import retrofit2.http.FieldMap;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.PUT;
import retrofit2.http.Part;
import retrofit2.http.QueryMap;
import retrofit2.http.Streaming;
import retrofit2.http.Url;
public interface RestService {
@GET
Call<string> get(@Url String url, @QueryMap WeakHashMap<string object=""> params);
@FormUrlEncoded
@POST
Call<string> post(@Url String url, @FieldMap WeakHashMap<string object=""> params);
@POST
Call<string> postRaw(@Url String url, @Body RequestBody body);
@FormUrlEncoded
@PUT
Call<string> put(@Url String url, @FieldMap WeakHashMap<string object=""> params);
@PUT
Call<string> putRaw(@Url String url, @Body RequestBody body);
@DELETE
Call<string> delete(@Url String url, @QueryMap WeakHashMap<string object=""> params);
@Streaming // Pour le téléchargement de gros fichiers sans saturer la mémoire
@GET
Call<responsebody> download(@Url String url, @QueryMap WeakHashMap<string object=""> params);
@Multipart
@POST
Call<string> upload(@Url String url, @Part MultipartBody.Part file);
}
</string></string></responsebody></string></string></string></string></string></string></string></string></string></string>
La création de l'instance de RestService est gérée par un RetrofitHolder, qui configure OkHttpClient pour les requêtes.
- Gestion des requêtes et du cycle de vie
2.1 Le pattern Builder pour RestClient
Pour encapsuler la logique des requêtes réseau et gérer les différents paramètres (URL, méthodes HTTP, paramètres, callbacks, indicateur de chargement), nous utilisons le pattern Builder. La classe RestClient est construite via son Builder, assurant que les paramètres sont définis une seule fois.
2.2 Callbacks personnalisés
La gestion des réponses réseau est cruciale. Nous définissons des interfaces pour gérer les différents événements : IRequest (début/fin de requête), ISuccess (succès de la requête), IFailure (échec de la requête), et IError (erreur réseau).
La classe RequestCallbacks implémente l'interface retrofit2.Callback pour gérer ces événements et déclencher les callbacks appropriés.
package com.flj.latte.net.callback;
import android.os.Handler;
import com.flj.latte.app.ConfigKeys;
import com.flj.latte.app.Latte;
import com.flj.latte.net.RestCreator;
import com.flj.latte.ui.loader.LatteLoader;
import com.flj.latte.ui.loader.LoaderStyle;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public final class RequestCallbacks implements Callback<string> {
private final IRequest REQUEST;
private final ISuccess SUCCESS;
private final IFailure FAILURE;
private final IError ERROR;
private final LoaderStyle LOADER_STYLE;
private static final Handler HANDLER = Latte.getHandler(); // Utilisation du Handler global de l'application
public RequestCallbacks(IRequest request, ISuccess success, IFailure failure, IError error, LoaderStyle style) {
this.REQUEST = request;
this.SUCCESS = success;
this.FAILURE = failure;
this.ERROR = error;
this.LOADER_STYLE = style;
}
@Override
public void onResponse(Call<string> call, Response<string> response) {
if (response.isSuccessful() && response.body() != null) {
if (SUCCESS != null) {
SUCCESS.onSuccess(response.body());
}
} else {
if (ERROR != null) {
ERROR.onError(response.code(), response.message());
}
}
onRequestFinish();
}
@Override
public void onFailure(Call<string> call, Throwable t) {
if (FAILURE != null) {
FAILURE.onFailure();
}
// Le callback IRequest.onRequestEnd() est géré dans onRequestFinish()
onRequestFinish();
}
private void onRequestFinish() {
// Afficher le loader seulement si un style est défini
if (LOADER_STYLE != null) {
// Utiliser le Handler pour s'assurer que les opérations UI se font sur le thread principal
HANDLER.post(() -> LatteLoader.stopLoading());
}
if (REQUEST != null) {
REQUEST.onRequestEnd();
}
}
}
</string></string></string></string>
2.3 Optimisation du Builder
Pour simplifier la gestion des paramètres, le RestClientBuilder est optimisé pour permettre une configuration plus fluide. Les paramètres sont préfixés par set et le lancement de la requête se fait via la méthode request().
- Intégration d'un indicateur de chargement (Loader)
3.1 AVLoadingIndicatorView
Nous intégrons la bibliothèque AVLoadingIndicatorView pour fournir une variété d'indicateurs de chargement animés. Elle est disponible sur GitHub.
3.2 Mise en cache des indicateurs
Pour optimiser les performances, au lieu d'utiliser la réflexion à chaque création d'indicateur, nous implémentons un mécanisme de mise en cache. Cela réduit la charge sur le processeur et améliore la fluidité de l'application.
3.3 Gestion des styles d'indicateur
Un enum LoaderStyle est créé pour représenter les différents types d'indicateurs disponibles, offrant une manière typée et sécurisée de sélectionner un style.
package com.flj.latte.ui.loader;
@SuppressWarnings("unused")
public enum LoaderStyle {
BallPulseIndicator,
BallGridPulseIndicator,
BallClipRotateIndicator,
BallClipRotatePulseIndicator,
SquareSpinIndicator,
BallClipRotateMultipleIndicator,
BallPulseRiseIndicator,
BallRotateIndicator,
CubeTransitionIndicator,
BallZigZagIndicator,
BallZigZagDeflectIndicator,
BallTrianglePathIndicator,
BallScaleIndicator,
LineScaleIndicator,
LineScalePartyIndicator,
BallScaleMultipleIndicator,
BallPulseSyncIndicator,
BallBeatIndicator,
LineScalePulseOutIndicator,
LineScalePulseOutRapidIndicator,
BallScaleRippleIndicator,
BallScaleRippleMultipleIndicator,
BallSpinFadeLoaderIndicator,
LineSpinFadeLoaderIndicator,
TriangleSkewSpinIndicator,
PacmanIndicator,
BallGridBeatIndicator,
SemiCircleSpinIndicator,
CustomIndicator // Pour les indicateurs personnalisés
}
3.4 Configuration et affichage du Loader
Le LatteLoader gère l'affichage et la dissimulation des indicateurs. Il prend en charge la configuration de la transparence, de la couleur et de la mise à l'échelle pour s'adapter à différentes tailles d'écran. L'utilisation de dialog.cancel() permet une meilleure gestion de la fermeture du dialogue.
3.5 Intégration dans les requêtes réseau
L'indicateur de chargement est désormais intégré au processus de requête réseau. Il s'affiche avant le début de la requête et se dissimule à la fin, que la requête réussisse, échoue ou génère une erreur. Cela améliore l'expérience utilisateur en fournissant un retour visuel.
- Fonctionnalités avancées du framework réseau
4.1 Support des requêtes POST/PUT brutes
Le framwork a été étendu pour supporter les requêtes POST et PUT avec un corps de requête brut (RequestBody), utile pour envoyer des données JSON directement.
4.2 Implémentation de la fonction d'upload
Une méthode upload a été ajoutée pour permettre le transfert de fichiers via des requêtes POST multipart.
- Gestion du téléchargement de fichiers
5.1 Conception et implémentation
La gestion du téléchargement de fichiers est une fonctionnalité complexe. Elle implique la gestion des flux de données, l'écriture sur le stockage externe et la mise à jour de la galerie système.
La classe utilitaire FileUtil fournit des méthodes pour créer des fichiers, gérer les formats MIME, et écrire des flux d'entrée dans des fichiers sur le stockage externe.
package com.flj.latte.util.file;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.webkit.MimeTypeMap;
import android.widget.TextView;
import com.flj.latte.app.Latte;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public final class FileUtil {
private static final String TIME_FORMAT = "_yyyyMMdd_HHmmss";
private static final String SDCARD_DIR = Environment.getExternalStorageDirectory().getPath();
public static final String UPLOAD_PHOTO_DIR = SDCARD_DIR + "/a_upload_photos/";
public static final String WEB_CACHE_DIR = SDCARD_DIR + "/app_web_cache/";
public static final String CAMERA_PHOTO_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getPath() + "/Camera/";
private static String getTimeFormatName(String timeFormatHeader) {
final Date date = new Date(System.currentTimeMillis());
final SimpleDateFormat dateFormat = new SimpleDateFormat("'" + timeFormatHeader + "'" + TIME_FORMAT, Locale.getDefault());
return dateFormat.format(date);
}
public static String getFileNameByTime(String timeFormatHeader, String extension) {
return getTimeFormatName(timeFormatHeader) + "." + extension;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private static File createDir(String sdcardDirName) {
final String dir = SDCARD_DIR + "/" + sdcardDirName + "/";
final File fileDir = new File(dir);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
return fileDir;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
public static File createFile(String sdcardDirName, String fileName) {
return new File(createDir(sdcardDirName), fileName);
}
private static File createFileByTime(String sdcardDirName, String timeFormatHeader, String extension) {
final String fileName = getFileNameByTime(timeFormatHeader, extension);
return createFile(sdcardDirName, fileName);
}
public static String getMimeType(String filePath) {
final String extension = getExtension(filePath);
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
}
public static String getExtension(String filePath) {
String suffix = "";
final File file = new File(filePath);
final String name = file.getName();
final int idx = name.lastIndexOf('.');
if (idx > 0) {
suffix = name.substring(idx + 1);
}
return suffix;
}
public static File saveBitmap(Bitmap mBitmap, String dir, int compress) {
final String sdStatus = Environment.getExternalStorageState();
if (!sdStatus.equals(Environment.MEDIA_MOUNTED)) {
return null;
}
FileOutputStream fos = null;
BufferedOutputStream bos = null;
File fileName = createFileByTime(dir, "DOWN_LOAD", "jpg");
try {
fos = new FileOutputStream(fileName);
bos = new BufferedOutputStream(fos);
mBitmap.compress(Bitmap.CompressFormat.JPEG, compress, bos);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
if (bos != null) bos.flush();
if (bos != null) bos.close();
if (fos != null) fos.flush();
if (fos != null) fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
refreshDCIM();
return fileName;
}
public static File writeToDisk(InputStream is, String dir, String name) {
final File file = FileUtil.createFile(dir, name);
BufferedInputStream bis = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(is);
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
byte data[] = new byte[1024 * 4];
int count;
while ((count = bis.read(data)) != -1) {
bos.write(data, 0, count);
}
bos.flush();
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bos != null) bos.close();
if (fos != null) fos.close();
if (bis != null) bis.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
public static File writeToDisk(InputStream is, String dir, String prefix, String extension) {
final File file = FileUtil.createFileByTime(dir, prefix, extension);
BufferedInputStream bis = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try {
bis = new BufferedInputStream(is);
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos);
byte data[] = new byte[1024 * 4];
int count;
while ((count = bis.read(data)) != -1) {
bos.write(data, 0, count);
}
bos.flush();
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (bos != null) bos.close();
if (fos != null) fos.close();
if (bis != null) bis.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return file;
}
private static void refreshDCIM() {
if (Build.VERSION.SDK_INT >= 19) {
MediaScannerConnection.scanFile(Latte.getApplicationContext(),
new String[]{Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getPath()},
null, null);
} else {
Latte.getApplicationContext().sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://" +
Environment.getExternalStorageDirectory())));
}
}
public static String getRawFile(int id) {
final InputStream is = Latte.getApplicationContext().getResources().openRawResource(id);
final BufferedInputStream bis = new BufferedInputStream(is);
final InputStreamReader isr = new InputStreamReader(bis);
final BufferedReader br = new BufferedReader(isr);
final StringBuilder stringBuilder = new StringBuilder();
String str;
try {
while ((str = br.readLine()) != null) {
stringBuilder.append(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
br.close();
isr.close();
bis.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return stringBuilder.toString();
}
public static void setIconFont(String path, TextView textView) {
final Typeface typeface = Typeface.createFromAsset(Latte.getApplicationContext().getAssets(), path);
textView.setTypeface(typeface);
}
public static String getAssetsFile(String name) {
InputStream is = null;
BufferedInputStream bis = null;
InputStreamReader isr = null;
BufferedReader br = null;
StringBuilder stringBuilder = null;
final AssetManager assetManager = Latte.getApplicationContext().getAssets();
try {
is = assetManager.open(name);
bis = new BufferedInputStream(is);
isr = new InputStreamReader(bis);
br = new BufferedReader(isr);
stringBuilder = new StringBuilder();
String str;
while ((str = br.readLine()) != null) {
stringBuilder.append(str);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) br.close();
if (isr != null) isr.close();
if (bis != null) bis.close();
if (is != null) is.close();
assetManager.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return stringBuilder != null ? stringBuilder.toString() : null;
}
public static String getRealFilePath(final Context context, final Uri uri) {
if (null == uri) return null;
final String scheme = uri.getScheme();
String data = null;
if (ContentResolver.SCHEME_FILE.equals(scheme)) {
data = uri.getPath();
} else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
final Cursor cursor = context.getContentResolver().query(uri, new String[]{MediaStore.Images.ImageColumns.DATA}, null, null, null);
if (null != cursor) {
if (cursor.moveToFirst()) {
final int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
if (index > -1) {
data = cursor.getString(index);
}
}
cursor.close();
}
}
return data;
}
}
La classe DownloadHandler orchestre le processus de téléchargement en utilisant ces utilitaires.
- Intercepteurs pour la simulation de serveur
6.1 Configuration des intercepteurs
OkHttp permet d'ajouter des intercepteurs pour intercepter et modifier les requêtes et les réponses. Nous utilisons cette fonctionnalité pour simuler un serveur backend.
6.2 Initialisation et utilisation
Un intercepteur de débogage (DebugInterceptor) est configuré pour intercepter des URL spécifiques et renvoyer des données JSON prédéfinies stockées dans les ressources res/raw.
L'intercepteur DebugInterceptor :
package com.flj.latte.net.interceptors;
import android.support.annotation.NonNull;
import android.support.annotation.RawRes;
import com.flj.latte.util.file.FileUtil;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class DebugInterceptor extends BaseInterceptor {
private final String debugUrl;
private final int debugRawId;
public DebugInterceptor(String debugUrl, @RawRes int rawId) {
this.debugUrl = debugUrl;
this.debugRawId = rawId;
}
private Response createResponse(Chain chain, String json) {
return new Response.Builder()
.code(200)
.addHeader("Content-Type", "application/json")
.body(ResponseBody.create(MediaType.parse("application/json"), json))
.message("OK")
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.build();
}
private Response debugResponse(Chain chain, @RawRes int rawId) {
final String json = FileUtil.getRawFile(rawId); // Lecture du fichier JSON depuis res/raw
return createResponse(chain, json);
}
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
final String url = chain.request().url().toString();
if (url.contains(debugUrl)) { // Si l'URL contient la chaîne de débogage
return debugResponse(chain, debugRawId); // Retourner la réponse simulée
}
// Sinon, laisser la requête continuer son chemin
return chain.proceed(chain.request());
}
}
Cela permet de tester l'application sans dépendre d'un serveur backend réel, en fournissant des réponses JSON locales pour les requêtes spécifiées.