Mapping documents into an object of a class to use with FirestoreRecyclerAdapter

Viewed 79

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
    }
1 Answers

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);
    }
Related