-
Notifications
You must be signed in to change notification settings - Fork 500
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
Conversation
…factory view model create method
|
||
@Test | ||
fun testInitialDemoState1() { | ||
val state = _initialStateProvider(DemoState1::class.java, Arg("Hello")) |
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.
These providers are gone. Lots of other tests cover this code too.
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.
I feel very good about this. 👍 from me.
Let's wait for @hellohuanlin, @elihart, and @marukami to chime in just in case.
* Looks for [MvRx.KEY_ARG] on the arguments of the fragments. | ||
*/ | ||
@Suppress("FunctionName") | ||
fun <T : Fragment> T._fragmentArgsProvider(): Any? = arguments?.get(MvRx.KEY_ARG) |
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)
* | ||
* 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 { | ||
inline fun <T : Fragment> T._activityArgsProvider(keyFactory: () -> String): Any? { |
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.
@RestrictTo(Library)
this too
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.
This seems great! I think it the api you landed on is a good compromise.
As to your open questions, I don't have any better suggestions, I think you've made strong points and thought this through well, so I don't have anything else to add. nice stuff :)
great to see it work without jvmstatic too
* | ||
* 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 { | ||
inline fun <T : Fragment> T._activityArgsProvider(keyFactory: () -> String): Any? { |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, let me explain the reasons a bit more in the comments.
* | ||
* 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 { | ||
inline fun <T : Fragment> T._activityArgsProvider(keyFactory: () -> String): Any? { | ||
val args: Any? = arguments?.get(MvRx.KEY_ARG) |
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.
could this chain to _fragmentArgsProvider
instead?
*/ | ||
fun create(fragment: Fragment, state: S): BaseMvRxViewModel<S> | ||
abstract fun <A> args(): A |
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.
is the implementation the same both subclasses? can we not have a default implementation?
* The [ViewModelContext] for a ViewModel created with an | ||
* activity scope (`val viewModel by activityViewModel<MyViewModel>`). | ||
*/ | ||
class ActivityViewModelContext( |
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.
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 comment
The 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 comment
The 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).
@@ -156,4 +113,38 @@ object MvRxViewModelProvider { | |||
} | |||
return null | |||
} | |||
|
|||
/** | |||
* For internal use only. Public for inline. |
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.
you can make this internal
and use @PublishedApi
instead of needing to make it public
"Attempt to create the MvRx state class ${stateClass.simpleName} has failed. One of the following must be true:" + | ||
"\n 1) The state class has default values for every property." + | ||
"\n 2) The state class has a secondary constructor for ${args?.javaClass?.simpleName ?: "a fragment argument"}." + | ||
"\n 3) The ViewModel using the state must has a companion object implementing MvRxFactory with an initialState function " + |
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.
nit: must has
* The [ViewModelContext] for a ViewModel created with an | ||
* activity scope (`val viewModel by activityViewModel<MyViewModel>`). | ||
*/ | ||
class ActivityViewModelContext( |
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.
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.
*/ | ||
fun create(activity: FragmentActivity, state: S): BaseMvRxViewModel<S> | ||
fun create(viewModelContext: ViewModelContext, state: S): VM? = null |
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.
The issue I ran into with having a single create in #148 was casting. If the consumer wants the FragmentViewModelContext
then you need to downcast the ViewModelContext
.
For me, cause Dagger injects the ViewModel
Factory implementation into the fragment, I would need to downcast for virtually all my ViewModel
s factories to use the FragmentViewModelContext
.
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.
@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:
interface MvRxViewModelFragmentFactory<VM: BaseMvRxViewModel<S>, S : MvRxState> : MvRxViewModelFactory<VM, S> {
fun createFragmentScopedViewModel(viewModelContext: FragmentViewModelContext, state: S) : VM?
override fun create(viewModelContext: ViewModelContext, state: S) = createFragmentScopedViewModel(viewModelContext as FragmentViewModelContext, state)
override fun initialState(viewModelContext: ViewModelContext) : S? = initialStateFragmentScoped(viewModelContext as FragmentViewModelContext)
fun initialStateFragmentScoped(viewModelContext: FragmentViewModelContext) : S?
}
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 comment
The 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)
class FactoryViewModelTest : BaseTest() { | ||
|
||
private class TestFactoryViewModel(initialState: FactoryState, val otherProp: Long) : TestMvRxViewModel<FactoryState>(initialState) { | ||
companion object : MvRxViewModelFactory<TestFactoryViewModel, FactoryState> { |
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.
Should TestFactoryViewModel
be using FragmentViewModelContext
?
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.
I think you are suggesting making the test actually verify that the a FragmentViewModelContext
is used? If so, I agree. Updated the test.
…reference, more robust tests
class FragmentViewModelContext( | ||
override val activity: FragmentActivity, | ||
override val rawArgs: Any?, | ||
val fragment: 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.
@BenSchwab You could make a <T : Fragment> fragment()
and <A : Activity> activity()
function that could help with casting to your own app's Activity/Fragment
…ialState (#181) #169 Added a new initialState method and ability to not use @JvmStatic all of these need keep rules. Closes #176 ** Note: Kotlin 1.3+ requires this keep rule as well: keep class kotlin.reflect.jvm.internal.impl.serialization.deserialization.builtins.BuiltInsLoaderImpl Per the discussion in #138, this is not a project specific rule, so not including it in the proguard file, but updated wiki to suggest it. Tested on sample app. Created #182 to prevent this from the future. Would be a great first task if anyone from the community wants to jump on it :)
Closes #83
Motivation:
If you need to use a value from args only in the viewModel (say an id to call an API) the only way to access in the view model is to manually find the arguments (painful) or add a "dead" field to state class.
If you need to access information to create
initialState
that is not stored in args, but say in your dagger component, there is no way to access it via a secondary constructor.Breaking changes
Note, activity is now referenceable as
viewModelContext.activity.
The second parameter was added to
MvRxFactory
because it guarantees that you cannot accidentally return a ViewModel of the wrong type increate
.MvRxFragmentViewModelFactory
Allow for Fragment ViewModel factory (#69) #148. I prefer the single factory in this PR, as if you used aMvRxFragmentViewModelFactory
but scoped your ViewModel to an activity, there will be a crash. This new approach forces that possibility to be acknowledged in code.New
initialState(viewModelContext: ViewModelContext)
. This gives access to the args and the activity/fragment before creating the state. You can now use dagger to inject dependencies into your state. The existing constructor based state creation still works, however, it has lower precedence thatinitialState
in the factory. If both are defined, the initialState from the factory will be used.@JvmStatic
. I wrote some java reflection to access the Companion object. It seems to be working fine, and should make onboarding a lot easier, as forgetting@JvmStatic
is very common.Open questions
ViewModelContext
. I hate overloading the word context, but it feels like that is what the combination of the owner and args is.ViewModelFactory
. As many times you will use a ViewModelFactory without needing args, it felt bad to force many declarations to have a meaningless<Any>
. Also the Factory has two generics, a third is a lot...Currently, the context has
rawArgs
which is just the untype arg object, andfun <T> args()
a convenience method to type args.Note: it is totally possible, and expected to be able to handle many types of args, which can be done cleaning with a
when
onrawArgs
.@gpeal @hellohuanlin @elihart