3
votes

LiveData onChanged n'est pas appelé lorsque les données changent

Je suis nouveau dans l'utilisation de LiveData et ViewModels et je ne comprends pas parfaitement comment les données sont mises à jour.

D'après le site Android Dev :

Au lieu de mettre à jour l'interface utilisateur chaque fois que les données de l'application changent, votre l'observateur peut mettre à jour l'interface utilisateur à chaque fois qu'il y a un changement.

Cette phrase n'a pas de sens pour moi. J'utilise une base de données Sqlite (sans Room) et j'obtiens les données de manière asynchrone dans la méthode getItems () du ViewModel . Cela signifie-t-il que lorsque des données sont ajoutées ou mises à jour dans la base de données, LiveData les détecte automatiquement et onChange est appelé?

Selon le développeur Android site, il dit de commencer à observer un objet LiveData dans onCreate . Avant d'utiliser LiveData , j'actualisais les données dans onResume en interrogeant la base de données puis en appelant notifyDataSetChanged().

Ceci le code que j'ai maintenant affiche les données lorsque le Fragment se charge, mais si je supprime ou ajoute de nouvelles données à la base de données, l'interface utilisateur ne reflète pas le changement. Je pense juste que je n'ai pas une compréhension de base du fonctionnement de LiveData et je ne trouve nulle part une bonne explication.

Aidez-moi StackOverflow, vous êtes mon seul espoir .

public class MyViewModel extends AndroidViewModel {
    private MutableLiveData<List<String>> items;
    private Application application;

    public MyViewModel(@NonNull Application application) {
        super(application);
        this.application = application;
    }

    public MutableLiveData<List<String>> getItems() {
        if (items == null) {
            items = new MutableLiveData<>();
            getItems();
        }
        return items;
    }

    private void getItems() {
        List<String> items = GetItems(); //async process

        this.items.setValue(items)
    }
}

public class ListFragment extends Fragment {

    @Override
    public void onCreate(final Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        MyViewModel model = ViewModelProviders.of(getActivity()).get(MyViewModel.class);

        final Observer<List<String>> observer = new Observer<List<String>>() {
            @Override
            public void onChanged(List<String> items) {
                //update UI
            }
        };

        model.getItems().observe(this, observer);
    }

}


5 commentaires

Le problème est que vous n'obtenez les éléments qu'une seule fois, dans votre méthode getItems () . Pour que cela fonctionne comme prévu, vous devrez utiliser un DAO de salle et observer le DAO dans le ViewModel. Lorsque l'observateur DAO dans le ViewModel détecte des modifications dans les données sous-jacentes, mettez à jour les LiveData, puis le fragment obtiendra les données mises à jour.


Je vois. Donc, si je n'utilise pas Room, est-il judicieux d'utiliser LiveData ?


Pouvez-vous rappeler la fonction private void getItems () une fois que vous avez supprimé ou ajouté des éléments à votre base de données? Si oui, rappelez-le.


Puis-je simplement rendre public getItems () ?


J'ai eu le même problème que l'OP, mais mon problème était que mon Fragment n'obtenait pas la même instance du ViewModel que mon Activité . Plus d'informations ici: stackoverflow .com / questions / 61373456 /… .


3 Réponses :


2
votes

Votre code semble correct. Vous configurez tout correctement et vous recevrez un rappel sur votre Observer dans le ListFragment à chaque fois que la valeur de LiveData change.

Cependant , avec ce code, la valeur LiveData ne changera jamais, donc votre LiveData's Observer ne sera appelé qu'une seule fois. Vous n'appelez MyViewModel.getItems qu'une seule fois, dans onCreate et cette méthode fera deux choses: renvoyer les LiveData ET récupérer la liste des éléments.

Une première approche pour vous serait de diviser votre méthode ViewModel getItems en deux: une pour obtenir les LiveData et une pour actualiser la liste actuelle des éléments. Dans le fragment, vous actualiseriez ensuite vos données dans onResume () , comme vous le faisiez auparavant.

Cela ressemblerait à ceci:

public class MyViewModel extends ViewModel {
    private final MutableLiveData<List<String>> items = new MutableLiveData<>();

    public MyViewModel() {
        // In actual code, don't forget to dispose that subscription in ViewModel.onCleared()
        myDatabase.getDatabaseObservable()
            .subscribe(new Subscriber<List<String>>() {
                @Override
                public void onCompleted() {
                }

                @Override
                public void onNext(List<String> newItems) {
                    items.setValue(newItems)
                }

                @Override
                public void onError() {
                }
            })
    }

    public LiveData<List<String>> getItemsLiveData() {
        return items;
    }
}

Pour aller plus loin, vous devrez changer la façon dont vous actualisez réellement les éléments. Si vous souhaitez être notifié dans votre ViewModel lorsque la base de données est modifiée, vous devrez également utiliser un pattern Observer dans la communication entre ViewModel et Database (Room with LiveData, RxJava's Observable ou le nouveau coroutine Flow sont de bons points de départ)

Voici un exemple de pseudo-algorithme utilisant RxJava (mais c'est à vous de choisir quelle bibliothèque Observer utiliser):

public class MyViewModel extends ViewModel {
    private final MutableLiveData<List<String>> items = new MutableLiveData<>();

    public LiveData<List<String>> getItemsLiveData() {
        return items;
    }

    public void refreshItems() {
        List<String> items = GetItems(); //async process

        this.items.setValue(items)
    }
}

public class ListFragment extends Fragment {

    private MyViewModel model;

    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        model = ViewModelProviders.of(getActivity()).get(MyViewModel.class);

        final Observer<List<String>> observer = new Observer<List<String>>() {
            @Override
            public void onChanged(List<String> items) {
                //update UI
            }
        };

        model.getItemsLiveData().observe(this, observer);
    }

    @Override
    public void onResume() {
        model.refreshItems()
    }
}

La dernière partie serait de créer cet Observable dans votre classe Database (soit avec une bibliothèque qui le fait automatiquement comme Room, soit en créant votre propre PublishSubject).


0 commentaires

4
votes

Vous ne récupérez les éléments qu'une seule fois si éléments privés MutableLiveData > n'a pas encore été initialisé:

public class MyViewModel extends AndroidViewModel {   
    ... 
    public MutableLiveData<List<String>> getItems() {
        dataRepository.getItems();
    }
    ...
}

Une façon de surmonter ce problème Le problème pourrait être d'utiliser ContentObserver pour vos données:

...
private final ContentObserver contentObserver = new ContentObserver(new Handler()) {
    @Override
    public void onChange(boolean selfChange) {
        // if there're any changes then perform the request and update the UI
        getItems(); 
    }
};

public LiveData<List<String>> getItems() {
    return new MutableLiveData<String>() {
        @Override
        public void onActive() {
           // your async operation here
           new Thread({
               List<String> items = db.getItems();
               postValue(items);
           }).start();

           contentResolver.registerContentObserver(<your data content uri>, true, contentObserver); 
        }

        @Override
        public void onInactive() {
            contentResolver.unregisterContentObserver(contentObserver); 
        }
    }
    return liveData;
}   
... 

N'oubliez pas de l'enregistrer pour être informé des changements dans la base de données: p>

...
contentResolver.registerContentObserver(<your data content uri>, true, contentObserver);
...

Je suggère également de créer une couche Repository distincte responsable de la récupération des données qui pourrait ressembler à ceci:

...
private final ContentObserver contentObserver = new ContentObserver(new Handler()) {
    @Override
    public void onChange(boolean selfChange) {
        // if there're any changes then perform the request and update the UI
        getItems(); 
    }
};
...


0 commentaires

0
votes

Votre code semble être configuré correctement. Mais la méthode onChanged () de vos Observers sera appelée une fois car aucune partie de votre code ne l'avertit de la modification que vous apportez aux données sous-jacentes.

Je vous suggère d'informer explicitement votre observateur des modifications des données en utilisant la méthode MutableLiveData de setValue () ou postValue () en fonction s'ils sont appelés respectivement depuis le thread principal ou le thread d'arrière-plan. Ou mieux encore, implémentez la base de données ROOM avec ses objets DAO (Data Access Object).

Voici un extrait de code de l'endroit où j'ai fait quelque chose de similaire si vous ne souhaitez pas utiliser ROOM pour l'instant:

class HomeViewModel : ViewModel() {

private val _budgetList = MutableLiveData<ArrayList<Budget>>()
val budgetList: LiveData<ArrayList<Budget>> = _budgetList

private val _billList = MutableLiveData<ArrayList<Bill>>()
val billList: LiveData<ArrayList<Bill>> = _billList

init {
    _budgetList.value = getBudgetList()
    _billList.value = getBills()
}

fun deleteBudget(pos: Int) {
    _budgetList.apply {
        val newValue = value?.apply { removeAt(pos) }  header position
        postValue(newValue)
    }
}

}

Après avoir supprimé une valeur de la liste, la nouvelle liste est envoyée au Livedata pour une mise à jour via la méthode postValue () . Le simple fait de supprimer cette valeur sans la publier ne déclenchera pas la méthode onchange () du Livedata de Observer . J'espère que cela aidera quelqu'un.


0 commentaires