7

I'm new to Firebase and NoSQL. I have an Android Demo, with a City Autocomplete Text Field in which I want to populate the cities I have from my Firebase DB, while typing.

{   "cities":{
        "Guayaquil":true,
        "Gualaceo":true,
        "Quito":true,
        "Quevedo":true,
        "Cuenca":true,
        "Loja":true,
        "Ibarra":true,
        "Manta":true
    }
}

This is what I have so far.

How can I retrieve from the DB cities that start with a letter (input from keyboard)? If I start typing "G", I want to receive "Guayaquil" and "Gualaceo".

If I use orderByValue always returns an empty snapshot.

If I use orderByKey return the whole list.

    Query citiesQuery = databaseRef.child("cities").startAt(input).orderByValue();
    citiesQuery.addValueEventListener(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            List<String> cities = new ArrayList<String>();
            for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
                cities.add(postSnapshot.getValue().toString());
            }

Note: If you can recommend a better data structure, you're welcome.

Community
  • 1
  • 1
Erick Medina
  • 73
  • 1
  • 1
  • 4
  • How big will this database of cities be? Would it be plausible for your app to read in the database once into a local copy (like an array of some sort) and then use local logic to only display the appropriate values while typing? – Ryan Sep 27 '16 at 02:12
  • Initially it will be around 10, but can increase up to 100.I wanted this feature to be "online", so I can add new cities without the need to store it locally – Erick Medina Sep 27 '16 at 02:52
  • Oh thats good. As far as the processor and network are concerned 100 small strings is virtually nothing, so it will definitely be fast. Let me clarify what I mean by storing it locally. I mean in memory during runtime, thats all. I dont mean storing a copy in the devices storage or anything like that. All I mean is, read the cities into an array and then work with that array to display the data. When the database changes, you can very easily update the array almost instantaneously, but I doubt it will be changing often. Check out my answer for a bit more explanation of what I was thinking. – Ryan Sep 27 '16 at 02:56
  • As an extra note, in my answer you can take the word "single" out of that event listener and that will make it real time. Then just add cityList.clear() before the loop and then whenever your database changes your entire arraylist will be refreshed. – Ryan Sep 27 '16 at 02:58

3 Answers3

11

@NicholasChen has identified the problem. But here's the way you'd implement using the 3.x SDK:

DatabaseReference cities = databaseRef.child("cities")
Query citiesQuery = cities.orderByKey().startAt(input).endAt(input+"\uf8ff");
citiesQuery.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        List<String> cities = new ArrayList<String>();
        for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
            cities.add(postSnapshot.getValue().toString());
        }

By starting at the user input and ending at the last string that starts with the user input, you get all matching items

For relatively short lists of items Ryan's approach will also work fine. But the above Firebase query will filter server-side.

Update

I just ran this code:

    DatabaseReference databaseRef = FirebaseDatabase.getInstance().getReference("39714936");

    String input = "G";

    DatabaseReference cities = databaseRef.child("cities");
    Query citiesQuery = cities.orderByKey().startAt(input).endAt(input + "\uf8ff");
    citiesQuery.addValueEventListener(new ValueEventListener() {
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) {
            List<String> cities = new ArrayList<String>();

            for (DataSnapshot postSnapshot : dataSnapshot.getChildren()) {
                cities.add(postSnapshot.getValue().toString());
            }
            System.out.println(cities);
        }

        @Override
        public void onCancelled(DatabaseError databaseError) {

        }
    });

And it printed:

true

true

So clearly matches two cities.

Feel free to test against my database: https://stackoverflow.firebaseio.com/39714936

Community
  • 1
  • 1
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
3

Try something like this to iterate over the children in the cities snapshot and add all the cities to an ArrayList of Strings.

ArrayList<String> cityList = new ArrayList<>();

databaseRef.child("cities").addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot dataSnapshot) {
        cityList.clear();
        for (DataSnapshot data : dataSnapshot.getChildren()){
            cityList.add(data.getKey);
        }

    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        Log.w(TAG, "getUser:onCancelled", databaseError.toException());
        // ...
    }
});

Editing this paragraph for clarity:

This will get all your cities read into the program memory so you can use that data to display the cities to the user. If the city list changes, so will the data the user sees. If the user is not online, this will not work. This puts a real time, online only listener on the database.

The logic in my mind is something like:

  1. Set a value listener on the text box.
  2. When user types, make a view display all the items in the array list that start with the same substring that was typed.
  3. Handle arrayIndex errors of course.

Hopefully this will get you on the right track. I am sure there are other ways you could implement it but this is what I would personally do. If you need help with the code to display the correct cities, start a chat with me and I can brainstorm with you.

Ryan
  • 1,988
  • 4
  • 21
  • 34
  • I see your point, but I wanted to keep that "autocomplete" feature online, so that I can add or delete cities, without the need to store locally. – Erick Medina Sep 27 '16 at 02:54
  • 1
    It would still be online the way I am doing it. No matter what you do you are going to have to read some data into a variable at some point or your app wont be able to display the city name. It would still support online deletes and if someone deletes a city while another user is typing it would still update the other users list while typing. I think I might be explaining the listener wrong, or maybe I am just misunderstanding the requirements. – Ryan Sep 27 '16 at 03:06
  • I guess your approach is correct. But mi idea was to send Firebase the first letters of a city, and in the Snapshot, get listed only the cities that begin with those letters. – Erick Medina Sep 27 '16 at 03:23
  • 1
    @ErikMedina I think Ryan has given proper solution. You just need to connect this array to your autocomplete and **when you add data online** it will be simply updated in your array list. If you don't understand the concept of listeners then I suggest you to go through firebase docs and try the given examples to get food grasp of basics. – Nishant Dubey Sep 27 '16 at 03:27
  • You could do that but I personally think that adding the event listener would be easier. Especially with such a small list. One problem I see with your approach is minor network blips. If their connection is spotty all those calls with each letter type might be noticeable. Still definitely doable tho. If I wasnt typing on my tiny cell phone right now I would come up with a block of code for your approach lol. Can't handle that on my phone tho. – Ryan Sep 27 '16 at 03:28
  • @ErikMedina if you do what you have said then you will be making api calls at each letter which will result in battery drain and abnormal data usage too for those on tight data plans. – Nishant Dubey Sep 27 '16 at 03:29
  • Yes, I agree with the extra battery and network usage. Every single keystroke would cause a lot of network checks to fire. When the listener would only update when the data changed. This way you only call the API when needed (probably just once considering cities will not likely be added many times in the middle of a user typing. But if they did its ok, this code will take care of that) – Ryan Sep 27 '16 at 03:33
  • I'll implement it this way. Thanks for the help! – Erick Medina Sep 27 '16 at 03:41
  • Not a problem, glad to help! – Ryan Sep 27 '16 at 03:42
0

Of course OrderByValue returns nothing because that's the booleans you have.

you can use the startAt and endAt methods to do so. (The below is Firebase 2.0's Code)

var ref = new Firebase("https://dinosaur-facts.firebaseio.com/dinosaurs");
ref.orderByKey().startAt("b").endAt("b\uf8ff").on("child_added", function(snapshot) {
  console.log(snapshot.key());
});

You can explore more on the Firebase 3 documentation site here.

What Ryan did was right. However, you have to implement startAt on the dataSnapshot to make sure that your "live" search works.

Nicholas
  • 1,883
  • 21
  • 39
  • Which is why this is tagged in Android?? – Nicholas Sep 27 '16 at 02:55
  • I dont think you use startAt with my method because I am mot executing a query, I am assigning a real time listener to listen for any changes to the database. – Ryan Sep 27 '16 at 03:08