Il y a 6 ans -
Temps de lecture 7 minutes
Rotation d’écran et communication entre composants avec Android Architecture Components
La documentation Android est truffée d’extraits de code permettant de résoudre certaines problématiques rencontrées par la plupart des développeurs, comme la gestion de la rotation d’écran ou la communication entre composants.
Avec les Android Architecture Components, annoncés en mai 2017, Google apporte de nouvelles solutions. Nous allons voir comment mettre en place les Architecture Components sur une petite application qui en a bien besoin.
Prenons l’exemple d’une application HelloApp dont le rôle est d’identifier un utilisateur et de lui dire « hello ». Le code source complet est disponible sur GitHub.
Ces écrans sont implémentés par une seule activité et 3 fragments. Les données sont stockées par l’activité.
Si on implémente cette application de façon minimale, on rencontre deux problèmes :
- Mauvaise expérience utilisateur : les données sont perdues à la rotation d’écran. Si l’utilisateur tourne son écran à l’étape 3, il est renvoyé à l’étape 1 et doit s’identifier à nouveau.
- Couplage entre l’activité et les fragments : chaque fragment conserve une référence vers l’activité sous forme d’un
Listener
. C’est du code passe-plat, et le couplage pourrait être réduit.
Dans la suite, nous allons explorer le code de l’implémentation minimale pour constater ces deux problèmes et utiliser les Android Architecture Components pour apporter des solutions.
Implémentation minimale
La classe MainActivity conserve les données à échanger entre les fragments (login et password). Elle doit étendre une interface Listener
pour chaque fragment qui veut communiquer avec elle :
[java]public class MainActivity
extends AppCompatActivity
implements LoginFragment.Listener, PasswordFragment.Listener {
private String mLogin;
private String mPassword;[/java]
Par exemple la méthode suivante implémente LoginFragment.Listener
. Elle est appelée quand on passe du premier au deuxième écran. Le rôle de cette méthode est de stocker le login au niveau de l’activité et de rediriger l’utilisateur vers le prochain fragment.
[java]public void onSubmitLogin(String login) {
mLogin = login;
Fragment passwordFragment = PasswordFragment.newInstance();
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.main_fl_container, passwordFragment)
.commit();
}[/java]
Une interface Listener est définie dans chaque fragment. Elle permet au fragment de référencer l’activité en limitant un peu le couplage, et d’associer le fragment à une autre activité.
[java]public class LoginFragment extends Fragment {
private Listener mListener;
public void onAttach(Context context) {
if (context instanceof Listener) mListener = (Listener) context;
}
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_login, container, false);
final EditText loginEditText = view.findViewById(R.id.login_et);
Button okButton = view.findViewById(R.id.login_bt_ok);
okButton.setOnClickListener(v ->
mListener.onSubmitLogin(String.valueOf(loginEditText.getText())));
return view;
}
interface Listener {
void onSubmitLogin(String text);
}
}
[/java]
Autre chose à noter, dans cette implémentation minimale, la rotation d’écran n’est pas gérée. La sauvegarde (avec onSaveInstanceState()
) et la restauration (avec onCreate()
) de l’état ne sont pas implémentées.
Utilisation des Architecture Components
Nous allons maintenant améliorer cette application pour supprimer le lien entre les fragments et l’activité via les Listeners, et pour gérer la rotation d’écran.
Commençons par créer un ViewModel
pour stocker les données d’affichage. Il contient le login, le mot de passe et un objet observable (LiveData
) représentant l’étape du parcours utilisateur.
Voici le code :
[java]class IdentificationViewModel extends ViewModel {
private String mLogin;
private String mPassword;
private MutableLiveData<IdentificationStep> mCurrentStep = new MutableLiveData<>();
public IdentificationViewModel() { mCurrentStep.setValue(IdentificationStep.LOGIN); }
LiveData<IdentificationStep> getCurrentStep() {
return mCurrentStep;
}
void goToHello() { mCurrentStep.setValue(IdentificationStep.HELLO); }
void goToPassword() { mCurrentStep.setValue(IdentificationStep.PASSWORD); }
String getLogin() {
return mLogin;
}
String getPassword() {
return mPassword;
}
}[/java]
En plus des données d’affichage, ce ViewModel
peut être utilisé par l’activité pour orchestrer la navigation entre fragments grâce au champ mCurrentStep
.
On commence à l’étape de login (IdentificationStep.LOGIN
), et on propose des méthodes pour passer d’une étape à l’autre (goToHello()
et goToPassword()
).
La classe IdentificationStep
est une énumération des étapes du parcours. Elle indique leur correspondance avec les différents fragments :
[java]enum IdentificationStep {
PASSWORD(PasswordFragment.class),
HELLO(HelloFragment.class),
LOGIN(LoginFragment.class);
private Class<? extends Fragment> mFragmentClass;
IdentificationStep(Class<? extends Fragment> fragmentClass) {
mFragmentClass = fragmentClass;
}
public Fragment createFragment() {
return mFragmentClass.newInstance();
}
}[/java]
MainActivity
récupère ensuite un IdentificationViewModel
et observe le changement d’étape courante gràce à l’accesseur getCurrentStep()
qui retourne un LiveData<IdentificationStep>
. Le changement d’étape active un observateur qui utilise le FragmentManager
pour passer au fragment suivant.
Voici le code :
[java]public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
IdentificationViewModel viewModel =
ViewModelProviders.of(this).get(IdentificationViewModel.class);
viewModel.getCurrentStep().observe(
this,
step -> {
step = Assert.notNull(step);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.main_fl_container, step.createFragment())
.commit();
});
}
}[/java]
NB : la classe ViewModelProviders
résout le premier problème (conserver les données à la rotation de l’écran). En effet, cette classe s’occupe de créer une instance du ViewModel
et de la conserver pendant la durée de vie effective de l’activité, c’est à dire depuis sa création jusqu’à sa destruction en ignorant la rotation d’écran.
Les fragments n’ont plus qu’à récupérer le ViewModel
. Pour ça on utilise à nouveau la classe ViewModelProviders
. Elle se souvient que nous avions déjà créé un IdentificationViewModel
plus tôt dans la vie de l’activité. On récupère donc l’instance du ViewModel
précédemment créée. Ainsi, les trois fragments partagent les mêmes données.
Ensuite on attache au bouton OK une fonction de rappel. Celle-ci stocke les informations dans le ViewModel
et active le changement d’étape.
Voici le code du LoginFragment
. Les autres fragments sont similaires.
[java]public class LoginFragment extends Fragment {
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_login, container, false);
final EditText loginEditText = view.findViewById(R.id.login_et);
Button okButton = view.findViewById(R.id.login_bt_ok);
IdentificationViewModel viewModel = ViewModelProviders.of(getActivity()).get(IdentificationViewModel.class);
okButton.setOnClickListener(
v -> {
viewModel.setLogin(String.valueOf(loginEditText.getText()));
viewModel.goToPassword();
});
return view;
}
}[/java]
Le deuxième problème a été résolu : le lien vers l’activité via l’interface Listener
n’existe plus. Maintenant, le fragment et l’activité sont totalement découplés et communiquent par l’intermédiaire du ViewModel
.
Conclusion
Pendant des années les développeurs Android ont utilisé certaines techniques pour répondre à leurs problématiques, comme le principe des Listener
s pour faire communiquer les fragments et les activités, ou encore l’utilisation des méthodes onSaveInstanceState()
et onRestoreInstanceState()
pour gérer la rotation d’écran. Ces techniques ont été encouragées par Google qui fournissait même des extraits de code dans la documentation du framework.
Avec les Android Architecture Components, Google change enfin d’approche et propose des composants techniques permettant de mieux résoudre ces problématiques. Les classes ViewModel
et LiveData
apportent une solution élégante et rendent le code plus simple.
Cet article se limitait à une portion réduite des Architecture Components, qui comportent d’autres bibliothèques permettant d’aller plus loin :
- Room est un ORM qui permet d’implémenter simplement la persistance d’informations dans une base de données locale en s’appuyant sur la puissance du SQL
- Paging Library est une bibliothèque pour implémenter facilement et efficacement le chargement progressif des données
Nous pouvons nous appuyer sur ces composants techniques pour implémenter les caractéristiques d’une application mobile moderne : usage hors ligne, gestion des changements de configuration, chargement progressif, découplage, testabilité, etc. Aujourd’hui, relativement peu d’applications embarquent ces caractéristiques, mais Google s’organise enfin pour nous donner les moyens d’attaquer ce problème.
Références
- Documentation Google sur la gestion de la rotation d’écran
- Doc. Google sur la communication entre composants
- Code source de l’application HelloApp sur GitHub :
- Room : ORM Android qui s’appuie sur la puissance du SQL
- Paging Library : composant pour implémenter le chargement progressif de données
- Codelab sur les Android Architecture Components
- Documentation de référence des Architecture Components
Commentaire