Skip to content
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

View model injection #37

Closed
TonicArtos opened this issue Jan 12, 2018 · 4 comments
Closed

View model injection #37

TonicArtos opened this issue Jan 12, 2018 · 4 comments
Milestone

Comments

@TonicArtos
Copy link

Hi. I like where v0.8.0 is going with adding an API for view model injection. However, there are some things I noted.

  1. I cannot inject the view model in the normal manner

    private val viewModel: MyViewModel by inject()
  2. I have to manually pull the activity or parent fragment in order to use shared view models.

    val viewModel = activity.getViewModel<MyViewModel>()

    For an activity, it is straightforward. However, for a parent fragment it is more complicated.

  3. Configuring the koin context with viewModel {} makes it look like nested contexts should inject/get the same instance of the view model as the parent context. This could be confusing to people who have not read the underlying code.


Presently, I have a different approach which hides the ViewModelProvider behind the koin context and allows for lazy injection.

This approach does require the context to be initiated in Fragment::onCreate or Activity::onCreate. Also, the ViewModels are KoinComponents eliminating constructor injection and negating the need for providing a custom ViewModelFactory.

/**
 * Main activity using view model injection with initialisation of koin context in onCreate.
 */
class MainActivity: FragmentActivity() {
    private val navigation: NavigationViewModel by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setProperty(Context.Activity, this)
    }
}

/**
 * Home fragment injecting navigation view model which is shared with activity.
 */
class HomeFragment: Fragment() {
    private val navigation: NavigationViewModel by inject()
    private val viewModel: HomeViewModel by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setProperty(Context.HomeFragment, this)
    }
}

/**
 * Context string constants.
 */
object Context {
    const val Activity = "Activity"
    const val HomeFragment = "HomeFragment"
}

/**
 * Get list of app modules for startKoin.
 */
fun appModules() = listOf(appModule)

/**
 * Koin application module.
 */
private val appModule = applicationContext {
    context(Activity) {
        viewModel<NavigationViewModel>()

        context(HomeFragment) {
            viewModel<HomeViewModel>()
        }
    }
}

/**
 * Add a view model to the context.
 * 
 * @param scopeType Scope type of ViewModelProvider. If not set an attempt will be made to determine it at runtime.
 * @param name Name of property scope object is set within. Default is same as context.
 */
private inline fun <reified T : ViewModel> Context.viewModel(
        scopeType: ScopeType? = null,
        name: String = this.name
) = when (name) {
    Scope.ROOT -> throw CantInjectViewModelException()
    else       -> factory {
        when (scopeType) {
            ScopeType.Activity -> ViewModelProviders.of(getActivity()).get(T::class.java)
            ScopeType.Fragment -> ViewModelProviders.of(getFragment()).get(T::class.java)
            null               -> try {
                ViewModelProviders.of(getActivity()).get(T::class.java)
            } catch (e: ClassCastException) {
                try {
                    ViewModelProviders.of(getFragment()).get(T::class.java)
                } catch (e: ClassCastException) {
                    throw CantInjectViewModelException()
                }
            }
        }
    }
}

sealed class ScopeType {
    object Activity : ScopeType()
    object Fragment : ScopeType()
}

private class CantInjectViewModelException :
        IllegalStateException("Injecting view model requires associated Fragment or FragmentActivity.")

private inline fun Context.getActivity(name: String = this.name) = getProperty<FragmentActivity>(name)
private inline fun Context.getFragment(name: String = this.name) = getProperty<Fragment>(name)
@arnaudgiuliani
Copy link
Member

Ok. Let me some time to check it 👍

@arnaudgiuliani
Copy link
Member

In a Fragment (support v4), you can already do getViewModel() - which uses koin-android-architecture extensions. It is effectively not lazy, and injecting a viewmodel in several fragments is problematic (it can recreate a different instance viewmodel).

Koin must not interfere with Lifecycle architecture and must be kept as an instance factory. To keep things simple, I will provide a lazy way of injecting your ViewModel from fragments/activity.
This will allow injecting the same viewmodel instance between activity and its fragments.

@mattmook
Copy link

What I've started to do in my code with v0.8.0 for lazy creation is to use the following (with same for Fragment):

inline fun <reified T : ViewModel> FragmentActivity.viewModel(): Lazy<T> {
    return lazy { getViewModel<T>() }
}

Then to use in an activity I'm writing private val myViewModel: MyViewModel by viewModel()

@arnaudgiuliani
Copy link
Member

That's perfectly right :)

It will be included in 0.8.1 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants