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
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.
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.
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
.
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.
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?