94 votes

Pourquoi l'observateur LiveData est déclenché deux fois pour un nouvel observateur attaché

Ma compréhension sur LiveData , c'est que, il sera le déclencheur de l'observateur sur l'état actuel de changement de données, et non pas une série historique de modification de l'état des données.

Actuellement, j'ai un MainFragment, qui effectuent Room opération d'écriture, de changer non saccagé des données, à la corbeille de données.

J'ai aussi un autre TrashFragment, qui observe à la corbeille de données.

Envisagez le scénario suivant.

  1. Il y a actuellement 0 saccagé des données.
  2. MainFragment est active fragment. TrashFragment n'est pas encore créé.
  3. MainFragment ajouté 1 saccagé des données.
  4. Maintenant, il y a 1 saccagé données
  5. Nous utilisons la navigation tiroir, remplacer, MainFragment avec TrashFragment.
  6. TrashFragments'observateur va d'abord recevoir onChanged, 0 mis à la corbeille de données
  7. Encore une fois, TrashFragments'observateur va ensuite recevoir onChanged, avec 1 saccagé données

Ce qui est hors de mon espoir est que, l'élément (6) ne devrait pas arriver. TrashFragment ne doit recevoir les dernières saccagé des données, qui est de 1.

Voici mes codes


TrashFragment.java

public class TrashFragment extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        ...

        noteViewModel.getTrashedNotesLiveData().removeObservers(this);
        noteViewModel.getTrashedNotesLiveData().observe(this, notesObserver);

MainFragment.java

public class MainFragment extends Fragment {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        ...

        noteViewModel.getNotesLiveData().removeObservers(this);
        noteViewModel.getNotesLiveData().observe(this, notesObserver);

NoteViewModel .java

public class NoteViewModel extends ViewModel {
    private final LiveData<List<Note>> notesLiveData;
    private final LiveData<List<Note>> trashedNotesLiveData;

    public LiveData<List<Note>> getNotesLiveData() {
        return notesLiveData;
    }

    public LiveData<List<Note>> getTrashedNotesLiveData() {
        return trashedNotesLiveData;
    }

    public NoteViewModel() {
        notesLiveData = NoteplusRoomDatabase.instance().noteDao().getNotes();
        trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
    }
}

Code qui traite avec Salle de

public enum NoteRepository {
    INSTANCE;

    public LiveData<List<Note>> getTrashedNotes() {
        NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao();
        return noteDao.getTrashedNotes();
    }

    public LiveData<List<Note>> getNotes() {
        NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao();
        return noteDao.getNotes();
    }
}

@Dao
public abstract class NoteDao {
    @Transaction
    @Query("SELECT * FROM note where trashed = 0")
    public abstract LiveData<List<Note>> getNotes();

    @Transaction
    @Query("SELECT * FROM note where trashed = 1")
    public abstract LiveData<List<Note>> getTrashedNotes();

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    public abstract long insert(Note note);
}

@Database(
        entities = {Note.class},
        version = 1
)
public abstract class NoteplusRoomDatabase extends RoomDatabase {
    private volatile static NoteplusRoomDatabase INSTANCE;

    private static final String NAME = "noteplus";

    public abstract NoteDao noteDao();

    public static NoteplusRoomDatabase instance() {
        if (INSTANCE == null) {
            synchronized (NoteplusRoomDatabase.class) {
                if (INSTANCE == null) {
                    INSTANCE = Room.databaseBuilder(
                            NoteplusApplication.instance(),
                            NoteplusRoomDatabase.class,
                            NAME
                    ).build();
                }
            }
        }

        return INSTANCE;
    }
}

Une idée de comment je peux les empêcher de recevoir des onChanged deux fois, pour une même donnée?


Démo

J'ai créé un projet de démonstration pour illustrer ce problème.

Comme vous pouvez le voir, après je effectuer une opération d'écriture (Cliquez sur AJOUTER à la CORBEILLE NOTE bouton) en MainFragment, lorsque je passe à TrashFragment, j'attends onChanged en TrashFragment ne sera appelée qu'une seule fois. Toutefois, il est appelé deux fois.

enter image description here

Projet de démonstration peut être téléchargée à partir de https://github.com/yccheok/live-data-problem

80voto

R. Zagórski Points 13234

J'ai introduit seulement un changement dans votre code:

noteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);

au lieu de:

noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);

en Fragments' onCreate(Bundle) méthodes. Et maintenant, il fonctionne de manière transparente.

Dans votre version, vous obtenu une référence de NoteViewModel commun aux deux Fragments (de l'Activité). ViewModel a Observer enregistré dans le Fragment précédent, je pense. Par conséquent, LiveData gardé référence à la fois Observers '(en MainFragment et TrashFragment) et a appelé les deux valeurs.

Donc je suppose que la conclusion peut-être, que vous devez obtenir de l' ViewModel de ViewModelProviders partir de:

  • Fragment en Fragment
  • Activity en Activity

Btw.

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

n'est pas nécessaire dans les Fragments, cependant je vous conseille de le mettre dans onStop.

44voto

Vasiliy Points 8645

Je bifurquais votre projet et testé un peu. De tout ce que je peux dire vous avez découvert un bug grave.

Pour faire de la reproduction et de l'enquête plus facile, j'ai édité votre projet un peu. Vous pouvez trouver du projet mis à jour ici: https://github.com/techyourchance/live-data-problem . J'ai également ouvert un pull demande de retour auprès de votre pension.

Pour être sûr que cela ne passe pas inaperçue, j'ai aussi ouvert un problème dans Google issue tracker:

Étapes pour reproduire:

  1. S'assurer que REPRODUCE_BUG est définie sur true dans MainFragment
  2. Installer l'application
  3. Cliquez sur "ajouter à la corbeille note" bouton
  4. Interrupteur à TrashFragment
  5. Notez qu'il y a juste un formulaire de notification LiveData avec la valeur correcte
  6. Interrupteur à MainFragment
  7. Cliquez sur "ajouter à la corbeille note" bouton
  8. Interrupteur à TrashFragment
  9. Notez qu'il y a deux notifications de LiveData, le premier avec la valeur incorrecte

Notez que si vous définissez REPRODUCE_BUG à false, alors le bug n'est pas se reproduire. Il démontre que l'abonnement LiveData dans MainFragment changé le comportement dans TrashFragment.

Résultat attendu: Juste une notification avec la valeur correcte dans tous les cas. Aucun changement de comportement dues à des abonnements.

Plus d'infos: j'ai regardé les sources un peu, et il ressemble les notifications d'être déclenchée par le fait que les deux LiveData de l'activation et de la nouvelle L'observateur de l'abonnement. Peut-être lié à la manière ComputableLiveData décharge onActive() le calcul de l'Exécuteur testamentaire.

18voto

Blcknx Points 719

Ce n'est pas un bug, c'est une fonctionnalité. Lire pourquoi!

Les observateurs méthode void onChanged(@Nullable T t) est appelé deux fois. C'est très bien.

La première fois, il est appelé au démarrage. La deuxième fois, il est appelé dès que la Chambre a chargé les données. Par conséquent, dès le premier appel de l' LiveData objet est toujours vide. Il est conçu de cette façon pour de bonnes raisons.

Deuxième appel

Commençons par le deuxième appel, votre point 7. La documentation de l' Room dit:

Chambre génère tout le code nécessaire à la mise à jour de la LiveData objet lorsqu'une base de données est mise à jour. Le code généré exécute la requête de façon asynchrone sur un thread d'arrière-plan en cas de besoin.

Le code généré est un objet de la classe ComputableLiveData mentionné dans d'autres publications. Il gère un MutableLiveData objet. Sur cette LiveData objet qu'il appelle LiveData::postValue(T value) qui appelle LiveData::setValue(T value).

LiveData::setValue(T value) des appels LiveData::dispatchingValue(@Nullable ObserverWrapper initiator). Cela exige LiveData::considerNotify(ObserverWrapper observer) avec l'observateur wrapper comme paramètre. Cela appelle enfin onChanged() lors de l'observateur avec le chargement de données en paramètre.

Premier appel

Maintenant, pour le premier appel, votre point 6.

Vous définissez votre qualité d'observateurs au sein de l' onCreateView() crochet méthode. Après ce stade, les changements de cycle de vie elle fait état de deux fois de venir visible, on start et on resume. La classe interne LiveData::LifecycleBoundObserver est notifié lors de ces changements d'état parce qu'il met en œuvre l' GenericLifecycleObserver interface, qui tient une méthode nommée void onStateChanged(LifecycleOwner source, Lifecycle.Event event);.

Cette méthode appelle ObserverWrapper::activeStateChanged(boolean newActive) comme LifecycleBoundObserver s'étend ObserverWrapper. La méthode activeStateChanged des appels dispatchingValue() qui appelle à son tour LiveData::considerNotify(ObserverWrapper observer) avec l'observateur wrapper comme paramètre. Cela appelle enfin onChanged() sur le observateur.

Tout cela se passe sous certaines conditions. J'avoue que je n'ai pas étudié toutes les conditions à l'intérieur de la chaîne de méthodes. Il y a deux changements d'état, mais onChanged() il est déclenché une fois, parce que les conditions de vérifier ce genre de choses.

La ligne de fond ici est, qu'il y est une chaîne de méthodes, qui est déclenché lors de changements du cycle de vie. Il est responsable pour le premier appel.

La ligne de fond

Je pense que rien ne va mal avec votre code. C'est tout simplement beau, que l'observateur est appelé lors de la création. De sorte qu'il peut se remplir avec les données initiales du modèle de vue. Qu'est ce qu'un observateur devrait le faire, même si la base de données une partie de la vue modèle est toujours vide lors de la première notification.

L'utilisation de la

La première notification indiquera que le modèle de vue est prêt pour l'affichage, malgré qu'il n'est pas encore chargé avec des données provenant de bases de données sous-jacente. La deuxième notification indique que ces données est prêt.

Quand vous pensez de la lenteur de db connections, c'est une approche raisonnable. Vous pouvez récupérer et afficher d'autres données à partir de la vue du modèle, déclenchée par la notification, qui ne vient pas de la base de données.

Android dispose d'une ligne directrice sur la façon de traiter avec la lenteur de la base de données de chargement. Ils suggèrent d'utiliser des espaces réservés. Dans cet exemple, l'écart n'est que de courte, qu'il n'y a aucune raison d'aller à une telle extension.

Annexe

Les deux Fragments de l'utilisation de leurs propres ComputableLiveData objets, c'est pourquoi le deuxième objet n'est pas préchargés à partir du premier fragment.

Penser également le cas de la rotation. Les données du modèle de vue ne change pas. Il n'a pas de déclencher une notification. Les modifications de l'état du cycle de vie seul déclencheur de la notification de la nouvelle vue.

11voto

Zhuinden Points 3074

Je lui ai arraché Vasiliy de la fourche de votre fourche à la fourchette et fait de réels débogage pour voir ce qui se passe.

Peut-être lié à la manière ComputableLiveData décharge onActive() le calcul de l'Exécuteur testamentaire.

De près. Le chemin de la Salle de LiveData<List<T>> exposer les oeuvres, c'est qu'il crée un ComputableLiveData, qui garde la trace de savoir si votre jeu de données a été invalidée en dessous dans la Chambre.

trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();

Ainsi, lorsque l' note tableau est écrit, puis le InvalidationTracker lié à la LiveData appellera invalidate() lorsqu'une écriture qui se passe.

  @Override
  public LiveData<List<Note>> getNotes() {
    final String _sql = "SELECT * FROM note where trashed = 0";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    return new ComputableLiveData<List<Note>>() {
      private Observer _observer;

      @Override
      protected List<Note> compute() {
        if (_observer == null) {
          _observer = new Observer("note") {
            @Override
            public void onInvalidated(@NonNull Set<String> tables) {
              invalidate();
            }
          };
          __db.getInvalidationTracker().addWeakObserver(_observer);
        }

Maintenant ce que nous devons savoir est que, ComputableLiveDatas' invalidate() sera effectivement actualiser le jeu de données, si le LiveData est active.

// invalidation check always happens on the main thread
@VisibleForTesting
final Runnable mInvalidationRunnable = new Runnable() {
    @MainThread
    @Override
    public void run() {
        boolean isActive = mLiveData.hasActiveObservers();
        if (mInvalid.compareAndSet(false, true)) {
            if (isActive) { // <-- this check here is what's causing you headaches
                mExecutor.execute(mRefreshRunnable);
            }
        }
    }
};

liveData.hasActiveObservers() est:

public boolean hasActiveObservers() {
    return mActiveCount > 0;
}

Donc, refreshRunnable effectivement ne fonctionne que si il y a un observateur actif (autant que je sache les moyens du cycle de vie est d'au moins commencé, et observe les données en direct).



Cela signifie que lorsque vous vous abonnez à TrashFragment, puis ce qui se passe, c'est que votre LiveData est stocké dans l'Activité de sorte qu'il est maintenu en vie même quand TrashFragment est parti, et conserve la valeur précédente.

Toutefois, lorsque vous ouvrez TrashFragment, puis TrashFragment souscrit, LiveData devient actif, ComputableLiveData contrôles d'invalidation (ce qui est vrai, comme il n'a pas été calculé car le live de données n'était pas active), le calcule de manière asynchrone sur le thread d'arrière-plan, et quand il est terminé, la valeur est affichée.

Ainsi, vous obtenez deux rappels parce que:

1.) première "onChanged" appel précédemment, la valeur retenue de l'LiveData maintenu en vie dans l'Activité du ViewModel

2.) deuxième "onChanged" appel est le nouveau résultat évaluée à partir de votre base de données, où le calcul a été déclenchée par les données en direct de la Salle est devenue active.


Donc, techniquement, c'est par la conception. Si vous voulez vous assurer que vous obtenez seulement le "plus récent et le plus grand" de la valeur, alors vous devriez utiliser un fragment d'étendue ViewModel.

Vous pouvez également commencer à observer en onCreateView(), et l'utilisation viewLifecycle pour le cycle de vie de votre LiveData (c'est un nouvel ajout de sorte que vous n'avez pas besoin de supprimer les observateurs en onDestroyView().

S'il est important que le Fragment voit la dernière valeur, même lorsque le Fragment n'est PAS actif et ne PAS l'observer, alors que le ViewModel est une Activité étendue, vous pouvez enregistrer un observateur dans l'Activité pour s'assurer qu'il existe un observateur actif sur votre LiveData.

6voto

katharmoi Points 104

C'est ce qui se passe sous le capot:

ViewModelProviders.of(getActivity())

Comme vous utilisez getActivity() il conserve votre NoteViewModel bien que la portée de MainActivity est vivant est donc de votre trashedNotesLiveData.

Quand vous ouvrez d'abord votre TrashFragment chambre des requêtes de la base de données et votre trashedNotesLiveData est rempli avec le saccagé valeur (Lors de la première ouverture il y a seulement un onChange() l'appel). Si cette valeur est mise en cache dans trashedNotesLiveData.

Ensuite, vous arrivez à le principal fragment ajouter un peu saccagé notes et aller à la TrashFragment de nouveau. Cette fois, vous êtes d'abord servi avec la valeur mise en cache dans trashedNotesLiveData tandis que la salle fait async requête. Lorsque la requête se termine, vous êtes apporté la dernière valeur. C'est pourquoi vous obtenez deux onChange() appelle.

Donc la solution est que vous devez nettoyer le trashedNotesLiveData avant de l'ouvrir TrashFragment. Cela peut être fait soit dans votre getTrashedNotesLiveData() la méthode.

public LiveData<List<Note>> getTrashedNotesLiveData() {
    return NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
}

Ou vous pouvez utiliser quelque chose comme cela SingleLiveEvent

Ou vous pouvez utiliser un MediatorLiveData qui intercepte la Salle généré et ne renvoie que des valeurs distinctes.

final MediatorLiveData<T> distinctLiveData = new MediatorLiveData<>();
    distinctLiveData.addSource(liveData, new Observer<T>() {
        private boolean initialized = false;
        private T lastObject = null;

        @Override
        public void onChanged(@Nullable T t) {
            if (!initialized) {
                initialized = true;
                lastObject = t;
                distinctLiveData.postValue(lastObject);
            } else if (t != null && !t.equals(lastObject)) {
                lastObject = t;
                distinctLiveData.postValue(lastObject);
            }

        }
    });

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X