Ce cours étant terminé, je vous propose ici d'améliorer la qualité de votre code, en vous donnant quelques trucs et astuces qui vous aideront à rendre celui-ci le plus propre et clair possible.
N'oubliez pas, pour rester un super développeur, qu'une de vos principales préoccupations sera de vous soucier constamment et au maximum de la qualité de votre code. ;)
Et comme toujours, si vous ne devez lire qu'un seul livre dans votre vie de développeur, c'est bien celui-là : Coder proprement, de Robert C Martin (la version anglaise est plus facilement trouvable).
Il existe sur Android des librairies que vous devez absolument connaître, car ces dernières vous permettront de vous simplifier un maximum la vie. Nous allons ici en voir deux :
IcePick : Pour gérer plus simplement la manipulation de vos bundles
ButterKnife : Pour faciliter la déclaration de vos vues dans votre activité
A travers ce chapitre, nous nous servirons de notre mini-application déjà créée dans la deuxième partie de ce cours, à savoir MyFragmentApp, que vous trouverez en téléchargement à ce lien.
Icepick : Gérer plus facilement vos bundles
Reprenons ensemble notre application, MyFragmentApp. Jusqu'à maintenant, pour gérer la sauvegarde de données dans notre bundle, nous devions redéfinir les méthodes onSaveInstanceState
et onRestoreInstanceState
et coder nous même la persistance des données...
Voici par exemple le code du fragment DetailFragment.java qui gère actuellement cette sauvegarde :
public class DetailFragment extends Fragment {
private int buttonTag;
...
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
//Restore last buttonTag if possible
if (savedInstanceState != null) {
int buttonTagRestored = savedInstanceState.getInt(KEY_BUTTONTAG, 0);
//Update TextView
this.updateTextView(buttonTagRestored);
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//Save buttonTag in Bundle when fragment is destroyed
outState.putInt(KEY_BUTTONTAG, buttonTag);
}
...
}
Maintenant, si je vous disais qu'avec IcePick on peut gérer cela BEAUCOUP plus facilement ? ;)
Allez c'est parti, je vous montre !
Tout d'abord, nous allons installer la librairie IcePick, disponible à ce lien, en utilisant notre gestionnaire de dépendances Gradle. Pour cela, modifiez les fichiers build.gradle de votre application MyFragmentApp :
allprojects {
repositories {
jcenter()
maven {url "https://clojars.org/repo/"}
}
}
dependencies {
compile 'frankiesardo:icepick:{{latest-version}}'
provided 'frankiesardo:icepick-processor:{{latest-version}}'
}
Une fois l'installation terminée, nous allons modifier notre classe DetailFragment.java afin d'implémenter IcePick :
public class DetailFragment extends Fragment {
...
// 1 - Adding @State annotation to indicate to Icepick to save it
@State int buttonTag;
...
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 2 - Restore all @State annotation variables in Bundle
Icepick.restoreInstanceState(this, savedInstanceState);
this.updateTextView(this.buttonTag);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// 3 - Save all @State annotation variables in Bundle
Icepick.saveInstanceState(this, outState);
}
...
}
Explications : Nous allons ici dire à IcePick de s'occuper à notre place du support de notre bundle :
Ligne 1 : En ajoutant l'annotation @State, nous disons à Icepick d'enregistrer cette variable dans le bundle. Pensez simplement à retirer le 'private' devant la variable.
Ligne 2 et Ligne 3 : Nous plaçons une instance de IcePick, afin qu'il s'exécute à chaque fois que nos fameuses méthodes pour gérer notre bundle sont appelées.
Lancez maintenant votre application (en mode tablette, rappelez-vous de notre problématique de la fin de la deuxième partie... ;) ).
Nous gérons maintenant la sauvegarde de nos données dans le Bundle les doigts dans le nez ! Si par exemple demain, vous deviez sauvegarder deux variables de plus, vous n'auriez plus qu'à écrire :
public class DetailFragment extends Fragment {
...
@State String nameToSaveInBundle;
@State String titleToSaveInBundle;
...
}
ButterKnife : Liez plus facilement vos vues à votre activité
Jusqu'à maintenant, afin de récupérer les vues dans notre activité et les sérialiser (les transformer en un objet JAVA), nous devions utiliser la triste méthode findViewById(R.id.your_view)
.
Cela peut s'avérer long et occuper beaucoup d'espaces dans une classe, notamment lorsque l'on souhaite récupérer un nombre important de vues. C'est là que ButterKnife fait son entrée :).
Afin de l'installer, vous devrez là aussi l'ajouter à votre fichier Gradle afin de créer une nouvelle dépendance :
dependencies {
compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}
Reprenons l'exemple de notre classe DetailFragment.java. Afin de récupérer et sérialiser notre TextView, nous faisions précédemment comme ce qui suit :
public class DetailFragment extends Fragment {
// 1 - Declare TextView
private TextView textView;
...
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_detail, container, false);
// 2 - Get and serialise textView from layout
this.textView = (TextView) view.findViewById(R.id.fragment_detail_text_view);
return(view);
}
...
}
Imaginez maintenant que vous ayez 10 TextViews et 10 Boutons... Ça en ferait des lignes de code et des findViewById à écrire !
Avec ButterKnife, nous pouvons réduire drastiquement cette déclaration :
public class DetailFragment extends Fragment {
// 1 - Adding @BindView in order to indicate to ButterKnife to get & serialise it
@BindView(R.id.fragment_detail_text_view) TextView textView;
...
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_detail, container, false);
// 2 - Telling ButterKnife to bind all views in layout
ButterKnife.bind(this, view);
return(view);
}
...
}
Explications : Nous allons dire à ButterKnife de créer à notre place les différents findViewByID
, en accolant l'annotation @BindView à chaque vue que nous souhaitons récupérer depuis notre layout. Enfin, nous devons également ajouter une instance de ButterKnife dans notre onCreateView( ) afin d'activer ce processus de récupération (que l'on appelle aussi bind).
Vous verrez que vous gagnerez énormément de temps et rendrez votre code beaucoup plus lisible grâce à cette superbe librairie. Merci beaucoup Jake Wharton ! :honte:
Pratiquez l'héritage de classe !
Un bon développeur est un développeur feignant. A travers les précédents chapitres de ce cours, vous avez dû vous rendre compte que nous répétions souvent les mêmes lignes de code.
Et ça, c'est un sacrilège ! :colere:
Si on reprend nos deux fragments de l'application MyFragmentApp, il faut pour chacun de nos fragments implémenter :
onCreateView( ) et ajouter à l'intérieur ButterKnife pour récupérer plus facilement nos vues
onActivityCreated( ) et onSaveInstanceState( ), en ajoutant à l'intérieur Icepick afin de gérer nos bundles
Ajouter potentiellement une méthode publique statique newInstance( ) nous permettant de normaliser la création de notre fragment
Cela représente pas mal de copier/coller, et d'une manière générale, les copier/coller sont à proscrire le plus souvent possible ! Vous devez toujours penser à réutiliser votre code, c'est un des buts ultimes de la programmation orientée objet :).
Ainsi, nous allons ici appliquer plusieurs concepts importants : l'héritage et les classes abstraites. Vous pouvez retrouver plus d'informations à leur sujet sur le cours dédié à JAVA (héritage et classes abstraites).
Pour faire simple, l'héritage consiste à récupérer automatiquement les propriétés et méthodes d'une classe parente, et d'une manière générale son comportement. Un peu comme vous quand vous héritez du sale caractère de vos parents... (Hein quoi ? Qui a dit ça ! :-°)
Les classes abstraites sont quant à elles, des classes que l'on ne peut pas instancier (donc pas de new avec elles). Elles servent surtout à créer des classes parentes... ;)
Nous allons donc ici créer une classe BaseFragment dont chacun de nos fragments héritera, afin d'éviter de répéter les mêmes lignes de code sur chaque fragment, les rendant ainsi plus lisibles :
Fichier BaseFragment.java :
public abstract class BaseFragment extends Fragment {
// 1 - Force developer implement those methods
protected abstract BaseFragment newInstance();
protected abstract int getFragmentLayout();
protected abstract void configureDesign();
protected abstract void updateDesign();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
// 2 - Get layout identifier from abstract method
View view = inflater.inflate(getFragmentLayout(), container, false);
// 3 - Binding Views
ButterKnife.bind(this, view);
// 4 - Configure Design (Developer will call this method instead of override onCreateView())
this.configureDesign();
return(view);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// 5 - Handling Bundle Restoration
Icepick.restoreInstanceState(this, savedInstanceState);
// 7 - Update Design (Developer will call this method instead of override onActivityCreated())
this.updateDesign();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// 6 - Handling Bundle Save
Icepick.saveInstanceState(this, outState);
}
}
Explications : Nous avons créé ici une classe abstraite BaseFragment, héritant de la classe Fragment. A l'intérieur, nous avons redéfini les méthodes les plus répétées au sein de MainFragment et DetailFragment :
Ligne 1 : Ces méthodes abstraites ont été créées pour être implémentées et surtout redéfinies dans les fragments héritant de BaseFragment. Pourquoi ? Tout simplement car elles sont utilisées juste en dessous, et que nous souhaitons forcer le développeur à les implémenter dans les futures classes enfants.
Ligne 2 : Au sein de la méthode onCreateView que nous avons redéfini, nous appelons la méthode abstraite getFragmentLayout( ). Celle-ci, déclarée dans nos fragments enfants, remontera l'identifiant du bon layout.
Ligne 3 : Toujours au sein de la méthode onCreateView que nous avons redéfini, nous lui ajoutons le support de ButterKnife.
Ligne 4 : Enfin, nous appelons notre méthode abstraite configureDesign( ) ici. Pourquoi ? Dans le but de permettre à nos fragments enfants de ne pas avoir à redéfinir la méthode onCreateView( ), mais plutôt d'appeler à la place configureDesign( ), plus facile à retenir et à comprendre ;) .
Ligne 5 et 6 : Nous implémentons le support de Icepick pour gérer plus facilement nos bundles.
Ligne 7 : Sur le même model que configureDesign( ), nous appelons ici notre méthode updateDesign() pour permettre à nos fragments enfants de ne pas avoir à redéfinir la méthode onActivityCreated( ), mais plutôt d'appeler à la place updateDesign( ).
Effectuons maintenant la modification de notre fragment enfant DetailFragment, afin qu'il n'hérite maintenant plus de la classe Fragment mais plutôt de BaseFragment :
Fichier DetailFragment.java :
public class DetailFragment extends BaseFragment {
@BindView(R.id.fragment_detail_text_view) TextView textView;
@State int buttonTag;
// --------------
// BASE METHODS
// --------------
@Override
protected BaseFragment newInstance() { return new DetailFragment(); }
@Override
protected int getFragmentLayout() { return R.layout.fragment_detail; }
@Override
protected void configureDesign() { }
@Override
protected void updateDesign() {
this.updateTextView(this.buttonTag);
}
// --------------
// UPDATE UI
// --------------
//Update TextView depending on TAG's button
public void updateTextView(int tag){
//Save tag in ButtonTag variable
this.buttonTag = tag;
switch (tag){
case 10:
this.textView.setText("You're a very good programmer !");
break;
case 20:
this.textView.setText("I do believe that Jon Snow is going to die in next season...");
break;
case 30:
this.textView.setText("Maybe Game of Thrones next season will get back in 2040 ?");
break;
default:
break;
}
}
}
Explications : Comme vous pouvez déjà le remarquer, notre classe est devenue très petite et plus compréhensible non ? ^^ C'est là toute la magie de l'héritage et des classes abstraites !
Nous avons ici fait hériter DetailFragment de BaseFragment et avons donc été contraints (obligés par le mot clé abstract) d'implémenter les 4 méthodes abstraites.
Prenez le temps de lire et relire (voir encore relire !) cette partie, afin de bien assimiler et comprendre la logique derrière l'héritage et les classes abstraites.
Le déclic ne se fera peut-être pas tout de suite, ne vous inquiétez pas. Prenez votre temps ! Une graine aura déjà germé dans votre tête, et ça c'est le principal.
Vous trouverez l'application MyFragmentApp mise à jour à ce lien.
Conclusion
Bravo ! Mine de rien, vous venez d'accomplir quelque chose d'assez important dans la vie d'un développeur, à savoir l'amélioration de la qualité de son code, grâce à des librairies et concepts au top.
Encore félicitations !