Firebase with Vaadin 14 Flow - The data provider hasn't ever called getLimit()

I'm trying to store data from a Vaadin 14 table in a firebase realtime db. I can call setValuesAsync, but I don't see any results in the firebase console in the db. When I reload the page, I get the error "The data provider hasn't ever called getLimit()". I tried to compare my code with "https://github.com/Artur-/vaadin-on-fire", but they don't call the function anywhere either. Here is my Code: TableView.java

package com.example.application.views.table;

import com.example.application.data.FirebaseDataProvider;
import com.example.application.data.db.Firebase;
import com.example.application.data.db.RunDB;
import com.example.application.data.entity.Run;
import com.example.application.data.service.SampleRunService;
import com.example.application.views.MainLayout;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.datepicker.DatePicker;
import com.vaadin.flow.component.dependency.Uses;
import com.vaadin.flow.component.formlayout.FormLayout;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.grid.GridVariant;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.icon.Icon;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.splitlayout.SplitLayout;
import com.vaadin.flow.component.textfield.NumberField;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.BeanValidationBinder;
import com.vaadin.flow.data.binder.ValidationException;
import com.vaadin.flow.router.BeforeEnterEvent;
import com.vaadin.flow.router.BeforeEnterObserver;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;

import java.io.IOException;
import java.time.LocalDate;
import java.util.Locale;
import java.util.Optional;

import com.vaadin.ui.DateField;
import org.springframework.beans.factory.annotation.Autowired;
import org.vaadin.artur.helpers.CrudServiceDataProvider;

@PageTitle("Table")
@Route(value = "table/:samplePersonID?/:action?(edit)", layout = MainLayout.class)
@Uses(Icon.class)
public class TableView extends Div implements BeforeEnterObserver {

    private final String RUN_ID = "samplePersonID";
    private final String RUN_EDIT_ROUTE_TEMPLATE = "table/%d/edit";

    private Grid<Run> grid = new Grid<>(Run.class);

    private TextField name;
    private NumberField distance;
    private NumberField time;
    private DatePicker date;

    private Button cancel = new Button("Abbrechen");
    private Button save = new Button("Speichern");
    private Button delete = new Button("Löschen");

    private BeanValidationBinder<Run> binder;

    private Run run;

    private SampleRunService sampleRunService;
    private FirebaseDataProvider<Run> dataProvider;

    public TableView(@Autowired SampleRunService sampleRunService) {
        try {
            Firebase.setup();
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("Unable to setup Firebase connection. See the log for details");
        }
        dataProvider = new FirebaseDataProvider<>(Run.class, RunDB.getRunsDb());
        grid.setDataProvider(dataProvider);
        this.sampleRunService = sampleRunService;
        addClassNames("table-view", "flex", "flex-col", "h-full");
        // Create UI
        SplitLayout splitLayout = new SplitLayout();
        splitLayout.setSizeFull();

        createGridLayout(splitLayout);
        createEditorLayout(splitLayout);

        add(splitLayout);

        grid.removeColumn(grid.getColumnByKey("key"));
        grid.removeColumn(grid.getColumnByKey("id"));
        grid.setColumnOrder(
                grid.getColumnByKey("name"),
                grid.getColumnByKey("distance"),
                grid.getColumnByKey("time"),
                grid.getColumnByKey("date")
        );
        grid.getColumnByKey("distance").setHeader("Distanz");
        grid.getColumnByKey("time").setHeader("Zeit");
        grid.getColumnByKey("date")
                .setHeader("Datum");

        grid.addThemeVariants(GridVariant.LUMO_NO_BORDER);
        grid.setHeightFull();

        // when a row is selected or deselected, populate form
        grid.asSingleSelect().addValueChangeListener(event -> {
            if (event.getValue() != null) {
                UI.getCurrent().navigate(String.format(RUN_EDIT_ROUTE_TEMPLATE, event.getValue().getId()));
            } else {
                clearForm();
                UI.getCurrent().navigate(TableView.class);
            }
        });

        // Configure Form
        binder = new BeanValidationBinder<>(Run.class);

        binder.forField(date)
                .withConverter(new LocalDateLongConverter())
                .bind(Run::getDate, Run::setDate);
        // Bind fields. This where you'd define e.g. validation rules

        binder.bindInstanceFields(this);

        cancel.addClickListener(e -> {
            clearForm();
            refreshGrid();
            date.setValue(LocalDate.now());
        });

        delete.addClickListener(e -> {
            clearForm();
            refreshGrid();
            date.setValue(LocalDate.now());
        });

        save.addClickListener(e -> {
            try {
                if (this.run == null) {
                    this.run = new Run();
                }
                binder.writeBean(this.run);

                sampleRunService.update(this.run);
                if (run.getKey() == null) {
                    RunDB.add(run);
                } else {
                    RunDB.update(run.getKey(), run);
                }


                clearForm();
                refreshGrid();
                date.setValue(LocalDate.now());
                Notification.show("SamplePerson details stored.");
                UI.getCurrent().navigate(TableView.class);
            } catch (ValidationException validationException) {
                Notification.show("An exception happened while trying to store the samplePerson details.");
            }
        });

    }

    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        Optional<Integer> samplePersonId = event.getRouteParameters().getInteger(RUN_ID);
        if (samplePersonId.isPresent()) {
            Optional<Run> samplePersonFromBackend = sampleRunService.get(samplePersonId.get());
            if (samplePersonFromBackend.isPresent()) {
                populateForm(samplePersonFromBackend.get());
            } else {
                Notification.show(
                        String.format("The requested samplePerson was not found, ID = %d", samplePersonId.get()), 3000,
                        Notification.Position.BOTTOM_START);
                // when a row is selected but the data is no longer available,
                // refresh grid
                refreshGrid();
                event.forwardTo(TableView.class);
            }
        }
    }

    private void createEditorLayout(SplitLayout splitLayout) {
        Div editorLayoutDiv = new Div();
        editorLayoutDiv.setClassName("flex flex-col");
        editorLayoutDiv.setWidth("400px");

        Div editorDiv = new Div();
        editorDiv.setClassName("p-l flex-grow");
        editorLayoutDiv.add(editorDiv);

        FormLayout formLayout = new FormLayout();
        name = new TextField("Name");
        distance = new NumberField("Distanz");
        time = new NumberField("Zeit");
        date = new DatePicker("Datum");
        date.setValue(LocalDate.now());
        date.setLocale(Locale.GERMANY);
        Component[] fields = new Component[]{name, distance, time, date};

        for (Component field : fields) {
            ((HasStyle) field).addClassName("full-width");
        }
        formLayout.add(fields);
        editorDiv.add(formLayout);
        createButtonLayout(editorLayoutDiv);

        splitLayout.addToSecondary(editorLayoutDiv);
    }

    private void createButtonLayout(Div editorLayoutDiv) {
        HorizontalLayout buttonLayout = new HorizontalLayout();
        buttonLayout.setClassName("w-full flex-wrap bg-contrast-5 py-s px-l");
        buttonLayout.setSpacing(true);
        cancel.addThemeVariants(ButtonVariant.LUMO_TERTIARY);
        save.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        delete.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
        buttonLayout.add(save, cancel, delete);
        editorLayoutDiv.add(buttonLayout);
    }

    private void createGridLayout(SplitLayout splitLayout) {
        Div wrapper = new Div();
        wrapper.setId("grid-wrapper");
        wrapper.setWidthFull();
        splitLayout.addToPrimary(wrapper);
        wrapper.add(grid);
    }

    private void refreshGrid() {
        grid.select(null);
        grid.getDataProvider().refreshAll();
    }

    private void clearForm() {
        populateForm(null);
    }

    private void populateForm(Run value) {
        this.run = value;
        binder.readBean(this.run);

    }
}

RunDB.java

package com.example.application.data.db;

import com.example.application.data.entity.Run;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseReference;

import java.time.LocalDate;
import java.util.HashMap;
import java.util.logging.Logger;

public class RunDB {

    private static final String DATABASE_NAMESPACE = "runs";

    public static DatabaseReference getRunsDb() {
        return Firebase.getDb().child(DATABASE_NAMESPACE);
    }

    public static void maybeCreateInitialData(DataSnapshot snapshot) {
        if (snapshot.hasChild(DATABASE_NAMESPACE)) {
            return;
        }

        add(new Run("Paul", 34.4, 120, LocalDate.now().toEpochDay()));
        add(new Run("Basti", 20.4, 10, LocalDate.now().toEpochDay()));
    }

    public static void add(Run run) {
        getRunsDb().push().setValueAsync(run);
    }

    protected static Logger getLogger() {
        return Logger.getLogger("RunsDB");
    }

    public static void update(String key, Run run) {
        getLogger().info("Set run " + key + " to " + run);
        HashMap<String, Object> toUpdate = new HashMap<>();
        toUpdate.put(key, run);
        getRunsDb().updateChildrenAsync(toUpdate);
    }

    public static void delete(Run run) {
        getLogger().info("Delete user " + run);
        HashMap<String, Object> toUpdate = new HashMap<>();
        toUpdate.put(run.getKey(), null);
        getRunsDb().updateChildrenAsync(toUpdate);

    }
}

FirebaseDataProvider.java

package com.example.application.data;


import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.vaadin.flow.data.provider.AbstractDataProvider;
import com.vaadin.flow.data.provider.DataProviderListener;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.function.SerializablePredicate;
import com.vaadin.flow.shared.Registration;

import java.util.LinkedHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

/**
 * A data provider connected to a given child in a Firebase database.
 *
 * @param <T>
 */
public class FirebaseDataProvider<T extends HasKey>
        extends AbstractDataProvider<T, SerializablePredicate<T>>
        implements ChildEventListener {

    private LinkedHashMap<String, T> data = new LinkedHashMap<>();
    private DatabaseReference databaseReference;
    private Class<T> type;
    AtomicInteger registeredListeners = new AtomicInteger(0);

    /**
     * Constructs a new Firebase data provider connected to the given database
     * reference.
     *
     * @param type              the entity type to use for items
     * @param databaseReference the reference containing the child nodes to include
     */
    public FirebaseDataProvider(Class<T> type,
                                DatabaseReference databaseReference) {
        this.databaseReference = databaseReference;
        this.type = type;
    }

    @Override
    public String getId(T item) {
        return item.getKey();
    }

    @Override
    public Stream<T> fetch(Query<T, SerializablePredicate<T>> query) {
        return data.values().stream();
    }

    @Override
    public int size(Query<T, SerializablePredicate<T>> query) {
        return data.size();
    }

    @Override
    public boolean isInMemory() {
        return false;
    }

    @Override
    public Registration addDataProviderListener(
            DataProviderListener<T> listener) {
        if (registeredListeners.incrementAndGet() == 1) {
            registerFirebaseListener();
        }
        Registration realRegistration = super.addDataProviderListener(listener);
        return () -> {
            realRegistration.remove();
            if (registeredListeners.decrementAndGet() == 0) {
                unregisterFirebaseListener();
            }
        };
    }

    private void registerFirebaseListener() {
        databaseReference.addChildEventListener(this);
    }

    private void unregisterFirebaseListener() {
        databaseReference.removeEventListener(this);
    }

    @Override
    public void onChildAdded(DataSnapshot snapshot, String previousChildName) {
        T added = snapshot.getValue(type);
        String key = snapshot.getKey();
        added.setKey(key);

        data.put(key, added);
        refreshAll();
    }

    @Override
    public void onChildChanged(DataSnapshot snapshot,
                               String previousChildName) {
        T updated = snapshot.getValue(type);
        String key = snapshot.getKey();
        updated.setKey(key);

        data.put(key, updated);
        refreshItem(updated);
    }

    @Override
    public void onChildRemoved(DataSnapshot snapshot) {
        data.remove(snapshot.getKey());
        refreshAll();
    }

    @Override
    public void onChildMoved(DataSnapshot snapshot, String previousChildName) {
    }

    @Override
    public void onCancelled(DatabaseError error) {
    }
}

I appreciate any help. Thank you very much. Paul


The fetch method in the data provider is supposed to only load one page of data at a time based on the offset and limit values from the query parameter. This is such a common omission that it has been implemented to actively check whether getLimit() and getOffset() have ever been run to help developers realize what's missing.

This doesn't happen before you add any data since then the size method returns 0 which in turn means that there isn't even any reason to call fetch.