Skip to content

Commit

Permalink
Merge pull request #71 from isaac-udy/viewmodel-results-lifecycle-bug
Browse files Browse the repository at this point in the history
Revert to using `addObserver` instead of `launchWhenCreated` in `LazyEnroResultChannel`
  • Loading branch information
isaac-udy-xero authored Mar 14, 2022
2 parents 8e1e744 + 1c2974e commit abda10b
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 9 deletions.
5 changes: 5 additions & 0 deletions enro-core/src/main/java/dev/enro/core/EnroExceptions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.enro.core

abstract class EnroException(message: String) : IllegalStateException(message)

class EnroLifecycleException(message: String) : EnroException(message)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package dev.enro.core.result.internal

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.*
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import dev.enro.core.EnroLifecycleException
import dev.enro.core.NavigationHandle
import dev.enro.core.getNavigationHandle
import dev.enro.core.result.EnroResultChannel
Expand All @@ -29,17 +32,22 @@ internal class LazyResultChannelProperty<T>(
val lifecycleOwner = owner as LifecycleOwner
val lifecycle = lifecycleOwner.lifecycle

lifecycle.coroutineScope.launchWhenCreated {
resultChannel = ResultChannelImpl(
navigationHandle = handle.value,
resultType = resultType,
onResult = onResult
).managedByLifecycle(lifecycle)
}
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event != Lifecycle.Event.ON_CREATE) return;
resultChannel = ResultChannelImpl(
navigationHandle = handle.value,
resultType = resultType,
onResult = onResult
).managedByLifecycle(lifecycle)
}
})
}

override fun getValue(
thisRef: Any,
property: KProperty<*>
): EnroResultChannel<T> = resultChannel!!
): EnroResultChannel<T> = resultChannel ?: throw EnroLifecycleException(
"LazyResultChannelProperty's EnroResultChannel is not initialised. Are you attempting to use the result channel before the result channel's lifecycle owner has entered the CREATED state?"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.enro.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dev.enro.core.EnroException
import dev.enro.core.NavigationHandle

@PublishedApi
Expand All @@ -15,6 +16,7 @@ internal class EnroViewModelFactory(
val viewModel = try {
delegate.create(modelClass) as T
} catch (ex: RuntimeException) {
if(ex is EnroException) throw ex
throw RuntimeException("Failed to created ${modelClass.name} using factory ${delegate::class.java.name}.\n" +
"This can occur if you are using an @HiltViewModel annotated ViewModel, but are not requesting the ViewModel from inside an @AndroidEntryPoint annotated Activity/Fragment.", ex)
}
Expand Down
120 changes: 120 additions & 0 deletions enro/src/androidTest/java/dev/enro/result/ViewModelResultTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package dev.enro.result

import android.os.Bundle
import android.view.View
import androidx.lifecycle.ViewModel
import androidx.test.core.app.ActivityScenario
import dev.enro.*
import dev.enro.annotations.NavigationDestination
import dev.enro.core.NavigationKey
import dev.enro.core.forward
import dev.enro.core.result.closeWithResult
import dev.enro.core.result.registerForNavigationResult
import dev.enro.expectFragment
import dev.enro.viewmodel.enroViewModels
import dev.enro.viewmodel.navigationHandle
import kotlinx.parcelize.Parcelize
import org.junit.Assert.assertEquals
import org.junit.Test

class ViewModelResultTests {
@Test
fun givenOrchestratedResultFlowManagedByViewModels_whenOrchestratedResultFlowExecutes_thenResultsAreReceivedCorrectly() {
ActivityScenario.launch(DefaultActivity::class.java)
.getNavigationHandle<NavigationKey>()
.forward(OrchestratorKey())

expectFragment<OrchestratorFragment>()
.viewModel
.let {
assertEquals("FirstStep -> SecondStep(SecondStepNested)", it.currentResult)
}
}
}


@Parcelize
class OrchestratorKey : NavigationKey

class OrchestratorViewModel : ViewModel() {
var currentResult = ""

val navigation by navigationHandle<NavigationKey>()
val resultOne by registerForNavigationResult<String>(navigation) {
currentResult = it
resultTwo.open(SecondStepKey())
}
val resultTwo by registerForNavigationResult<String>(navigation) {
currentResult = "$currentResult -> $it"
}

init {
resultOne.open(FirstStepKey())
}
}

@NavigationDestination(OrchestratorKey::class)
class OrchestratorFragment : TestFragment() {
val viewModel by enroViewModels<OrchestratorViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.hashCode()
}
}

@Parcelize
class FirstStepKey : NavigationKey.WithResult<String>

class FirstStepViewModel : ViewModel() {
private val navigation by navigationHandle<FirstStepKey>()
init {
navigation.closeWithResult("FirstStep")
}
}

@NavigationDestination(FirstStepKey::class)
class FirstStepFragment : TestFragment() {
private val viewModel by enroViewModels<FirstStepViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.hashCode()
}
}

@Parcelize
class SecondStepKey : NavigationKey.WithResult<String>

class SecondStepViewModel : ViewModel() {
private val navigation by navigationHandle<SecondStepKey>()
private val nested by registerForNavigationResult<String>(navigation) {
navigation.closeWithResult("SecondStep($it)")
}
init {
nested.open(SecondStepNestedKey())
}
}

@NavigationDestination(SecondStepKey::class)
class SecondStepFragment : TestFragment() {
private val viewModel by enroViewModels<SecondStepViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.hashCode()
}
}


@Parcelize
class SecondStepNestedKey : NavigationKey.WithResult<String>

class SecondStepNestedViewModel : ViewModel() {
private val navigation by navigationHandle<SecondStepNestedKey>()
init {
navigation.closeWithResult("SecondStepNested")
}
}

@NavigationDestination(SecondStepNestedKey::class)
class SecondStepNestedFragment : TestFragment() {
private val viewModel by enroViewModels<SecondStepNestedViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.hashCode()
}
}

0 comments on commit abda10b

Please sign in to comment.