MutableStateFlow<RealmUUID?> throws exception when trying to emit null second time.
#Test
fun mutableStateFlow_null_test() {
val flow = MutableStateFlow<RealmUUID?>(null)
runBlocking {
this.launch {
flow.collect {
println(it)
}
}
for (i in 0 .. 10) {
flow.tryEmit(null)
delay(100)
flow.tryEmit(RealmUUID.random())
delay(100)
}
}
}
It throws
class kotlinx.coroutines.internal.Symbol cannot be cast to class io.realm.kotlin.types.RealmUUID (kotlinx.coroutines.internal.Symbol and io.realm.kotlin.types.RealmUUID are in unnamed module of loader 'app')
java.lang.ClassCastException: class kotlinx.coroutines.internal.Symbol cannot be cast to class io.realm.kotlin.types.RealmUUID (kotlinx.coroutines.internal.Symbol and io.realm.kotlin.types.RealmUUID are in unnamed module of loader 'app')
at io.realm.kotlin.internal.RealmUUIDImpl.equals(RealmUUIDImpl.kt:61)
at kotlin.jvm.internal.Intrinsics.areEqual(Intrinsics.java:167)
at kotlinx.coroutines.flow.StateFlowImpl.updateState(StateFlow.kt:329)
at kotlinx.coroutines.flow.StateFlowImpl.setValue(StateFlow.kt:318)
at kotlinx.coroutines.flow.StateFlowImpl.tryEmit(StateFlow.kt:370)
at com.test.app.ExampleUnitTest$mutableStateFlow_null_test$1.invokeSuspend(ExampleUnitTest.kt:39)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at
This only happens with MutableStateFlow<RealmUUID?> but works fine with any primitives or data class or even with data class TestData(val realmUUID : RealmUUID?) and MutableStateFlow<TestData?>(null)
Related
I have a method to fetch devices using coroutines and use viewModelScope.launch to run coroutines. I want to catch errors using CoroutineExceptionHandler, but I get the error:
E/[Koin]: Instance creation error : could not create instance for [Factory:'com.test.presentation.viewModel.ActivateDeviceViewModel']: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlin.jvm.internal.Intrinsics.checkNotNullParameter, parameter context
kotlin.coroutines.CombinedContext.plus(Unknown Source:2)
kotlinx.coroutines.CoroutineContextKt.newCoroutineContext(CoroutineContext.kt:33)
kotlinx.coroutines.BuildersKt__Builders_commonKt.launch(Builders.common.kt:52)
kotlinx.coroutines.BuildersKt.launch(Unknown Source:1)
kotlinx.coroutines.BuildersKt__Builders_commonKt.launch$default(Builders.common.kt:47)
kotlinx.coroutines.BuildersKt.launch$default(Unknown Source:1)
com.test.presentation.viewModel.ActivateDeviceViewModel.fetchDevices(ActivateDeviceViewModel.kt:42)
com.test.presentation.viewModel.ActivateDeviceViewModel.<init>(ActivateDeviceViewModel.kt:29)
com.test.di.ModulesKt$viewModelModule$1$2.invoke(Modules.kt:63)
com.test.di.ModulesKt$viewModelModule$1$2.invoke(Modules.kt:63)
org.koin.core.instance.InstanceFactory.create(InstanceFactory.kt:51)
org.koin.core.instance.FactoryInstanceFactory.get(FactoryInstanceFactory.kt:36)
org.koin.core.registry.InstanceRegistry.resolveInstance$koin_core(InstanceRegistry.kt:103)
org.koin.core.scope.Scope.resolveInstance(Scope.kt:236)
org.koin.core.scope.Scope.access$resolveInstance(Scope.kt:34)
org.koin.core.scope.Scope$get$1.invoke(Scope.kt:199)
org.koin.core.time.MeasureKt.measureDurationForResult(Measure.kt:75)
org.koin.core.scope.Scope.get(Scope.kt:198)
com.test.presentation.fragment.ActivateDeviceFragment$special$$inlined$inject$default$1.invoke(ComponentCallbackExt.kt:69)
kotlin.SynchronizedLazyImpl.getValue(LazyJVM.kt:74)
com.test.presentation.fragment.ActivateDeviceFragment.getViewModel(ActivateDeviceFragment.kt:39)
com.test.presentation.fragment.ActivateDeviceFragment.access$getViewModel(ActivateDeviceFragment.kt:37)
com.test.presentation.fragment.ActivateDeviceFragment$handleActivateDeviceResult$1$1.invokeSuspend(ActivateDeviceFragment.kt:51)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
androidx.lifecycle.DispatchQueue.drainQueue(DispatchQueue.kt:75)
androidx.lifecycle.DispatchQueue.resume(DispatchQueue.kt:54)
androidx.lifecycle.LifecycleController$observer$1.onStateChanged(LifecycleController.kt:40)
androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
androidx.lifecycle.LifecycleRegistry.forwardPass(LifecycleRegistry.java:265)
androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:307)
androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:148)
androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:134)
androidx.fragment.app.Fragment.performStart(Fragment.java:3024)
androidx.fragment.app.FragmentStateManager.start(FragmentStateManager.java:568)
androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:277)
androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1327)
androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2757)
androidx.fragment.app.FragmentManager.dispatchStart(FragmentManager.java:2707)
androidx.fragment.app.Fragment.performStart(Fragment.java:3028)
androidx.fragment.app.FragmentStateManager.start(FragmentStateManager.java:568)
androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:277)
androidx.fragment.app.FragmentStore.moveToExpectedState(FragmentStore.java:113)
androidx.fragment.app.FragmentManager.moveToState(FragmentManager.java:1327)
androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:2757)
androidx.fragm.
Please tell me what am I doing wrong and how to fix it? Thanks
fragment
class ActivateDeviceFragment : Fragment(R.layout.fragment_activate_device) {
private val viewModel: ActivateDeviceViewModel by inject()
// ....
}
view model
class ActivateDeviceViewModel constructor(
private val activateDeviceUseCase: ActivateDeviceUseCase,
) : ViewModel() {
init {
fetchDevices()
}
private val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
Timber.tag("test").d(throwable)
}
private fun fetchDevices() {
viewModelScope.launch(exceptionHandler) {
}
}
}
koin modules
val useCaseModule = module {
single { ActivateDeviceUseCase() }
}
val viewModelModule = module {
viewModel { ActivateDeviceViewModel(get() }
}
It is not related to Koin.
The log shows that the exceptionHandler in viewModelScope.launch(exceptionHandler) is actually null, and which throws NullPointerException.
It is because you trigger the fetchDevices() inside init{}, in which the instantiation of exceptionHandler is not ready (means it is null).
Therefore, you should not call fetchDevices() inside init{}. For example, you could call this in your Fragment onViewCreated(){ viewModel.fetchDevices } instead. It will be fine.
I've got the following ViewModel:
class FundamentalsViewModel: ViewModel() {
var fundamentalsLiveData = MutableLiveData<WrappedResult<DataResponse>>()
private val repository = FundamentalsRespository()
private var job: Job? = null
fun getData(type: String) {
if(job == null || job?.isActive == false) {
fundamentalsLiveData.value = WrappedResult.Loading
job = viewModelScope.launch(Dispatchers.IO) {
try {
val response = repository.getData(type)
withContext(Dispatchers.Main) {
fundamentalsLiveData.value = WrappedResult.Success(response)
}
} catch(e: Exception) {
withContext(Dispatchers.Main) {
fundamentalsLiveData.value = WrappedResult.Failure(e)
}
}
}
}
}
}
Everything works perfectly in-house, but out in the field I'm getting Crashylitics reports that say this:
"Fatal Exception: java.lang.NullPointerException
Parameter specified as non-null is null: method kotlin.j0.d.u.p, parameter symbol"
on the line that is:
fundamentalsLiveData.value = WrappedResult.Loading
There is no other information in the crash log. How is it possible that there is any NPE here? The WrappedResult is a typical Kotlin sealed class that looks like this:
sealed class WrappedResult<out T> {
data class Success<out T: Any>(val data:T) : WrappedResult<T>()
data class Failure(val error: Throwable) : WrappedResult<Nothing>()
data class CallFailure(val error: String) : WrappedResult<Nothing>()
object Loading : WrappedResult<Nothing>()
}
I found the issue and it had to do with the fragment having setRetainInstance(false). If the activity was killed on a task out, this happened coming back in. I haven't yet been able to fully understand how why this would happen since the view model was created with the fragment's scope, but changing to setRetainInstance(true) stopped the crash.
So i'm trying to implement MVI pattern in android with RxJava, but i want to handle the thrown error in a state, together with success and loading, is there anyway to handle the error not from subscribe(onError = xxx)
PROCESS
sealed class AuthResult : MviResult {
sealed class LoadUserResult : AuthResult() {
object Loading : LoadUserResult()
data class Success(val user: User) : LoadUserResult()
data class Fail(val error: Throwable) : LoadUserResult()
}
}
private val loadUser =
ObservableTransformer<LoadUserAction, LoadUserResult> { actions ->
actions.flatMap {
userManager.getCurrentUser()
.map<LoadUserResult> { LoadUserResult.Success(it) }
.onErrorReturn(LoadUserResult::Fail) // HERE? // EDIT FOR THE ANSWER: REMOVE THIS
.subscribeOn(schedulerProvider.io())
.observeOn(schedulerProvider.ui())
.startWith(LoadUserResult.Loading)
}.onErrorReturn(LoadUserResult::Fail) // ANSWER: ADD THIS TO CATCH API ERROR
}
var actionProcess =
ObservableTransformer<AuthAction, AuthResult> { actions ->
actions.publish { s->
Observable.merge(
s.ofType(LoadUserAction::class.java).compose(loadUser),
s.ofType(SignInWithGoogleAction::class.java).compose(signInWithGoogle)
)
}
}
VIEWMODEL
fun combine(): Observable<AuthViewState> {
return _intents
.map(this::actionFromIntent)
.compose(actionProcess)
.scan(AuthViewState.idle(), reducer)
.distinctUntilChanged()
.replay(1)
.autoConnect(0)
}
FRAGMENT
disposable.add(viewModel.combine().subscribe(this::response))
private fun response(state: AuthViewState) {
val user = state.user
if (user.uid.isBlank() && user.email.isBlank() && user.username.isBlank()) {
Timber.i("user: $user")
} else {
Timber.i("user: $user")
Toast.makeText(requireContext(), "Will navigate to MainActivity", Toast.LENGTH_SHORT)
.show()
}
// HANDLE THE ERROR HERE?
if (state.error != null) {
Toast.makeText(requireContext(), "Error fetching user", Toast.LENGTH_SHORT).show()
Timber.e("Error loading user ${state.error.localizedMessage}")
}
}
THE ERROR i got was
2020-06-03 22:42:15.073 25060-25060/com.xxx W/System.err: io.reactivex.exceptions.OnErrorNotImplementedException: The exception was not handled due to missing onError handler in the subscribe() method call. Further reading: https://github.com/ReactiveX/RxJava/wiki/Error-Handling | com.google.android.gms.tasks.RuntimeExecutionException: com.google.android.gms.common.api.ApiException: 10:
The error you're receiving here is due to you calling .subscribe() in your Fragment. That variant of the .subscribe() (the one that accepts only one parameter -- the onNext consumer callback) will only notify the consumer when the stream successfully emits an item (in this case, AuthViewState). However, when you observable stream encounters an error, RxJava doesn't have a good way to handle it, since an error callback was not provided in .subscribe(). Therefore, it throws the error you've encountered above.
NOTE: RxJava has many overloads of Observable.subscribe(), some of which accept a consumer callback for error handling.
However, if your goal is to have the Observable always successfully emit an AuthViewState, even if an error was encountered, you could make use of Observable.onErrorReturn() (or a similar error handling function provided by RxJava). An example usage of that would be:
sealed class ViewState {
object Loading : ViewState()
data class Success(val username: String) : ViewState()
data class Error(val error: Throwable) : ViewState()
}
class UserProfileViewModel(
private val userService: UserService
) {
fun getViewState(): Observable<ViewState> {
return Observable
.merge(
Observable.just(ViewState.Loading),
userService
.getUserFromApi()
.map { user -> ViewState.Success(user.username) }
)
.onErrorReturn { error -> ViewState.Error(error) }
}
}
I am trying to retrieve data from firebase with await(),
when I am trying to do this without a result wrapper the code works , but the same thing crashes with:
"java.lang.IllegalStateException: Task is not yet complete"
The next code crash
suspend fun isUserRegisteredOnServer(): Result<Exception, Boolean> =
try {
val result = userRef.get().await().exists()
Result.build { result }
}
catch (e : Exception) {
Result.Error(e)
}
The following doesn't
suspend fun tempIsRegistered() : Boolean
{
return userRef.get().await().exists()
}
Result class:
sealed class Result <out E,out V > {
data class Value<out V>(val value : V) : Result<Nothing, V>()
data class Error<out E>(val error : E) : Result<E, Nothing>()
companion object Factory
{
inline fun <V> build(function : () -> V): Result<Exception, V> =
try {
Value(function.invoke())
}catch (e: Exception) {
Error(e)
}
}
}
It's also worth noting I am calling theses functions from
CoroutineScope(IO).launch {}
Stack trace:
2019-09-05 18:50:54.121 23507-23561/E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: , PID: 23507
java.lang.IllegalStateException: Task is not yet complete
at com.google.android.gms.common.internal.Preconditions.checkState(Unknown Source:29)
at com.google.android.gms.tasks.zzu.zzb(Unknown Source:121)
at com.google.android.gms.tasks.zzu.getResult(Unknown Source:12)
at firebase.database.FirestoreDatabaseRepository.fetchUserLists(FirestoreDatabaseRepository.kt:79)
at com.mainfragment.MainFragmentViewModel$handleEvent$1.invokeSuspend(MainFragmentViewModel.kt:74)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
Go ahead and use the Tasks API:
try {
val result = Tasks.await(userRef.get(), 2, TimeUnit.SECONDS)
Result.build { result }
}
catch (e : Exception) {
Result.Error(e)
}
I'm trying to experiment a proper way to convert a complete task into a sealed class easy to read when performing a get request on a document (at this time and I will see later for collections request).
import com.google.android.gms.tasks.Task
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestoreException
import timber.log.Timber
fun <T> Task<DocumentSnapshot?>.toDocumentResult(parser: (documentSnapshotExisting: DocumentSnapshot) -> T): DocumentResult<T>?{
val documentResult: DocumentResult<T> = if(isSuccessful){
val documentSnapshot: DocumentSnapshot = result!!
if(documentSnapshot.exists()){
try {
DocumentResult.Found(parser.invoke(documentSnapshot))
}
catch (e: java.lang.Exception){
DocumentResult.ParserException<T>(documentId = documentSnapshot.id, e = e)
}
}else{
DocumentResult.NotFound(documentSnapshot.id)
}
}else{
DocumentResult.Error(exception!! as FirebaseFirestoreException)
}
documentResult.log()
return documentResult
}
sealed class DocumentResult<T>{
abstract fun log()
class Found<T>(val o: T): DocumentResult<T>() {
override fun log() {
Timber.tag("DocumentResult").w("$o")
}
}
class NotFound<T>(val documentId: String): DocumentResult<T>() {
override fun log() {
Timber.tag("DocumentResult").w("documentId: $documentId doesn't exist")
}
}
class ParserException<T>(val documentId: String, val e: Exception): DocumentResult<T>() {
override fun log() {
Timber.tag("DocumentResult").e("ParserException: ${e.localizedMessage?:e.message?:"error"}, documentId: $documentId")
}
}
class Error<T>(val e: FirebaseFirestoreException): DocumentResult<T>() {
override fun log() {
Timber.tag("DocumentResult").e("FirebaseFirestoreException - code: ${e.code.name}, ${e.localizedMessage?:e.message?:"error"}")
}
}
}
With this snippet, I can do this :
activity.firestore.documentAvailableLanguages().get().addOnCompleteListener { task ->
val documentResult = task.toDocumentResult { AvailableLanguages.toObject(it) }
when(documentResult){
is DocumentResult.Found -> { /* My converted object */ }
is DocumentResult.NotFound -> { /* document not found */}
is DocumentResult.Error-> { /* FirebaseFirestoreException */}
is DocumentResult.ParserException -> { /* Conversion didn't work, exception */ }
}
}
My question is :
1) Can we reasonably ensure that Task.exception is always not null and instance of FirebaseFirestoreException when isSuccessFul is false ?
2) Are we sure that task.result is always not null when task.isSuccessful is true ?
Thanks in advance
For both questions, please note that
a Task is "successful" when the work represented by the task is finished as expected, with no errors. On the orter side, a Task is "complete" when the work represented by the Task is finished, regardless of its "success" or "failure". There may be or may or may be not an error, you'll have to check for that.
An already successfully completed Task returns a DocumentSnapshot which will never have the value of null. If the requested document does not exist, you'll get an empty DocumentSnapshot object not null. This also means that if you'll call exists():
documentSnapshot.exists() //Will returns false
And if are calling getData() method:
documentSnapshot.getData() //An exception will be thrown
If the Taks is not "successful", the Exception that is trown by task.getException() is an instanceof FirebaseFirestoreException. Please note that Task's getException() method:
Returns the exception that caused the Task to fail. Returns null if the Task is not yet complete, or completed successfully.