Faire du neuf avec du vieux sur Android
DISCLAIMER : Cet article devait initialement être publié sur le blog octo mais ayant pris un peu de retard, il a moins de valeur aujourd’hui donc je le mets ici… ca faisait longtemps que je n’avais pas publié sur mon blog
On ne peut pas vraiment parler de vieux quand on parle d’Android même s’il est vrai que les sdk 3 (1.5) et 4 (1.6) tendent à disparaître (respectivement 1% et 1.8% du parc). Mais lorsqu’on a réalisé une application basée sur le sdk 1.6 et qu’on souhaite bénéficier du push ou bien adresser les tablettes, on veut éviter de perdre 90% de ses utilisateurs… Voilà quelques éléments pour y parvenir.
Notifications Push
Comme Maxence le précisait dans son article traitant du sujet, les notifications push ne fonctionnent qu’à partir du SDK 8 (2.2). On pourrait se contenter de migrer vers cette version mais on perdrait alors 16% des utilisateurs (contre 40% en février 2011).
Que se passe-t-il exactement ? Sur un device au SDK antérieur, vous aurez l’erreur PHONE_REGISTRATION_ERROR au moment de vous enregistrer au service de Google.
Le problème ne vient pas du code écrit pour s’enregistrer (un simple Intent), ni même de la configuration du manifest (des permissions et un receiver), et pas non plus du code pour recevoir les messages (un BroadcastReceiver).
Tout ceci est disponible dans le SDK 4, et on peut donc le mettre en place sans risquer d’avoir une UnsupportedOperationException. Par contre, afin d’éviter l’erreur à l’enregistrement du device, on va ajouter le bout de code suivant :
if (SysUtils.getSystemVersion() >={ // register to C2DM }
On n’utilise pas la constante Build.VERSION_CODES.FROYO qui n’est disponible qu’à partir du SDK 8 et donc on utilise sa valeur (8). Cette petite astuce permet de mettre le push à disposition des téléphones récents tout en supportant les téléphones plus anciens.
Support des tablettes
Écrans XL
La logique voudrait que les tablettes soient des écrans xlarge (>= 960dp x 720dp). Dans les faits ce n’est pas tout à fait vrai, il existe des tablettes dans la catégorie large (>= 640dp x 480dp). Google fournit dorénavant la propriété smallestWidth pour déterminer à partir de quelle largeur minimum un layout doit être sélectionné mais ce n’est disponible qu’en version 3.2. Nous avons pris le parti d’utiliser xlarge.
Pour ce faire, vous aurez besoin d’un répertoire layout-xlarge dans les ressources (et éventuellement layout-xlarge-land pour le mode paysage plus fréquemment utilisé pour les tablettes) : aucun problème pour ajouter ce répertoire même dans un projet en 1.6.
Le second point important est la quantité de données à afficher sur un écran de cette taille. On a tendance à regrouper plusieurs écrans en un seul pour profiter de tout l’espace et par conséquent de créer une nouvelle activité. Le problème qui se pose alors est comment réutiliser le code des mes deux, voire trois activités smartphone pour en constituer une seule pour tablette ?
Comme un exemple et un dessin valent tous les blablas, prenons l’exemple d’Appaloosa, notre service de store privé :

Le client Android fournit plusieurs activités pour les smartphones (liste d’applications, détail d’une application et commentaires d’une application) et une seule pour les tablettes. La première chose à faire est donc de choisir la bonne activité en fonction du device. Pour cela on créé une activité de démarrage :
public class LaunchActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent startActivity = null;
if ((getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) >= 4) {
startActivity = new Intent(this,
AppListAndDetailActivity.class);
} else {
startActivity = new Intent(this, AppListActivity.class);
}
startActivity(startActivity);
finish();
}
}
Comme pour le push, on utilise la valeur de la constante Configuration.SCREENLAYOUT_SIZE_XLARGE (4) plutôt que la constante elle-même, disponible à partir du SDK 9. Pour le reste, il s’agit simplement de lancer la bonne activité.
Défragmentation des écrans
Concernant le layout, on pourrait se contenter de concevoir un layout adapté mais ce serait dommage d’aborder les tablettes sans faire usage des Fragments. D’autant que Google fournit depuis mars 2011 une librairie de rétrocompatibilité (Compatibility Package) permettant d’utiliser les fragments dans toutes les applications avec un sdk >= 1.6.
Avant la bataille

res/layout/app_list_screen.xml: layout contenant la ListView des applications.AppListActivity.java: contenant le code pour charger la liste des applications (AsyncTask pour l’appel du web service, ListAdapter pour l’affichage des lignes…).res/layout/app_detail_screen.xml: layout contenant les différentes info de l’application.AppDetailActivity.java: contenant le code pour charger le détail d’une application (divers AsyncTask pour le détail, les screenshots, le téléchargement du binaire…)
Utilisation des fragments

Après découpage en fragments, on a… beaucoup plus de fichiers ! Oui, en effet, mais c’est nécessaire pour éviter la duplication de code. Reprenons chaque fichier un par un :
Déménagement des layout.xml dans les fragment.xml
L’essentiel du fichier app_list_screen.xml se retrouve dans app_list_fragment.xml : la liste, le loading (chargement de la liste), l’empty message (message en cas de liste vide). Ne reste dans le fichier initial que le code suivant (entête et fragment) :
<LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <include layout="@layout/header_layout"></include> <fragment class="com.octo.appaloosa.activity.AppListFragment" android:id="@+id/appListFragment" android:layout_width="fill_parent" android:layout_height="fill_parent"></fragment> </LinearLayout>
Des activités au fragments, il n’y a qu’un pas
AppListActivity.java (après refactoring) ne contient plus grand chose et étends FragmentActivity au lieu d’une simple Activity. Tous les événements onClick restent dans l’activité :
public class AppnListActivity extends FragmentActivity {
private AppListFragment listFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.app_list_screen);
listFragment = (AppListFragment) getSupportFragmentManager().findFragmentById(R.id.appListFragment);
}
@Override
protected void onResume() {
super.onResume();
listFragment.loadApplications();
}
AppListFragment.java contient dorénavant le code pour interagir avec la vue, utiliser les AsyncTask pour les web services, etc… :
public class AppListFragment extends Fragment {
private List<Application> mApplicationsList = new ArrayList<Application>();
private ListView mApplicationsListView;
private AppListListener listener = new AppListListener() {
public void onComplete(List<Application> apps) {
mApplicationsList = apps
((BaseAdapter)mApplicationsListView).notifyDataSetChanged();
}
};
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.app_list_fragment, container, false);
mApplicationsListView = (ListView)view.findViewById(R.id.applicationListView);
mApplicationsListView.setAdapter(new AppListAdapter(mApplicationsList, getActivity()));
return view;
}
public void loadApplications() {
new AppListAsyncTask(getActivity(), listener).execute();
}
}
Le mécanisme est assez proche d’une activité, les termes changent un peu (onCreateView au lieu de onCreate par exemple). Par ailleurs, il existe un fragment tout fait pour les listes (ListFragment), que nous n’avons pas utilisé car le layout contenait un peu plus qu’une simple liste (loading notamment).
La version tablette
Je vous épargne l’écran de détail qui est basé sur le même principe pour aborder l’écran tablette qui cumule la liste et le détail. app_list_detail_screen.xml est donc relativement simple puisqu’il utilise les mêmes fragments que les smartphones :
<LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1"> <fragment class="com.octo.appaloosa.activity.AppListFragment" android:id="@+id/appListFragment" android:layout_weight="1" android:layout_width="0dp" android:layout_height="fill_parent"></fragment> <fragment class="com.octo.appaloosa.activity.AppDetailFragment" android:id="@+id/appDetailFragment" android:layout_weight="2.5" android:layout_width="0dp" android:layout_height="fill_parent"></fragment> </LinearLayout>
Faire le bon choix
Un point intéressant à regarder est l’action d’un click sur un élément de la liste. En effet, si on se trouve dans le cas du smartphone, on lance l’activité de détail (AppDetailActivity). Si on se trouve dans le cas de la tablette, on charge le détail dans le fragment associé :
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
AppDetailFragment detailFragment = (AppDetailFragment) getFragmentManager().findFragmentById(R.id.appDetailFragment);
if (detailFragment == null) {
Intent intent = new Intent(getActivity(), AppDetailActivity.class);
intent.putExtra("application", mApplicationsList.get(position));
startActivity(intent);
} else {
if (detailFragment.getCurrentApplication() == null || detailFragment.getCurrentApp().getId().equals(mApplicationsList.get(position).getId()) == false) {
detailFragment.loadApplicationDetail(mApplicationsList.get(position));
}
}
}
Au final, on a beaucoup plus de classes et de fichiers, mais les fragments (java + xml) sont réutilisables, ce qui nous permet d’envisager toute sorte d’interface et surtout envisager les tablettes, voire les TV…
Conclusion
En l’état actuel des choses, il possible de couvrir un spectre de SDKs relativement large (1.6 -> 4.0) et un spectre d’écrans complet (small -> xlarge) avec une seule application. Toutefois, Google fournit la possibilité de proposer plusieurs apk pour une même application, par exemple si la version tablette contient des images HD assez lourdes que l’on souhaite épargner aux smartphones…
On dit souvent qu’Android est fragmenté (je ne le nierai pas complètement) mais cet article démontre que malgré la multitude de devices (qui fait aussi la richesse de la plateforme), on peut adresser 99% des utilisateurs sans trop de difficultés.
La sortie récente de Android 4.0 (Ice Cream Sandwich) promet de réunir les développements tablettes et smartphones mais en attendant d’avoir un parc conséquent d’appareils basés sur cette dernière mouture, il faudra continuer a jouer d’astuces pour cibler le plus grand nombre d’utilisateurs.