Why this method runs before the other method finishes? - java

I am kinda new to Android programming. I am building a simple application and I have a "follow-unfollow" concept on it. What I simply want is, if the current user follows the user he/she is exploring, I want him/her to see "unfollow" button. If not following, there should be a "follow" button. On my UserProfileActivity class I have a method called onPrepareOptionsMenu() and inside this method I can set the buttons.
#Override
public boolean onPrepareOptionsMenu(Menu menu) {
MenuItem follow = menu.findItem(R.id.action_follow);
MenuItem unfollow = menu.findItem(R.id.action_unfollow);
Bundle bundle = this.getIntent().getExtras();
if(isFollowing(bundle.getString("userid")) == true){
follow.setVisible(false);
unfollow.setVisible(true);
}
else{
follow.setVisible(true);
unfollow.setVisible(false);
}
return super.onPrepareOptionsMenu(menu);
}
Also, I have another method called isFollowing() and it returns a boolean "true" if current user follows the other user, it returns "false" if not. It is the simplest way that I have thought to solve this issue.
public boolean isFollowing(String userID){
isFollowingResult = false;
firebaseDatabase = FirebaseDatabase.getInstance();
databaseReference = firebaseDatabase.getReference();
final DatabaseReference followingData = databaseReference.child("followingData");
followingData.child(currentUser.getUid()).child(userID).addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
if(dataSnapshot.exists())
isFollowingResult = true;
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
return isFollowingResult;
}
When I run this, and when I click on a user's profile onPrepareOptionsMenu() method is called and inside it isFollowing() method is called. The problem is, it does not wait for the isFollowing() method to run and finish running and it immediately sees it as "false" and always shows "follow" button. Any suggestion would be appreciated.
Thanks.

Make follow and unfollow the fields of the class. Then change their visibility in onDataChange(DataSnapshot) method.

There are multiple issues with the code.
Firstly, your isFollowing() function is setting up the listener, on the data field, but it'll only get called when the data changes. In this case, you may only want to read the data once:
https://firebase.google.com/docs/database/android/read-and-write#read_data_once
Secondly, the use of a listener implies asynchronicity. Meaning, you'll need to wait until you get the callback later in order to get the value you want.
The ideal solution in order to maintain responsiveness of your app is to maintain a local "copy" of the value in your database with a listener that constantly updates that value. That way, you can query the state of your variable quickly (since it's stored/replicated locally) and still be up to date with your database (with the listener).
This will also prevent each "read" from going all the way to the service and back and also remove the need for your UI to wait to render correctly (accurately).

Related

Android: Infinite loop with LiveData<Boolean>

This gets called when a button is clicked
#Override
public void onFavoriteIconClicked() {
viewModel.isFavoriteExist(test.getId()).observe(getViewLifecycleOwner(), new Observer<Boolean>() {
#Override
public void onChanged(Boolean aBoolean) {
viewModel.isFavoriteExist(test.getId()).removeObserver(this);
if (aBoolean) {
binding.addToFavorite.setImageResource(R.drawable.non_fav);
viewModel.delete(test);
} else if (getActivity() != null) {
Test test2 = new Test(test.getId(), test.getName());
viewModel.insert(test2);
binding.addToFavorite.setImageResource(R.drawable.fav);
}
}
});
}
If the test object exists in the Favorite database, I have to delete it. After deleting, it calls this again (since it observed a chane) and inserts it again.
It keeps looping infinitely. Is there a better way to implement this or stop this?
It seems like some business logic has entered your view (Activity) class.
Since LiveData & Room are meant to be used when receiving updates about Database changes is needed, and your use of the DB is not requiring constant updates, I would suggest going with a more direct approach.
First, Remove the use of LiveData from your Database. Use simple return values.
Your view (Activity/Fragment) can then tell the view model that a button was clicked.
#Override
public void onFavoriteIconClicked() {
viewModel.onFavoriteClicked()
}
The view will observe the view model in order to receive the correct icon to show.
Something like:
viewModel.favoriteIcon.observe(getViewLifecycleOwner(), new Observer<Integer>() {
#Override
public void onChanged(Integer iconResId) {
binding.addToFavorite.setImageResource(iconResId)
}
}
Now the viewModel can handle the logic (or better add a Repository layer - See Here)
Upon click, Check if entry exist in DB.
If exists: remove it from DB and set favoriteIcon value:
favoriteIcon.setValue(R.drawable.non_fav)
If doesn't exist: Add it to DB and set favoriteIcon value.
favoriteIcon.setValue(R.drawable.fav)
For a good tutorial about using Room & LiveData - as well as doing so using the View/ViewModel/Repository pattern, check this link

Android Java - Race Condition in OnCreate with two Observers and making lists

sorry if this is a convoluted question. Working on creating an app for a college course and I'm running into (what appears to be) a race condition in my OnCreate method.
TL;DR - sometimes my spinner populates and I can get an index from it. Sometimes it's not populated yet when trying to get a specific index. Details and code below.
The app is a "course scheduler" for a college student.
I'm creating an Activity that displays existing course information and allows you to edit it. In the OnCreate method for this Activity, I am filling a spinner for "Mentors" for the course and a spinner for which "Term" the course belongs in. This information is being pulled from a Room DB.
I have a seperate activity for a new course and for editing a course. For the "new course" activity, everything works fine. I getAllMentors() or getAllTerms() successfully and fill the spinner list.
For the "Edit Course" Activity, there's an extra step involved and it seems to be causing me some issues.
When editing a course, I pass the intent from the originating Activity with all the necessary EXTRAS. This is successful.
In OnCreate for EditCourseActivity, I do the following:
I get the mentorID from the EXTRA that's passed in from the originating Activity.
I access my MentorViewModel and call my getAllMentors() method which returns LiveData> of all mentors in the db.
because it returns LiveData, I use an observer and loop through the LiveData adding the Name of each mentor to a List and the
entire mentor to a List.
I populate my spinner with the information in List full of mentor names.
then I do a for loop, looping through List looking for one that has the same id as what I grabbed form the EXTRA in step 1.
If I find a match in that list, I call a getMentorName() method to snag their name as a string.
I have a methond getIndex(spinner, string) that will loop through the provided spinner, trying to find a match for the string that's
passed in (mentors name) that I grabbed that should match the ID of
the mentor assigned to the course. This method returns index location
of the matched string in the spinner.
I set the spinner selection to the index found.
I do basically the same process for term.
Me being a new developer, I'm not used to OnCreate running the code synchronously.
Because of this, it appears that I have a race condition somewhere between populating the List of mentor names that populates the spinner, and calling my getIndex() method.
Sometimes the spinner is populated and getIndex works properly and sets the correct mentor. Sometimes the spinner is empty and my getIndex() returns -1 (which it should do in a no-find situation) that populates the spinner with the first item in the list (once it's populated).
protected void onCreate(Bundle savedInstanceState) {
//////////////////////////Handling Mentor spinner menu/////////////////////////////////////////////////
int mentorId = courseData.getIntExtra(EXTRA_COURSE_MENTOR_ID, -1);
final ArrayAdapter<String> sp_CourseMentorAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_spinner_item, mentorNameList);
sp_CourseMentorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
sp_CourseMentor.setAdapter(sp_CourseMentorAdapter);
final MentorViewModel mentorViewModel = ViewModelProviders.of(this).get(MentorViewModel.class);
//Mentor test = mentorViewModel.getMentorById(mentorId);
mentorViewModel.getAllMentors().observe(this, new Observer<List<Mentor>>() {
#Override
public void onChanged(#Nullable List<Mentor> mentorList) {
if (mentorList != null) {
for (Mentor m : mentorList) {
mentorNameList.add(m.getMentor_name());
mentorListMentor.add(m);
}
}
sp_CourseMentorAdapter.notifyDataSetChanged();
}
});
for(Mentor m: mentorListMentor){
if (m.getMentor_id()==mentorId){
String test = m.getMentor_name();
int spinnerSelectionM2 = getIndexM(sp_CourseMentor, test);
sp_CourseMentor.setSelection(spinnerSelectionM2);
}
}
Is there a way to get them to run asynchronously? Somehow to get the observer doing my getAllMentors() to complete first and populate the spinner, THEN have the for loop run?
Or a better way to handle this?
Thanks in advance.
Room always runs the code on a separated thread, not the Main/UI thread. You can change that behavior with
allowMainThreadQueries()
after initializating your database. This will make the query run first, populate your list and then run your for-loop code. I do not recommend this approach, since it is a bad practice to make queries on the UI thread.
You have two options:
Change your foor loop to a function and call it after adding the values from the observer:
mentorViewModel.getAllMentors().observe(this, new Observer<List<Mentor>>() {
#Override
public void onChanged(#Nullable List<Mentor> mentorList) {
if (mentorList != null) {
for (Mentor m : mentorList) {
mentorNameList.add(m.getMentor_name());
mentorListMentor.add(m);
}
lookForMentor();
}
}
});
private void lookForMentor() {
for(Mentor m: mentorListMentor){
if (m.getMentor_id()==mentorId){
String test = m.getMentor_name();
int spinnerSelectionM2 = getIndexM(sp_CourseMentor, test);
sp_CourseMentor.setSelection(spinnerSelectionM2);
}
}
}
Put the for inside the observer, change the Room DAO to return a List and use LiveData on your own viewmodel:
MentorViewModel.java:
MentorViewModel extends ViewModel {
private MutableLiveData<List<Mentor>> _mentorsLiveData = new MutableLiveData<List<Mentor>>();
public LiveData<List<Mentor>> mentorsLiveData = (LiveData) _mentorsLiveData;
void getAllMentors(){
//room db query
_mentorsLiveData.postValue(mentorsList);
}
}
EditActivity.java:
mentorsViewModel.getAllMentors();
mentorViewModel.mentorsLiveData.observe(this, new Observer<List<Mentor>>() {
#Override
public void onChanged(#Nullable List<Mentor> mentorList) {
mentorsListMentor.addAll(mentorList);
sp_CourseMentorAdapter.notifyDataSetChanged();
for(Mentor m: mentorListMentor){
if (m.getMentor_id()==mentorId){
String test = m.getMentor_name();
int spinnerSelectionM2 = getIndexM(sp_CourseMentor, test);
sp_CourseMentor.setSelection(spinnerSelectionM2);
}
}
}
}
});

addValueChangeListener doen't trigger in android

I have a app in building proccess in some where i need to get data from FirebaseDatabase and show them in custom list view here my code part of it for onDataChange method
myDatabase=FirebaseDatabase.getInstance();
myRef= myDatabase.getReference().child("TvSeries");
myAuth = FirebaseAuth.getInstance();
myUser = myAuth.getCurrentUser();
myRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for(DataSnapshot currentData : dataSnapshot.getChildren()){
if(currentData.child("tCategory").child("tPrimary").getValue().toString().equals("Aksiyon")){ }
selectedCategoryList.add(new DataForRow(currentData.getKey(),
currentData.child("tCategory").child("tPrimary").getValue().toString(),
currentData.child("tReleaseDate").getValue().toString()));
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
Integer size =selectedCategoryList.size();
Log.d("Size:", size.toString());
When i put in breakpoint onDataChange method it works but otherwise it didnt any suggestion is very helpful. Have a nice day all.
Your selectedCategoryList list is always empty because onDataChange() method has an asynchronous behaviour which means that is called even before you are try to add those objects of DataForRow class to the list. A quick solve for this problem would be to declare and use your selectedCategoryList list only inside the onDataChange() method or if you want to use it outside, you need to create your own callback and for that, I recommend you see the last part of my answer from this post.
Firebase works asynchronously. You probably got the data from firebase after you program executed the line with Log. As Tristan mentioned, if you put your Log inside of the listener, it will work

Unable to fetch SharedPreferences

I'm trying to get familiar with using SharedPreferences, by building a simple test app where I store and retrieve user preferences by using a class that extends 'PreferenceActivity'.
The problem is that every time I shut down the app and start it again I'm unable to load the SharedPreferences values that I earlier selected.
In the MainActivity's onCreate method I am first calling a method 'loadPreferences', then creating an ImageView and a button.
I assigned onclick listener to the button that creates and starts a new intent.
...onClick(View v){
Intent intent = new Intent(MainActivity.this, MyPrefsActivity.class);
startActivity(intent);
}
In MyPrefsActivity class I have a ListPreference that has a 'Preference.OnPreferenceChangeListener'.
... {
#Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
imageNumber = Integer.parseInt(newValue.toString());
return true;
}
};
Upon return from MyPrefsActivity to MainActivity in the 'onResume':
protected void onResume() {
super.onResume();
savePreferences();
loadPreferences();
}
private void savePreferences(){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = prefs.edit();
editor.putInt("imageNumber", MyPrefsActivity.imageNumber);
editor.apply();
}
private void loadPreferences (){
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
int imageNumb = prefs.getInt("imageNumber", 0);
switch (imageNumb){
case 0:
imageView.setImageResource(R.mipmap.image1);
break;
case 1:
imageView.setImageResource(R.mipmap.image2);
break;
case 2:
imageView.setImageResource(R.mipmap.image3);
break;
default:
imageView.setImageResource(R.mipmap.image4);
}
}
When 'loadPreferences' is called for the first time upon startup, 'imageNumb' allways has a value 0, instaed of the value that I assigned to on previous run in method 'savePreferences'.
I noticed that after startup when I enter 'MyPreferencesAction' for the first time and open the ListPreferences, the checkbox that I selected on the last run is already selected for me. It seems like my selection is saved but when I try to load SharedPreferences am I messing up something???
You have to save your preferences in onPause event. Right now you are saving it in onResume when values you want are no longer there.
#Override
protected void onPause(){
super.onPause();
savePreferences();
}
#Override
protected void onResume() {
super.onResume();
loadPreferences();
}
There is another issue with your code. You are saving preferences in MainActivity, but you are changing them in MyPrefsActivity. That will not work. You have to save changes to the preferences in MyPrefsActivity, use above load/save pattern in there too. If you don't have any preference changes happening in MainActivity, you can safely omit calling savePreferences from it.
Depending on MyPrefsActivity declaration calling MyPrefsActivity.imageNumber from MainActivity may not be safe, you will have to change that code too. Like I said, most likely you don't need it at all in MainActivity (if you saving are only preferences values that are set in MyPrefsActivity) and that should be part of MyPrefsActivity.
Also Preference.OnPreferenceChangeListener is probably redundant since it's main usage is to be invoked when the value of this Preference has been changed by the user and is about to be set and/or persisted. This gives the client a chance to prevent setting and/or persisting the value.
I realized there was a lot of needless complexity in my code. So I got completely rid of the method savePreferences(). Instead i simplified the PreferenceChangeListeners method onPreferenceChange(...):
This seems to me to be the most simplest way to update SharedPreferences when using PreferenceActivity.
public boolean onPreferenceChange(Preference preference, Object newValue) {
preference.getEditor().
putInt("imageNumber", Integer.parseInt(newValue.toString())).apply();
return true;
}
Now I save the SharedPreferences manually, only when 'onPreferenceChange' is called. Not on 'onResume', 'onStop', 'onRestart' or on 'onPause'.
Please inform me if this is a bad way to change SharedPreferences.
Quoting Dalija Prasnikar: "reference.OnPreferenceChangeListener is probably redundant since it's main usage is to be invoked when the value of this Preference has been changed by the user and is about to be set and/or persisted. This gives the client a chance to prevent setting and/or persisting the value."
If you understood you correctly Dalija, there is a 'onPreferenceChange(...)' like method that does the job but does not have a boolean return value but void? I was not able to find any examples, could you please show or point to an examle?

Walk-around to actionPerformed() returning void

I am trying to use the MVC design.
In the model I wanted a method like this
public boolean changeSomeData(...){
boolean b;
//create a dialog with an OK button
return b;
}
I want the method to return TRUE if the changes were actually made. The changes are done inside the actionPerformed method of the OK button.
My problem is that I can't write b=true; inside the actionPerform of the OK button, because I have to declare b as final in order to use it in the actionPerformed().
What I did is creating a class
private class MyBoolean {
boolean b;
}
and then
public boolean changeSomeData(...){
MyBoolean myBoolean;
//create a dialog with an OK button
actionPerformed(){
//make changes in the data
myBoolean.b=true;
}
boolean b = myBoolean.b;
return b;
}
But I don't feel good about this solution and I wanted to know if it is correct what I did and if there is a better solution.
Should I better throw an exception if the changes aren't made? (for example, if the user clicks "cancel" instead of "ok")
In the model I wanted a method like this ... //create a dialog with an OK button
I'd say this is a flaw already, since the model should not do anything with views directly.
A better approach would be to open the dialog (using the controller), register the controller for the ActionEvent of "OK" (and thus actionPerformed) and then do whatever changes should be done in that method.
Edit:
You might want to consider the following rough approach:
The views register themselves or associated classes to the model as listeners. Whenever the model is changed it fires events to notify the views of the change.
The controller registers itself on the views and is notified when the views change. If a user changes data, the controller then might open the dialog and only commit the changes of the user signals "OK". Thus the model has never to check itself if data needs to be changed. That is actually the controller's task and if the controller passes changes to the model, it should apply them.
A better way to achieve your task is to keep a variable on the dialog that indicates if a successful change was made. Then have a method that your model class calls to retrieve the value and return it.
Something like:
public boolean changeSomeData(...){
//create a dialog with an OK button
return dialog.isSuccess();
}
One way you could make this code a bit cleaner...
public boolean changeSomeData() {
// note that this is not a class boolean, no need to do extra autoboxing.
boolean dataChanged = false;
// check the old value against the new value
// for classes
if (oldvalue.equals(newValue)) {
oldValue = newValue;
dataChanged = true;
}
// for pimitives (built-ins)
if (oldvalue == newValue) {
oldValue = newValue;
dataChanged = true;
}
// odds are good that the above action performed was supposed to call this
// changeSomeData() and not the other way around.
// if you must fire actionPerformed() when data has changed, then do so
// like this, otherwise if it was added as part of the "solution" you can
// skip it.
if (dataChanged) {
actionPeformed();
}
return dataChanged;
}
Note that this code is Controller code, as it manipulates the model directly, and (possibly) updates views.

Categories