1

I have implemented SearchableSpinner to my project. It's inside an Fragment.

I'm using Realm as the database. In my onCreateView method I have this...

@Override
public View onCreateView(LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.ncrdocument, container, false);
    realm = Realm.getDefaultInstance();
    documents = new ArrayList<>();
    documents = realm.where(MaterialDoc.class).findAll();
    ArrayAdapter<MaterialDoc> adapter = new ArrayAdapter<>(this.getContext(), android.R.layout.simple_list_item_1, documents);
    matList.setAdapter(adapter);
.
.
.
return view;

The data loads fine, it shows them correctly, but when I try to search the spinner, my app crashes and I get this error.

An exception occured during performFiltering()
java.lang.IllegalStateException: Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created
at io.realm.BaseRealm.checkIfValid(BaseRealm.java:383)
at io.realm.MaterialDocRealmProxy.realmGet$document_number(MaterialDocRealmProxy.java:126)
at com.my.application.test.Model.MaterialDoc.getDocumentNumber(MaterialDoc.java:29)
at com.my.application.test.Model.MaterialDoc.toString(MaterialDoc.java:42)
at android.widget.ArrayAdapter$ArrayFilter.performFiltering(ArrayAdapter.java:480)
at android.widget.Filter$RequestHandler.handleMessage(Filter.java:234)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:145)
at android.os.HandlerThread.run(HandlerThread.java:61)
01-09 12:13:06.649 18606-18606/com.my.application.test D/AndroidRuntime: Shutting down VM
01-09 12:13:06.669 18606-18606/com.my.application.test E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.my.application.test, PID: 18606
java.lang.NullPointerException: Attempt to invoke interface method 'int java.util.List.size()' on a null object reference
at android.widget.ArrayAdapter.getCount(ArrayAdapter.java:330)
at android.widget.AdapterView.checkFocus(AdapterView.java:947)
at android.widget.AdapterView$AdapterDataSetObserver.onInvalidated(AdapterView.java:1070)
at android.widget.AbsListView$AdapterDataSetObserver.onInvalidated(AbsListView.java:8297)
at android.database.DataSetObservable.notifyInvalidated(DataSetObservable.java:50)
at android.widget.BaseAdapter.notifyDataSetInvalidated(BaseAdapter.java:59)
at android.widget.ArrayAdapter$ArrayFilter.publishResults(ArrayAdapter.java:513)
at android.widget.Filter$ResultsHandler.handleMessage(Filter.java:282)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:145)
at android.app.ActivityThread.main(ActivityThread.java:6939)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1404)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1199)
Nongthonbam Tonthoi
  • 12,667
  • 7
  • 37
  • 64
sharp
  • 1,191
  • 14
  • 39

2 Answers2

7

The accepted answer by @SarathKn is not efficient at all, you copy/detach the results from the Realm and you lose auto-updates and lazy evaluation, you copy out the entire result set immediately.

The usage of results.copyFromRealm() is frown upon except when you actually need an unmanaged object, which is typically ONLY if you need to send the object through GSON, or if you need to modify the object in an undoable manner (save/cancel changes).

Using realm.copyFromRealm() to "avoid illegal thread access" is a wrong answer (unless you are 100% sure that that is what you are looking for, and you know what you are doing).


The actual problem instead of said hack-fix is that you are using ArrayAdapter's default Filter implementation that executes the filtering on the background thread, while you actually need to filter the RealmResults on the UI thread (because it's a result set that you are showing on the UI thread).

If you check the source code for ArrayAdapter's Filterable implementation, it's the following:

@Override
public @NonNull Filter getFilter() {
    if (mFilter == null) {
        mFilter = new ArrayFilter();
    }
    return mFilter;
}

/**
 * <p>An array filter constrains the content of the array adapter with
 * a prefix. Each item that does not start with the supplied prefix
 * is removed from the list.</p>
 */
private class ArrayFilter extends Filter {
    @Override
    protected FilterResults performFiltering(CharSequence prefix) {
        final FilterResults results = new FilterResults();

        if (mOriginalValues == null) {
            synchronized (mLock) {
                mOriginalValues = new ArrayList<>(mObjects);
            }
        }

        if (prefix == null || prefix.length() == 0) {
            final ArrayList<T> list;
            synchronized (mLock) {
                list = new ArrayList<>(mOriginalValues);
            }
            results.values = list;
            results.count = list.size();
        } else {
            final String prefixString = prefix.toString().toLowerCase();

            final ArrayList<T> values;
            synchronized (mLock) {
                values = new ArrayList<>(mOriginalValues);
            }

            final int count = values.size();
            final ArrayList<T> newValues = new ArrayList<>();

            for (int i = 0; i < count; i++) {
                final T value = values.get(i);
                final String valueText = value.toString().toLowerCase();

                // First match against the whole, non-splitted value
                if (valueText.startsWith(prefixString)) {
                    newValues.add(value);
                } else {
                    final String[] words = valueText.split(" ");
                    for (String word : words) {
                        if (word.startsWith(prefixString)) {
                            newValues.add(value);
                            break;
                        }
                    }
                }
            }

            results.values = newValues;
            results.count = newValues.size();
        }

        return results;
    }

    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
        //noinspection unchecked
        mObjects = (List<T>) results.values;
        if (results.count > 0) {
            notifyDataSetChanged();
        } else {
            notifyDataSetInvalidated();
        }
    }
}

But performFiltering is executed on a background thread and publishResults is executed on the UI thread.

In reality, you should replace this filter implementation with a Realm filter that is executed on the UI thread.

@Override
public @NonNull Filter getFilter() {
    if (mFilter == null) {
        mFilter = new RealmFilter();
    }
    return mFilter;
}

/**
 * <p>An array filter constrains the content of the array adapter with
 * a prefix. Each item that does not start with the supplied prefix
 * is removed from the list.</p>
 */
private class RealmFilter extends Filter {
    @Override
    protected FilterResults performFiltering(CharSequence prefix) {
        return new FilterResults();
    }

    @Override
    protected void publishResults(CharSequence constraint, FilterResults results) {
        String prefix = constraint.toString().trim();
        //noinspection unchecked
        RealmQuery<MaterialDoc> query = realm.where(MaterialDoc.class);
        if(prefix == null || "".equals(prefix)) {
            /* do nothing */
        } else {
            if(prefix.contains(" ")) {
                String[] words = prefix.split("\\s");
                boolean isFirst = true;
                for(String word : words) {
                    if(!"".equals(word)) {
                        if(isFirst) {
                            isFirst = false;
                        } else {
                            query = query.or();
                        }
                        query = query.beginsWith(/*enter query field name*/, word, Case.INSENSITIVE);
                    }
                }
            } else {
                query = query.beginsWith(/* enter query field name*/, prefix, Case.INSENSITIVE);
            }
        }
        mObjects = query.findAll();
        notifyDataSetChanged();
    }
}

But I've answered this here (and for RecyclerView here) before by linking to this Filter solution that isn't conceptually wrong from Realm usage perspective.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
0

You have to be careful when you deal with realmresults across threads. The realm results from one thread you can't use in some other. So try to get a copy of your result set (Which is detached from active realm instance) and give this copy to your adapter. Realm has copyFromRealm method for doing this.

Modify your code like this

    Realm realm = Realm.getDefaultInstance();
    RealmResults<MaterialDoc> realmResults = realm.where(MaterialDoc.class).findAll();
    List<MaterialDoc> documents = realm.copyFromRealm(realmResults);
    ArrayAdapter<MaterialDoc> adapter = new ArrayAdapter<>(this.getContext(), android.R.layout.simple_list_item_1, documents);
    matList.setAdapter(adapter);
Sarath Kn
  • 2,680
  • 19
  • 24