InsertKoinIO / koin

Koin - a pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform
https://insert-koin.io
Apache License 2.0
9.03k stars 715 forks source link

Inject Activity #49

Closed stanete closed 6 years ago

stanete commented 6 years ago

Hi,

Koin is awesome but I'm having an issue. Some libraries, like Firebase Analytics, depend on the current Activity to perform some actions like setting a current screen.

class Analytics(private val activity: Activity) {

  private val firebaseAnalytics: FirebaseAnalytics = FirebaseAnalytics.getInstance(activity)

  fun setCurrentScreen(screenName: String) {
    firebaseAnalytics.setCurrentScreen(activity, screenName, null)
  }
}
class MainPresenter(analytics: Analytics) { 

}
class MainActivity : AppCompatActivity() {

  val presenter : MainPresenter by inject()

}

Is this possible with Koin? If yes, then I don't see how.

arnaudgiuliani commented 6 years ago

Hello,

I'm currently writing recipes for such use. You can bind your Activity in your activity :

class Analytics() {
  lateinit var activity: Activity
  private val firebaseAnalytics: FirebaseAnalytics = FirebaseAnalytics.getInstance(activity)

  fun setCurrentScreen(screenName: String) {
    firebaseAnalytics.setCurrentScreen(activity, screenName, null)
  }
}
class MainActivity : AppCompatActivity() {

  val presenter : MainPresenter by inject()

    override fun onStart() {
        super.onStart()
        presenter.activity = this
    }
}
stanete commented 6 years ago

Thank you for your quick response. The issue is that I don't want my presenter to depend on any activity. The only class who should know about the activity is Analytics which is the one who needs it. How can I achieve that with Koin?

I would like to avoid doing something like this:

class MainActivity : AppCompatActivity() {

  val presenter : MainPresenter by inject()

    override fun onStart() {
        super.onStart()
        presenter.analytics.activity = this
    }
}
arnaudgiuliani commented 6 years ago

Ok. Then, if you don't want to keep in in your presenter, pass it through function parameter?

class MainPresenter(analytics: Analytics) { 

  fun doSomething(context : Context){
  // ... use your activity here
  }  

}

Activity and all Android components are somewhat "outside" of Koin. The only thing that we could do, is about Fragment instance (because they are created manually).

stanete commented 6 years ago

Hmm I understand. So there is no nice way to provide an Activity to a Koin module. I don't know how Koin really works underneath yet, I'll have to study it. But it should be nice if we could develop a nice Koin solution to this. A lot of external frameworks depend on some Android component (like the Activity in the case of Firebase). Do you see it possible?

fredy-mederos commented 6 years ago

Maybe a curren-activity-provider could me implemented using ActivityLifecycleCallbacks It is just a thought, I will try it later.

arnaudgiuliani commented 6 years ago

Interesting idea. Don't know if it's in the scope of Koin to provide such thing, or just a recipe to help about such situation.

arnaudgiuliani commented 6 years ago

I think this has to be a "side" solution to implement if you want to keep an activity under the hand. This solution has to be documented for online reference.

Bodo1981 commented 6 years ago

I came accross the same issue to inject android framework components (activity, fragment, ...) in some dependencies. so it would be great is koin can support this. if you do not want to include it in the core koin project how about adding it to the android module?

in the meanwhile has someone a solution other than the already mentioned?

btw GREAT LIBRARY!!!

vpotvin commented 6 years ago

Similar issue trying to inject an instance of an Anko SQLite Database Helper. Would be amazing if KOIN could be Android Context-aware somehow to facilitate these kinds of dependencies.

arnaudgiuliani commented 6 years ago

If I understand, could be nice to allow inject current Activity, right?

given class:

class Analytics(val activity: Activity)

we could the a Koin module with currentActivity() which resolves current Activity:

val module = applicationContext{
    factory { Analytics(currentActivity())}
}

this way, each time you ask for Analytics instance, you will resolve it with current Activity.

Bodo1981 commented 6 years ago

That will be the most used case with activity.

But maybe koin could provide a more generic way to inject also other classes

val module = applicationContext {
    factory { Analytics(currentContext<GenericType>()) }
}

The provided (GenericType) class is the class where your Analytics class will be injected, e.g.

class AnalyticsActivity : Activity, Navigator {
    val analytics: Analytics by inject() // in this case GenericType can be (Analytics-)Activity or Navigator
}
class AnalyticsFragment : Fragment, Navigator {
    val analytics: Analytics by inject() // in this case GenericType can be (Analytics-)Fragment or Navigator
}

Can this be achieved with koin?

sho5nn commented 6 years ago

In Dagger, If want depend to Activity, At the timing of injecting Presenter, providing an instance of Activity to inject() and solve it.

The javadoc of AndroidInjector<T>.Builder#seedInstance(T) may be helpful. https://github.com/google/dagger/blob/master/java/dagger/android/AndroidInjector.java

Bodo1981 commented 6 years ago

Great idea! Maybe we could add an optional parameter (varargs or single value) to the inject delegate to provide additional context related objects

class AnalyticsActivity : Activity, Navigator {
    val analytics: Analytics by inject(this, this as Navigator, ...) // pass additional optional parameters to the inject delegate
}
arnaudgiuliani commented 6 years ago

I 've add Koin parameters to release 0.9.0.

you will be able to use parameters in your definition. Given class:

class MyPresenter(val activity : MyActivity)

We can use parameters to be injected with by inject()

val module = applicationContext {
   factory { params -> MyPresenter(params["activity"])}
}

Injecting the parameter:

class MyActivity : AppCompatActivity(){

   // Ask for MyPresenter injection and provide parameters
   val presenter : MyPresenter by inject( parameters = mapOf("activity" to this))
}

stay tunedđź‘Ť

Bodo1981 commented 6 years ago

Great to hear!

Any timetable when 0.9.0 will be released?

arnaudgiuliani commented 6 years ago

release 0.9.0 is scheduled for end of this week or next week.

fredy-mederos commented 6 years ago

"parameters" feature will be available for viewModels too ??? I noticed in v0.9.0-rc1 viewModels are being created with emptyMap of parameters. Thanks

arnaudgiuliani commented 6 years ago

Yes ;) đź‘Ť Currently merging some feature branches

On Tue, 27 Feb 2018 at 17:28 fredy-mederos notifications@github.com wrote:

"parameters" feature will be available for viewModels too ??? I noticed in v0.9.0-rc1 viewModels are being created with emptyMap of parameters. Thanks

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/Ekito/koin/issues/49#issuecomment-368937630, or mute the thread https://github.com/notifications/unsubscribe-auth/ACQFcPag2m-N73VWGbhYT8hVMMThucr_ks5tZC0NgaJpZM4SCHgP .

--

Arnaud GIULIANI - Mobile & Cloud Tel : +33 6 78 81 72 12 - mail : agiuliani@ekito.fr EKITO - 15 rue Gabriel PĂ©ri 31000 Toulouse web : www.ekito.fr / www.ekito.fr/people

westonal commented 6 years ago

Is this still the best way to achieve this?

I made a little helper:

inline fun <reified T> Activity.injectActivity(): Lazy<T> =
    inject(parameters = { mapOf("activity" to this) })
class MyActivity : AppCompatActivity(){

   // Ask for MyPresenter injection and provide parameters
   val presenter : MyPresenter by injectActivity()
}
dgngulcan commented 6 years ago

I am using parameters like below @westonal.

LocationModule.class

factory { (activity: MainActivity) -> FusedLocationProviderClient(activity) }

MainActivity.class

val provider: FusedLocationProviderClient by inject { parametersOf(this@MainActivity) }
arnaudgiuliani commented 6 years ago

Yes, injection parameters are the best in that case!

Dmitry-Borodin commented 5 years ago

Is there a way to create dependencies for scopes? Passing data around with custom strings optional parameters looks weird, is there a way to pass scope-local dependencies to create scope, and use it for dependencies inside this scope?

I have similar usecase as above, but activity injects presenter, that depends on LocationProvider, that uses FusedLocationClient, requiring activity. So Activity should be part of the scope, not part of specific inject.

kassim commented 5 years ago

^ agree, it would be nice if we could start a scope with a number of module params, in the same way we call startKoin()

GorkemKarayel commented 5 years ago

@dgngulcan thx

syt0r commented 5 years ago

How can the initial situation be solved? I have a similar situation: there is Interactor that needs activity reference and I'm injecting this interactor into viewModel constructor, but this way I can't use custom arguments.

BillingInteractor(activity: Activity) {
...
}

MyViewModel(private val billingInteractor: BillingInteractor) : ViewModel() {
...
}
bkoruznjak commented 4 years ago

@SYtor you could do something like:

fun provideYourModule() = module {
    viewModel { (activity: Activity) -> 
        MyViewModel(billingInteractor = BillingInteractor(activity))
    }
}

and in your activity:

private val myViewModel: MyViewModel by viewModel { parametersOf(this@Activity)}

makaroffandrey commented 4 years ago

@bkoruznjak Please be aware that your solution may cause your activity to leak. For example if the screen was rotated. In this case, an Activity will be recreated but the new Activity will get the same instance of MyViewModel, which still holds a reference to the old Activity which will prevent it from being garbage collected.

moizalidv commented 4 years ago

@makaroffandrey makes a good point. @arnaudgiuliani any suggestion to avoid leaks?

makaroffandrey commented 4 years ago

@moizalidv I ended up creating an extension function that creates a custom scope that is tied to the Activity and has the Activity itself in the dependency tree. This allows for the injection of Activity-dependent components without leaks. https://github.com/InsertKoinIO/koin/issues/428#issuecomment-595950513

It's a shame that Koin cannot do something like that out of the box.

Oleksandr32 commented 3 years ago

You can do this with scope. For that your activity must extends ScopeActivity For example, class MainActivity : ScopeActivity()

My example is for MainNavigation:

class MainNavigationImpl(private val navController: NavController) : MainNavigation

my module will looks like:

val presentationModule = module {

    scope<MainActivity> {
        scoped<MainNavigation> {
            val navController = get<MainActivity>().findNavController(R.id.root_navigation_host_fragment)
            MainNavigationImpl(navController)
        }
    }

    scope<ChatsFragment> {
        scoped { ChatsPresenter(get()) }
    }
}

then I can easily use my dependency (for example, MainNavigation):

class ChatsPresenter(private val navigation: MainNavigation)