0

Context

I've been attempting to display data from Firestore in a FirestoreRecyclerAdapter, however this data is basically nested Maps, so a direct approach using the Query function is not ideal. Here is an image of my data structure:

Firestore Data Structure

Notice that ServiceOrder, client and vehicle are all Maps. In my Java code, ServiceOrder is made up of a Client and Vehicle objects.

So, if I were to use .setQuery(query, ServiceOrder.class), it would attempt to Map all of the data into ServiceOrder objects. But since my document is structured the way it is, that is not possible.

Issue

I suppose this could be fixed by mapping all documents into an object of a new class, similar to what is done here: https://medium.com/firebase-tips-tricks/how-to-map-an-array-of-objects-from-cloud-firestore-to-a-list-of-objects-122e579eae10.

Even though I can see how it could be done using a normal RecyclerView and using a custom adapter, could the same solution be used in FirestoreRecyclerAdapter? Because I did try to create something akin to the solution in the link, but couldn't get it to work.

My code

Here is where I'm setting up the RecyclerView and Querying the data from Firestore:

 private void setupRecyclerView() {

            RecyclerView recyclerView = findViewById(R.id.recyclerViewOs);

            Query query = osRef.orderBy("ServiceOrder",
                    Query.Direction.ASCENDING); //This is the issue.
                     //How could I map the documents here?

            FirestoreRecyclerOptions<ServiceOrder> options =
                    new FirestoreRecyclerOptions.Builder<ServiceOrder>()
                            .setQuery(query, ServiceOrder.class)
                            .build();

            listAdapter = new FirestoreAdapter(options);

            recyclerView.setHasFixedSize(true);
            recyclerView.setLayoutManager(new LinearLayoutManager(this));
            recyclerView.setAdapter(listAdapter);
        }

My FirestoreRecyclerAdapter, where I'm binding my Views. The onBindViewHolder returns NPE for every View. This is the problem with the nested Maps described early.

public class FirestoreAdapter extends FirestoreRecyclerAdapter<ServiceOrder, FirestoreAdapter.ViewHolder> {


    @Override
    protected void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull ServiceOrder model) {
        holder.osIdItem.setText(String.valueOf(model.getId()));
        holder.osClientItem.setText(model.getPaymentForm());
        holder.osDateItem.setText(model.getPaymentForm());
        holder.osValueItem.setText(String.valueOf(model.getTotalValue()));
    }

And finally, my ServiceOrder class. Getters/Setters were removed to increase readability.

public class ServiceOrder {

    public Client client;
    public Vehicle vehicle;
    private String service;
    private String observation;
    private String paymentForm;
    private String date;
    private double totalValue;
    private int id;

    public ServiceOrder() {
    }

    private ServiceOrder(ServiceOrderBuilder serviceOrderBuilder){
        this.client = serviceOrderBuilder.client;
        this.vehicle = serviceOrderBuilder.vehicle;
        this.service = serviceOrderBuilder.service;
        this.paymentForm = serviceOrderBuilder.paymentForm;
        this.observation = serviceOrderBuilder.observation;
        this.totalValue = serviceOrderBuilder.value;
        this.date = serviceOrderBuilder.date;

    }

    public static class ServiceOrderBuilder {

        private Vehicle vehicle;
        private Client client;
        private final String service;
        private final String paymentForm;
        private final int id;
        private final double value;
        private final String date;
        private String observation;

        public ServiceOrderBuilder(Client client, Vehicle vehicle,
                                   String service, String paymentForm,
                                   int id, double value, String date) {
            this.client = client;
            this.vehicle = vehicle;
            this.service = service;
            this.paymentForm = paymentForm;
            this.id = id;
            this.value = value;
            this.date = date;
        }


        public ServiceOrder.ServiceOrderBuilder observation(String observation) {
            this.observation = observation;
            return this;
        }

        public ServiceOrder build() {
            ServiceOrder serviceOrder = new ServiceOrder(this);
            return serviceOrder;

        }
    }
}

My attempt

As suggested in another post, I attempted to create a new ServiceOrderDocument in order to map all documents into an object of this class. The class:


public class ServiceOrderDocument {

        ServiceOrder serviceOrder;

        public ServiceOrderDocument() {}

        public ServiceOrderDocument(ServiceOrder serviceOrder) {
            this.serviceOrder = serviceOrder;
        }

        @PropertyName("ServiceOrder")
        public ServiceOrder getServiceOrder() {
            return serviceOrder;
        }
    }

Ànd pass this into the Adapter found in the private void setupRecyclerView(). However, the Adapter expects a QuerySnapshot, so I feel like I'm stuck here.

Reproducing the issue

If you'd like to try it out yourself, the best way would be to have three Classes, with one of them having objects from the other two. A example would be a Sale class having objects from Salesman and Product.

Proceed to write a Sale object into your Firestore database, and see how it creates a nested document. Then, try to display that Sale in a RecyclerView using FirestoreRecyclerAdapter. Your onBindViewHolder should have a Sale model that would get the data from it's getters.

Edit

So using a List to get the content seems to work at a first glance, by using Cast I could pass it as a adapter for the FirestoreRecyclerAdapter, however, it does not work for the startListening() methods. Here's what I did:

 private void setupRecyclerView() {

        docRef.get().addOnCompleteListener(task -> {
            if (task.isSuccessful()) {
                DocumentSnapshot document = task.getResult();
                if (document.exists()) {
                    services = document.toObject(ServiceOrderDocument.class).services;

                    RecyclerView recyclerView = findViewById(R.id.recyclerViewOs);
                    Query query = osRef.orderBy("ServiceOrder",
                            Query.Direction.ASCENDING);

                    FirestoreRecyclerOptions<ServiceOrder> options =
                            new FirestoreRecyclerOptions.Builder<ServiceOrder>()
                                    .setQuery(query, ServiceOrder.class)
                                    .build();

                    // listAdapter = new FirestoreAdapter(options);
                    services = (List<ServiceOrder>) new FirestoreAdapter(options);

                    recyclerView.setHasFixedSize(true);
                    recyclerView.setLayoutManager(new  LinearLayoutManager(this));
                    recyclerView.setAdapter((RecyclerView.Adapter) services);

                }
            }
        });
        }

However, the following issue is created:

    @Override
    protected void onStart() {
        super.onStart();
        listAdapter.startListening();//NPE Error
        services.startListening();//Can't use method
    }
  • The Adapter expects a QuerySnapshot, that's correct. Be sure to be of the correct `ServiceOrderDocument` type. This question is some kind of duplicate, but I will not close it, as maybe someone will try to explain it better than I did. – Alex Mamo Sep 14 '22 at 09:23
  • Since you're the one who wrote the blogpost I linked, I could use `ServiceOrderDocument` as a parameter to `toObject()`, but how could I go about making sure it works as an Adapter for `FirestoreRecyclerAdapter`? – Leonardo Maito Sep 14 '22 at 12:38
  • The blog post indicates how to do that with an array, not with an object (map). But you can achieve the exact same thing using an object, and you can each document in your adapter class to an object of type `ServiceOrderDocument`. But in my opinion, the first solution will be much simpler. – Alex Mamo Sep 14 '22 at 13:08
  • I decided to try the simple solution first, using the array. With some type casting I managed to use a List and pass it as an Adapter to `FirestoreRecylerAdapter`. However, I can't use the `startListening()` method anymore. Check the Edit for more information on my attempt. – Leonardo Maito Sep 14 '22 at 13:44
  • `startListening()` is a part of the Firebase UI library. However, you can attach and detach the lister as explained [here](https://stackoverflow.com/questions/48861350/should-i-actually-remove-the-valueeventlistener/48862873). – Alex Mamo Sep 14 '22 at 14:19
  • I see. Will try it out. Just did some testing and my code doesn't actually run since `FirestoreAdapter cannot be cast to java.util.List`. I'm going to have to look for another solution. – Leonardo Maito Sep 14 '22 at 16:47
  • That's the expected behavior, you can't cast a an object of type FirestorAdaper into a List. – Alex Mamo Sep 14 '22 at 17:16
  • Then how exactly am I supposed to use my `ServiceOrderDocument`? Or the List `services`, in this case? – Leonardo Maito Sep 14 '22 at 17:30
  • Set the type of the adapter to be ServiceOrderDocument. – Alex Mamo Sep 15 '22 at 06:11
  • @AlexMamo that worked. I'm not sure what I was doing before, but once I set the adapter to be ServiceOrderDocument, and made the correspondent changes, I got the data I need into the Recycler. I'm going to post it as an answer in case another user stumbles upon this issue. – Leonardo Maito Sep 15 '22 at 19:16
  • There is already an answer in the earlier question of yours. Besides that, the solution is already provided in the last comment above. – Alex Mamo Sep 15 '22 at 19:21
  • My bad, I just felt like gathering all that information into a single answer, but I don't mind if it is deleted. – Leonardo Maito Sep 15 '22 at 19:26
  • I was referring to this [answer](https://stackoverflow.com/questions/73662910/why-does-data-from-firestore-db-returns-as-null-in-firestorerecycleradapter/73694519#73694519), as it provides the solution to your problem, right? – Alex Mamo Sep 16 '22 at 05:33
  • Correct. I had to make some changes to other parts of the code, but that is the overall solution. – Leonardo Maito Sep 16 '22 at 13:14
  • So it's good to hear that. Can I help with other information regarding that [question](https://stackoverflow.com/questions/73662910/why-does-data-from-firestore-db-returns-as-null-in-firestorerecycleradapter/73694519#73694519)? – Alex Mamo Sep 17 '22 at 10:42

1 Answers1

0

For those looking for an answer, read Alex Mamo comments and his post at: https://medium.com/firebase-tips-tricks/how-to-map-an-array-of-objects-from-cloud-firestore-to-a-list-of-objects-122e579eae10.

For my solution, I did need a ServiceDocument class to help me map my documents so I could use it in my Adapter. This is what the class looks like:

public class ServiceDocument {

    public ServiceOrder serviceOrder;

    public ServiceDocument() {
    }

    @PropertyName("serviceOrder")
    public ServiceOrder getServiceOrder() {
        return serviceOrder;
    }
}

Then, in your Activity or wherever you are managing your Recyler/Adapter, you would need something like this

    Query query = osRef.orderBy("serviceOrder",
                 Query.Direction.ASCENDING);

   FirestoreRecyclerOptions<ServiceDocument> options =
    new FirestoreRecyclerOptions.Builder<ServiceDocument>()
                                    .setQuery(query, ServiceDocument.class)
                                    .build();

    listAdapter = new FirestoreAdapter(options);

    recyclerView.setHasFixedSize(true);
    recyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
    recyclerView.setAdapter(listAdapter);

Last but not least, make sure your Adapter class has been adapted for your new class.

public class FirestoreAdapter extends FirestoreRecyclerAdapter<ServiceDocument, FirestoreAdapter.ViewHolder> {

    public FirestoreAdapter(@NonNull FirestoreRecyclerOptions<ServiceDocument> options) {
        super(options);
    }

    @Override
    protected void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull ServiceDocument model) {
        holder.osIdItem.setText(String.valueOf(model.getServiceOrder().getId()));
        holder.osClientItem.setText(model.getServiceOrder().getClient().getName());
        holder.osDateItem.setText(model.getServiceOrder().getDate());
        holder.osValueItem.setText(String.valueOf(model.getServiceOrder().getTotalValue()));
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {

        LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());

        View view = layoutInflater.inflate(R.layout.layout_os_item, parent, false);

        return new FirestoreAdapter.ViewHolder(view);
    }