Firebase provides awesome Realtime Database service, but it is a bit outstanding from my, and I suggest that not only my, experience when this engine is used as main local storage (offline mode).
What do I expect when app is requesting some data is to see some indication of the process. But I have not found any direct functionality to get it working.
So, I created custom solution and would like to get reviews from community.
The idea is simple: bind a ValueEventListener to some version tag which creates a request with SingleValueEvent callback updating data and changing its state.
Suppose, i have a User object and for versioning I add ver property with timestamp content:
users : {
id : {
name : 'Mr. Anderson',
ver : '<timestamp>'
}
}
To see data state I introduce enum State, interface StateIndicatable, interface OnStateChangeListener:
enum State{ INIT, LOADING, LOADED, FAILED }
interface OnStateChangeListener{
void stateChanged(State newState);
}
interface StateIndicatable{
State getState();
void setOnStateChangeListener(OnStateChangeListener listener);
void clearOnStateChangeListener();
}
I wouldn't like to change the User model and I will use a wrapper which will exist on the level of view/presentation/view_model:
I'd like to create an abstract Wrapper around any versionable model:
abstract class FirebaseVersionedModelWrapper<M> implements StateIndicatable{
int version;
protected WeakReference<M> modelRef;
State state;
OnStateChangeListener onStateChangeListener;
DatabaseReference verRef;
FirebaseVersionedModelWrapper(M model, String firebasePath, OnStateChangeListener onStateChangeListener){
this.modelRef = new WeakReference(model);
this.state = State.INIT;
this.onStateChangeListener = onStateChangeListener;
verRef = FirebaseDatabase.getInstance().getReference(firebasePath + "/ver");
verRef.keepSynced(true);
verRef.addValueEventListener(
new OnValueEventListener(){
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
if(modelRef.get()==null){
verRef.removeEventListener(this);
return;
}
if(dataSnapshot.hasChild("ver"){
if(dataSnapshot.child("ver").getValue(Integer.class) != version){
requestUpdate();
}
}
else
setState(State.FAILED);
}
#Override
public void onCancelled(DatabaseError databaseError) {
if(modelRef.get()==null){
verRef.removeEventListener(this);
return;
}
setState(State.FAILED);
}
}
);
}
private void requestUpdate(){
setState(State.LOADING);
DatabaseReference ref = FirebaseDatabase
.getInstance
.getReference(firebasePath);
ref.keepSynced(true);
ref.addListenerForSingleValueEvent(
new OnValueEventListener(){
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
M model = modelRef.get();
if (model==null) return;
if(dataSnapshot.hasChild("ver")){
version = dataSnapshot.child("ver").getValue(Integer.class);
setFields(model, dataSnapshot);
setState(State.LOADED);
}
else{
setState(State.FAILED);
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
setState(State.FAILED);
}
}
);
}
private void setState(State newState){
if (this.state == newState) return;
this.state = newState;
if (onStateChangeListener != null)
onStateChangeListener.stateChanged(newState);
}
#Override
public State getState(){
return this.state;
}
#Override
public void setOnStateChangeListener(OnStateChangeListener listener){
this.onStateChangeListener = listener;
}
#Override
public void clearOnStateChangeListener(){
this.onStateChangeListener = null;
}
protected abstract void setFields(M model, DataSnapshot dataSnapshot);
}
Here DatabaseReference#keepSynced(true) is called to be sure that the request on data path will return fresh data. This is discussable approach and not completely confirmed in my practice now.
Then I wrap the User into UserWrapper:
class UserWrapper extends FirebaseVersionedModelWrapper<User>{
FirebasedUser(User userModel, String userPath, OnStateChangeListener listener){
super(userModel, userPath, listener);
}
#Override
void setFields(User model, DataSnapshot dataSnapshot){
// setting values for model
}
}
And eventually I can instantiate the wrapper and bind indication routines to OnStateChangeListener callback:
User user = new User();
UserWrapper wrapper = new UserWrapper(
user,
new OnStateChangeListener(){
#Override
void stateChanged(State newState){
if(newState == State.LOADED){
showUserDetails();
}
else if(newState == State.FAILD){
showErrorState();
}
else{
showLoadingIndicator();
}
}
}
);
Well, as I said, any opinions and alternative solutions posted here are much appreciated!
the code is typed from memory and may have mistakes
Related
I'm using leak canary for detecting memory leaks. From my activity, I'm calling a method from other class for updating data in firebase realtime-database, on addOnCompleteListener() I'm showing a success toast, and on addOnFailureListener() showing error toast. As toast requires context I've extended Application to get Application context, I haven't passed context form my activity, because reading some article I came to know that passing context can cause a memory leak. Here is my class for updating in the database.
public class FirebaseUpdaterr extends Application {
private Context context;
private DatabaseReference ref=FirebaseDatabase.getInstance().getReference();
private FirebaseUser user= FirebaseAuth.getInstance().getCurrentUser();
public FirebaseUpdaterr(){
this.context=getApplicationContext();
}
public void retriveBook(String bookId, final BookInfo bookInfo){
final Book[] book = new Book[1];
ref.child("Books").child(bookId).addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
book[0] =dataSnapshot.getValue(Book.class);
bookInfo.onCallback(book[0]);
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
});
}
public void accept(final BookRequestData bookRequestData){
retriveBook(bookRequestData.getReqBookId(), new BookInfo() {
#Override
public void onCallback(Book book) {
Map<String, Object> childUpdates = new HashMap<>();
childUpdates.put("/Books/"+bookRequestData.getReqBookId(),book);
childUpdates.put("/Requests/"+bookRequestData.getReqId()+"/status",StaticValues.REQUESTE_ACCEPTED);
ref.updateChildren(childUpdates)
.addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
if(task.isSuccessful()){
Toasty.success( context, bookRequestData.getReqUserName()+"'s request accepted", Toast.LENGTH_SHORT, true).show();
}
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toasty.error(context, bookRequestData.getReqUserName()+"'s request is not accepted", Toast.LENGTH_SHORT, true).show();
}
});
}
});
}
}
My BookInfoActivity is large. I've added only possible reason for Memory leaks.
public class BookInfoActivity extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
BookViewModelFactory modelFactory=new BookViewModelFactory(bookLight.getBookId());
BookViewModel viewModel = ViewModelProviders.of(this,modelFactory).get(BookViewModel.class);
LiveData<Book> liveData = viewModel.getBookLiveData();
liveData.observe(this, new Observer<Book>() {
#Override
public void onChanged(Book bookLive) {
//other stuffs
}
checkSameReq(new FirebaseCallBack() {
#Override
public void onCallback(final BookRequestData reqData) {
requestBtn.setOnClickListener(new View.OnClickListener()
{
if(requested){
FirebaseUpdaterr fireUpdate=new FirebaseUpdaterr();
fireUpdater.accept(bookRequest);
}
}
});
}
});
}
private void checkSameReq( final FirebaseCallBack firebaseCallBack) {
ref = mdatabase.getReference();
sameReqCheck=ref.child("Users").child(book.getOwnerID()).child("pendingRequest").orderByChild("reqUserId").equalTo(user.getUid());
sameReqCheckValEventListener=new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
boolean sameReqCheck=false;
final BookRequestData[] requestData = {null};
Log.e("shanto2", String.valueOf(dataSnapshot.getChildrenCount()));
for (DataSnapshot reqSnapshot: dataSnapshot.getChildren()) {
BookRequestData bookRequestData=reqSnapshot.getValue(BookRequestData.class);
if(bookRequestData.getReqBookId().equals(book.getBookId())){
sameReqCheck=true;
requestData[0] =bookRequestData;
break;
}
}
if(!sameReqCheck){
requestBooks.setText(REQUEST_BOOK);
}else{
requestBooks.setText(CANCEL_REQUEST);
}
bookStatusSetter(book);
if(book.getAcceptedReqId().length()>0){
requestRef=ref.child("Requests").child(book.getAcceptedReqId());
reqEventListener=new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
BookRequestData bookRequestData=dataSnapshot.getValue(BookRequestData.class);
if(book.getCurrentOwnerId().equals(user.getUid()) && bookRequestData.getStatus()==StaticValues.REQUESTE_ACCEPTED){
requestBooks.setText(GOT_IT);
requestData[0] =bookRequestData;
}
firebaseCallBack.onCallback(requestData[0]);
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
}
};
requestRef.addValueEventListener(reqEventListener);
}else {
firebaseCallBack.onCallback(requestData[0]);
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
};
sameReqCheck.addListenerForSingleValueEvent(sameReqCheckValEventListener);
}
}
After inspecting with leakcanary I've found following logs, Where is the error?
The provided leak trace shows no code of yours, this is most likely an Android leak. It could be a bug in AOSP, or a bug in a manufacturer implementation. You can provide this information to LeakCanary (https://github.com/square/leakcanary/issues/new/choose => Leak in Android SDK / support library) and provide the Android API version and manufacturer so that it gets tagged as a "library leak" in future releases.
Store app context It is not a good practice but It should not cause a memory leak because you only have 1 app instance . It is difficult to see what is happening but inline listeners and callbacks should be avoided. I suggest store listeners and callbacks in prĂvate attributes. Init them when activity is started and then put them to null when activity is stopped.
I had the same problem.
But the problem is not the Toast, but the context, so I created an extended function, where I leave the context as global, and use the local context to show the toast. take a look
Extension.kt
import android.widget.Toast.LENGTH_SHORT
import android.widget.Toast
import android.view.View
fun Context.toast(text: String, duration: Int = LENGTH_SHORT) {
Toast.makeText(
applicationContext,
text,
duration
).show()
}
fun Context.toastCustom(text: String, durationCustom: Int = LENGTH_SHORT) {
Toast(applicationContext).apply {
duration = durationCustom
view = View.inflate(applicationContext, R.layout.custom_toast, null)
val textV = view?.findViewById(R.id.toastCustomTv) as TextView
textV.text = text
show()
}
}
from Activity call
toast("message to toast", LENGTH_SHORT)
from Fragment call
requireContext().toast("message to toast", LENGTH_SHORT)
from Composable call
val localContext = LocalContext.current
localContext.toastCustom("message to toast", Toast.LENGTH_SHORT)
Toasts is leaking not anymore
Simple thing I would like to do (see in the picture)
Display a view with info coming from 2 different places in Firebase so that it behaves in a professional way scrolling UP and DOWN
I have a list of movies and on each of them I would like the user to specify a rating and see it
In DB I created 2 structures to have the list of movies on one side and the ratings per user on the other
Problem using FirebaseRecyclerAdapter
My problem is that scrolling fast up and down the list, the visualization of the information coming from the second reference (the rating) is loaded on a different time (asynchronous call) and this is not acceptable to see this (little) delay building the view. Is this a limitation of FirebaseRecyclerView?
Because viewHolders are reused in the recycleView I reset and reload each time in populateView() the rating values and this doesn't help. Once retrieved I'm oblidged to get them again if the user scroll the view (see the setOnlistener in populateView()
Setting a listener in populateView cause also to have as many listener as the number of times populateView() is executed (if you scroll UP and DOWN it's many times).
Solutions / Workaround ?
Is there a correct way to do it preventing the problem? Or is it a limitation?
What about performance with my implementation where the listener is inside populateView() and there are MANY listener created?
Below some things I'm thinking on:
Prevent viewHolders to be recycled and just load once?
Override some other methods of RecyclerView? I tried with parseSnapshot() but it's the same problem...
Change the DB structure to have all the info in one list (I don't think it's the good one because it means adding rating information of each user to movie list)
Add a loading spinner on the rating part so that the rating is displayed only when the asyncrhonous call to firebase is completed (don't like it) without the today effect of: "changing star color in front of the user".
My Implementation
From FirebaseRecyclerAdapter
#Override
protected void populateViewHolder(final MovieViewHolder viewHolder, final Movie movie, final int position) {
String movieId = this.getRef(position).getKey();
// Oblidged to show no rating at the beginning because otherwise
// if a viewHolder is reused it has the values from another movie
viewHolder.showNoRating();
//---------------------------------------------
// Set values in the viewHolder from the model
//---------------------------------------------
viewHolder.movieTitle.setText(movie.getTitle());
viewHolder.movieDescription.setText(movie.getDescription());
//-----------------------------------------------------
// Ratings info are in another DB location... get them
// but call is asynchronous so PROBLEM when SCROLLING!
//-----------------------------------------------------
DatabaseReference ratingMovieRef = mDbRef.child(Constants.FIREBASE_LOCATION_RATINGS).child(currentUserId).child(movieId);
ratingQuoteRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
RatingMovie ratingMovie = dataSnapshot.getValue(RatingMovie.class);
Rating rating = Rating.NO_RATING;
if (ratingMovie != null) {
rating = Rating.valueOf(ratingMovie.getRating());
}
// Set the rating in the viewholder (through anhelper method)
viewHolder.showActiveRating(rating);
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
from MovieViewHolder
public class QuoteViewHolder extends RecyclerView.ViewHolder {
public CardView cardView;
public TextView movieTitle;
public TextView movieDescription;
public ImageView ratingOneStar;
public ImageView ratingTwoStar;
public ImageView ratingThreeStar;
public QuoteViewHolder(View itemView) {
super(itemView);
movieTitle = (TextView)itemView.findViewById(R.id.movie_title);
movieDescription = (TextView)itemView.findViewById(R.id.movie_descr);
// rating
ratingOneStar = (ImageView)itemView.findViewById(R.id.rating_one);
ratingTwoStar = (ImageView)itemView.findViewById(R.id.rating_two);
ratingThreeStar = (ImageView)itemView.findViewById(R.id.rating_three);
}
/**
* Helper to show the color on stars depending on rating value
*/
public void showActiveRating(Rating rating){
if (rating.equals(Rating.ONE)) {
// just set the good color on ratingOneStar and the others
...
}
else if (rating.equals(Rating.TWO)) {
// just set the good color
...
} else if (rating.equals(Rating.THREE)) {
// just set the good color
...
}
/**
* Initialize the rating icons to unselected.
* Important because the view holder can be reused and if not initalised values from other moviecan be seen
*/
public void initialiseNoRating(){
ratingOneStar.setColorFilter(ContextCompat.getColor(itemView.getContext(), R.color.light_grey));
ratingTwoStar.setColorFilter(....
ratingThreeStar.SetColorFilter(...
}
You can sort of cache the ratings using a ChildEventListener. Basically just create a separat one just for the Ratings node, and have it store the ratings in a Map. Then using the RecyclerAdapter you will retrieve from the Map if the rating is available, if it is not, have the rating listener update the recyclerview as soon as is has downloaded the rating. This is one strategy you could go about, doing it, you will have to manually copy/paste some classes from the FirebaseUI library and set some fields public for this to work.
Usage would be something like this
private MovieRatingConnection ratingConnection;
// inside onCreate
ratingConnection = new MovieRatingConnection(userId, new MovieRatingConnection.RatingChangeListener() {
#Override
public void onRatingChanged(DataSnapshot dataSnapshot) {
if (recyclerAdapter != null) {
if (dataSnapshot != null) {
int index = recyclerAdapter.snapshots.getIndexForKey(dataSnapshot.getKey());
recyclerAdapter.notifyItemChanged(index);
}
}
}
});
Query movieQuery = FirebaseDatabase.getInstance().getReference().child("Movies");
recyclerAdapter = new FirebaseRecyclerAdapter(movieQuery...) {
#Override
public void populateViewHolder(RecyclerView.ViewHolder viewHolder, Object model, int position) {
//...
final String key = getRef(position).getKey();
viewHolder.showActiveRating(ratingConnection.getRating(key));
}
};
and MovieRatingConnection would be a class like this
public class MovieRatingConnection {
private MovieRatingListener listener;
public MovieRatingConnection(String userId, RatingChangeListener changeListener) {
Query query = FirebaseDatabase.getInstance().getReference().child("MovieRatings").child(userId);
listener = new MovieRatingListener(query, changeListener);
}
public Rating getRating(String key) {
return listener.getRating(key);
}
public void cleanup() {
if (listener != null) {
listener.unregister();
}
}
public static class MovieRatingListener implements ChildEventListener {
public interface RatingChangeListener {
public void onRatingChanged(DataSnapshot snapshot);
}
private Query query;
private HashMap<String, Rating> ratingMap = new HashMap<>();
private RatingChangeListener changeListener;
public MovieRatingListener(Query query, RatingChangeListener changeListener) {
this.query = query;
this.changeListener = changeListener;
query.addChildEventListener(this);
}
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
if (dataSnapshot != null) {
ratingMap.put(dataSnapshot.getKey(), dataSnapshot.getValue(Rating.class));
changeListener.onRatingChanged(dataSnapshot);
}
}
#Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
if (dataSnapshot != null) {
ratingMap.put(dataSnapshot.getKey(), dataSnapshot.getValue(Rating.class));
changeListener.onRatingChanged(dataSnapshot);
}
}
#Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
ratingMap.remove(dataSnapshot.getKey());
changeListener.onRatingChanged(null);
}
#Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
public Rating getRating(String key) {
if (ratingMap.get(key) != null) {
return ratingMap.get(key);
} else {
return new Rating(); // default value/null object
}
}
public void unregister() {
query.removeEventListener(this);
}
}
}
I am using Firebase in my app, along with RxJava.
Firebase is capable of notify your app whenever something changed in the backend data (addition, removals, changes, ...).
I am trying to combine the feature of Firebase with RxJava.
The data I am listening for is called Leisure, and the Observable emits LeisureUpdate which contains a Leisure and the type of update (add, remove, moved, changed).
Here is my method which allows to subscribe to this events.
private Observable<LeisureUpdate> leisureUpdatesObservable;
private ChildEventListener leisureUpdatesListener;
private int leisureUpdatesSubscriptionsCount;
#NonNull
public Observable<LeisureUpdate> subscribeToLeisuresUpdates() {
if (leisureUpdatesObservable == null) {
leisureUpdatesObservable = Observable.create(new Observable.OnSubscribe<LeisureUpdate>() {
#Override
public void call(final Subscriber<? super LeisureUpdate> subscriber) {
leisureUpdatesListener = firebase.child(FirebaseStructure.LEISURES).addChildEventListener(new ChildEventListener() {
#Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
final Leisure leisure = convertMapToLeisure((Map<String, Object>) dataSnapshot.getValue());
subscriber.onNext(new LeisureUpdate(leisure, LeisureUpdate.ADDED));
}
#Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
final Leisure leisure = convertMapToLeisure((Map<String, Object>) dataSnapshot.getValue());
subscriber.onNext(new LeisureUpdate(leisure, LeisureUpdate.CHANGED));
}
#Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
final Leisure leisure = convertMapToLeisure((Map<String, Object>) dataSnapshot.getValue());
subscriber.onNext(new LeisureUpdate(leisure, LeisureUpdate.REMOVED));
}
#Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
final Leisure leisure = convertMapToLeisure((Map<String, Object>) dataSnapshot.getValue());
subscriber.onNext(new LeisureUpdate(leisure, LeisureUpdate.MOVED));
}
#Override
public void onCancelled(FirebaseError firebaseError) {
subscriber.onError(new Error(firebaseError.getMessage()));
}
});
}
});
}
leisureUpdatesSubscriptionsCount++;
return leisureUpdatesObservable;
}
First off, I would like to use Observable.fromCallable() method in order to create the Observable, but I guess it is impossible, since Firebase uses callbacks, right?
I keep a single instance of the Observable in order to always have one Observable where multiple Subscriber can subscribe.
The problem comes when everyone unsubscribe and I need to stop listening for the events in Firebase.
I didn't find anyway to make the Observable understand if there is any subscription still. So I keep counting how many calls I got to subscribeToLeisuresUpdates(), with leisureUpdatesSubscriptionsCount.
Then every time someone wants to unsubscribe it has to call
#Override
public void unsubscribeFromLeisuresUpdates() {
if (leisureUpdatesObservable == null) {
return;
}
leisureUpdatesSubscriptionsCount--;
if (leisureUpdatesSubscriptionsCount == 0) {
firebase.child(FirebaseStructure.LEISURES).removeEventListener(leisureUpdatesListener);
leisureUpdatesObservable = null;
}
}
This is the only way I found to make the Observable emits items when there is a subscriber, but I feel like there must be an easier way, specially understanding when there is no more subscribers listening to the observable.
Anyone who encountered a similar problem or have a different approach?
You can use Observable.fromEmitter, something along these lines
return Observable.fromEmitter(new Action1<Emitter<LeisureUpdate>>() {
#Override
public void call(final Emitter<LeisureUpdate> leisureUpdateEmitter) {
final ValueEventListener listener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// process update
LeisureUpdate leisureUpdate = ...
leisureUpdateEmitter.onNext(leisureUpdate);
}
#Override
public void onCancelled(DatabaseError databaseError) {
leisureUpdateEmitter.onError(new Throwable(databaseError.getMessage()));
mDatabaseReference.removeEventListener(this);
}
};
mDatabaseReference.addValueEventListener(listener);
leisureUpdateEmitter.setCancellation(new Cancellable() {
#Override
public void cancel() throws Exception {
mDatabaseReference.removeEventListener(listener);
}
});
}
}, Emitter.BackpressureMode.BUFFER);
Put this in your Observable.create() at the end.
subscriber.add(Subscriptions.create(new Action0() {
#Override public void call() {
ref.removeEventListener(leisureUpdatesListener);
}
}));
I suggest you to check as reference(or just use it) one of the next libraries:
RxJava : https://github.com/nmoskalenko/RxFirebase
RxJava 2.0: https://github.com/FrangSierra/Rx2Firebase
One of them works with RxJava and the other one with the new RC of RxJava 2.0. If you are interested of it, you can see the differences between both here.
Here is a sample code for using RxJava2 with Firebase's CompletionListener:
Completable.create(new CompletableOnSubscribe() {
#Override
public void subscribe(final CompletableEmitter e) throws Exception {
String orderKey = FirebaseDatabase.getInstance().getReference().child("orders").push().getKey();
FirebaseDatabase.getInstance().getReference().child("orders").child(orderKey).setValue(order,
new DatabaseReference.CompletionListener() {
#Override
public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
if (e.isDisposed()) {
return;
}
if (databaseError == null) {
e.onComplete();
} else {
e.onError(new Throwable(databaseError.getMessage()));
}
}
});
}
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
Another amazing library which will help you to wrap all firebase realtime database logic under rx patterns.
https://github.com/Link184/Respiration
Here you can create your firebase repository and extend it from GeneralRepository for example:
#RespirationRepository(dataSnapshotType = Leisure.class)
public class LeisureRepository extends GeneralRepository<Leisure>{
protected LeisureRepository(Configuration<Leisure> repositoryConfig) {
super(repositoryConfig);
}
// observable will never emit any events if there are no more subscribers
public void killRepository() {
if (!behaviorSubject.hasObservers()) {
//protected fields
behaviorSubject.onComplete();
databaseReference.removeEventListener(valueListener);
}
}
}
You can "kill" your repository in that way:
// LeisureRepositoryBuilder is a generated class by annotation processor, will appear after a successful gradle build
LeisureRepositoryBuilder.getInstance().killRepository();
But I think for your situation will be better to extend com.link184.respiration.repository.ListRepository to avoid data mapping from java.util.Map to Leisure model through LeisureUpdate
I'm trying to read from Firebase (offline), but the method onDataChange is never called,
private class MyAuthStateListener implements Firebase.AuthStateListener {
#Override
public void onAuthStateChanged(AuthData authData) {
if (authData != null) {
// user is logged in
userId = authData.getUid();
mFirebase = mFirebase.child(authData.getUid()).child("xxx");
Query mQuery = mFirebase.orderByChild("externalId").equalTo(externalId);
mQuery.addListenerForSingleValueEvent(new MyValueEventListener());
}
}
}
private class MyValueEventListener implements ValueEventListener {
#Override
public void onDataChange(DataSnapshot mDataSnapshot) {
if (mDataSnapshot.hasChildren()) {
Iterable<DataSnapshot> i = mDataSnapshot.getChildren();
Iterator<DataSnapshot> mIterator = i.iterator();
if (mIterator.hasNext()) {
mArrayList.clear();
}
while (mIterator.hasNext()) {
DataSnapshot c = mIterator.next();
mArrayList.add(c.getValue(mObject.class));
}
}
onTaskComplete(); // call the notifyDataSetChange() on the adapter.
}
#Override
public void onCancelled(FirebaseError firebaseError) {
}
}
Have I done something wrong?
Online it works well, but offline it won't works.
The new objects are created with a FAB that open a new activity (to modify the object) and than I return to that activity.
Thank you all for the help.
So I'll try to keep this question as to-the-point as possible, but it will involve code snippets that traverse an entire codepath.
For context, I am fairly new and completely self-taught for Android dev, so please notify me of any clear misunderstandings/poor organization throughout. The main focus of the question is bug I am experiencing now, which is that, after a network request, the variable that was supposed to be set as a result of that network request is null, because the code moved forward before the network request completed.
Here is my activity method. It is supposed to populate the mFriends variable with the result of mUserPresenter.getUserList(), which is (unfortunately) null:
/**
* Grabs a list of friends, populates list with UserAdapter
*/
#Override
public void onResume(){
super.onResume();
mUserPresenter = new UserPresenter();
mFriends = mUserPresenter.getUserList();
if (mGridView.getAdapter() == null) {
UserAdapter adapter = new UserAdapter(getActivity(), mFriends);
mGridView.setAdapter(adapter);
}
else{
((UserAdapter)mGridView.getAdapter()).refill(mFriends);
}
}
Here is how I am structuring my UserPresenter method getUserList:
public List<User> getUserList()
{
ApiService.get_friends(this);
return mUserList;
}
The real magic happens in the ApiService class:
public static void get_friends(final UserPresenter userPresenter){
ApiEndpointInterface apiService = prepareService();
apiService.get_friends().
observeOn(AndroidSchedulers.mainThread())
.subscribe(
new Action1<List<User>>()
{
#Override
public void call(List<User> users) {
userPresenter.setList(users);
}
}
);
}
My thinking was, that by calling userPresenter.setList(users) in ApiService, that would set mUserList to the response from the api request. However, instead, mUserList == null at the time that getUserList responds.
Any ideas of how I can structure this?
I have also started to learn something similar. Here, I would rather use callbacks.
In your presenter,
public void setList(List<User> users) {
yourView.setUserList(users);
}
And your activity which implements a view (MVP)
#Override
public void setUserList(List<User> users) {
((UserAdapter)mGridView.getAdapter()).refill(mFriends);
}
Also, check that retrofit is not returning null list.
I have a made a small app when I was learning about all this. It fetches user data from GitHub and shows in a list. I was also working with ORMLite and Picasso so some db stuff is there. Dagger Dependency is also used (but you can ignore that). Here's the link.
Here's how my Presenter behaves:
private DataRetrieverImpl dataRetriever;
#Override
public void getUserList(String name) {
dataRetriever.getUserList(name);
}
#Override
public void onEvent(DataRetrieverEvent event) {
UserList userList = (UserList)event.getData();
mainView.setItems(userList);
}
DataRetrieverImpl works as a module (sort of).
private DataRetriever dataRetriever;
restAdapter = new RestAdapter.Builder().setEndpoint(SERVER_END_POINT).build();
dataRetriever = restAdapter.create(DataRetriever.class);
public void getUserList(final String name) {
Log.i(TAG, "getting user list for: " + name);
Observable<UserList> observable = dataRetriever.getUserList(name);
Log.i(TAG, "subscribe to get userlist");
observable.subscribe(new Action1<UserList>() {
#Override
public void call(UserList userList) {
eventBus.post(new DataRetrieverEvent("UserList", userList));
// save to database
for (User user : userList.getItems()) {
Log.i(TAG, user.getLogin());
try {
dbHelper.create(user);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}, new Action1<Throwable>() {
#Override
public void call(Throwable throwable) {
throwable.printStackTrace();
}
});
}
And DataRetriever is interface for retrofit. I'm sorry for the naming confusion.
public interface DataRetriever {
#GET("/search/users")
public Observable<UserList> getUserList(#Query("q") String name);
}
Any my Activity,
#Override
public void setItems(final UserList userList) {
runOnUiThread(new Runnable() {
#Override
public void run() {
UserAdapter userAdapter = (UserAdapter)recyclerView.getAdapter();
userAdapter.setUserList(userList);
userAdapter.notifyItemRangeInserted(0, userAdapter.getItemCount());
}
});
}