I'm writing this after hours searching online. I am trying to fetch specific sub-collection in firestore using dynamic path. I figured that whatever I'm doing wrong is probably about async tasks, but I just couldn't figure out how to solve the problem myself.
I tried using AsyncTask class (which is, if im not mistaken, deprecated now) but it didn't work for me.
I am using firebase-ui's adapter and combining it with geohashing from firestore I want to return specific query to the place the user is closest to.
Main Activity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
FirebaseFirestore db = FirebaseFirestore.getInstance();
private FirestoreRecyclerAdapter adapter;
private RecyclerView mFirestoreList;
Query dishQuery;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home_activity);
mFirestoreList = findViewById(R.id.firestore_list);
Spinner mEmptyListMessage = findViewById(R.id.empty_list_message);
new getRightQuery().getQuery(new Callback() {
@Override
public void firebaseResponseCallback(Query q) {
dishQuery = q;
}
});
FirestoreRecyclerOptions<Dish> options = new FirestoreRecyclerOptions.Builder<Dish>().setQuery(dishQuery, Dish.class).build();
adapter = new FirestoreRecyclerAdapter<Dish, DishViewHolder>(options) {
@NonNull
@Override
public DishViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_single, parent, false);
return new DishViewHolder(view);
}
@Override
public void onChildChanged(@NonNull ChangeEventType type, @NonNull DocumentSnapshot snapshot, int newIndex, int oldIndex) {
super.onChildChanged(type, snapshot, newIndex, oldIndex);
}
@Override
protected void onBindViewHolder(@NonNull DishViewHolder holder, int position, @NonNull Dish model) {
holder.name.setText(model.getName());
holder.starCount.setText(String.valueOf(model.getStarCount()));
holder.isVegan.setText(String.valueOf(model.getIsVegan()));
}
@Override
public void onError(@NonNull FirebaseFirestoreException e) {
super.onError(e);
Log.e("Error", "got error" + e.getMessage());
}
};
mFirestoreList.setHasFixedSize(true);
mFirestoreList.setLayoutManager(new LinearLayoutManager(this));
mFirestoreList.setAdapter(adapter);
}
@Override
protected void onStop() {
super.onStop();
adapter.stopListening();
}
@Override
protected void onStart() {
super.onStart();
adapter.startListening();
}
private class DishViewHolder extends RecyclerView.ViewHolder {
private TextView name, starCount, isVegan;
public DishViewHolder(@NonNull View itemView) {
super(itemView);
name = itemView.findViewById(R.id.name);
starCount = itemView.findViewById(R.id.star_count);
isVegan = itemView.findViewById(R.id.is_vegan);
}
}
}
getRightQuery.java
public class getRightQuery {
private String a;
private final FirebaseFirestore db = FirebaseFirestore.getInstance();
private Query query;
public getRightQuery(){
}
public void getQuery(Callback callback) {
List<DocumentSnapshot> matchingDocs = new ArrayList<>();
final GeoLocation center = new GeoLocation(32.16051, 34.84002);
final double radiusInM = 50 * 1000;
// Each item in 'bounds' represents a startAt/endAt pair. We have to issue
// a separate query for each pair. There can be up to 9 pairs of bounds
// depending on overlap, but in most cases there are 4.
List<GeoQueryBounds> bounds = GeoFireUtils.getGeoHashQueryBounds(center, radiusInM);
final List<Task<QuerySnapshot>> tasks = new ArrayList<>();
for (GeoQueryBounds b : bounds) {
Query q = db.collection("Restaurants")
.orderBy("geohash")
.startAt(b.startHash)
.endAt(b.endHash);
tasks.add(q.get());
}
// Collect all the query results together into a single list
Tasks.whenAllComplete(tasks)
.addOnCompleteListener(t -> {
for (Task<QuerySnapshot> task : tasks) {
QuerySnapshot snap = task.getResult();
for (DocumentSnapshot doc : snap.getDocuments()) {
double lat = doc.getDouble("lat");
double lng = doc.getDouble("lng");
// We have to filter out a few false positives due to GeoHash
// accuracy, but most will match
GeoLocation docLocation = new GeoLocation(lat, lng);
double distanceInM = GeoFireUtils.getDistanceBetween(docLocation, center);
if (distanceInM <= radiusInM) {
matchingDocs.add(doc);
Query query = db.collection("Restaurants").document(matchingDocs.get(0).getId()).collection("menu");
Log.d("finish", matchingDocs.get(0).getId());
callback.firebaseResponseCallback(query);
}
}
}
});
}
}
EDITED
Callback.java
import com.google.firebase.firestore.Query;
interface Callback{
void firebaseResponseCallback(Query q);//whatever your return type is.
}
screenshot of how firestore database is designed
I get the following respone:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.dishy, PID: 6122
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.dishy/com.example.dishy.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'com.google.firebase.firestore.ListenerRegistration com.google.firebase.firestore.Query.addSnapshotListener(com.google.firebase.firestore.MetadataChanges, com.google.firebase.firestore.EventListener)' on a null object reference
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2817)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6541)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'com.google.firebase.firestore.ListenerRegistration com.google.firebase.firestore.Query.addSnapshotListener(com.google.firebase.firestore.MetadataChanges, com.google.firebase.firestore.EventListener)' on a null object reference
at com.firebase.ui.firestore.FirestoreArray.onCreate(FirestoreArray.java:63)
at com.firebase.ui.common.BaseObservableSnapshotArray.addChangeEventListener(BaseObservableSnapshotArray.java:97)
at com.firebase.ui.firestore.FirestoreRecyclerAdapter.startListening(FirestoreRecyclerAdapter.java:51)
at com.example.dishy.MainActivity.onStart(MainActivity.java:116)
at android.app.Instrumentation.callActivityOnStart(Instrumentation.java:1333)
at android.app.Activity.performStart(Activity.java:6992)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2780)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892)
at android.app.ActivityThread.-wrap11(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1593)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6541)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
Edited
For anyone having trouble getting data using dynamic queries (created by other tasks in other classes) here is my WORKING code:
MainActivity.java
public class MainActivity extends AppCompatActivity {
FirebaseFirestore db = FirebaseFirestore.getInstance();
private FirestoreRecyclerAdapter adapter;
private RecyclerView mFirestoreList;
Query dishQuery = db.collection("o");
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home_activity);
mFirestoreList = findViewById(R.id.firestore_list);
FirestoreRecyclerOptions<Dish> options = new FirestoreRecyclerOptions.Builder<Dish>().setQuery(dishQuery, Dish.class).build();
adapter = new FirestoreRecyclerAdapter<Dish, DishViewHolder>(options) {
@NonNull
@Override
public DishViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_single, parent, false);
return new DishViewHolder(view);
}
@Override
public void onChildChanged(@NonNull ChangeEventType type, @NonNull DocumentSnapshot snapshot, int newIndex, int oldIndex) {
super.onChildChanged(type, snapshot, newIndex, oldIndex);
adapter.notifyDataSetChanged();
}
@Override
protected void onBindViewHolder(@NonNull DishViewHolder holder, int position, @NonNull Dish model) {
holder.name.setText(model.getName());
holder.starCount.setText(String.valueOf(model.getStarCount()));
holder.isVegan.setText(String.valueOf(model.getIsVegan()));
}
@Override
public void onError(@NonNull FirebaseFirestoreException e) {
super.onError(e);
Log.e("Error", "got error" + e.getMessage());
}
};
mFirestoreList.setHasFixedSize(true);
mFirestoreList.setLayoutManager(new LinearLayoutManager(this));
mFirestoreList.setAdapter(adapter);
/////
new getRightQuery().getQuery(new Callback() {
@Override
public void firebaseResponseCallback(Query q) {
q.addSnapshotListener(new EventListener<QuerySnapshot>() {
@Override
public void onEvent(@Nullable QuerySnapshot value, @Nullable FirebaseFirestoreException error) {
adapter.stopListening();
adapter.updateOptions(new FirestoreRecyclerOptions.Builder<Dish>().setQuery(q, Dish.class).build());
adapter.startListening();
adapter.notifyDataSetChanged();
}
});
}
});
}
@Override
protected void onStop() {
super.onStop();
adapter.stopListening();
}
@Override
protected void onStart() {
super.onStart();
adapter.startListening();
}
private class DishViewHolder extends RecyclerView.ViewHolder {
private TextView name, starCount, isVegan;
public DishViewHolder(@NonNull View itemView) {
super(itemView);
name = itemView.findViewById(R.id.name);
starCount = itemView.findViewById(R.id.star_count);
isVegan = itemView.findViewById(R.id.is_vegan);
}
}
}
I used a "dummy" query just to create an empty recyclerview (as Alex Mamo stated in the comments) and then populated the data and updated the adapter using
adapter.updateOptions()