I am trying to interact with an API and see whether the user exists on that API by checking the JSON Response by comparing current email (stored in SharedPrefs) with the emails returned from API. If the user exist, a flag is set true so that the app doesn't send a POST request to save the new user and if it is false, the user gets saved in API.
So, this is the UEC (UserExistenceChecker) class
public class UEC extends AppCompatActivity {
List<SavePlace> userInfo;
String name;
boolean flag;
SharedPreferences sharedPref;
public UEC(SharedPreferences sharedPref){
this.sharedPref = sharedPref;
}
#Override
protected void onCreate(#Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
public boolean checkIfUserExists() {
email = sharedPref.getString("userEmail", "");
Retrofit retrofitCheckUser = ApiClientSavePlace.getClient();
ApiInterfaceSavePlace apiInterfaceSavePlace = retrofitCheckUser.create(ApiInterfaceSavePlace.class);
final Call<List<SavePlace>> checkUser = apiInterfaceSavePlace.getSavePlaces();
checkUser.enqueue(new Callback<List<SavePlace>>() {
#Override
public void onResponse(Call<List<SavePlace>> call, Response<List<SavePlace>> response) {
userInfo = response.body();
try {
if(userInfo.size()!=0){
for (int i = 0; i <= userInfo.size(); i++) {
String emailReturned = userInfo.get(i).getEmail();
Log.d("response", "email returned: " + emailReturned);
Log.d("sharedpref", "email: " + email);
if (emailReturned.equals(email)) {
Log.d("response:", "email match?: " + emailReturned.equals(email));
flag = true;
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt("userID", userInfo.get(i).getId());
Log.d("ID returned", String.valueOf(userInfo.get(i).getId()));
editor.apply();
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public void onFailure(Call<List<SavePlace>> call, Throwable throwable) {
Log.d("RESPONSE", "FAILED CHECKING USER ID/SOMETHING HAPPENED");
}
});
return flag;
}
}
In this class, I have made a boolean flag with default value false
and this is how I call the method checkIfUserExists() from MainActivity.java
public class MainActivity{
SharedPreferences sharedPref =
PreferenceManager.getDefaultSharedPreferences(getBaseContext());
UEC uec = new UEC(sharedPref);
boolean userExists = uec.checkIfUserExists();
if (userExists) {
Log.d("USERSTATUS", String.valueOf(sharedPref.getInt("userID", 0)));
} else {
Log.d("USERSTATUS", "FALSE:DOESNT EXIST");
Log.d("USERSTATUS", String.valueOf(sharedPref.getInt("userID", 0)));
}
}
Now, the problem is that, according to the logs, else condition is always true because the flag is always false even though I am setting it true in checkIfUserExists() method.
and the interesting thing about logs is that this
05-13 15:27:54.278 1613-1613/xyz.gautamhans.locus D/USERSTATUS: FALSE:DOESNT
EXIST
05-13 15:27:54.278 1613-1613/xyz.gautamhans.locus D/USERSTATUS: 12
comes first and then this comes in logs after above log
05-13 15:27:55.746 1613-1613/xyz.gautamhans.locus D/response: email
returned: some-email#gmail.com
05-13 15:27:55.749 1613-1613/xyz.gautamhans.locus D/sharedpref: email:
some-email#gmail.com
05-13 15:27:55.749 1613-1613/xyz.gautamhans.locus D/response: email
returned: some-email#gmail.com
05-13 15:27:55.749 1613-1613/xyz.gautamhans.locus D/sharedpref: email:
some-email#gmail.com
05-13 15:27:55.749 1613-1613/xyz.gautamhans.locus D/response: email match?:
true
05-13 15:27:55.749 1613-1613/xyz.gautamhans.locus D/ID returned: 12
which means that it detected the email and set the sharedpref
but the flag is still false.
From a quick look at your code it looks like that the enqueue method is causing for the boolean to only be changed to true after you call the checkIfUserExists() method.
This is what you're seeing in your logs, due to the asynchronous nature of the enqueue method, all the code within your onResponse() and onFailure() is only executed after everything else in a background thread.
To help avoid this you could implement a callback method so that whenever the onResponse() method is finished you call the method to check if the user exists. In the code below the callback method is onUserExists() which replaces the true boolean flag and I've also included an else statement if the user doesn't exist which will trigger a second callback, the onUserDoesNotExist() method. These callback methods will trigger the code in the MainActivity within the onUserExists() and onUserDoesNotExist() methods there.
public void checkIfUserExists(OnUserExistsCallback onUserExistsCallback) {
email = sharedPref.getString("userEmail", "");
Retrofit retrofitCheckUser = ApiClientSavePlace.getClient();
ApiInterfaceSavePlace apiInterfaceSavePlace = retrofitCheckUser.create(ApiInterfaceSavePlace.class);
final Call<List<SavePlace>> checkUser = apiInterfaceSavePlace.getSavePlaces();
OnUserExistsCallback callback = onUserExistsCallback;
checkUser.enqueue(new Callback<List<SavePlace>>() {
#Override
public void onResponse(Call<List<SavePlace>> call, Response<List<SavePlace>> response) {
userInfo = response.body();
try {
if(userInfo.size()!=0){
for (int i = 0; i <= userInfo.size(); i++) {
String emailReturned = userInfo.get(i).getEmail();
Log.d("response", "email returned: " + emailReturned);
Log.d("sharedpref", "email: " + email);
if (emailReturned.equals(email)) {
Log.d("response:", "email match?: " + emailReturned.equals(email));
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt("userID", userInfo.get(i).getId());
Log.d("ID returned", String.valueOf(userInfo.get(i).getId()));
editor.apply();
callback.onUserExists();
break;
} else {
callback.onUserDoesNotExist();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public void onFailure(Call<List<SavePlace>> call, Throwable throwable) {
Log.d("RESPONSE", "FAILED CHECKING USER ID/SOMETHING HAPPENED");
}
});
return flag;
}
For this to work you need to create the OnUserExistsCallback interface.
public interface OnUserExistsCallback {
void onUserExists();
void onUserDoesNotExist();
}
Finally this is how your MainActivity would now look.
public class MainActivity {
SharedPreferences sharedPref =
PreferenceManager.getDefaultSharedPreferences(getBaseContext());
UEC uec = new UEC(sharedPref);
uec.checkIfUserExists(new OnUserExistsCallback() {
#Override
public void onUserExists() {
Log.d("USERSTATUS", String.valueOf(sharedPref.getInt("userID", 0)));
}
#Override
public void onUserDoesNotExist() {
Log.d("USERSTATUS", "FALSE:DOESNT EXIST");
Log.d("USERSTATUS", String.valueOf(sharedPref.getInt("userID", 0)));
}
);
}
Not sure if this will compile and run successfully as I haven't ran this code myself. Hopefully it will and that it will solve your problem.
The behaviour is due to Asynchronous execution of checkUser.enqueue(new Callback<List<SavePlace>>(). So when you call this this method from checkIfUserExists(), your execution thread will not wait for checkUser.enqueue() to complete. Instead it will immediately go to the next line and return the current flag value which is false. checkUser.enqueue() will be executed in a background thread and you get the result in onResponse() method. The behaviour is correct as per your code. Please try to handle the scenarios asynchronously as that is the recommended approach for a Network call.
The problem is that you are dealing with asynchronous function call when you call this method
uec.checkIfUserExists();
the code inside this function is executed in normal way until the line where you make your api call here
final Call<List<SavePlace>> checkUser = apiInterfaceSavePlace.getSavePlaces();
which makes a web api call in background thread and your function executes in normal way and returns which means you will get flag as false value.
and when the background task is finished then the code inside
public void onResponse()
method is executed which is a callback method and you get your values with true flag.
Solution
You should wait for your api call to complete and then perform any check on user exist or not.
So one simple way is to put your user exist check inside onResponse() callback method itself.
And if you want to handle it in your activity or fragment you can create your own callback method and pass it to checkIfUserExists();
Something like this
public interface MyInterface{
public void onCallback(boolean isUserExists);
}
and in your activity
uec.checkIfUserExists(
new MyInterface(){
#Override
public void onCallback(boolean isUserExists){
if (isUserExists) {
//your code
}
else{
//your code
}
}
}
);
Make changes to you checkIfUserExists() method like this
public void checkIfUserExists(final MyInterface myInterface) {
//your code
checkUser.enqueue(new Callback<List<SavePlace>>(final MyInterface myInterface) {
#Override
public void onResponse(Call<List<SavePlace>> call, Response<List<SavePlace>> response) {
userInfo = response.body();
try {
//your code
if (emailReturned.equals(email)) {
flag = true;
}
//pass your flag to callback method here.
myInterface.onCallback(flag);
}
} catch (Exception e) {
e.printStackTrace();
}
}
#Override
public void onFailure(Call<List<SavePlace>> call, Throwable throwable) {
//Handle failure
}
});
}
Related
I don't know if this is even possible. Please note, that I've been researching on JobScheduler only one day, so I'm sorry If I got something wrong.
So I just created one MainActivity, from it, I directly want to schedule a job, and make it periodical, say every 5 seconds I need to hit my REST endpoint and return a random integer (I got that part in Spring Boot covered).
From what I saw, in JobScheduler's onStartJob(), AsyncTask gets initialized, and its doPostExecute gets defined and method execute() is called on AsyncTask object, something like this:
#Override
public boolean onStartJob(JobParameters params) {
mJobExecutor = new JobExecutor() {
Override
protected void onPostExecute(int x){
Toast.makeText(getApplicationContext(), "Integer returned from backend " + x, Toast.LENGTH_SHORT).show();
}
}
mJobExecutor.execute();
return true;
}
I actually placed a Retrofit call to my backend instead of AsyncTask, like this:
public class MyJobScheduler extends JobService {
private Retrofit mRetrofit = RetrofitClient.getRetrofitInstance();
private MessageService mMessageService = mRetrofit.create(MessageService.class);
private JobParameters params;
#Override
public boolean onStartJob(JobParameters params) {
this.params = params;
Log.i("TAG", "onStartJob: fetching data begins");
fetchData();
return true;
}
#Override
public boolean onStopJob(JobParameters params) {
return false;
}
private void fetchData(){
Call<Integer> call = mMessageService.getData();
call.enqueue(new Callback<Integer>() {
#Override
public void onResponse(Call<Integer> call, Response<Integer> response) {
if (!response.isSuccessful()){
Toast.makeText(getApplicationContext(), "erorr", Toast.LENGTH_SHORT);
return;
}
Toast.makeText(getApplicationContext(), "Integer returned " + response.body(), Toast.LENGTH_SHORT).show();
jobFinished(params, false);
}
#Override
public void onFailure(Call<Integer> call, Throwable t) {
Toast.makeText(getApplicationContext(), "errorrrrrr", Toast.LENGTH_SHORT).show();
return;
}
});
}
}
Back in my MainActivity, I have in onResume this piece of code which checks for result status:
#Override
protected void onResume() {
super.onResume();
int res = mJobScheduler.schedule(mJobInfo);
if (res == JobScheduler.RESULT_SUCCESS){
Log.i("res", "success");
} else Log.i("res", "fail");
}
Finally I get error like this:
E/AndroidRuntime: FATAL EXCEPTION: OkHttp Dispatcher
Process: ftn.sit.jobscheduler, PID: 17710
java.lang.BootstrapMethodError: Exception from call site #1 bootstrap method
at retrofit2.DefaultCallAdapterFactory$ExecutorCallbackCall$1.onResponse(DefaultCallAdapterFactory.java:77)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:150)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:504)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:764)
Caused by: java.lang.ClassCastException: Bootstrap method returned null
at retrofit2.DefaultCallAdapterFactory$ExecutorCallbackCall$1.onResponse(DefaultCallAdapterFactory.java:77)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:150)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:504)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:764)
I don't know what is wrong...
I am trying to make a method called sendRegisterUserMessage() that sends a User object to the Firebase db if the User does not already exist within the database. For some reason the method always returns false despite finding the object in the database with the listener.
Anyways I invoke this method in my MainActiviy in onCreate():
MessageSenderHandler.getInstance().sendRegisterUserMessage();
In my MessageSenderHandler class:
//Checks if user is Registered- if not it sends a RegisterUser message
public void sendRegisterUserMessage() {
System.out.println("registered: " +DatabaseManager.getInstance().isCurrentUserRegistered()); //This prints false no matter what
if (DatabaseManager.getInstance().isCurrentUserRegistered() == false) {
FirebaseDatabase.getInstance().getReference().child(MessageTypes.REGISTER_USER_MESSAGE).child(
CurrentUser.getInstance().getUserId())
.setValue(
new RegisterUserMessage()
);
}
}
Then here is my isCurrentUserRegistered() method in my DatabaseManager class
public boolean isCurrentUserRegistered() {
Query query = databaseReference.child("/" + MessageTypes.REGISTER_USER_MESSAGE + "/" + CurrentUser.getInstance().getUserId());
query.addValueEventListener(isUserRegisteredDataListener);
System.out.println("REGISTERED:" + isUserRegisteredDataListener.isUserRegisterd()); //This also prints false no matter what
return isUserRegisteredDataListener.isUserRegisterd();
}
Here is my IsUerRegisteredDataListener
public class IsUserRegisteredDataListener implements ValueEventListener {
static boolean exists;
private static IsUserRegisteredDataListener isUserRegisteredDataListener;
public IsUserRegisteredDataListener() {
isUserRegisteredDataListener = this;
}
public static IsUserRegisteredDataListener getInstance() {
if (isUserRegisteredDataListener == null) {
isUserRegisteredDataListener = new IsUserRegisteredDataListener();
}
return isUserRegisteredDataListener;
}
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
this.exists = dataSnapshot.exists();
System.out.println("EXISTS" + exists)); //This prints true
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
public boolean isUserRegisterd() {
return exists;
}
}
I don't understand why the first two methods print FALSE but the one with the actual data listener prints TRUE inside the listener
public boolean isCurrentUserRegistered() {
Query query = databaseReference.child("/" + MessageTypes.REGISTER_USER_MESSAGE + "/" + CurrentUser.getInstance().getUserId());
query.addValueEventListener(isUserRegisteredDataListener);
System.out.println("REGISTERED:" + isUserRegisteredDataListener.isUserRegisterd()); //This also prints false no matter what
return isUserRegisteredDataListener.isUserRegisterd();
}
In this method, you have to wait for firebase result to get isUserRegistered. So your IsUserRegisteredDataListener class can not work what you wish.
Can you show where you initialize isUserRegisteredDataListener field?
I want to have a Splash screen that has an inderteminate ProgressDialog and its progress gets updated by async calls from within a Presenter class (from MVP architecture).
I have a number of API calls to make to my BaaS server and for every successfull call, I would like to update the progress bar.
What's the best way to accomplish this?
I have been trying using EventBus to send notifications to my SplashActivity but it seems that all the API calls are first completed and only then the bus notifications are getting consumed and updating the UI.
What I have done so far is:
SplashActivity:
#Subscribe(threadMode = ThreadMode.MAIN)
public void onProgressBar(String event) {
Timber.d("onProgressBar");
if(event.contains("Done")) {
roundCornerProgressBar.setProgress(100);
} else {
roundCornerProgressBar.setProgress(roundCornerProgressBar.getProgress() + 10);
}
textViewTips.setText(event);
}
Presenter:
InstanceID iid = InstanceID.getInstance(ctx);
String id = iid.getId();
mDataManager.getPreferencesHelper().putInstanceId(id);
GSUtil.instance().deviceAuthentication(id, "android", mDataManager);
GSUtil.instance().getPropertySetRequest("PRTSET", mDataManager);
GSUtil:
public void deviceAuthentication(String deviceId, String deviceOS, final DataManager mDataManager) {
gs.getRequestBuilder().createDeviceAuthenticationRequest()
.setDeviceId(deviceId)
.setDeviceOS(deviceOS)
.send(new GSEventConsumer<GSResponseBuilder.AuthenticationResponse>() {
#Override
public void onEvent(GSResponseBuilder.AuthenticationResponse authenticationResponse) {
if(mDataManager != null) {
mDataManager.getPreferencesHelper().putGameSparksUserId(authenticationResponse.getUserId());
}
EventBus.getDefault().post("Reading player data");
}
});
}
public void getPropertySetRequest(String propertySetShortCode, final DataManager mDataManager) {
gs.getRequestBuilder().createGetPropertySetRequest()
.setPropertySetShortCode(propertySetShortCode)
.send(new GSEventConsumer<GSResponseBuilder.GetPropertySetResponse>() {
#Override
public void onEvent(GSResponseBuilder.GetPropertySetResponse getPropertySetResponse) {
GSData propertySet = getPropertySetResponse.getPropertySet();
GSData scriptData = getPropertySetResponse.getScriptData();
try {
JSONObject jObject = new JSONObject(propertySet.getAttribute("max_tickets").toString());
mDataManager.getPreferencesHelper().putGameDataMaxTickets(jObject.getInt("max_tickets"));
jObject = new JSONObject(propertySet.getAttribute("tickets_refresh_time").toString());
mDataManager.getPreferencesHelper().putGameDataTicketsRefreshTime(jObject.getLong("refresh_time"));
} catch (JSONException e) {
e.printStackTrace();
}
EventBus.getDefault().post("Game data ready");
EventBus.getDefault().post("Done!");
}
});
}
Right now I am just showing you 2 API calls, but I will need another 2.
Thank you
I found the answer! It's easier that I thought, which is unfortunate as I spend about 4 hours on this:
First, I created two new methods on my MVPView interface:
public interface SplashMvpView extends MvpView {
void updateProgressBarWithTips(float prog, String tip);
void gameDataLoaded();
}
Then, in the presenter itself, I call every API call and for every call, I update the View with the updateProgressBarWithTips method and when everything is completed, I finalise it so I can move from Splash screen to Main screen:
private void doGSData(String id) {
getMvpView().updateProgressBarWithTips(10, "Synced player data");
GSAndroidPlatform.gs().getRequestBuilder().createDeviceAuthenticationRequest()
.setDeviceId(id)
.setDeviceOS("android")
.send(new GSEventConsumer<GSResponseBuilder.AuthenticationResponse>() {
#Override
public void onEvent(GSResponseBuilder.AuthenticationResponse authenticationResponse) {
if(mDataManager != null) {
mDataManager.getPreferencesHelper().putGameSparksUserId(authenticationResponse.getUserId());
}
getMvpView().updateProgressBarWithTips(10, "Synced game data");
GSAndroidPlatform.gs().getRequestBuilder().createGetPropertySetRequest()
.setPropertySetShortCode("PRTSET")
.send(new GSEventConsumer<GSResponseBuilder.GetPropertySetResponse>() {
#Override
public void onEvent(GSResponseBuilder.GetPropertySetResponse getPropertySetResponse) {
GSData propertySet = getPropertySetResponse.getPropertySet();
GSData scriptData = getPropertySetResponse.getScriptData();
try {
JSONObject jObject = new JSONObject(propertySet.getAttribute("max_tickets").toString());
mDataManager.getPreferencesHelper().putGameDataMaxTickets(jObject.getInt("max_tickets"));
jObject = new JSONObject(propertySet.getAttribute("tickets_refresh_time").toString());
mDataManager.getPreferencesHelper().putGameDataTicketsRefreshTime(jObject.getLong("refresh_time"));
} catch (JSONException e) {
e.printStackTrace();
}
getMvpView().gameDataLoaded();
}
});
}
});
}
I hope this helps someone, if you're using MVP architecture.
Cheers
From an Android activity I'm sending http requests driven by the user pressing buttons on the UI.
I don't want multiple requests running at the same time (OutlookClient crashes).
My question is: is it possible to have race conditions due to the callbacks with the results writing the same boolean (using runOnUiTread) that is read before sending a new request?
Thanks
// Should this be either "volatile" or atomic ??
private boolean isThereAPendingRequest = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
genericClient = clientInitializer.create(this);
// ...
isThereAPendingRequest = true; // still have to login
Futures.addCallback(genericClient.logon(this, scopes), new FutureCallback<Boolean>() {
#Override
public void onSuccess(Boolean result) {
// ...
isThereAPendingRequest = false;
}
#Override
public void onFailure(#NonNull Throwable t) {
// ...
isThereAPendingRequest = false;
}
});
// ...
}
// ...
public void getBookings(View view){
if(isThereAPendingRequest){
Toast.makeText(getApplicationContext(), "There's already a pending request. Try in a few seconds.", Toast.LENGTH_LONG).show();
return;
}
isThereAPendingRequest = true;
Futures.addCallback( genericClient.getCalendarEvents(), new FutureCallback<List<List>>(){
#Override
public void onSuccess(final List<List> resultCalendars) {
Log.d("APP", "Success. Result: "+resultCalendars);
runOnUiThread(new Runnable() {
#Override
public void run() {
// ..
isThereAPendingRequest = false;
}
}
}
// ..
}
public void sendBooking(View view){
if(isThereAPendingRequest){
Toast.makeText(getApplicationContext(), "There's already a pending request. Try in a few seconds.", Toast.LENGTH_LONG).show();
return;
}
isThereAPendingRequest = true;
Futures.addCallback( genericClient.sendBooking( booker, title), new FutureCallback<List<String>>(){
#Override
public void onSuccess(final List<String> resultBooking) {
Log.d("APP", "Success. Result: "+resultBooking);
runOnUiThread(new Runnable() {
#Override
public void run() {
// ...
isThereAPendingRequest = false;
}
});
}
#Override
public void onFailure(Throwable t) {
Log.e( "APP", "Delete error. Cause: "+t.getLocalizedMessage() );
// ...
Toast.makeText(getApplicationContext(), "Fail!", Toast.LENGTH_LONG).show();
isThereAPendingRequest = false;
}
});
}catch(Exception ex){
// logger
isThereAPendingRequest = false;
}
}
UPDATE: this is one of the function called in the Futures..
public ListenableFuture<List<List>> getCalendarEvents(){
// logger
final SettableFuture<List<List>> future = SettableFuture.create();
DateTime now = new DateTime(DateTimeZone.UTC);
DateTime workDayEnd = new DateTime( now.getYear(), now.getMonthOfYear(), now.getDayOfMonth(), 23, 59, 0 );
Futures.addCallback(
mClient .getMe()
.getCalendarView()
.addParameter("startDateTime", now)
.addParameter("endDateTime", workDayEnd)
.read(),
new FutureCallback<OrcList<Event>>() {
#Override
public void onSuccess(final OrcList<Event> result) {
// ...
future.set(myList);
}
#Override
public void onFailure(#NonNull Throwable t) {
// ...
future.setException(t);
}
}
);
return future;
}
If getBookings and setBookings are both invoked on the UI thread all the time, you should be fine. You know that by the time isThereAPendingRequest is set to false, the request must have already completed and therefore you are safe to go. By the way, Futures.addCallback has an alternate signature that allows you to explicitly pass in an Executor, so if you use that you don't need to call runOnUiThread which reduces some code nesting.
However, if you intend to invoke these methods concurrently, I see at least one race condition that requires locks to prevent. More details on that if you're interested.
Edit for completeness:
The question states that your goal is to prevent two requests from running at the same time. The are two cases where that can happen:
isThereAPendingRequest==false, but there is actually a pending request. Your code so far is safe from this, since you only set it to false after the request has been completed. You don't even need volatile here.
getBookings and/or setBookings are called on different threads. What happens if they both reach if(isThereAPendingRequest) at the same time? They can simultaneously (and correctly) see that it is false, set it to true, then both independently send a request and cause you to crash.
You don't need to worry about (1), and (2) should not be a problem as long as you always invoke those methods on the same thread.
I am using Realm + Retrofit2
I am trying to implement following :
UI asks DataManger for data.
DataManger returns cached data, and checks if data has expired then calls for fresh data.
When fresh data is saved in Realm NetworkManager triggers event which is captured by UI for updating data.
Issue
When NetworkHelper saves the data in Realm, after commitTransaction() due to onChangeListeners of RealmObjects, the code in DataManger onCall() part is executed again, which again calls NetworkHelper for new data, which subsequently again saves data from the network and process goes into infinite loop. I tried gitHubUser.removeChangeListeners() at multiple points but it still not working. Please point out anything fundamentally being wrong or the correct way to implement with Realm.
Implemented codes are as follows:
DataManager
public Observable<GitHubUser> getGitHubUser(final String user){
return databaseHelper.getGitHubUser(user).doOnNext(new Action1<GitHubUser>() {
#Override
public void call(GitHubUser gitHubUser) {
if(gitHubUser==null || !isDataUpToDate(CACHE_TIME_OUT,gitHubUser.getTimestamp())){
if(gitHubUser!=null)
System.out.println("isDataUpToDate = " + isDataUpToDate(CACHE_TIME_OUT,gitHubUser.getTimestamp()));
networkHelper.getGitHubUserRxBus(user);
}
}
});
}
DataBaseHelper
public Observable<GitHubUser> saveGitHubUser(GitHubUser user, String userId) {
realmInstance.beginTransaction();
user.setUserId(userId);
user.setTimestamp(System.currentTimeMillis());
GitHubUser userSaved = realmInstance.copyToRealm(user);
Observable<GitHubUser> userSavedObservable = userSaved.asObservable();
realmInstance.commitTransaction();
return userSavedObservable;
}
public Observable<GitHubUser> getGitHubUser(String user){
System.out.println("DatabaseHelper.getGitHubUser");
GitHubUser result = realmInstance.where(GitHubUser.class).contains("userId",user, Case.INSENSITIVE).findFirst();
if(result != null){
return result.asObservable();
}else{
return Observable.just(null);
}
}
NetworkHelper
public void getGitHubUserRxBus(final String user){
System.out.println("NetworkHelper.getGitHubUserRxBus");
retroFitService.user(user)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<GitHubUser, Observable<GitHubUser>>() {
#Override
public Observable<GitHubUser> call(GitHubUser gitHubUser) {
System.out.println("NetworkHelper.call");
return databaseHelper.saveGitHubUser(gitHubUser,user);
}
}).subscribe(new Action1<GitHubUser>() {
#Override
public void call(GitHubUser gitHubUser) {
if (rxBus.hasObservers()) {
System.out.println("NetworkHelper.call");
rxBus.send(gitHubUser);
}
}
});
}
Activity
subscription.add(dataManager.getGitHubUser("gitHubUserName")
.subscribe(new Subscriber<GitHubUser>() {
#Override
public void onCompleted() {
System.out.println("LoginActivity.call" + " OnComplete");
}
#Override
public void onError(Throwable e) {
System.out.println("throwable = [" + e.toString() + "]");
}
#Override
public void onNext(GitHubUser gitHubUser) {
System.out.println("LoginActivity.call" + " OnNext");
if (gitHubUser != null) {
sampleResponseText.setText(gitHubUser.getName() + " timestamp " + gitHubUser.getTimestamp());
}
onCompleted();
}
}));
subscription.add(rxBus.toObserverable().subscribe(new Action1<Object>() {
#Override
public void call(Object o) {
if(o instanceof GitHubUser){
GitHubUser gitHubUser = ((GitHubUser)o);
sampleResponseText.setText(gitHubUser.getName() + " time " + gitHubUser.getTimestamp());
}
}
}));
UPDATE
Finally Solved it by following in DataManger:
return Observable.concat(databaseHelper.getGitHubUser(user).take(1),
networkHelper.getGitHubUser(user))
.takeUntil(new Func1<GitHubUser, Boolean>() {
#Override
public Boolean call(GitHubUser gitHubUser) {
boolean result = gitHubUser!=null && isDataUpToDate(CACHE_TIME_OUT,gitHubUser.getTimestamp());
System.out.println("isDataUpToDate = " + result);
return result;
}
});
I think you have a loop going in your code:
1) You create an observable from a RealmResults in getGithubUser().Realm observables will emit every time you change data that might effect them.
2) You call networkHelper.getGitHubUserRxBus(user) after retrieving the user from Realm.
3) When getting a user from the network, you save it to Realm, which will trigger the Observable created in 1) to emit again, which creates your cycle.
To break it, you can do something like result.asObservable().first() in getGitHubUser() as that will only emit once and then complete, but it depends on your use case if that is acceptable.