P4b - Programmation mobile

2 - Fragments / Persistance / Threads

Adrien Krähenbühl IUT Robert Schuman

Les fragments

Qu’est-ce qu’un fragment ?

Un fragment :

  • est une portion de l’interface graphique,
  • est lié à une Activity (ne peut exister seul),
  • possède son propre layout et son propre cycle de vie.

Lorsqu’une activité est démarrée, elle peut ajouter, remplacer, supprimer des fragments à son interface.

Cycle de vie d’un fragment

  • Un fragment n’a pas vocation à revenir en arrière dans son cycle de vie : on préfèrera créer une nouvelle instance du fragment.
  • Comme pour les activités, on peut redéfinir n’importe quelle méthode du cycle de vie d’un fragment.

Créer un Fragment

  1. Il faut ajouter cette dépendance au fichier build.gradle :
dependencies {
    implementation "androidx.fragment:fragment:1.4.1"
}
  1. Il faut déclarer une classe :
import androidx.fragment.app.Fragment;

class ExampleFragment extends Fragment {
    public ExampleFragment() {
        super(R.layout.example_fragment);
    }
}

… associée à un layout res/layout/example_fragment.xml.

Tout ceci est automatisé dans Android Studio.

Ajouter un fragment à une activité - XML

<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://..."
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.example.ExampleFragment" />

res/layout/example_activity.xml

Uniquement dans le layout

  • Il faut utiliser un FragmentContainerView dans le layout de l’activité
  • L’attribut android:name indique la classe du fragment à insérer.

Ajouter un fragment à une activité - classe

public class ExampleActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null)
            getSupportFragmentManager()
                .beginTransaction()
                .setReorderingAllowed(true)
                .add(R.id.fragment_container_view, ExampleFragment.class, null)
                .commit();
  }
}

Avec layout + code

  • Il faut utiliser un FragmentContainerView dans le layout de l’activité sans spécifier l’attribut android:name.
  • L’activité utilise un FragmentManager pour ajouter/remplacer/supprimer un fragment dans ce layout.
  • L’uilisation classique consiste à mettre en place le Fragment dans la méthode onCreate de l’activité.

Le gestionnaire de fragments

Un FragmentManager
  • gére les fragments au seins des activités et des fragments,
  • est accessible dans les activités et fragments via get[Support|Parent|Child]FragmentManager(),
  • genère des FragmentTransaction pour manipuler les fragments,
  • gère une pile (stack) des transactions.

Les transactions

FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.fragmentContainer, FragmentExample.class, null);
transaction.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
transaction.addToBackStack(null);
transaction.commit();
...
fragmentManager.popBackStack();
replace( containerId, fragmentClass, bundle )
Remplace le contenu du conteneur containerId par une nuovelle instance d’un fragmentClass en lui transmettant un bundle.

setTransition(id) Ajoute un effet visuel au changement de fragment.

commit() Exécute la transaction dès que le thread principal est prêt.

addToBackStack(name)
Ajoute la transaction en cours à la pile des transactions.

popBackStack(name) Rejoue la dernière transaction à l’envers.

ViewModels

Qu’est-ce que les ViewModels ?

Les ViewModel permettent de faire persister des données des activités et des fragments en dehors de leur cycle de vie.

Un ViewModel :

  • est toujours associé à un scope (un fragment ou une activité),
  • reste accessible tant que le scope existe, via un ViewModelProvider,
  • peut garder des données partagées entre une activité et ses fragments,
  • stocke les données dans des LiveData<T>,
  • ne doit jamais contenir de référence vers une activité ou un fragment.

Exemple de ViewModel

public class MyViewModel extends ViewModel {
  private MutableLiveData<List<User>> users;
  public LiveData<List<User>> getUsers() {
    if (users == null) {
      users = new MutableLiveData<List<User>>();
      loadUsers();
    }
    return users;
  }
  private void loadUsers() { ... }
}
public class MyActivity extends Activity {
  public void onCreate(Bundle state) {
    MyViewModel model =
      new ViewModelProvider(this)
          .get(MyViewModel.class);
    model.getUsers()
       .observe(this, users -> {
         // update UI
       });
  }
}
LiveData<T>
Stocke des objets de type T et implémente le design pattern Observer (méthode observe()) pour réagir lorsque ses données sont modifiées.
ViewModelProvider(scope).get(Class)
Crée pour ce scope un objet de type Class, sous-classe de ViewModel. Le second appel de cette méthode d’une même classe pour un même scope retourne l’instance pré-existante.

ViewModels partagés entre Fragments

public class SharedViewModel extends ViewModel {
    private final MutableLiveData<Item> selected = new MutableLiveData<Item>();
    public void select(Item item) {
        selected.setValue(item);
    }
    public LiveData<Item> getSelected() { return selected; }
}
public class ListFragment extends Fragment {
    private SharedViewModel model;
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        model = new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        itemSelector.setOnClickListener(item -> {
            model.select(item);
        });
    }
}
public class DetailFragment extends Fragment {
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        SharedViewModel model =
                new ViewModelProvider(requireActivity()).get(SharedViewModel.class);
        model.getSelected().observe(getViewLifecycleOwner(), item -> {
            // Update the UI.
        });
    }
}

Les préférences partagées

Qu’est-ce que les préférences partagées ?

Les SharedPreferences stockent des associations clé-valeur
dans un fichier.

Les SharedPreferences
  • s’ouvrent toujours en mode MODE_PRIVATE depuis la v17,
  • ont un fichier par défaut pour chaque application,
  • ne sont pas dédiées au paramétrage de l’application : on peut y stocker toute information clé-valeur que l’on souhaite persister.

Lecture/écriture des préférences partagées

Sauvegarde
SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt("high-score", newHighScore);
editor.apply();
  • Accès aux préférences partagées depuis l’activité.
  • L’Editor ajoute/modifie des entrées avec putInt(key, value), putString(key,value), putFloat(key,value) etc.
  • apply() modifie les préférences en mémoire mais ne les écrit pas immédiatement dans le fichier. Utiliser commit() pour cela.
Lecture
int highScore = prefs.getInt("high-score", 0);
  • Utiliser getInt(key,defaultValue), getString(key,defautlValue), etc.

À propos des préférences partagées

1 - Ne pas perdre ses clés
Stocker les clés dans les fichiers resources et utiliser la méthode statique getString(id) de Context pour y accéder.
int highScore = prefs.getInt(getString(R.id.pref_high_score), 0);


2 - Cohésion SharedPreferences/ViewModel
  • L’écriture des références partagées sont souvent réalisées lors de la fermeture de l’application (méthode onStop())
  • La lecture est souvent réalisée lors de la création de l’activité (méthode onCreate())

En effet, lorsque l’application est lancée, les données sont gérées de préférences dans un ViewModel.

Les thread

Processus

  • Dans un système multitâches, le système donne l’illusion que tous les processus s’exécutent en même temps : chacun est en fait exécuté à son tour pendant un tout petit intervalle de temps.

  • Dans un système multiprocesseurs, des processus peuvent s’exécuter effectivement en même temps, i.e. en parallèle.

Threads

Un processus est composé d’un ou plusieurs threads
qui s’exécutent de manière concurrente.

Un thread
  • est aussi appelé processus léger,
  • se crée beaucoup plus rapidement qu’un processus (ordre 100).
Intérêt
  • Améliore les performances lorsqu’un processus utilise beaucoup d’E/S. L’attente d’une E/S n’est pas bloquante pour le processus car un autre thread s’exécute pendant ce temps.
  • Dans le cas d’un système multi-coeurs ou multi-processeurs, les threads permettent de paralléliser des calculs.
  • Dans les systèmes interactifs à base d’interfaces graphiques, les threads permettent d’améliorer la réactivité du programme.

Et avec Android ?


Il y a plusieurs pages de la documentation sur les Threads :

Dans la suite, nous allons voir deux façon de faire des requêtes HTTP :

  1. Avec la classe HttpURLConnection
  2. Avec la bibliothèque Volley

Les requêtes HTTP

La classe HttpURLConnection

URL url = new URL("http://...");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
    connection.setRequestMethod("GET");
    connection.connect();
    InputStream in = new BufferedInputStream(connection.getInputStream());
    readStream(in);
}
finally {
    connection.disconnect();
}

Il y a plusieurs étapes à suivre pour effectuer une requête HTTP :

  1. Initialiser la connexion à partir de l’URL.
  2. Préparer la requête avec l’URI et les entêtes.
  3. Ajouter un corps si besoin (exemple : requête POST).
    \(\rightarrow\) utiliser setDoOutput(true) et écrire sur getOutputStream()
  4. Envoyer la requête
  5. Lire la réponse : entêtes et/ou données.
  6. Clore la connexion.

Permissions et JSON

1 - Attention, pour qu’une application accède à internet, elle doit déclarer qu’elle a besoin d’y accéder dans le fichier AndroidManifest.xml :

<uses-permission android:name="android.permission.INTERNET" />

2 - Exemple de requête GET pour obtenir des données JSON :

URL url = new URL('http://...');
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.connect();

InputStream in = new BufferedInputStream(connection.getInputStream());

StringBuilder sb = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
    sb.append(line + "\n");
}
in.close();

JSONObject json = new JSONObject(sb.toString());

C’est manuel, mais on a vite fait une méthode qui encapsule tout ça.

Volley

Qu’est-ce que Volley ?

Volley est une bibliothèque Android qui permet de faire des requêtes HTTP simplement et rapidement.

En particulier, Volley gère une file d’attente des requêtes et permet d’abandonner des requêtes à tout moment.

dependencies {
    ...
    implementation 'com.android.volley:volley:1.2.1'
}

Déclaration de la dépendance dans build.gradle.

Faire une requête simple

TextView textView = (TextView) findViewById(R.id.text);
StringRequest stringRequest = new StringRequest(
    Request.Method.GET, "http://...",
    new Response.Listener<String>() {
        @Override
        public void onResponse(String response) {
            textView.setText("Response is: "+ response);
        }
    },
    new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            textView.setText("That didn't work!");
        }
    }
);
RequestQueue queue = Volley.newRequestQueue(this);
queue.add(stringRequest);
  • Il y a plusieurs autres types de requêtes selon ce que l’on attend en réponse : JSONObjectRequest, JSONArrayRequest, ImageRequest.
  • Constructeurs : Construct(method,url,listener,errorListener)
  • Les requêtes sont exécutées par une RequestQueue.

Une pile des requêtes unique

Une RequestQueue ayant besoin d’une Context, il est d’usage de l’encapsuler dans un singleton.

public class MyRequestQueue {
    private static MyRequestQueue instance = null;
    private RequestQueue requestQueue;
    private MyRequestQueue(Context context) {
        requestQueue = Volley.newRequestQueue(context.getApplicationContext());
    }
    public static synchronized MyRequestQueue getInstance(Context context) {
        if (instance == null)
            instance = new MyRequestQueue(context);
        return instance;
    }
    public static synchronized MyRequestQueue getInstance() {
        return instance;
    }
    public <T> void add(Request<T> req) {
        requestQueue.add(req);
    }
}
MySingleton.getInstance(this.getApplicationContext());
MySingleton.getInstance().add(request);

Voir documentation pour exemple plus complet.

Requête JSON avec Volley

JsonObjectRequest request = new JsonObjectRequest(
    Request.Method.GET, "http://...", null,
    new Response.Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject response) {
            textView.setText("Response: " + response.toString());
        }
    },
    new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) { ... }
    }
);

MyRequestQueue.getInstance().add(request);

Il y a des constructeurs alternatifs, en particulier :

  • Construct(url,listener,errorListener) où la méthode par défaut est GET.
  • Construct(method,url,jsonRequest,listener,errorListener)jsonRequest peut être null ou contenir des données POST.