-
Notifications
You must be signed in to change notification settings - Fork 498
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added parentFragmentViewModel() #247
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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,60 @@ inline fun <T, reified VM : BaseMvRxViewModel<S>, 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 top-most parent Fragment. | ||
*/ | ||
inline fun <T, reified VM : BaseMvRxViewModel<S>, reified S : MvRxState> T.parentFragmentViewModel( | ||
viewModelClass: KClass<VM> = VM::class, | ||
crossinline keyFactory: () -> String = { viewModelClass.java.name } | ||
): Lazy<VM> where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { | ||
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: java.lang.IllegalStateException) { | ||
if (e.message == notFoundMessage) { | ||
fragment = fragment.parentFragment | ||
} else { | ||
throw e | ||
} | ||
} | ||
} | ||
|
||
// 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!!) | ||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this use case seems weird to me, because the lifecycle of the target fragment isn't necessarily tied to the fragment that is calling this. Couldn't the target fragment be destroyed, and the viewmodel cleared, while this fragment is still using it? is it that bad to share viewmodels via an activity scoped viewmodel in this case? I'm not particularly opposed to it, but am curious to hear more about the need. Once we have these new delegates it would be great if we updated the wiki with a little guide about when we recommend using each There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @elihart I think it alleviates some of the potential ambiguity in the parentFragment case because it allows you to be explicit what the ViewModel will be scoped to. I haven't actually needed it myself but @littleGnAl suggested it and it seems reasonable and was simple enough. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gpeal @elihart In our project, the If someone needs to get the But if this function is needed, I think the logic should be similar to the logic of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @littleGnAI, ah, the DialogFragment case makes sense to me |
||
*/ | ||
inline fun <T, reified VM : BaseMvRxViewModel<S>, reified S : MvRxState> T.targetFragmentViewModel( | ||
viewModelClass: KClass<VM> = VM::class, | ||
crossinline keyFactory: () -> String = { viewModelClass.java.name } | ||
): Lazy<VM> where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { | ||
val targetFragment = requireNotNull(targetFragment) { "There is no target fragment for ${this::class.java.simpleName}!" } | ||
MvRxViewModelProvider.get( | ||
viewModelClass.java, | ||
S::class.java, | ||
FragmentViewModelContext(this.requireActivity(), targetFragment._fragmentArgsProvider(), targetFragment), | ||
keyFactory() | ||
) | ||
.apply { subscribe(this@targetFragmentViewModel, subscriber = { postInvalidate() }) } | ||
} | ||
|
||
/** | ||
* [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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<CounterState>(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}" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
android:layout_width="match_parent" | ||
android:layout_height="match_parent"> | ||
|
||
<TextView | ||
android:id="@+id/textView" | ||
android:layout_width="match_parent" | ||
android:layout_height="match_parent" | ||
android:gravity="center" /> | ||
|
||
</FrameLayout> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||
xmlns:app="http://schemas.android.com/apk/res-auto" | ||
android:layout_width="match_parent" | ||
android:layout_height="match_parent" | ||
android:orientation="vertical"> | ||
<androidx.appcompat.widget.Toolbar | ||
android:id="@+id/toolbar" | ||
android:layout_width="match_parent" | ||
android:layout_height="?attr/actionBarSize" | ||
app:navigationIcon="@drawable/ic_arrow_back" | ||
/> | ||
<TextView | ||
android:id="@+id/textView" | ||
android:layout_width="match_parent" | ||
android:layout_height="0dp" | ||
android:gravity="center" | ||
android:layout_weight="1" /> | ||
|
||
<FrameLayout | ||
android:id="@+id/childContainer" | ||
android:layout_width="match_parent" | ||
android:layout_height="0dp" | ||
android:layout_weight="4" /> | ||
</LinearLayout> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
won't
fragment
already be the top parent fragment at this point? since the above while loop only breaks when there is no parent fragment?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems like
fragment
would benull
, but if seems like we could eliminate this loop if we track parentFragment in the above loopThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@BenSchwab @elihart Yes, it would be null. I wrote it that way but the code was harder to read despite being shorter. Writing it this way made the intention of each section more explicit and easier to grok.