GithubHelp home page GithubHelp logo

rewe-digital / katana Goto Github PK

View Code? Open in Web Editor NEW
176.0 176.0 8.0 787 KB

Lightweight, minimalistic dependency injection library for Kotlin & Android

License: MIT License

Kotlin 100.00%
android dependency-injection java jvm kotlin service-locator

katana's People

Contributors

bjdupuis avatar dineshvg avatar svenjacobs avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

katana's Issues

Best way to inject dependencies into Fragment provided by parent Fragment / Activity

In Android DI you quite often share dependencies between Fragments via the parent Fragment or the Activity.

Katana allows to define dependsOn components. One would think, that you would just use that:

class MyFragment: Fragment(), KatanaTrait {
  override val component = createComponent {
    modules = listOf(
      createSupportFragmentModule(this),
      myFragmentModule,
      // some other modules
    ),
    dependsOn = listOf(MyApp.applicationComponent, (requireActivity() as KatanaTrait).component)
  }

  val dependency: MyDependencySuppliedByActivity by inject()
}

But there are multiple problems with this:

  1. Getting the Activity component the way I outlined above doesn't work as the Activity isn't available when the Fragment component is created. So we would have to by lazy {} create the component or create it in onAttach/onCreate. That in turn means that we would need to either by lazy { injectNow() } all injections in the Fragment or manually set them in onAttach/onCreate as well.
  2. As Katana does not allow overrides such chains of components obviously can lead to OverrideExceptions, especially when in the case of Activity -> ParentFragment -> ChildFragment. A good example for that is Glide which should always be bound to the hosting Activity/Fragment to ensure that it stops image requests and cleans up when the context of the ImageView is gone. So it is not unusual to find one bind<Glide> per Fragment module which becomes awkward when having child Fragments as you have to name all those binds. Similar things can happen with nested NavHostFragmentss and the according NavControllers.

An alternative which solves 2, but not 1 is to specifically inject stuff provided by the parent Fragment/Activity within the (child) Fragment module. Thus we can omit any potential overrides as we don't want them in the Fragment anyway:

val myFragmentModule = createModule {

  bind<MyDependency> { singleton { (get<Fragment>(SUPPORT_FRAGMENT).activity as KatanaTrait).injectNow() }
}

But this somehow feels a bit strange as well as I suddenly exactly define from where I want to get my dependencies supplied. That part just feels more ioc with the dependsBy approach.

Any best practises here?

Rename singleton

Think about renaming singleton inside Module. The name might be misleading to some users as a singleton in Katana is related to a Component. There only exist one instance per component and not as some might think per application.

What would be a better name?

  • scoped
  • componentScoped
  • componentSingleton

ViewModel support

First of all I'd like to thank you guys. This is my first time using dependency injection and I learned a lot from playing around with Katana. Having somewhat similar syntax to Koin, Kodein, and Dagger, made it that much easier for me to cross-reference and learn DI in general.

Seeing as there's no support for ViewModel(?) I created my own. What do you guys think?

/**
 * A ViewModelFactory that works alongside dependency injection.
 *
 * @param viewModel The already injected ViewModel.
 * @return A ViewModelProvider.Factory to be used with ViewModelProviders.of
 */
class KatanaViewModelFactory(private val viewModel: ViewModel) : ViewModelProvider.Factory {
    override fun <T : ViewModel?> create(modelClass: Class<T>): T = viewModel as? T ?: modelClass.newInstance()
}


/**
 * Declares a [ViewModel] dependency binding as a singleton.
 * Only one instance (per component) will be created.
 *
 * The ViewModelFactory is also created here because we need the unique class name of your [ViewModel].
 * @param body Body of binding declaration
 *
 * @see Module.factory
 * @see Module.singleton
 */
inline fun <reified T : ViewModel> Module.viewModel(crossinline body: ProviderDsl.() -> T) {
    val name : String = T::class.java.simpleName
    singleton<ViewModel>(name, body = body)
    singleton(name = "${name}Factory") { KatanaViewModelFactory(get(name)) }
}


/**
 * Inject the ViewModel declared from [Module.viewModel].
 *
 * The scope is tied to the host [Activity]. This is called from a [Fragment].
 * @return [ViewModel]
 */
inline fun <reified VM : ViewModel, T> T.viewModel(): Lazy<VM> where T : KatanaTrait, T : Fragment =
        lazy { ViewModelProviders.of(requireActivity(), injectNow("${VM::class.java.simpleName}Factory")).get(VM::class.java) }


/**
 * Inject the ViewModel declared from [Module.viewModel].
 * This is called from an [Activity].
 * @return [ViewModel]
 */
inline fun <reified VM : ViewModel, T> T.viewModel(): Lazy<VM> where T : KatanaTrait, T : AppCompatActivity =
        lazy { ViewModelProviders.of(this, injectNow("${VM::class.java.simpleName}Factory")).get(VM::class.java) }


/**
 * Inject the ViewModel declared from [Module.viewModel] with an assignment operator.
 *
 * The scope is tied to the host [Activity]. This is called from a [Fragment].
 * @return [ViewModel]
 */
// This has conflicts with the above. Use this if you're using [KatanaFragmentDelegate]
// and you're inside the onInject callback.
/* inline fun <reified VM : ViewModel,T> T.viewModel(): VM where T: KatanaTrait, T: Fragment=
   ViewModelProviders.of(requireActivity(),injectNow("${VM::class.java.simpleName}Factory")).get(VM::class.java)
*/

The module creation would look something like this

createModule {
        singleton { ApplicationDatabase.getInstance(Application.instance) }
        singleton { get<ApplicationDatabase>().mainDao() }
        singleton { MainRepository.getInstance(get()) }
        viewModel { MainViewModel(get()) }
    }

Inject it in your Activity/Fragment:

val viewModel: MainViewModel by viewModel()

Side notes (For modules that depend on activities):
In fragments you can inject() and declare a Component without callbacks by using lazy.
You just have to do everything in onActivityCreated because of the Activity-Fragment relationship.
But I think you already know this.

KatanaFragmentDelegate.kt:
Since Fragments are instantiated before Activities, the component initialization must be delayed until the Activity was created.

class MyFragment : Fragment(), KatanaTrait {
    // All of these are by lazy
    override val component: Component by lazy { getComponent() }
    private val viewModel: MainViewModel by viewModel()
    private val myObj: MyObject by inject()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // Don't put code here that relies on activity dependencies
    }
    
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        // Put it here
        viewModel.doStuff()
        myObj.doStuff()
    }
}

Single generic class singleton retreival should get injected

Currently, if one tries to declare a single singleton of a generic class without naming it, and runs get(), type erasure will prevent it to be resolved. The expected behavior is that if there is only one such declaration, it should be injected. If there are many, there should be an error (even if their type parameters are different) unless they are all named. If the type params don't match what the get() is injecting to, then it should just be considered if it will only crash on use (not so good), or if there is a way to catch and report this beforehand.

Add singletonSet to module DSL

Often we have a pattern in Android to have a list of Intents that can be processed by an implementation of:

interface TaskHandler {
	fun canHandle(action: String): Boolean

	fun run()
}

And we need to instantiate these classes with Katana, and then put them into one big list/set and then run:

injectNow<Set<TaskHandler>>().first { it.canHandle(intent.action) }.run()

Currently this can only be done by tagging (some implementations are registered multiple times with different constructor params) each singleton/factory and then using get<TaskImplementation>(TaskTag) to build the list/set in one big singleton<Set<TaskHandler>> { }, this is very tedious and error-prone.

The proposition is to have a singletonSet<TaskHandler> { TaskOne(get(), get()) } that will just accumulate all those instances into one set that can be retrieved with get<Set<TaskHandler>>() since the instances themselves are of no real interest. Any request for an individual instance could be an error that it needs to be get as a set.

I'm still not sure about whether this should only be a Set or maybe a List and Map too. A map could avoid instantiating all the classes, and just have the intent's action as the key and the handler for it as the value.

This feature might not be only for Android, but in Android there's another little point to consider if the intent's other properties are needed.

Future of Katana

Google recently announced the new Hilt dependency injection library for Android based on Dagger. Hilt tackles many of the "problems" of Dagger โ€“ especially it's complexity โ€“ and simplifies dependency injection with Dagger. Presumably Google will advertise Hilt as the DI solution for Android. Other AndroidX support libraries will probably add Hilt support in the future, too.

While Katana's core functionality does not depend on Android and works well in Kotlin JVM, Katana was written with Android in mind from day one. I welcome Google's approach of simplifying and unifying DI on Android. As a single developer working on Katana mostly in my spare time, I cannot compete with a team of Google engineers working on Hilt & Dagger full time ๐Ÿ˜‰ As of today Katana works well and is in use in a few production Android applications successfully. If you are using Katana, there's no need to migrate to Hilt (immediately). However I recommend that any new Android project uses Hilt instead of Katana. Katana will go into maintenance mode. No new features will be developed. Critical bugs will be fixed, should they arise.

I thank everybody for their support and hope you understand my decision. Of course pull requests for bugfixes are still very welcome ๐Ÿ˜ƒ

Clean up Gradle build files

With the new androidx-viewmodel artifact there now is some duplication in the gradle.build.kts files. Project should have a single root Gradle file to reduce duplication.

Add getOrNull()

For cases like class SomeClass(val one: IClass?, val two: IClass?) and we have an if that is supposed to decide which of the two should be injected and which should be null.

And in the code I just do one?. ... and two?. ...

Add `provider()` to `ProviderDsl`

Add provider() to ProviderDsl to inject a dependency's factory instead of creating a new instance for injection. Maybe it should fail-fast if trying to use provider() to inject a singleton { } declaration though...

Environment specific configuration

We should be able to tweak the behaviour of Katana per environment. For instance we could use an ArrayMap instead of a HashMap in an Android environment for improved memory usage.

We should introduce an EnvironmentContext interface which can be plugged into Katana.

interface EnvironmentContext {

    fun <K, V> mapFactory(): () -> MutableMap<K, V>
}

object DefaultEnvironmentContext : EnvironmentContext {

    override fun <K, V> mapFactory() = { HashMap<K, V>() }
}

katana-androidx-viemodel: Couldn't inline method call

Since updating Katana to Kotlin 1.3.30 (Katana version 1.6.1), usages of katana-androidx-viemodel inline functions result in a compiler exception couldn't inline method call. This seems to be a compiler issue and is tracked here.

Middleware support

Think about implementing an interface for (custom) middlewares that could implement advanced logging or dependency graph analysis for example.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.