-
Notifications
You must be signed in to change notification settings - Fork 499
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
Add initialState to MvRxFactory, give access to args in create ViewModel #169
Changes from all commits
db8d9c8
5e81915
ec5e674
92f7de4
edbb23e
3df423c
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,6 +1,8 @@ | ||
package com.airbnb.mvrx | ||
|
||
import android.arch.lifecycle.ViewModelProviders | ||
import android.support.annotation.RestrictTo | ||
import android.support.annotation.RestrictTo.Scope.LIBRARY | ||
import android.support.v4.app.Fragment | ||
import android.support.v4.app.FragmentActivity | ||
import kotlin.properties.ReadOnlyProperty | ||
|
@@ -23,8 +25,7 @@ inline fun <T, reified VM : BaseMvRxViewModel<S>, reified S : MvRxState> T.fragm | |
viewModelClass: KClass<VM> = VM::class, | ||
crossinline keyFactory: () -> String = { viewModelClass.java.name } | ||
) where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { | ||
val stateFactory: () -> S = ::_fragmentViewModelInitialStateProvider | ||
MvRxViewModelProvider.get(viewModelClass.java, this, keyFactory(), stateFactory) | ||
MvRxViewModelProvider.get(viewModelClass.java, S::class.java, FragmentViewModelContext(this.requireActivity(), _fragmentArgsProvider(), this), keyFactory()) | ||
.apply { subscribe(this@fragmentViewModel, subscriber = { postInvalidate() }) } | ||
} | ||
|
||
|
@@ -48,91 +49,39 @@ inline fun <T, reified VM : BaseMvRxViewModel<S>, reified S : MvRxState> T.activ | |
viewModelClass: KClass<VM> = VM::class, | ||
noinline keyFactory: () -> String = { viewModelClass.java.name } | ||
) where T : Fragment, T : MvRxView = lifecycleAwareLazy(this) { | ||
val stateFactory: () -> S = { _activityViewModelInitialStateProvider(keyFactory) } | ||
if (requireActivity() !is MvRxViewModelStoreOwner) throw IllegalArgumentException("Your Activity must be a MvRxViewModelStoreOwner!") | ||
MvRxViewModelProvider.get(viewModelClass.java, requireActivity(), keyFactory(), stateFactory) | ||
MvRxViewModelProvider.get(viewModelClass.java, S::class.java, ActivityViewModelContext(requireActivity(), _activityArgsProvider(keyFactory)), keyFactory()) | ||
.apply { subscribe(this@activityViewModel, subscriber = { postInvalidate() }) } | ||
} | ||
|
||
/** | ||
* For internal use only. Public for inline. | ||
* | ||
* Looks for [MvRx.KEY_ARG] on the arguments of the fragment receiver to create an instance of the State class. | ||
* The state class must have a matching single arg constructor. | ||
* Looks for [MvRx.KEY_ARG] on the arguments of the fragments. | ||
*/ | ||
@Suppress("FunctionName") | ||
@RestrictTo(LIBRARY) | ||
fun <T : Fragment> T._fragmentArgsProvider(): Any? = arguments?.get(MvRx.KEY_ARG) | ||
|
||
/** | ||
* For internal use only. Public for inline. | ||
* | ||
* If no MvRx fragment args exist it attempts to use a empty constructor. Otherwise an exception is thrown. | ||
* Looks for [MvRx.KEY_ARG] on the arguments of the fragment receiver. | ||
* | ||
* Also adds the fragment's MvRx args to the host Activity's [MvRxViewModelStore] so that they can be used to recreate initial state | ||
* in a new process. | ||
*/ | ||
@Suppress("FunctionName") | ||
inline fun <reified S : MvRxState, T : Fragment> T._activityViewModelInitialStateProvider(keyFactory: () -> String): S { | ||
val args: Any? = arguments?.get(MvRx.KEY_ARG) | ||
@RestrictTo(LIBRARY) | ||
inline fun <T : Fragment> T._activityArgsProvider(keyFactory: () -> String): Any? { | ||
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.
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. the idea of "activity args" is confusing to me since the args are still coming from a fragment, and the function receiver is a 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. Agreed, let me explain the reasons a bit more in the comments. |
||
val args: Any? = _fragmentArgsProvider() | ||
val activity = requireActivity() | ||
if (activity is MvRxViewModelStoreOwner) { | ||
activity.mvrxViewModelStore._saveActivityViewModelArgs(keyFactory(), args) | ||
} else { | ||
throw IllegalArgumentException("Your Activity must be a MvRxViewModelStoreOwner!") | ||
} | ||
return _initialStateProvider(S::class.java, args) | ||
} | ||
|
||
/** | ||
* For internal use only. Public for inline. | ||
* | ||
* Looks for [MvRx.KEY_ARG] on the arguments of the fragment receiver to create an instance of the State class. | ||
* The state class must have a matching single arg constructor. | ||
* | ||
* If no MvRx fragment args exist it attempts to use a empty constructor. Otherwise an exception is thrown. | ||
*/ | ||
@Suppress("FunctionName") | ||
inline fun <reified S : MvRxState, T : Fragment> T._fragmentViewModelInitialStateProvider(): S { | ||
val args: Any? = arguments?.get(MvRx.KEY_ARG) | ||
return _initialStateProvider(S::class.java, args) | ||
} | ||
|
||
/** | ||
* For internal use only. Public for inline. | ||
* | ||
* Looks for [MvRx.KEY_ARG] in intent extras on activity receiver to create an instance of the State class. | ||
* The state class must have a matching single arg constructor. | ||
* | ||
* If no MvRx activity args exist it attempts to use a empty constructor. Otherwise an exception is thrown. | ||
*/ | ||
@Suppress("FunctionName") | ||
inline fun <reified S : MvRxState, T : FragmentActivity> T._activityViewModelInitialStateProvider(): S { | ||
val args: Any? = intent.extras?.get(MvRx.KEY_ARG) | ||
return _initialStateProvider(S::class.java, args) | ||
} | ||
|
||
/** | ||
* For internal use only. Public for inline. | ||
* | ||
* Searches [stateClass] for a single argument constructor matching the type of [args]. If [args] is null, then | ||
* no arg constructor is invoked. | ||
* | ||
*/ | ||
@Suppress("FunctionName") // Public for inline. | ||
fun <S : MvRxState> _initialStateProvider(stateClass: Class<S>, args: Any?): S { | ||
val argsConstructor = args?.let { | ||
val argType = it::class.java | ||
|
||
stateClass.constructors.firstOrNull { constructor -> | ||
constructor.parameterTypes.size == 1 && isAssignableTo(argType, constructor.parameterTypes[0]) | ||
} | ||
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
return argsConstructor?.newInstance(args) as? S | ||
?: try { | ||
stateClass.newInstance() | ||
} catch (@Suppress("TooGenericExceptionCaught") e: Throwable) { | ||
null | ||
} | ||
?: throw IllegalStateException( | ||
"Attempt to auto create the MvRx state class ${stateClass.simpleName} has failed. It must have default values for every property or a " + | ||
"secondary constructor for ${args?.javaClass?.simpleName ?: "a fragment argument"}. " | ||
) | ||
return args | ||
} | ||
|
||
/** | ||
|
@@ -143,8 +92,7 @@ inline fun <T, reified VM : BaseMvRxViewModel<S>, reified S : MvRxState> T.viewM | |
crossinline keyFactory: () -> String = { viewModelClass.java.name } | ||
) where T : FragmentActivity, | ||
T : MvRxViewModelStoreOwner = lifecycleAwareLazy(this) { | ||
val stateFactory: () -> S = { _activityViewModelInitialStateProvider() } | ||
MvRxViewModelProvider.get(viewModelClass.java, this, keyFactory(), stateFactory) | ||
MvRxViewModelProvider.get(viewModelClass.java, S::class.java, ActivityViewModelContext(this, intent.extras?.get(MvRx.KEY_ARG)), keyFactory()) | ||
} | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,26 +4,92 @@ import android.support.v4.app.Fragment | |
import android.support.v4.app.FragmentActivity | ||
|
||
/** | ||
* Implement this in the companion object of a MvRxViewModel if your ViewModel needs more dependencies than just initial state. | ||
* If all you need is initial state, you don't need to implement this at all. | ||
* Implement this on your ViewModel's companion object for hooks into state creation and ViewModel creation. For example, if you need access | ||
* to the fragment or activity owner for dependency injection. | ||
*/ | ||
interface MvRxViewModelFactory<S : MvRxState> { | ||
interface MvRxViewModelFactory<VM : BaseMvRxViewModel<S>, S : MvRxState> { | ||
|
||
/** | ||
* @param viewModelContext [ViewModelContext] which contains the ViewModel owner and arguments. | ||
* @param state The initial state to pass to the ViewModel. In a new process, state will have all [PersistState] annotated members restored, | ||
* therefore you should never create a custom state in this method. To customize the initial state, override [initialState]. | ||
* | ||
* @return The ViewModel. If you return `null` the ViewModel must have a single argument | ||
* constructor only taking the initial state. | ||
*/ | ||
fun create(viewModelContext: ViewModelContext, state: S): VM? = null | ||
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. The issue I ran into with having a single create in #148 was casting. If the consumer wants the For me, cause Dagger injects the 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. @marukami Yeah, that is a downside, but I don't see a safe way around this. It seems too risky to have a ViewModelFactory that will crash if you use an activity scope. I think, if in your application you know that you will only use fragment scoped view models, you could do something like:
That way you are explicitly performing the unsafe cast. Does this seem reasonable? 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. With the changes @gpeal suggested for the Fragment been a generic, I think an extension function like this should work nicely. @Suppress("UNCHECKED_CAST")
inline fun <F: Fragment> ViewModelContext.withFragmentFactory(factory: (F) -> ViewModel) =
(this as FragmentViewModelContext).let { it.fragment as F }.let(factory) |
||
|
||
/** | ||
* This will be called when your ViewModel needs to created. This *needs* to be annotated with [JvmStatic]. | ||
* @param state: The initial state for your ViewModel. This will be populated from activity persisted state. | ||
* The initial state for the ViewModel. Override this if the initial state requires information from arguments, or the ViewModel owner. | ||
* This function will take precedence over any secondary constructors defined in the state class, [S]. | ||
* | ||
* The return value of this function will be transformed with any [PersistState] values before being used in [create]. | ||
* | ||
* @return the initial state. If `null`, the state class constructors will be used for initial state creation. | ||
*/ | ||
fun create(activity: FragmentActivity, state: S): BaseMvRxViewModel<S> | ||
fun initialState(viewModelContext: ViewModelContext): S? = null | ||
|
||
} | ||
|
||
/** | ||
* Implement this in the companion object of a MvRxViewModel if your ViewModel needs more dependencies than just initial state. | ||
* If you only need the [FragmentActivity] then you can use the [MvRxViewModelFactory]. | ||
* If all you need is initial state, you don't need to implement this at all. | ||
* Creation context for the ViewModel. Includes the ViewModel store owner (either an activity or fragment), and fragment arguments | ||
* set via [MvRx.KEY_ARG]. | ||
* | ||
* For activity scoped ViewModels see [ActivityViewModelContext]. | ||
* For fragment scoped ViewModels see [FragmentViewModelContext]. | ||
* | ||
* Never store a reference to this context, or the activity/fragment. | ||
* | ||
*/ | ||
interface MvRxFragmentViewModelFactory<S : MvRxState> { | ||
sealed class ViewModelContext { | ||
/** | ||
* The activity which is using the ViewModel. | ||
*/ | ||
abstract val activity: FragmentActivity | ||
|
||
/** | ||
* Convenience method to type [activity]. | ||
*/ | ||
@Suppress("UNCHECKED_CAST") | ||
fun <A : FragmentActivity> activity() : A = activity as A | ||
|
||
/** | ||
* Fragment arguments set via [MvRx.KEY_ARG]. | ||
*/ | ||
abstract val args: Any? | ||
|
||
/** | ||
* Convenience method to type [args]. | ||
*/ | ||
@Suppress("UNCHECKED_CAST") | ||
fun <A> args(): A = args as A | ||
} | ||
|
||
/** | ||
* The [ViewModelContext] for a ViewModel created with an activity scope (`val viewModel by activityViewModel<MyViewModel>`). Although a fragment | ||
* reference is available when an activity scoped ViewModel is first created, during process restoration, activity scoped ViewModels will be created | ||
* _without_ a fragment reference, so it is only safe to reference the activity. | ||
*/ | ||
class ActivityViewModelContext( | ||
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. It's not immediately clear to me why we need to separate fragment and activity contexts, since an activity scoped viewmodel is still initially created for a specific fragment, and with arguments from that fragment. I suppose it's just during process restart that we don't have the original fragment anymore? That's subtlety makes this requirement/distinction not immediately clear 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. Fragment access is needed for simple Dagger support. With access to the fragment, all you need to do is inject a ViewModel factory. With #148 I just forward the MvRx factory invoke to the dagger injected factory in the 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. @elihart Yes, during process activity scoped ViewModels will be recreated when the activity is created. So even in a normal creation sequence we will pass it only the activity (even though a fragment reference is technically accessible). |
||
override val activity: FragmentActivity, | ||
override val args: Any? | ||
) : ViewModelContext() | ||
|
||
/** | ||
* The [ViewModelContext] for a ViewModel created with a | ||
* fragment scope (`val viewModel by fragmentViewModel<MyViewModel>`). | ||
*/ | ||
class FragmentViewModelContext( | ||
override val activity: FragmentActivity, | ||
override val args: Any?, | ||
/** | ||
* The fragment owner of the ViewModel. | ||
*/ | ||
val fragment: 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. @BenSchwab You could make a |
||
) : ViewModelContext() { | ||
/** | ||
* This will be called when your ViewModel needs to created. This *needs* to be annotated with [JvmStatic]. | ||
* @param state: The initial state for your ViewModel. This will be populated from fragment args and persisted state. | ||
* Convenience method to type [fragment]. | ||
*/ | ||
fun create(fragment: Fragment, state: S): BaseMvRxViewModel<S> | ||
@Suppress("UNCHECKED_CAST") | ||
fun <F : Fragment> fragment() : F = fragment as F | ||
} |
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.
@BenSchwab
@RestrictTo(Library)