Publié par
Il y a 2 semaines · 7 minutes · Mobile

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 :

  1. 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.
  2. 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 :

public class MainActivity
        extends AppCompatActivity
        implements LoginFragment.Listener, PasswordFragment.Listener {

    private String mLogin;
    private String mPassword;

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.

public void onSubmitLogin(String login) {

    mLogin = login;

    Fragment passwordFragment = PasswordFragment.newInstance();
    getSupportFragmentManager()
            .beginTransaction()
            .replace(R.id.main_fl_container, passwordFragment)
            .commit();
}

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é.

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);
    }
}

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 :

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;
    }
}

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 :

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();
    }
}

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 :

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();
                });
    }
}

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.

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;
    }
}

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 Listeners 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

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *