What is the point of a basic Riverpod Provider in Flutter? - android

I'm fairly new to Flutter and am now implementing Riverpod. I've studied a number of guides and tutorials, and they all seem to start with the same simple example - either a basic provider which provides "Hello World", or an integer, as follows:
final countProvider = Provider<Int>((ref) => 0);
final hellowWorldProvider = Provider<String>((ref) => "Hello world");
There is then a Consumer object within a StatelessWidget which calls watch() this provider to get the value.
My question is, what is the point of this? Not a single one of the tutorials that I've found show a way of modifying the value returned by the provider, so it will always be the same. What advantage is there in using the provider here? Why call watch() on a value that doesn't change? You could just hard code the value in the Widget, or if that's frowned upon, use a constants file or equivalent.
Please tell me what I'm missing here, thanks!

Here are 2 use cases.
1. Knowing if a user is logged out
A simple use-case of a basic Provider is knowing if the user is logged in without depending on a userProvider directly.
Take a look at this model and provider:
class UserModel {
final String fullName;
final String email;
UserModel(this.fullName, this.email);
}
final userProvider = StateProvider<UserModel?>((ref) {
return UserModel("Person User", "person#stackoverflow.com");
});
final isLoggedInProvider = Provider<bool>((ref) {
bool isLoggedIn = ref.watch(userProvider) != null;
return isLoggedIn;
});
Say we have a logout method that sets the userProvider state value to null:
void logout(WidgetRef ref) {
ref.read(userProvider.state).state = null;
}
Immediately log out is called, every consumer listening to isLoggedInProvider would react to the "logged out" state of the user.
2. Dependency Injection
Say we have a basic Provider that provides a repository like so:
final dataRepositoryProvider = Provider<DataRepository>((ref) {
return DataRepository();
});
class DataRepository {
List<String> _data = [];
List<String> getSortedData() {
_data.sort();
return _data;
}
}
And there's a StateNotifierProvider that depends on DataRepository - If we don't want to keep creating new objects of DataRepository, it could be injected like so:
final dataStateNotifierProvider = StateNotifierProvider<DataStateNotifier, List<String>>((ref) {
var repo = ref.read(dataRepositoryProvider);
return DataStateNotifier(repo);
});
class DataStateNotifier extends StateNotifier<List<String>> {
final DataRepository _dataRepository;
DataStateNotifier(this._dataRepository) : super([]);
void getData(){
state = _dataRepository.getSortedData();
}
}
I hope this helps you understand.

Related

How to display data in Fragment and Activity (both independent) from same Snapshot Listener (Firebase)

Would like to have your help on my weird problem that currently I am facing. I tried for couple of days but no luck and finally decided to post here to take help.
I created a Snapshot Listener attached to a Collection in Firebase defined as follows :-
public class FirebaseTypingStatusLiveData extends LiveData<List<documentSnapshot>> {
// Logging constant
private static final String TAG = "FirebaseQueryLiveData";
// Document Reference
private final DocumentReference documentReference;
// Listener
private final MyDocumentListener listener = new MyDocumentListener();
// Handler
private final Handler handler = new Handler();
private ListenerRegistration listenerRegistration;
// Flag to remove listener
private boolean listenerRemovePending = false;
private MutableLiveData <List<documentSnapshot> mutableLiveData = new MutableLiveData<>();
// Constructor
public FirebaseTypingStatusLiveData(DocumentReference documentReference) {
this.documentReference = documentReference;
}
public LiveData<List<documentSnapshot>> checknow(){
// Add listener
if (!Listeners.LIVESAMPLE.containsKey(documentReference)) {
listenerRegistration = documentReference.addSnapshotListener(listener);
Listeners.LIVESAMPLE.put(documentReference, listenerRegistration);
} else {
listenerRegistration = Listeners.LIVETYPINGSTATUSSAMPLE.get(documentReference);
}
return mutableLiveData;
}
// Listener definition
private class MyDocumentListener implements EventListener<DocumentSnapshot> {
#Override
public void onEvent(#Nullable DocumentSnapshot documentSnapshot, #Nullable
FirebaseFirestoreException e) {
Log.d(TAG, "onEvent");
// Check for error
if (e != null) {
// Log
Log.d(TAG, "Can't listen to query snapshots: " + documentSnapshot
+ ":::" + e.getMessage());
return;
}
setValue(documentSnapshot);
mutableLiveData.setValue(documentSnapshot);
}
}
}
}
The snapshot reads the data perfectly and advised as and when data is available.
The snapshot data is getting displayed 1. in Fragment (not part of Activity that i am talking about) 2. Activity through two view models that have the same code as follows :
#NonNull
public LiveData<List<documentSnapshot>> getDataSnapshotLiveData() {
Firestore_dB db = new Firestore_dB();
DocumentReference docref = db.get_document_firestore("Sample/"+docID);
FirebaseTypingStatusLiveData firebaseTypingStatusLiveData = new
FirebaseTypingStatusLiveData(docref);
return firebaseTypingStatusLiveData.checknow();
}
The Fragment & Activity code is also same except changing owner which are as follows :-
LiveData<List<documentSnapshot>> liveData = viewmodel.getDataSnapshotLiveData();
liveData.observe(this, new Observer<List<documentSnapshot>>() {
#Override
public void onChanged(DocumentReference docreef) {
String name = docreef.get("name");
stringname.setText(name); // The text is displaying either in Fragment or in Activity but not in both.
});
My problem is i need data in both i.e. Fragment & Activity whereas I am getting data either in Fragment or in Activity depending upon the code which I commented.
Kindly advise where I am making mistake. Thanks in Advance
Honestly, I am not sure that my answer wouldn't lead you away to the false way, but you can try.
My guess is that your problem could be somehow connected with ViewModel sharing.
There is a well-known task How to share Viewmodel between fragments.
But in your case, that can't help, because you have to share ViewModel between activities (now you have two separate ViewModels and that could be problem with Firestore EventListeners).
Technically you can share ViewModel between activities (I haven't try since usually I use Single activity pattern). For that as a owner parameter in ViewModelProvider constructor you can set instance of your custom Application class (but you have implement interface ViewModelStoreOwner for it). After that both in your activity and in your fragment you can get the same ViewModel with the Application class-instance:
val sharedViewModel = ViewModelProvider(mainApplication, viewModelFactory).get(SharedViewModel::class.java)
I made LiveData static that listens to changes in source data and provide updated content were ever required in different Activity.

Dao method returns List<String> while I need a Map<String,Integer>

In an Android app using Architecture Components I have the following view model:
public class MainViewModel extends AndroidViewModel {
private final MutableLiveData<List<String>> mUnchecked = new MutableLiveData<>();
private LiveData<List<String>> mChecked;
public void setUnchecked(List<String> list) {
mUnchecked.setValue(list);
}
public LiveData<List<String>> getChecked() { // OBSERVED BY A FRAGMENT
return mChecked;
}
public MainViewModel(Application app) {
super(app);
mChecked = Transformations.switchMap(mUnchecked,
list-> myDao().checkWords(list));
}
The purpose of the above switchMap is to check, which of the words passed as a list of strings, do exist in a Room table:
#Dao
public interface MyDao {
#Query("SELECT word FROM dictionary WHERE word IN (:words)")
LiveData<List<String>> checkWords(List<String> words);
The above code works well for me!
However I am stuck with wanting something slightly different -
Instead of the list of strings, I would prefer to pass a map of strings (words) -> integers (scores):
public void setUnchecked(Map<String,Integer> map) {
mUnchecked.setValue(map);
}
The integers would be word scores in my game. And once the checkWords() has returned the results, I would like to set the scores to null for the words not found in the Room table and leave the other scores as they are.
The programming code would be easy (iterate through mChecked.getValue() and set to null for the words not found in the list returned by the DAO method) - but how to "marry" it with my LiveData members?
TL;DR
I would like to change my view model to hold maps instead of the lists:
public class MainViewModel extends AndroidViewModel {
private final MutableLiveData<Map<String,Integer>> mUnchecked = new MutableLiveData<>();
private final MutableLiveData<Map<String,Integer>> mChecked = new MutableLiveData<>();
public void setUnchecked(Map<String,Integer> map) {
mUnchecked.setValue(map);
}
public LiveData<Map<String,Integer>> getChecked() { // OBSERVED BY A FRAGMENT
return mChecked;
}
public MainViewModel(Application app) {
super(app);
// HOW TO OBSERVE mUnchecked
// AND RUN myDao().checkWords(new ArrayList<>(mUnchecked.getValue().keys()))
// WRAPPED IN Executors.newSingleThreadScheduledExecutor().execute( ... )
// AND THEN CALL mChecked.postValue() ?
}
How to achieve that please? Should I extend MutableLiveData or maybe use MediatorLiveData or maybe use Transformations.switchMap()?
UPDATE:
I will try the following tomorrow (today is too late in the evening) -
The Dao method I will change to return a list instead of LiveData:
#Query("SELECT word FROM dictionary WHERE word IN (:words)")
List<String> checkWords(List<String> words);
And then I will try to extend the MutableLiveData:
private final MutableLiveData<Map<String,Integer>> mChecked = new MutableLiveData<>();
private final MutableLiveData<Map<String,Integer>> mUnchecked = new MutableLiveData<Map<String,Integer>>() {
#Override
public void setValue(Map<String,Integer> uncheckedMap) {
super.setValue(uncheckedMap);
Executors.newSingleThreadScheduledExecutor().execute(() -> {
List<String> uncheckedList = new ArrayList<>(uncheckedMap.keySet());
List<String> checkedList = WordsDatabase.getInstance(mApp).wordsDao().checkWords(uncheckedList);
Map<String,Integer> checkedMap = new HashMap<>();
for (String word: uncheckedList) {
Integer score = (checkedList.contains(word) ? uncheckedMap.get(word) : null);
checkedMap.put(word, score);
}
mChecked.postValue(checkedMap);
});
}
};
Well, what you have there in the update probably works, though I wouldn't create a new Executor for every setValue() call — create just one and hold onto it in your MutableLiveData subclass. Also, depending on your minSdkVersion, you might use some of the Java 8 stuff on HashMap (e.g., replaceAll()) to simplify the code a bit.
You could use MediatorLiveData, though in the end I think it would result in more code, not less. So, while from a purity standpoint MediatorLiveData is a better answer, that may not be a good reason for you to use it.
Frankly, this sort of thing isn't what LiveData is really set up for, IMHO. If this were my code that I were working on right now, I'd be using RxJava for the bulk of it, converting to LiveData in the end. And, I'd have as much of this as possible in a repository, rather than in a viewmodel. While your unchecked-to-checked stuff would be a tricky RxJava chain to work out, I'd still prefer it to the MutableLiveData subclass.
What EpicPandaForce suggests is an ideal sort of LiveData-only approach, though I don't think he is implementing your algorithm quite correctly, and I am skeptical that it can be adapted easily to your desired algorithm.
In the end, though, the decision kinda comes down to: who is going to see this code?
If this code is for your eyes only, or will live in a dusty GitHub repo that few are likely to look at, then if you feel that you can maintain the MutableLiveData subclass, we can't really complain.
If this code is going to be reviewed by co-workers, ask your co-workers what they think.
If this code is going to be reviewed by prospective employers... consider RxJava. Yes, it has a learning curve, but for the purposes of getting interest from employers, they will be more impressed by you knowing how to use RxJava than by you knowing how to hack LiveData to get what you want.
Tricky question!
If we check the source code for Transformations.switchMap, we see that:
1.) it wraps the provided live data with a MediatorLiveData
2.) if the wrapped live data emits an event, then it invokes a function that receives the new value of wrapped live data, and returns a "new" live data of a different type
3.) if the "new" live data of a different type differs from the previous one, then the observer of the previous one is removed, and it's added to the new one instead (so that you only observe the newest LiveData and don't accidentally end up observing an old one)
With that in mind, I think we can chain your switchMap calls and create a new LiveData whenever myDao().checkWords(words) changes.
LiveData<List<String>> foundInDb = Transformations.switchMap(mWords, words -> myDao().checkWords(words));
LiveData<Map<String, Integer>> found = Transformations.switchMap(foundInDb, (words) -> {
MutableLiveData<Map<String, Integer>> scoreMap = new MutableLiveData<>();
// calculate the score map from `words` list
scoreMap.setValue(map);
return scoreMap;
});
this.mFound = found;
Please verify if what I'm telling you is correct, though.
Also if there are a bunch of words, consider using some async mechanism and scoreMap.postValue(map).

manage null boolean with RxJava2 PublishSubject

I'm implementing the MVP design pattern. My presenter receives the new values from the view. I want to manage the state of a next button by automatically check if everything is valid when values are updated on the view.
In my form I have an optional part which is displayed only if the user select the correct option.
In this optional part I have a binary question. If the part is not displayed I need to set the value of the question to null on the Presenter side.
For example, the user select the option and the optional part is displayed. The user select the answer. Then the user change the option and the optional part is hidden. In that case I need to set the answer to the optional question to null, for the answer to not be already selected if the user display the optional part again.
To do so, I call a method on the Presenter with a null value instead of true/false.
Here is the code:
private final PublishSubject<Boolean> mObsOptionalAnswer = PublishSubject.create();
public MyPresenter(){
// Combine all the values together to enable/disable the next button
Observable.combineLatest(
// ... other fields
// I need this to return false if the optional part is
// displayed but nothing is selected
mObsOptionalAnswer.map(this::isValid),
(...) -> ...
).subscrible(enable ->{
mView.enableBtn(enable);
});
}
public void myFunction(Boolean isSomething){
// ... some code
mObsOptionalAnswer.onNext(isSomething);
}
private boolean isValid(Boolean value){
return value != null;
}
The problem is, since RxJava 2, null values are not allowed in the onNext() method.
So, how am I supposed to manage that?
If you want to be able to send a null value, you can use a wrapper. In this configuration, you send the wrapper, which isn't null even if the value itself is.
public class BooleanWrapper {
public final Boolean value;
public BooleanWrapper(Boolean value) {
this.value = value;
}
}
Your PublishSubject<Boolean> becomes a PublishSubject<BooleanWrapper> and you just have to create the wrapper and de-reference your Boolean when needed :
mObsOptionalAnswer.onNext(new BooleanWrapper(isSomething));
and
mObsOptionalAnswer.map(wrapper -> this.isValid(wrapper.value))
If you need to do that more than once in your code, you can create a generic wrapper (as described by this tutorial) :
public class Optional<M> {
private final M optional;
public Optional(#Nullable M optional) {
this.optional = optional;
}
public boolean isEmpty() {
return this.optional == null;
}
public M get() {
return optional;
}
}
you could use a constante Boolean object
public static final Boolean RESET_VALUE = new Boolean(false);
and you can emit this instead of emitting null. The receiver would have to check against this instance and behaving accordingly. Eg.
.subscrible(enable ->{
if (enable != RESET_VALUE) {
mView.enableBtn(enable);
}
});

MvvmCross Android - ViewModel never garbage collected

I noticed that MvxMessenger subscriptions were being invoked multiple times due to multiple instances of the same ViewModel. I read a bit about unsubscribing and disposing the tokens (which works and prevents multiple invocations) but I wanted to see the ViewModel being garbage collected naturally and the Messenger subscription along with it.
I wanted to set up a test project for Android similar to this one https://github.com/slodge/MessengerHacking. So here are the two ViewModels
public class FirstViewModel : MvxViewModel
{
private string _hello = "Hello MvvmCross";
public string Hello
{
get { return _hello; }
set { _hello = value; RaisePropertyChanged(() => Hello); }
}
private MvxCommand _showSecond;
public ICommand ShowSecond {
get {
_showSecond = _showSecond ?? new MvxCommand(() => ShowViewModel<SecondViewModel> ());
return _showSecond;
}
}
}
And
public class SecondViewModel : MvxViewModel
{
private readonly IMvxMessenger _messenger;
private readonly MvxSubscriptionToken _token;
public SecondViewModel(IMvxMessenger messenger) {
_messenger = messenger;
_token = _messenger.Subscribe<MyMessage> ((message) => {
Debug.WriteLine("incoming message");
});
}
private MvxCommand _send;
public ICommand Send {
get {
_send = _send ?? new MvxCommand(() => _messenger.Publish (new MyMessage (this)));
return _send;
}
}
private MvxCommand _garbageCollect;
public ICommand GarbageCollect {
get {
_garbageCollect = _garbageCollect ?? new MvxCommand(() => GC.Collect ());
return _garbageCollect;
}
}
}
Then I just have two MvxActivities bound to these ViewModels. If I go to SecondViewModel and click send I see the subscribed event fire off once. If I go back and forth between the First and SecondViewModel these event subscriptions build up and clicking Send fires each of them. Clicking GarbageCollect doesn't seem to make any difference (I'm hoping to see it invoked only once after clicking this).
It feels as though when I click the back button from SecondViewModel, once the MvxActivity is destroyed then SecondViewModel should be eligible for garbage collection.
Another thing I noticed is that even if I Subscribe without saving it into a token, the behaviour is the same. The only way I have successfully got the events to stop firing is by saving the token and calling Unsubscribe or Dispose on the token, however it feels like SecondViewModel is still not getting garbage collected in this case.
Could this be something to do with the recent changes to Xamarin.Android? Or is there something I just don't get!
Many thanks
I know it is too late for answer, but for the sake of reference:
Short Answer:
In view(MvxActivity), handle DestroyCalled event like this:
DestroyCalled += (s, e) =>
{
if (ViewModel is IDisposable)
(ViewModel as IDisposable).Dispose();
};
In viewmodel, implement IDisposable interface:
public new void Dispose()
{
base.Dispose();
//Unsubscribe messages here
}
Long Answer:
http://slodge.blogspot.co.uk/2013/11/n42-is-my-viewmodel-visible-can-i-kill.html

return an object Android

I want to return an object with some things in them.
Here is the declaration;
Object user_det = get_user_det();
Here is the function code:
private Object get_user_det() {
Firebase f_user = new Firebase("https://myapp.firebaseio.com/User/");
f_user.addListenerForSingleValueEvent(new ValueEventListener(){
#Override
public void onDataChange(DataSnapshot snap_user) {
// TODO Auto-generated method stub
Iterable<DataSnapshot> rs = snap_user.getChildren();
Iterator<DataSnapshot> irs = rs.iterator();
long allNum2 = snap_user.getChildrenCount();
int maxNum2 = (int)allNum2;
int count_user = 1;
while(irs.hasNext())
{
if(count_user <= maxNum2)
{
Firebase user_data = new Firebase("https://myapp.firebaseio.com/");
AuthData authData = user_data.getAuth();
Map<String, Object> nPost = (Map<String, Object>) irs.next().getValue();
String db_email = nPost.get("email_addr").toString();
if (authData != null) {
String usr_email = authData.getProviderData().get("email").toString();
if(usr_email.equals(db_email))
{
//NB: I WANT TO ADD THE FOLLOWING INTO THE OBJECT
String disp_name = nPost.get("disp_name").toString();
String real_name = nPost.get("real_name").toString();
}
} else {
System.out.println("Failed");
}
}
count_user++;
}
}
#Override
public void onCancelled(FirebaseError arg0) {
// TODO Auto-generated method stub
}
});
return null; //NB: I NEED TO RETURN THE OBJECT HERE.
}
I want to return the string disp_name and real_name but they are inside the addListenerForSingleValueEvent, so how do I get them out and return it to the function.
I have wrote "NB" in the code where I need help with.
Thanks for your time.
If you want to return an object from your method in java, do it like this:
The Object class:
This contains the structure of your Object, and defines what data will be in it. Also includes methods to easily get the data.
private class myObject {
private String name;
private String realName;
//The constructor, so you can set the data when creating the Object.
public myObject (String disp_name, String real_name) {
name = disp_name;
realName = real_name;
}
//Getter methods, to get the data.
public String getRealName() {return realName;}
public String getDisplayName() {return name;}
}
Your code:
private Object get_user_det() {
myObject o; //Declare it, so it can be returned.
...
String disp_name = nPost.get("disp_name").toString();
String real_name = nPost.get("real_name").toString();
o = new myObject(disp_name, real_name); //create it and set the data.
...
return myobject; //return the new Object with the data.
}
To get the data from the Object:
myObject o = get_user_det(); //Call the metod which return our Object.
String realName = o.getRealName(); //Get the data from the Object.
String displayName = o.getDisplayName;
In your case, it would be much easier to use a String array.
Hope this helps.
It's probably easiest to see what's going on, if you add some printlns to your code:
private Object get_user_det() {
Firebase f_user = new Firebase("https://myapp.firebaseio.com/User/");
System.out.println("Adding listener");
f_user.addListenerForSingleValueEvent(new ValueEventListener(){
#Override
public void onDataChange(DataSnapshot snap_user) {
System.out.println("Data received");
}
#Override
public void onCancelled(FirebaseError arg0) {
// TODO Auto-generated method stub
}
});
System.out.println("Returning");
return null; //NB: I NEED TO RETURN THE OBJECT HERE.
}
If you execute this code, you will see that it logs:
Adding listener
Returning
Data received
Most likely, this is not what you expected. But hopefully, it makes sense if you read my explanation below.
Asynchronous loading
When you register your listener:
f_user.addListenerForSingleValueEvent(new ValueEventListener(){
You tell Firebase to start listening for events. It goes off and starts retrieving the data from the server.
Since retrieving the data may take some time, it does this retrieval asynchronously so that your thread isn't blocked. Once the data is completely retrieved, Firebase calls the onDataChange method in your listener.
Between the time you start listening and the time onDataChange is called, your code continues executing. So there is no way to return data that is loaded asynchronously, because by the time your function returns, the data isn't loaded yet.
Solutions
Disclaimer: I am not an expert at solving this problem in Java, so there may be problems with my solutions. If^H^HWhen you find any, please report them in the comments.
I know of three possible solutions to the problem:
force the code to wait for the data to be returned
return a Future that at some point will contain the data
pass a callback into get_user_det and call that function once the data is available
You will probably be tempted to selected option 1, since it matches most closely with your mental modal of loading data. While this is not necessarily wrong, keep in mind that there is a good reason that the loading is done asynchronously. It might be worth taking the "learning how to deal with asynchronicity" penalty now.
Instead of writing up examples for all solutions, I'll instead refer to some relevant questions:
Retrieving data from firebase returning NULL (an answer that uses approach 3)
Is waiting for return, ok?
Java wait() & notify() vs Android wait() & notify() (a question from a user taking approach 1)
How it works:
Firebase uses reflection to build a JSON tree object to save to the database. When you retrieve this JSON tree, you can cast it back to your original object. Just like serializing and deserializing. This means you do not need to handle the keys and values when trying to "rebuild" your object like you are. It can all be done like so:
YourObject object = (YourObject) dataSnapshot.getValue(YourObject.class);
Notice the YourObject.class in the getValue(). This tells firebase to reflect through this class and find the appropriate accessors with the dataSnapshot.
How to do it
Be sure that your object has:
Accessors Appropriate getters and setters for ALL fields - (or annotated with #JsonIgnore if you wish to not save a particular field)
Empty constructor. Your object must provide a constructor that does not modify itself at all.
What your object should look like:
public class YourObject {
private String displayName;
private String realName;
public YourObject() { /*Empty constructor needed for Firebase */ }
// Accessors
public void setRealName(String realName){
this.realName = realName;
}
public String getRealName(){
return this.realName;
}
public String getDisplayName(){
return this.displayName;
}
public void setDisplayName(String displayName){
this.displayName = displayName;
}
}
Then, in any of the firebase callbacks, you can just cast your DataSnapshot in to your object:
public void onDataChange(DataSnapshot snap_user) {
YourObject object = new Object;
if(snap_user.getValue() != null) {
try {
object = (YourObject) snap_user.getValue(YourObject.class); <-- Improtant!!!
} catch(ClassCastException ex) {
ex.printStackTrace();
}
}
return object;
}
Also
It seems you are retrieving many objects. When doing this, I find it best to use the onChildEventListener then for each of the YourObjects in that node, onChildAdded(DataSnapshot ds, String previousChild); will be called.

Categories

Resources