0

I have made this code that could retrieve from realm 50k items and show it in recyclerview:

public class ListAirportFragment
        extends Fragment {
    Realm realm;
    List<AirportR> airports = new ArrayList<>();
    RealmResults<AirportR> airps;
    RecyclerView recyclerView;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_list_airport, container, false);

        RealmConfiguration defaultConfig = new RealmConfiguration.Builder().deleteRealmIfMigrationNeeded().build();
        realm = Realm.getInstance(defaultConfig);

        recyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview);
        SearchView searchView = (SearchView) rootView.findViewById(R.id.searchview);

        final RealmResults<AirportR> airps = realm.where(AirportR.class).findAll();

        airports = realm.copyFromRealm(airps, 0);

        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        final AirportAdapter adapter = new AirportAdapter(airports, getActivity());
        recyclerView.setAdapter(adapter);

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                adapter.getFilter().filter(newText);
                return true;
            }
        });

        return rootView;
    }

    private class AirportAdapter
            extends RecyclerView.Adapter<RecyclerView.ViewHolder>
            implements Filterable {
        private List<AirportR> originalAirports;
        private List<AirportR> listAirports;
        private Context context;
        private AirportFilter filter;
        private boolean isLoading;
        private int visibleThreshold = 5;
        private int lastVisibleItem, totalItemCount;
        private OnLoadMoreListener mOnLoadMoreListener;


        public AirportAdapter(List<AirportR> airports, Context context) {
            this.originalAirports = airports;
            this.listAirports = airports;
            this.context = context;
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.airport_show, parent, false);
            AirportClass holder = new AirportClass(view);
            return holder;
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            AirportR airportR = listAirports.get(position);

            AirportClass mHolder = (AirportClass) holder;

            mHolder.country.setText(airportR.getIsoCountry());
            mHolder.name.setText(airportR.getName());
        }

        @Override
        public int getItemCount() {
            return listAirports.size();
        }

        @Override
        public Filter getFilter() {
            if(filter == null) {
                filter = new AirportFilter(this, originalAirports);
            }
            return filter;
        }

        private class AirportFilter
                extends Filter {
            private final AirportAdapter adapter;

            private final List<AirportR> originalList;

            private final List<AirportR> filteredList;

            private AirportFilter(AirportAdapter adapter, List<AirportR> originalList) {
                super();
                this.adapter = adapter;
                this.originalList = new LinkedList<>(originalList);
                this.filteredList = new ArrayList<>();
            }

            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                filteredList.clear();
                final FilterResults results = new FilterResults();

                if(constraint.length() == 0) {
                    filteredList.addAll(originalList);
                } else {
                    final String filterPattern = constraint.toString().toLowerCase().trim();

                    for(final AirportR airportR : originalList) {
                        if(airportR.getName().contains(filterPattern)) {
                            filteredList.add(airportR);
                        }
                    }
                }
                results.values = filteredList;
                results.count = filteredList.size();
                return results;
            }

            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                adapter.listAirports.clear();
                adapter.listAirports.addAll((ArrayList<AirportR>) results.values);
                adapter.notifyDataSetChanged();
            }
        }

        private class AirportClass
                extends RecyclerView.ViewHolder {
            TextView name, country;
            ImageView image;

            public AirportClass(View itemView) {
                super(itemView);
                name = (TextView) itemView.findViewById(R.id.name);
                country = (TextView) itemView.findViewById(R.id.country);
                image = (ImageView) itemView.findViewById(R.id.imageView);
            }
        }
    }
}

but I have two problems:

  1. the charging of the 50k items is so slow (30 seconds after I go to the fragment with a button) and so I don't know how to make it fast: it's possible to charge only 50 airports for time for example? It's possible it with realm?
  2. the filter is not working correctly, for example if I search Lowell and I digit lo it does not show Lowell, why this bug?

Thank you for answer

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
ste9206
  • 1,822
  • 4
  • 31
  • 47
  • `contains` is case sensitive – Blackbelt Nov 16 '16 at 09:23
  • I thnk you should load only few item(say 10) at a time and then on scrolling load next set of item(10). For this you should use https://guides.codepath.com/android/Endless-Scrolling-with-AdapterViews-and-RecyclerView. May be for this you have to change in your api as well. – Sachin Saxena Nov 16 '16 at 09:23
  • 1
    I haven't used realm so far, but have you tried not using `copyFromRealm`? realm itself should be lazy-loading, but `copyFromRealm` actually seems to *create* the 50K objects. – Thorsten Dittmar Nov 16 '16 at 09:25
  • @Blackbelt what could I use instead of contains? – ste9206 Nov 16 '16 at 09:26
  • @ThorstenDittmar I have to use it because I have some problems with thread – ste9206 Nov 16 '16 at 09:26
  • 2
    No you don't have to use it. Fix your threading issues. – Murat Karagöz Nov 16 '16 at 09:28
  • @MuratK. I've used findAllAsync() but realm gives me this error: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created – ste9206 Nov 16 '16 at 09:38
  • Well if Realm says you shouldn't access the managed Realm object from another thread, then maybe you shouldn't access the managed Realm object from another thread. Copying out 50000 elements instead of fixing your threading issues doesn't actually solve your problem. – EpicPandaForce Nov 16 '16 at 12:28

3 Answers3

3

I have made this code that could retrieve from realm 50k items and show it in recyclerview:

final RealmResults<AirportR> airps = realm.where(AirportR.class).findAll(); airrports = realm.copyFromRealm(airps, 0);

That's because you're copying 50000 objects from your zero-copy database on your UI thread.

Solutions:

1.) don't copy the elements out from the zero-copy database

2.) copy out 50000 elements into memory on a background thread and have fun with the memory usage


Honestly, it's quite obvious that #2 isn't a real solution, so instead, you should follow the practices of how to use Realm's lazy query evaluation feature and managed objects, rather than trying to hack it by calling realm.copyFromRealm()

So this is solution #1 :

public class ListAirportFragment
        extends Fragment {
    Realm realm;
    RealmResults<AirportR> airps;
    RecyclerView recyclerView;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_list_airport, container, false);

        RealmConfiguration defaultConfig = new RealmConfiguration.Builder().deleteRealmIfMigrationNeeded().build();
        realm = Realm.getInstance(defaultConfig);

        recyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview);
        SearchView searchView = (SearchView) rootView.findViewById(R.id.searchview);

        airps = realm.where(AirportR.class).findAll();

        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
        final AirportAdapter adapter = new AirportAdapter(realm, airps, getActivity());
        recyclerView.setAdapter(adapter);

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                return false;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                adapter.getFilter().filter(newText);
                return true;
            }
        });

        return rootView;
    }

    private class AirportAdapter
            extends RecyclerView.Adapter<RecyclerView.ViewHolder>
            implements Filterable {
        private RealmResults<AirPort> listAirports;
        private Context context;
        private Realm realm;

        private final RealmChangeListener realmChangeListener = new RealmChangeListener() {
            @Override
            public void onChange(Object element) {
                notifyDataSetChanged();
            }
        };

        public AirportAdapter(Realm realm, RealmResults<AirportR> airports, Context context) {
            this.realm = realm;
            this.listAirports = airports;
            this.listAirports.addChangeListener(realmChangeListener);
            this.context = context;
        }

        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.airport_show, parent, false);
            AirportClass holder = new AirportClass(view);
            return holder;
        }

        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            AirportR airportR = listAirports.get(position);

            AirportClass mHolder = (AirportClass) holder;

            mHolder.country.setText(airportR.getIsoCountry());
            mHolder.name.setText(airportR.getName());
        }

        @Override
        public int getItemCount() {
            if(listAirports == null || !listAirports.isValid()) {
                return 0;
            }
            return listAirports.size();
        }

        @Override
        public Filter getFilter() {
            if(filter == null) {
                filter = new AirportFilter(this);
            }
            return filter;
        }

        private class AirportFilter
                extends Filter {
            private final AirportAdapter adapter;

            private AirportFilter(AirportAdapter adapter) {
                super();
                this.adapter = adapter;
            }

            @Override
            protected FilterResults performFiltering(CharSequence constraint) {
                final FilterResults results = new FilterResults();
                return results;
            }

            @Override
            protected void publishResults(CharSequence constraint, FilterResults results) {
                if(adapter.listAirports != null && adapter.listAirports.isValid()) {
                    adapter.listAirports.removeChangeListener(adapter.realmChangeListener);
                }
                if(constraint.length() == 0) {
                    adapter.listAirports = adapter.realm.where(AirportR.class).findAll();
                } else {
                    final String filterPattern = constraint.toString().toLowerCase().trim();
                    adapter.listAirports = adapter.realm.where(AirportR.class)
                                                .contains("fieldToQueryBy", filterPattern, Case.INSENSIIVE) // TODO: change field
                                                .findAll();
                }
                adapter.listAirports.addChangeListener(adapter.realmChangeListener);
                adapter.notifyDataSetChanged();
            }
        }

        private class AirportClass
                extends RecyclerView.ViewHolder {
            TextView name, country;
            ImageView image;

            public AirportClass(View itemView) {
                super(itemView);
                name = (TextView) itemView.findViewById(R.id.name);
                country = (TextView) itemView.findViewById(R.id.country);
                image = (ImageView) itemView.findViewById(R.id.imageView);
            }
        }
    }
}

This solution didn't use RealmRecyclerViewAdapter and managed adding/removing change listener manually, but you can also use RealmRecyclerViewAdapter for a better looking solution.

Community
  • 1
  • 1
EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • thank you for your help..I know there is another good solution, using RealmRecyclerViewAdapter...the only problem I've seen is that I have a problem with filtering – ste9206 Nov 16 '16 at 12:39
  • You should execute the filtering of the RealmResults in `publishResults()` instead of `performFiltering()`, that way you can replace the dataset on the UI thread without copying them out directly from the result set. – EpicPandaForce Nov 16 '16 at 12:41
  • This solution I gave you doesn't use `RealmRecyclerViewAdapter`, for example, but it also does what that would give you. – EpicPandaForce Nov 16 '16 at 12:43
  • I know it, I've made this question: http://stackoverflow.com/questions/40630322/how-to-implement-filterable-in-realmrecyclerviewadapter , now I'll try to change and edit it to give this code works – ste9206 Nov 16 '16 at 12:45
  • Also answered this at http://stackoverflow.com/a/40632613/2413303 using `RealmRecyclerViewAdapter` – EpicPandaForce Nov 16 '16 at 13:00
2

With this many items in a list do you really think anybody is actually going to scroll through it? I mean, are you sure a list of items is necessary here or would just a search bar be more efficient?

If you want to keep your set up though I would suggest 3 improvements:

  1. Don't load all available data at once. Add for example 50 objects to the adapter and then when you pass scroll position 30 or so on your RecyclerView add the next 50 results to your adapter. Something along the lines of the code snippet I posted below. (To listen to the scroll position look at this question: How to implement endless list with RecyclerView?) EpicPandaForce's answer addresses this problem better

  2. Don't let your searchbar filter the populated adapter. Use the fact that RealmResults are auto-updating views on the database so you can actually execute a new transaction on it to filter the data using realm without re-fetching the data. (See Realm docs and api reference)

  3. Do all loading and searching on a separate thread so to not block the main thread with it. You could for example use rx Observables.

This should fix your performance problems and you don't have to implement the search yourself, but can instead rely on Realms optimized performance.

Code snippet for 1.: Only left for the record. EpicPandaForce's answer addresses this in a better way

private void addToAdapter() {
        final int maxElements = 50;
        final int startPosition = adapter.getItemCount();
        int endPosition = startPosition + maxElements();
        if (endPosition > realmResults.size()) {
            endPosition = realmResults.size() - 1;
        }

        // there might be better ways to do it. Pretty new to Realm
        for (int i = startPosition; i < endPosition; i++) {
            /* for this your adapter has to hold a List as a field 
               and expose a public add method that appends to it*/
            adapter.add(realmResults.get(i));
        }

        adapter.notifyItemRangeInserted(startPosition, endPosition);
    } 
Community
  • 1
  • 1
Daniel W.
  • 623
  • 6
  • 14
  • Disagree with point 2). Querying the database once again will not improve your performance whatsoever if you already have it in a list. You can use a second filtered list to put in the queried results. – Murat Karagöz Nov 16 '16 at 09:33
  • Iterating a list of 50k elements and doing comparisons on it will definitely be slower than using a DB query to get a limited result set. However I've not used Realm myself extensively and don't know the actual performance benefits. Just basing it off of the top answer in this question http://softwareengineering.stackexchange.com/questions/171024/never-do-in-code-what-you-can-get-the-sql-server-to-do-well-for-you-is-this/171033 which to be fair is about SQL – Daniel W. Nov 16 '16 at 09:42
  • @DanielW. 1.) so I have to do a query every time the user scrolls? – ste9206 Nov 16 '16 at 10:03
  • Actually I will update my answer to suggest something probably a bit better. You can use the `RealmResults::get(int position)` method together with a for-loop to get a number of results and add them to the list adapter then use the length of the list adapter as the next start index for the loop when you retrieve a new number of results on scroll – Daniel W. Nov 16 '16 at 10:20
  • 1
    @ste9206 but it is important that you drop the `copyFromRealm` this is where probably a lot of the performance issue lies – Daniel W. Nov 16 '16 at 10:48
0

You should return results from another thread. If you have 50k.. is to much for main thread to process. You should use a lazy loading for 50 elements. And filter search is case sensitive. Make both lowercase.

Cătălin Florescu
  • 5,012
  • 1
  • 25
  • 36