rewe-digital / katana Goto Github PK
View Code? Open in Web Editor NEWLightweight, minimalistic dependency injection library for Kotlin & Android
License: MIT License
Lightweight, minimalistic dependency injection library for Kotlin & Android
License: MIT License
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:
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.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 NavHostFragments
s and the according NavController
s.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?
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
Introduce Dependabot or Renovate.
Think about renaming injectNow()
to get()
.
Add additional metadata to generated POM like OSS license, author, etc.
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()
}
}
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.
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.
Think about refactoring katana-core
for multiplatform support.
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 ๐
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.
Currently null
values of (eager) singletons are not handled properly.
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
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...
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>() }
}
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.
JCenter is sunsetting on May 1, 2021, per https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/
Katana is published on JCenter, and not on Maven Central, so anyone using it will be affected.
Knowing that Katana is not actively developed anymore, I'm not sure anyone has any serious expectation that the library would be re-published elsewhere.
What are your thoughts on publishing Katana on Maven Central or another channel?
Think about implementing an interface for (custom) middlewares that could implement advanced logging or dependency graph analysis for example.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.