Search code examples
androidrx-java2android-livedatarx-androidandroid-jetpack

NullPointerException on turning Flowable into LiveData using LiveDataReactiveStreams


I'm trying to turn a Flowable into a LiveData but I can't make it works:

Flowable: (in repository)

    fun searchMyObjectByName(query: String): Flowable<Array<MyObjectResponse>> {
        return rest.searchMyObjectByName(query)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
    }

LiveData: (in ViewModel)

    private val _myObject = MutableLiveData<ArrayList<MyObject>>()
    val myObject: LiveData<ArrayList<MyObject>>
        get() = _myObject
   fun searchMyObjectByNameLiveData(query: String) {
        _myObject.value = LiveDataReactiveStreams.fromPublisher(repo.searchMyObjectByName(query).map { it -> responseToObject(it) }).value
    }

Observer: (in Fragment@OnCreateView)

// should be empty at first and then restore the value ...
 val resultObserver = Observer<ArrayList<MyObject>> { result -> adapter.replace(result) } //l.46
 model.myObject.observe(viewLifecycleOwner, resultObserver)

the searching is triggered by the user:

model.searchMyObjectByNameLiveData(query)

And the NPE error I get:

 E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.xxx.xxx, PID: 13700
    java.lang.NullPointerException: result must not be null
        at com.xxx.xxx.ui.search.SearchFragment$onCreateView$resultObserver$1.onChanged(SearchFragment.kt:46)
        at com.xxx.xxx.ui.search.SearchFragment$onCreateView$resultObserver$1.onChanged(SearchFragment.kt:26)
        at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
        at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:149)
        at androidx.lifecycle.LiveData.setValue(LiveData.java:307)
        at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
        at com.xxx.xxx.ui.search.SearchViewModel.searchMedicByNameTest2(SearchViewModel.kt:56)
        at com.xxx.xxx.ui.search.SearchFragment.startSearching(SearchFragment.kt:137)
        at com.xxx.xxx.ui.search.SearchFragment$onCreateOptionsMenu$2.onQueryTextChange(SearchFragment.kt:81)
        at androidx.appcompat.widget.SearchView.onTextChanged(SearchView.java:1187)
        at androidx.appcompat.widget.SearchView$10.onTextChanged(SearchView.java:1725)
        at android.widget.TextView.sendOnTextChanged(TextView.java:10631)
        at android.widget.TextView.handleTextChanged(TextView.java:10721)
        at android.widget.TextView$ChangeWatcher.onTextChanged(TextView.java:13477)
        at android.text.SpannableStringBuilder.sendTextChanged(SpannableStringBuilder.java:1267)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:576)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:507)
        at android.text.SpannableStringBuilder.replace(SpannableStringBuilder.java:37)
        at android.view.inputmethod.BaseInputConnection.replaceText(BaseInputConnection.java:869)
        at android.view.inputmethod.BaseInputConnection.setComposingText(BaseInputConnection.java:636)

If I observe directly on the method, it's working:

 val resultObserver = Observer<ArrayList<MyObject>> {result -> adapter.replace(result)} 
 model.searchMyObjectByNameLiveData("query").observe(viewLifecycleOwner, resultObserver)

but this is not what I wan't since I don't have the input of the user at this point.

Thanks for your help.

edit:

Listener in the Fragment

override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.options_menu, menu)
        super.onCreateOptionsMenu(menu, inflater)
        Timber.i("onCreateOptionsMenu")

        searchView = SearchView((context as MainActivity).supportActionBar!!.themedContext)
        menu.findItem(R.id.search).apply {
            setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW or MenuItem.SHOW_AS_ACTION_IF_ROOM)
            actionView = searchView
        }

        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextSubmit(query: String): Boolean {
                return false
            }

            override fun onQueryTextChange(query: String): Boolean {
                when (newText.length) {
                    in 0..2 -> adapter.clear();
                    else -> {
                        model.searchMyObjectByNameLiveData(query)
                    }
                }
                return false
            }
        })
    }

edit #2:

    LINENUMBER 47 L0
    ALOAD 0
    GETFIELD com/xxx/xxx/ui/search/SearchFragment$onCreateView$1.this$0 : Lcom/xxx/xxx/ui/search/SearchFragment;
    INVOKEVIRTUAL com/xxx/xxx/ui/search/SearchFragment.getAdapter ()Lcom/xxx/xxx/adapter/AdapterMedicSearch;
    ALOAD 1
    DUP
    LDC "result"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullExpressionValue (Ljava/lang/Object;Ljava/lang/String;)V
    INVOKEVIRTUAL com/xxx/xxx/adapter/AdapterMedicSearch.replace (Ljava/util/ArrayList;)V
   L1

Solution

  • I would say, the issue is as follows:

    val resultObserver = Observer<ArrayList<MyObject>> { result -> adapter.replace(result) } //l.46
    

    It seems that result is null.

    Just follow the stacktrace from button to top

    java.lang.NullPointerException: result must not be null
        at com.xxx.xxx.ui.search.SearchFragment$onCreateView$resultObserver$1.onChanged(SearchFragment.kt:46)
        at com.xxx.xxx.ui.search.SearchFragment$onCreateView$resultObserver$1.onChanged(SearchFragment.kt:26)
        at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
        at androidx.lifecycle.LiveData.dispatchingValue(LiveData.java:149)
        at androidx.lifecycle.LiveData.setValue(LiveData.java:307)
        at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
        at com.xxx.xxx.ui.search.SearchViewModel.searchMedicByNameTest2(SearchViewModel.kt:56)
    

    It seems that onChange of resultObserver is called, and the lambda throws an NPE, because the result data is null.

    What happens if you add a if != null check?.

    All in all, I would say this is a race-condition, which needs proper sync, but without the full code it will be quite hard to say why the first value is null. A hint that it is a race-condition might be, that when debugging it, it will work, because of changed timing.

    Update: NPE

    I could not reproduce your issue, but when interoping with Java, it might happen, that a NPE happens

    class ReactiveX {
        @Test
        fun nullValue() {
            val mutableLiveData = MutableLiveData<List<MyObject>>()
    
            Handler(Looper.getMainLooper()).post {
                mutableLiveData.value = null
            }
    
            val resultObserver = Observer<List<MyObject>> { result: List<MyObject>? ->
                val returnedVal: List<MyObject> = Adapter.replace(result)
            }
            mutableLiveData.observeForever(resultObserver)
        }
    }
    
    internal data class MyObject(private val s: String)
    
    public class Adapter {
        static List<MyObject> replace(List<MyObject> result) {
            return result;
        }
    }
    

    NPE

    java.lang.NullPointerException: Adapter.replace(result) must not be null
        at com.example.playgroundapp.ReactiveX$nullValue$resultObserver$1.onChanged(ReactiveX.kt:19)
        at com.example.playgroundapp.ReactiveX$nullValue$resultObserver$1.onChanged(ReactiveX.kt:9)
    

    Solution: Probably check for null, before calling #replace.

    Why do I think it is a race-condition?

    Well, I did not expect, that NULL is a valid value. I though when accessing the LiveData it should always return a value (not null)

    @Test
    fun fromCallableNotCalledWithoutObserve() {
        val subscribeActualCalled = AtomicBoolean(false)
        val sync = Flowable.fromCallable {
            subscribeActualCalled.compareAndSet(false, true)
            42
        }
    
        val fromPublisher = LiveDataReactiveStreams.fromPublisher(sync)
        val value = fromPublisher.value
    
        assertThat(value).isNull()
        assertThat(subscribeActualCalled.get()).isFalse()
    }
    

    fromCallableNotCalledWithoutObserve displays, that value will always be null, if you do not call any subscribe method.

    @Test
    fun valueStillNull() {
        val initValue = AtomicInteger(-1)
        val subscribeActualCalled = AtomicBoolean(false)
        val flowable = Flowable.fromCallable {
            subscribeActualCalled.compareAndSet(false, true)
            Thread.sleep(1000)
            42
        }.subscribeOn(Schedulers.io())
    
        val fromPublisher = LiveDataReactiveStreams.fromPublisher(flowable)
        val value = fromPublisher.value
    
        fromPublisher.observeForever {
            initValue.compareAndSet(-1, it ?: 666)
        }
    
        assertThat(value).isNull()
        assertThat(subscribeActualCalled.get()).isTrue()
        assertThat(initValue.get()).isEqualTo(-1)
    }
    

    valueStillNull displays that accessing value for the LiveData without a proper #observe*-method will result in the default-value, in this case null.

    What does this code do?

       fun searchMyObjectByNameLiveData(query: String) {
            _myObject.value = LiveDataReactiveStreams.fromPublisher(repo.searchMyObjectByName(query).map { it -> responseToObject(it) }).value
        }
    

    It actually sets _myObject.value = null, because of

    @Nullable
    public T getValue() {
        Object data = mData;
        if (data != NOT_SET) {
            //noinspection unchecked
            return (T) data;
        }
        return null;
    }
    

    Without a proper observe* on the returned LiveData, there will be no value. Therefore you are setting the LiveData _myObject to null.

     val resultObserver = Observer<ArrayList<MyObject>> { result -> adapter.replace(result) } //l.46
     model.myObject.observe(viewLifecycleOwner, resultObserver)
    

    The resultObserver get called with null and fails.

    Why does this work?

    val resultObserver = Observer<ArrayList<MyObject>> {result -> adapter.replace(result)} 
     model.searchMyObjectByNameLiveData("query").observe(viewLifecycleOwner, resultObserver)
    

    You are subscribing via LiveData#observe* to the Flowable. The value is received and pushed to the resultObserver. Because the value is not null you do not get a NPE.

    Sounds good?