From 6db71d4bd80764a86e0d52047aba3aecf7e1e5b4 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Sat, 22 Jun 2019 23:53:17 -0700 Subject: [PATCH 1/5] Added parentFragmentViewModel() --- .../kotlin/com/airbnb/mvrx/MvRxExtensions.kt | 36 +++++++++++- .../com/airbnb/mvrx/ViewSubscriberTest.kt | 57 +++++++++++++++++++ .../com/airbnb/mvrx/sample/MainFragment.kt | 7 +++ .../features/parentfragment/ChildFragment.kt | 30 ++++++++++ .../features/parentfragment/ParentFragment.kt | 44 ++++++++++++++ sample/src/main/res/layout/fragment_child.xml | 12 ++++ .../src/main/res/layout/fragment_parent.xml | 25 ++++++++ sample/src/main/res/navigation/nav_graph.xml | 7 +++ 8 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 sample/src/main/java/com/airbnb/mvrx/sample/features/parentfragment/ChildFragment.kt create mode 100644 sample/src/main/java/com/airbnb/mvrx/sample/features/parentfragment/ParentFragment.kt create mode 100644 sample/src/main/res/layout/fragment_child.xml create mode 100644 sample/src/main/res/layout/fragment_parent.xml diff --git a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt index dc43a4863..caaa4a9cb 100644 --- a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt +++ b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt @@ -1,10 +1,10 @@ package com.airbnb.mvrx -import androidx.lifecycle.ViewModelProviders import androidx.annotation.RestrictTo import androidx.annotation.RestrictTo.Scope.LIBRARY import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.ViewModelProviders import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KClass import kotlin.reflect.KProperty @@ -29,6 +29,40 @@ inline fun , reified S : MvRxState> T.fragm .apply { subscribe(this@fragmentViewModel, subscriber = { postInvalidate() }) } } +/** + * Gets or creates a ViewModel scoped to a parent fragment. This delegate will walk up the parentFragment hierarchy + * until it finds a Fragment that can provide the correct ViewModel. If no parent fragments can provide the ViewModel, + * a new one will be created in the direct parent of the curent Fragment. + */ +inline fun , reified S : MvRxState> T.parentFragmentViewModel( + viewModelClass: KClass = VM::class, + crossinline keyFactory: () -> String = { viewModelClass.java.name } +): Lazy where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { + requireNotNull(parentFragment) { "There is no parent fragment!" } + val factory = MvRxFactory { error("No ViewModel for this Fragment.") } + var fragment: Fragment? = this + var viewModel: VM? = null + val key = keyFactory() + while (fragment != null) { + try { + viewModel = ViewModelProviders.of(fragment, factory).get(key, viewModelClass.java) + .apply { subscribe(this@parentFragmentViewModel, subscriber = { postInvalidate() }) } + break + } catch (e: IllegalStateException) { + fragment = fragment.parentFragment + } + } + if (viewModel == null) { + val viewModelContext = FragmentViewModelContext(this.requireActivity(), _fragmentArgsProvider(), parentFragment as Fragment) + viewModel = MvRxViewModelProvider.get(viewModelClass.java, S::class.java, viewModelContext, keyFactory()) + } + // There is a mismatch between the compiler inference and Android Studio inference. + // This code doesn't compile without the cast. + // May be related to: https://blog.jetbrains.com/kotlin/2019/06/kotlin-1-3-40-released/ + @Suppress("USELESS_CAST") + viewModel as VM +} + /** * [activityViewModel] except it will throw [IllegalStateException] if the ViewModel doesn't already exist. * Use this for screens in the middle of a flow that cannot reasonably be an entrypoint to the flow. diff --git a/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt b/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt index 7d1823f65..aa3eda782 100644 --- a/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt +++ b/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import androidx.fragment.app.Fragment import org.junit.Assert.assertEquals import org.junit.Test @@ -463,4 +464,60 @@ class FragmentSubscriberTest : BaseTest() { fun duplicateUniqueOnlySubscribeThrowIllegalStateException() { createFragment(containerId = CONTAINER_ID) } + + class ParentFragment : BaseMvRxFragment() { + + val viewModel: ViewSubscriberViewModel by fragmentViewModel() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = FrameLayout(requireContext()) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + childFragmentManager.beginTransaction() + .add(ChildFragment(), "child") + .commit() + } + + override fun invalidate() { + } + } + + class ChildFragment : BaseMvRxFragment() { + + val viewModel: ViewSubscriberViewModel by parentFragmentViewModel() + + override fun invalidate() { + } + } + + @Test + fun testParentFragment() { + val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) + val childFragment = parentFragment.childFragmentManager.findFragmentByTag("child") as ChildFragment + assertEquals(parentFragment.viewModel, childFragment.viewModel) + } + + class ParentFragmentWithoutViewModel : BaseMvRxFragment() { + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = FrameLayout(requireContext()) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + childFragmentManager.beginTransaction() + .add(ChildFragment(), "child1") + .commit() + childFragmentManager.beginTransaction() + .add(ChildFragment(), "child2") + .commit() + } + + override fun invalidate() { + } + } + + @Test + fun testChildFragmentsCanShareViewModelWithoutParent() { + val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) + val childFragment1 = parentFragment.childFragmentManager.findFragmentByTag("child1") as ChildFragment + val childFragment2 = parentFragment.childFragmentManager.findFragmentByTag("child2") as ChildFragment + assertEquals(childFragment1.viewModel, childFragment2.viewModel) + } } diff --git a/sample/src/main/java/com/airbnb/mvrx/sample/MainFragment.kt b/sample/src/main/java/com/airbnb/mvrx/sample/MainFragment.kt index 044f68cd5..e20aed837 100644 --- a/sample/src/main/java/com/airbnb/mvrx/sample/MainFragment.kt +++ b/sample/src/main/java/com/airbnb/mvrx/sample/MainFragment.kt @@ -28,6 +28,13 @@ class MainFragment : BaseFragment() { clickListener { _ -> navigateTo(R.id.action_main_to_helloWorldEpoxyFragment) } } + basicRow { + id("parent_fragments") + title("Parent/Child ViewModel") + subtitle(demonstrates("parentFragmentViewModel")) + clickListener { _ -> navigateTo(R.id.action_main_to_parentFragment) } + } + basicRow { id("random_dad_joke") title("Random Dad Joke") diff --git a/sample/src/main/java/com/airbnb/mvrx/sample/features/parentfragment/ChildFragment.kt b/sample/src/main/java/com/airbnb/mvrx/sample/features/parentfragment/ChildFragment.kt new file mode 100644 index 000000000..30e3b2314 --- /dev/null +++ b/sample/src/main/java/com/airbnb/mvrx/sample/features/parentfragment/ChildFragment.kt @@ -0,0 +1,30 @@ +package com.airbnb.mvrx.sample.features.parentfragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.airbnb.mvrx.BaseMvRxFragment +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.sample.R +import com.airbnb.mvrx.withState +import kotlinx.android.synthetic.main.fragment_parent.textView + +class ChildFragment : BaseMvRxFragment() { + + private val viewModel: CounterViewModel by parentFragmentViewModel() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_child, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + textView.setOnClickListener { + viewModel.incrementCount() + } + } + + override fun invalidate() = withState(viewModel) { state -> + textView.text = "ChildFragment: Count: ${state.count}" + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/airbnb/mvrx/sample/features/parentfragment/ParentFragment.kt b/sample/src/main/java/com/airbnb/mvrx/sample/features/parentfragment/ParentFragment.kt new file mode 100644 index 000000000..3ca9472d8 --- /dev/null +++ b/sample/src/main/java/com/airbnb/mvrx/sample/features/parentfragment/ParentFragment.kt @@ -0,0 +1,44 @@ +package com.airbnb.mvrx.sample.features.parentfragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.setupWithNavController +import com.airbnb.mvrx.BaseMvRxFragment +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.sample.R +import com.airbnb.mvrx.sample.core.MvRxViewModel +import com.airbnb.mvrx.withState +import kotlinx.android.synthetic.main.fragment_parent.textView +import kotlinx.android.synthetic.main.fragment_parent.toolbar + +data class CounterState(val count: Int = 0) : MvRxState +class CounterViewModel(state: CounterState) : MvRxViewModel(state) { + fun incrementCount() = setState { copy(count = count + 1) } +} + +class ParentFragment : BaseMvRxFragment() { + + private val viewModel: CounterViewModel by fragmentViewModel() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_parent, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + toolbar.setupWithNavController(findNavController()) + textView.setOnClickListener { + viewModel.incrementCount() + } + childFragmentManager.beginTransaction() + .replace(R.id.childContainer, ChildFragment()) + .commit() + } + + override fun invalidate() = withState(viewModel) { state -> + textView.text = "ParentFragment: Count: ${state.count}" + } +} \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_child.xml b/sample/src/main/res/layout/fragment_child.xml new file mode 100644 index 000000000..5254c0eb8 --- /dev/null +++ b/sample/src/main/res/layout/fragment_child.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_parent.xml b/sample/src/main/res/layout/fragment_parent.xml new file mode 100644 index 000000000..475f50411 --- /dev/null +++ b/sample/src/main/res/layout/fragment_parent.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/navigation/nav_graph.xml b/sample/src/main/res/navigation/nav_graph.xml index 14dcf7c7f..dca10da00 100644 --- a/sample/src/main/res/navigation/nav_graph.xml +++ b/sample/src/main/res/navigation/nav_graph.xml @@ -40,6 +40,9 @@ app:exitAnim="@anim/anim_exit" app:popEnterAnim="@anim/anim_pop_enter" app:popExitAnim="@anim/anim_pop_exit" /> + + \ No newline at end of file From 1062853d01911b999fd4304770a2683153fdf8f2 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Mon, 24 Jun 2019 09:13:03 -0700 Subject: [PATCH 2/5] Start with parentFragment --- mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt index caaa4a9cb..657958961 100644 --- a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt +++ b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt @@ -40,7 +40,7 @@ inline fun , reified S : MvRxState> T.paren ): Lazy where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { requireNotNull(parentFragment) { "There is no parent fragment!" } val factory = MvRxFactory { error("No ViewModel for this Fragment.") } - var fragment: Fragment? = this + var fragment: Fragment? = parentFragment var viewModel: VM? = null val key = keyFactory() while (fragment != null) { From 7058f476710fd448a531580fd45722c05c495028 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Mon, 8 Jul 2019 20:59:58 -0500 Subject: [PATCH 3/5] Subscribe to the top parent Fragment --- .../kotlin/com/airbnb/mvrx/MvRxExtensions.kt | 9 ++++-- .../com/airbnb/mvrx/ViewSubscriberTest.kt | 28 ++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt index 657958961..76c8cdd4e 100644 --- a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt +++ b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt @@ -32,7 +32,7 @@ inline fun , reified S : MvRxState> T.fragm /** * Gets or creates a ViewModel scoped to a parent fragment. This delegate will walk up the parentFragment hierarchy * until it finds a Fragment that can provide the correct ViewModel. If no parent fragments can provide the ViewModel, - * a new one will be created in the direct parent of the curent Fragment. + * a new one will be created in top-most parent Fragment. */ inline fun , reified S : MvRxState> T.parentFragmentViewModel( viewModelClass: KClass = VM::class, @@ -53,7 +53,12 @@ inline fun , reified S : MvRxState> T.paren } } if (viewModel == null) { - val viewModelContext = FragmentViewModelContext(this.requireActivity(), _fragmentArgsProvider(), parentFragment as Fragment) + // ViewModel was not found. Create a new one in the top-most parent. + var topParentFragment = parentFragment + while (topParentFragment?.parentFragment != null) { + topParentFragment = topParentFragment.parentFragment + } + val viewModelContext = FragmentViewModelContext(this.requireActivity(), _fragmentArgsProvider(), topParentFragment!!) viewModel = MvRxViewModelProvider.get(viewModelClass.java, S::class.java, viewModelContext, keyFactory()) } // There is a mismatch between the compiler inference and Android Studio inference. diff --git a/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt b/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt index aa3eda782..21a5c756b 100644 --- a/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt +++ b/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt @@ -471,12 +471,6 @@ class FragmentSubscriberTest : BaseTest() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) = FrameLayout(requireContext()) - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - childFragmentManager.beginTransaction() - .add(ChildFragment(), "child") - .commit() - } - override fun invalidate() { } } @@ -492,7 +486,8 @@ class FragmentSubscriberTest : BaseTest() { @Test fun testParentFragment() { val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) - val childFragment = parentFragment.childFragmentManager.findFragmentByTag("child") as ChildFragment + val childFragment = ChildFragment() + parentFragment.childFragmentManager.beginTransaction().add(childFragment, "child").commit() assertEquals(parentFragment.viewModel, childFragment.viewModel) } @@ -520,4 +515,23 @@ class FragmentSubscriberTest : BaseTest() { val childFragment2 = parentFragment.childFragmentManager.findFragmentByTag("child2") as ChildFragment assertEquals(childFragment1.viewModel, childFragment2.viewModel) } + + class EmptyMvRxFragment : BaseMvRxFragment() { + override fun invalidate() { + } + } + + @Test + fun testCreatesViewModelInTopMostFragment() { + val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) + val middleFragment = Fragment() + parentFragment.childFragmentManager.beginTransaction().add(middleFragment, "middle").commitNow() + val childFragment1 = ChildFragment() + middleFragment.childFragmentManager.beginTransaction().add(childFragment1, "child1").commitNow() + + val childFragment2 = ChildFragment() + parentFragment.childFragmentManager.beginTransaction().add(childFragment2, "child2").commitNow() + + assertEquals(childFragment1.viewModel, childFragment2.viewModel) + } } From 6191938cad3bfe121638d290d9b0bc64cb17968a Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Mon, 8 Jul 2019 21:14:53 -0500 Subject: [PATCH 4/5] Added parentFragmentViewModel --- .../kotlin/com/airbnb/mvrx/MvRxExtensions.kt | 42 +++++++----- ...riberTest.kt => FragmentSubscriberTest.kt} | 64 ++++++++++++++++--- 2 files changed, 82 insertions(+), 24 deletions(-) rename mvrx/src/test/kotlin/com/airbnb/mvrx/{ViewSubscriberTest.kt => FragmentSubscriberTest.kt} (87%) diff --git a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt index 76c8cdd4e..dca8379ef 100644 --- a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt +++ b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt @@ -41,31 +41,41 @@ inline fun , reified S : MvRxState> T.paren requireNotNull(parentFragment) { "There is no parent fragment!" } val factory = MvRxFactory { error("No ViewModel for this Fragment.") } var fragment: Fragment? = parentFragment - var viewModel: VM? = null val key = keyFactory() while (fragment != null) { try { - viewModel = ViewModelProviders.of(fragment, factory).get(key, viewModelClass.java) + return@lifecycleAwareLazy ViewModelProviders.of(fragment, factory).get(key, viewModelClass.java) .apply { subscribe(this@parentFragmentViewModel, subscriber = { postInvalidate() }) } - break } catch (e: IllegalStateException) { fragment = fragment.parentFragment } } - if (viewModel == null) { - // ViewModel was not found. Create a new one in the top-most parent. - var topParentFragment = parentFragment - while (topParentFragment?.parentFragment != null) { - topParentFragment = topParentFragment.parentFragment - } - val viewModelContext = FragmentViewModelContext(this.requireActivity(), _fragmentArgsProvider(), topParentFragment!!) - viewModel = MvRxViewModelProvider.get(viewModelClass.java, S::class.java, viewModelContext, keyFactory()) + + // ViewModel was not found. Create a new one in the top-most parent. + var topParentFragment = parentFragment + while (topParentFragment?.parentFragment != null) { + topParentFragment = topParentFragment.parentFragment } - // There is a mismatch between the compiler inference and Android Studio inference. - // This code doesn't compile without the cast. - // May be related to: https://blog.jetbrains.com/kotlin/2019/06/kotlin-1-3-40-released/ - @Suppress("USELESS_CAST") - viewModel as VM + val viewModelContext = FragmentViewModelContext(this.requireActivity(), _fragmentArgsProvider(), topParentFragment!!) + return@lifecycleAwareLazy MvRxViewModelProvider.get(viewModelClass.java, S::class.java, viewModelContext, keyFactory()) + .apply { subscribe(this@parentFragmentViewModel, subscriber = { postInvalidate() }) } +} + +/** + * Gets or creates a ViewModel scoped to a target fragment. Throws [IllegalStateException] if there is no target fragment. + */ +inline fun , reified S : MvRxState> T.targetFragmentViewModel( + viewModelClass: KClass = VM::class, + crossinline keyFactory: () -> String = { viewModelClass.java.name } +): Lazy where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { + val targetFragment = requireNotNull(targetFragment) { "There is no target fragment!" } + MvRxViewModelProvider.get( + viewModelClass.java, + S::class.java, + FragmentViewModelContext(this.requireActivity(), targetFragment._fragmentArgsProvider(), targetFragment), + keyFactory() + ) + .apply { subscribe(this@targetFragmentViewModel, subscriber = { postInvalidate() }) } } /** diff --git a/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt b/mvrx/src/test/kotlin/com/airbnb/mvrx/FragmentSubscriberTest.kt similarity index 87% rename from mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt rename to mvrx/src/test/kotlin/com/airbnb/mvrx/FragmentSubscriberTest.kt index 21a5c756b..7e0f1e880 100644 --- a/mvrx/src/test/kotlin/com/airbnb/mvrx/ViewSubscriberTest.kt +++ b/mvrx/src/test/kotlin/com/airbnb/mvrx/FragmentSubscriberTest.kt @@ -475,7 +475,7 @@ class FragmentSubscriberTest : BaseTest() { } } - class ChildFragment : BaseMvRxFragment() { + class ChildFragmentWithParentViewModel : BaseMvRxFragment() { val viewModel: ViewSubscriberViewModel by parentFragmentViewModel() @@ -486,7 +486,7 @@ class FragmentSubscriberTest : BaseTest() { @Test fun testParentFragment() { val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) - val childFragment = ChildFragment() + val childFragment = ChildFragmentWithParentViewModel() parentFragment.childFragmentManager.beginTransaction().add(childFragment, "child").commit() assertEquals(parentFragment.viewModel, childFragment.viewModel) } @@ -497,10 +497,10 @@ class FragmentSubscriberTest : BaseTest() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { childFragmentManager.beginTransaction() - .add(ChildFragment(), "child1") + .add(ChildFragmentWithParentViewModel(), "child1") .commit() childFragmentManager.beginTransaction() - .add(ChildFragment(), "child2") + .add(ChildFragmentWithParentViewModel(), "child2") .commit() } @@ -511,8 +511,8 @@ class FragmentSubscriberTest : BaseTest() { @Test fun testChildFragmentsCanShareViewModelWithoutParent() { val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) - val childFragment1 = parentFragment.childFragmentManager.findFragmentByTag("child1") as ChildFragment - val childFragment2 = parentFragment.childFragmentManager.findFragmentByTag("child2") as ChildFragment + val childFragment1 = parentFragment.childFragmentManager.findFragmentByTag("child1") as ChildFragmentWithParentViewModel + val childFragment2 = parentFragment.childFragmentManager.findFragmentByTag("child2") as ChildFragmentWithParentViewModel assertEquals(childFragment1.viewModel, childFragment2.viewModel) } @@ -526,12 +526,60 @@ class FragmentSubscriberTest : BaseTest() { val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) val middleFragment = Fragment() parentFragment.childFragmentManager.beginTransaction().add(middleFragment, "middle").commitNow() - val childFragment1 = ChildFragment() + val childFragment1 = ChildFragmentWithParentViewModel() middleFragment.childFragmentManager.beginTransaction().add(childFragment1, "child1").commitNow() - val childFragment2 = ChildFragment() + val childFragment2 = ChildFragmentWithParentViewModel() parentFragment.childFragmentManager.beginTransaction().add(childFragment2, "child2").commitNow() assertEquals(childFragment1.viewModel, childFragment2.viewModel) } + + class FragmentWithTarget : BaseMvRxFragment() { + val viewModel: ViewSubscriberViewModel by targetFragmentViewModel() + + var invalidateCount = 0 + + override fun invalidate() { + invalidateCount++ + } + } + + @Test + fun testTargetFragment() { + val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) + val targetFragment = EmptyMvRxFragment() + parentFragment.childFragmentManager.beginTransaction().add(targetFragment, "target").commitNow() + val fragmentWithTarget = FragmentWithTarget() + fragmentWithTarget.setTargetFragment(targetFragment, 123) + parentFragment.childFragmentManager.beginTransaction().add(fragmentWithTarget, "fragment-with-target").commitNow() + // Make sure subscribe works. + assertEquals(1, fragmentWithTarget.invalidateCount) + fragmentWithTarget.viewModel.setFoo(1) + assertEquals(2, fragmentWithTarget.invalidateCount) + } + + @Test + fun testTargetFragmentsShareViewModel() { + val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) + val targetFragment = EmptyMvRxFragment() + parentFragment.childFragmentManager.beginTransaction().add(targetFragment, "target").commitNow() + val fragmentWithTarget1 = FragmentWithTarget() + fragmentWithTarget1.setTargetFragment(targetFragment, 123) + parentFragment.childFragmentManager.beginTransaction().add(fragmentWithTarget1, "fragment-with-target1").commitNow() + val fragmentWithTarget2 = FragmentWithTarget() + fragmentWithTarget2.setTargetFragment(targetFragment, 123) + parentFragment.childFragmentManager.beginTransaction().add(fragmentWithTarget2, "fragment-with-target2").commitNow() + assertEquals(fragmentWithTarget1.viewModel, fragmentWithTarget2.viewModel) + } + + /** + * This would be [IllegalStateException] except it fails during the Fragment transaction so it's a RuntimeException. + */ + @Test(expected = RuntimeException::class) + fun testTargetFragmentWithoutTargetCrashes() { + val (_, parentFragment) = createFragment(containerId = CONTAINER_ID) + val fragmentWithTarget = FragmentWithTarget() + parentFragment.childFragmentManager.beginTransaction().add(fragmentWithTarget, "fragment-with-target").commitNow() + } } From 364b15b407b2f0784e014bcc4a86321ac6bf1e3c Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Wed, 10 Jul 2019 08:25:14 -0700 Subject: [PATCH 5/5] Cleanup --- .../main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt index dca8379ef..327cf86fc 100644 --- a/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt +++ b/mvrx/src/main/kotlin/com/airbnb/mvrx/MvRxExtensions.kt @@ -38,16 +38,21 @@ inline fun , reified S : MvRxState> T.paren viewModelClass: KClass = VM::class, crossinline keyFactory: () -> String = { viewModelClass.java.name } ): Lazy where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { - requireNotNull(parentFragment) { "There is no parent fragment!" } - val factory = MvRxFactory { error("No ViewModel for this Fragment.") } + requireNotNull(parentFragment) { "There is no parent fragment for ${this::class.java.simpleName}!" } + val notFoundMessage by lazy { "There is no ViewModel of type ${VM::class.java.simpleName} for this Fragment!" } + val factory = MvRxFactory { error(notFoundMessage) } var fragment: Fragment? = parentFragment val key = keyFactory() while (fragment != null) { try { return@lifecycleAwareLazy ViewModelProviders.of(fragment, factory).get(key, viewModelClass.java) .apply { subscribe(this@parentFragmentViewModel, subscriber = { postInvalidate() }) } - } catch (e: IllegalStateException) { - fragment = fragment.parentFragment + } catch (e: java.lang.IllegalStateException) { + if (e.message == notFoundMessage) { + fragment = fragment.parentFragment + } else { + throw e + } } } @@ -68,7 +73,7 @@ inline fun , reified S : MvRxState> T.targe viewModelClass: KClass = VM::class, crossinline keyFactory: () -> String = { viewModelClass.java.name } ): Lazy where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { - val targetFragment = requireNotNull(targetFragment) { "There is no target fragment!" } + val targetFragment = requireNotNull(targetFragment) { "There is no target fragment for ${this::class.java.simpleName}!" } MvRxViewModelProvider.get( viewModelClass.java, S::class.java,