sergejsha / magnet Goto Github PK
View Code? Open in Web Editor NEWDependency injection library for modular Android applications
License: Apache License 2.0
Dependency injection library for modular Android applications
License: Apache License 2.0
Does it make sense to explicitly add @Target(TYPE)
and @Retention(SOURCE)
to the @MagnetizeImplementations
?
Now this annotation can be applied to everything.
Does it make sense to make method DependencyScope::register
return this
?
Then instead of having this:
DependencyScope dependencyScope = Magnet.createDependencyScope().subscope();
dependencyScope.register(Dep1.class, dep1);
dependencyScope.register(Dep2.class, dep2);
dependencyScope.register(Dep3.class, dep3);
List<FooImpl> stepFactory = implManager.get(
FooImpl.class,
dependencyScope
);
we would have chained calls:
List<FooImpl> stepFactory = implManager.get(
FooImpl.class,
Magnet.createDependencyScope()
.subscope()
.register(Dep1.class, dep1)
.register(Dep2.class, dep2)
.register(Dep3.class, dep3)
);
When can we expect release version 3.7 ?
Hi, I'm a graphic designer and I like to collaborate with open source projects. Do you know that the graphic image of a project is very important? thinking about it I would like to design a logo for your Project "Magnet".
I will be pleased to collaborate with you.
Working on a legacy code base where you slowly integrate DI, it might be helpful to allow secondary constructors to construct objects "the old way" still. Right now Magnet bails out with
error: Classes annotated with interface magnet.Instance must have exactly one constructor.
whenever a class has more than one constructor, even if the second constructor in question is private and therefor should not even be visible to Magnet.
While Magnet itself is written in Kotlin, it outputs Java code, which is totally fine in general and perfectly interoperable with Java and Kotlin projects.
However, for Kotlin-only projects actually executing javac
only once and even for very few files adds quite a bit time overhead, especially when there are many modules to compile.
It would be very cool if Magnet would get a Kotlin code generating processor so that overhead could be avoided.
So, imagine I want to inject a ViewModel
and would want to follow the (Dagger) approach outlined here: https://proandroiddev.com/viewmodel-with-dagger2-architecture-components-2e06f06c9455
This is the factory I need to use to provide instances (in androidx.lifecycle.ViewModelProvider
):
public interface Factory {
@NonNull
<T extends ViewModel> T create(@NonNull Class<T> modelClass);
}
What would be my options in Magnet? As far as I know there is no Map-Injection. Injecting a List<ViewModel>
would be possible, but this would mean all ViewModel
s would have to be instantiated before I could check which is the right one. If I would somehow lazy
evaluate the thing by injecting a List<Provider<ViewModel>>
(don't know if this is even possible), I have the issue that at runtime the actual type the provider provides is erased, so I wouldn't be able to pick the right one.
Now obviously I could create a separate factory instance for all my ViewModels, but this looks awkward:
val scope = getScope()
val viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java) { modelClass ->
MyViewModel(scope.getSingle<FirstDep>(), scope.getSingle<SecondDep>(), ...)
}
Magnet generates broken factory when "inject into list" is used with generic classes.
@Instance(type = OverviewRepo::class)
internal class DefaultOverviewRepo<I : Item>(
overviewDataSource: List<OverviewDataSource<I>>
) : OverviewRepo<I> {
...
}
e: /Users/sergej/Projects/a3/a3-client-android/overview-repo/build/generated/source/kapt/debug/de/halfbit/a3/overview/repo/DefaultOverviewRepoMagnetFactory.java:11: error: <identifier> expected
List<OverviewDataSource<I>> overviewDataSource = scope.getMany(OverviewDataSource<I>.class);
Version: 2.0
Magnet should validate single dependencies at build-time as following:
If my implementation has input argument of parametrised type T
like this:
@Implementation(forType = Foo.class)
public class FooImpl<T extends Number> implements Foo {
public FooImpl(T dependency) {
}
}
then compilation of generated class MagnetFooImplFactory
gets failed:
public final class MagnetFooImplFactory implements Factory<Foo> {
@Override
public Foo create(DependencyScope dependencyScope) {
T dependency = dependencyScope.require(T.class);
return new FooImpl(dependency);
}
}
The magnet-3.3-rc3.jar
has a packaging issue. It includes the file mockito-extensions/org.mockito.plugins.MockMaker
(which was not present in Magnet 3.1 used previously) that makes my build fail with
Execution failed for task ':my-module:transformResourcesWithMergeJavaResForDebugAndroidTest'.
> More than one file was found with OS independent path 'mockito-extensions/org.mockito.plugins.MockMaker'
Now when I add a packagingOptions
attribute to remedy this issue (exclude
or pickFirst
), my tests crash, because the mockito-android
MockMaker I have to use isn't configured at all any more (either because I excluded
all instances of that file or because the inline mockmaker from your file's definition is picked pickFirst
).
Could you please re-package without that file?
Many thanks!
Create scope visitor capable of visiting all scope instances and sub-scopes recursively.
So I guess this came up before, but anyways :) I want to be able to inject generic implementations and right now Magnet hinders me to do that because of some class name validation issue:
const val RATINGS_STORAGE = "ratings-storage"
data class RatingsModel(...)
interface ModelStore<T> {
fun load(): T?
fun save(data: T)
}
@Instance(type = ModelStore::class, classifier = RATINGS_STORAGE)
fun provideRatingsModelStore(): ModelStore<RatingsModel> {
return /* create the store for the RatingsModel */
}
}
@Instance(type = RatingsManager::class)
class RatingsManager internal constructor(
@Classifier(RATINGS_STORAGE) private val store: ModelStore<RatingsData>
) {
...
}
This fails with
error: Method must return instance of path.to.ModelStore as declared by interface magnet.Instance
public static final de.aoksystems.ma.abp.modelstore.ModelStore<RatingsData> provideRatingsModelStore()
Now I thought maybe I could outsmart the validation and use
fun provideRatingsModelStore(): ModelStore<*> { ... }
but this didn't work either. And naturally, Kotlin doesn't let me use the plain ModelStore
type without any generic arguments, as I could do with Java:
public class StaticProvision {
@Instance(type = ModelStore.class, classifier = RATINGS_STORAGE)
static ModelStore provideRatingsModelStore() {
return /* create the store for the RatingsModel */;
}
}
This version of course compiles just fine.
What are my options here (beside starting to write Java code again :))?
Magnet ignores default values while injecting parameters into constructors and methods in Kotlin.
Expected behavior: Magnet does not inject values for parameters with defaults.
interface OverviewDataSource<I : Item> {
fun getNextPage(path: String, nextPageToken: String?): Single<Page<I>>
}
@Override
public OverviewRepo create(Scope scope) {
OverviewDataSource overviewDataSource = scope.getSingle(OverviewDataSource.class, "web-data-source");
return new DefaultOverviewRepo(overviewDataSource);
}
leads to the following warning at compilation
Note: /Projects/a3/a3-client-android/overview-repo/build/generated/source/kapt/debug/de/halfbit/a3/overview/repo/DefaultOverviewRepoMagnetFactory.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
Version: 2.0-RC6
I tried to use the new Lazy
injection today and stumbled upon a code generation issue, where unless I annotate all Lazy
injections with @JvmSuppressWildcards
, Magnet will create invalid, non-compiling Java code like this:
Lazy<? extends List<? extends Bar>> things = new SingleLazy(scope, ? extends List<? extends Bar>.class, "");
for a usage like this:
@Instance(type = Foo::class)
class Foo(bars: Lazy<List<Bar>>) { ... }
So yes, using bars: Lazy<@JvmSuppressWildcards List<Bar>>
can be used as a workaround, but I think since you got plain bars: List<Bar>
working as well, you could eventually do something about that :)
In SelectorAttributeParser the delimiter is defined as follows:
private val DELIMITER = Regex("[?!\s|.]+")
This unfortunately matches on ! which means it won't even compile a selector using != or !in as it sees too many values in the string and even if it did compile it would not properly handle the nots since it would no longer have the !.
After Kotlin version was updated from 1.4.32 to 1.5.20 in an Android project, the Magnet throws an exception during Gradle build (that occurs in Magnet-generated class):
.../build/tmp/kapt3/stubs/main/.../.../.../.../.... . java:9 error:Unexpected compilation error, please file the bug at: https://github.com/beworker/magnet/issues. Message:
Unsupported KotlinClassMetadata of type null
public class .....
^
(irrelevant info was replaced with dots (...))
Up until recently we stumbled - only by accident - on the issue that certain instances that should be scoped rather narrowly "leak" to the root scope if they do not depend on things that are explicitely provided by the child scope. This is bad for two reasons:
Now I could think of several possibilities how to "fix" this problem:
scoping = Scoping.DIRECT
. By "all" I mean of course the root nodes of the specific sub scope tree, but since it's not always clear what the root nodes are (especially if new dependencies are added), it's probably safer to scope everything that should not be global / land in the root scope with Scoping.DIRECT
.scoping
parameters wisely.I feel right now a little "blind" and would really like to have option 3, but the Scope
interface does not provide this information, one would have to use reflection to access MagnetScope
and it's parent
, childrenScopes
and instanceManager
properties and then again look into MagnetInstanceManager
to see what is recorded in the particular instance.
How would you handle this particular issue?
package app;
import magnet.Instance;
import magnet.Scope;
@Instance(type = UnderTest.class)
public class UnderTest {
public UnderTest(Scope parentScope) {
}
}
generates Factory, which fails with the following error message:
java.lang.AssertionError: Compilation produced the following errors:
/SOURCE_OUTPUT/app/UnderTestMagnetFactory.java:11: error: cannot find symbol
return new UnderTest(parentScope);
^
symbol: variable parentScope
location: class app.UnderTestMagnetFactory
Version: 3.1-beta1
From time to time you come across use cases where you only need to instantiate objects when a certain condition is entered. Dagger supports this with Lazy<Type>
injections or even by allowing Provider<Type>
s to be injected, in case not a single, but multiple, state-holding instances are needed.
How would such a thing be possible in Magnet? At first I thought custom Factories could be used for this, but then I realized you'd only ever inject what the factory provides and not the factory itself.
Any ideas? :)
Version: 1.0-rc1
const val APPLICATION = "application" // exists in different module
@Instance(type = FitnessTrackerManager::class)
fun createFitnessTrackerManager(@Classifier(APPLICATION) context: Context): FitnessTrackerManager = // ...
generates the following code
public final class FitnessTrackerManagerKt {
@org.jetbrains.annotations.NotNull()
@magnet.Instance(type = path.to.FitnessTrackerManager.class)
public static final path.to.FitnessTrackerManager createFitnessTrackerManager(@org.jetbrains.annotations.NotNull()
@magnet.Classifier()
android.content.Context context) {
return null;
}
}
which fails to compile with
e: path/to/FitnessTrackerManagerKt.java:12: error: annotation @Classifier is missing a default value for the element 'value'
@magnet.Classifier()
^
Will magnet support Kotlin 1.8?
After first tests I get in logs Unexoected compilation error with message: Unsupported KotlinClassMetadata of type null.
When declaring
Instance(
types = [Foo::class, Bar::class],
classifier = "foo"
)
internal class FooBar(): Foo, Bar {}
all types use shared classifier value. Expected behavior: classifier should be declarable per type.
A solution could be to remove types
from the Instance
annotation and apply a new InstanceAlias
annotation for each new instance type as following:
@Instance(type = Foo::class, classifier = "foo")
@InstanceAlias(type = Bar::class, classifier = "bar")
internal class FooBar(): Foo, Bar {}
Magnet generates a couple of classes that count towards Jacoco's method and instruction coverage. Now it's possible to ignore those based on wildcards (e.g. *MagnetIndexer
, *MagnetFactory
), even nicer however would be if the types itself would be annotated as generated code, so they would be automatically ignored.
Newer Jacoco versions just need to find a class or runtime annotation that contains the string Generated
in it's simple name to recognize ignorable types and go ahead. We have this here in our code base (Kotlin):
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.CONSTRUCTOR)
@Retention(AnnotationRetention.BINARY) // this is translated to `RetentionPolicy.CLASS` in
annotation class Generated
val context = dependencyScope.require<Context>()
instead of
val context = dependencyScope.require(Context::class.java)
It makes sense to present the whole scope hierarchy (including retained instances) in the error log when "unsatisfied dependency" error is thrown. Developer should be able to suppress this extended log message when needed (e.g. in release build).
Where Can I find samples in Java for using this library ?
When I execute ./dumpapp magnet scope I see in console:
No stetho-enabled processes running
I checked also with with magnet version 3.6-rc1 and 3.5 version for de.halfbit:magnetx-app-stetho-scope:3.5" and this combination works.
magnet : '3.6-rc1',
magnet : [
kotlin : "de.halfbit:magnet-kotlin:$versions.magnet",
processor : "de.halfbit:magnet-processor:$versions.magnet",
appExtension: "de.halfbit:magnetx-app:$versions.magnet",
appStetho : "de.halfbit:magnetx-app-stetho-scope:3.5"
],
Magnet should fail the build if more than one factory for detected optional dependency was found.
Below is an example of the scope hierarchy, properly reflecting lifecycle of Android components, including ViewModels:
ApplicationScope
\
ViewModelScope(activity)
\
ActivityScope
\
\ ViewModelScope(fragment)
\ /
FragmentScope
\
\ ViewModelScope(fragment)
\ /
FragmentScope
...
By modelling scopes like this, we ensure that:
This kind of hierarchy is hard to maintain with Magnet because there is no way to declare, that ViewModel instances must reside within the required scopes, not below or above them. This problem should be solved by "type binding" feature described below.
Let's declare view model scope to see how type binding can be applied. For defining ViewModelScope(activity) and ActivityScope following syntax can be used:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val scope = appScope
.getOrCreateSubscope(this) {
bind<CatalogViewModel>()
...
}
.createSubscope {
bind(this@FragmentActivity)
...
}
}
getOrCreateSubscope(activity: Activity)
is an extension function (doesn't exist yet, will be a part of one of extension modules) capable of either getting the scope from already existing ViewModel
associated with the given activity
instance, or creating a new one. Same approach is applied for getting/creating fragment scope.
bind<CatalogViewModel>
is a type binding function - actual topic of this proposal. It supposes to be another method in Scope
interface. In contrast to already existing bind(catalogViewModel)
method, <T> bind(Class<T>)
does not bind an instance of type T to the scope, but rather binds the type itself to the scope, meaning whenever an instance of this type needs to be instantiated, Magnet will ensure that it's allocated in exactly that scope. If while instantiating, some dependencies cannot be satisfied (e.g. they lie down the scope dependency tree), Magnet will fails with a runtime error.
Proposed scoping of bound types is unlimited Scoping.TOPMOST
. Besides ViewModel case, type binding can be applied to any other type and it will work in exactly the same way.
By adding type binding we can drastically simplify injection of ViewModels with Magnet:
a) ViewModels won't require any custom factories
b) ViewModels will be seen in scope dumps
c) ViewModels could potentially have dependencies to other ViewModels
d) ViewModel scopes (and other instances created there) will be destroyed together with the ViewModel itself.
e) ViewModels can be simple interface/class types without any dependency onto androidx specific ViewModel types.
@realdadfish What do you think?
The issue happens when new instance is created in scope, which already has an instance of same type.
Solution: instances should be collected and stay in scope.
Going down the rabbit hole to provide easy testing of Android Activitys and Fragments (see https://gist.github.com/realdadfish/4c65e2da781bebb1479bc4d4624c5fb7) I found myself in the pity position that I cannot override Magnet's use of an annotated factory to create an instance.
While the factory should be used when no specific instance is bound in the scope, I want to be able to explicitly bind a specific (mocked) instance into a scope for testing purposes:
val rootScope = Magnet.createRootScope()
rootScope.bind(MyViewModel::class.java, mock<MyViewModel>())
At first I thought this could not work because I annotated the implementation itself:
@Instance(
type = MyViewModel::class,
factory = ViewModelFactory::class,
scoping = Scoping.UNSCOPED
)
class MyViewModel : ViewModel() { ... }
so I extracted it like so:
// Jetpack's ViewModel of course has no interface :(
abstract class MyViewModel : ViewModel() { ... }
@Instance(
type = MyViewModel::class,
factory = ViewModelFactory::class,
scoping = Scoping.UNSCOPED
)
class MyViewModelImpl : MyViewModel() { ... }
but still, Magnet would again use the annotated factory to create an instance of the object, instead of just using the one that I supplied to it.
How can I make Magnet accept my mocked instance?
Sometimes when starting an android application, it gets such an error.
Any idea what this could be caused by?
Here part of Stacktrace:
java.lang.IllegalStateException: Single instance requested, while many instances are stored: {class com.example.ServerTimeProviderServerTimeUpdaterMagnetFactory=magnet.internal.InstanceBucket$InjectedInstance@ddea8f8}
at magnet.internal.InstanceBucket.getSingleInstance(InstanceBucket.java:63)
at magnet.internal.MagnetScope.findOrInjectOptional(MagnetScope.java:317)
at magnet.internal.MagnetScope.getSingle(MagnetScope.java:97)
So i'm getting this error, i've already tried to remove all build folders and so on, but it didn't help.
Task :networking:kaptDebugKotlin
/Users/xxx/Documents/xx/bonus/networking/build/tmp/kapt3/stubs/debug/xx/xxx/ma/abp/networking/StaticProvisionKt.java:166: error: Unexpected compilation error, please file the bug at https://github.com/beworker/magnet/issues. Message: Cannot verify type declaration.
public static final CsrfTokenRepository providesCsrfTokenRepository(@org.jetbrains.annotations.NotNull()
^
This is really strange because another very similar class works just fine:
@org.jetbrains.annotations.NotNull()
@magnet.Instance(type = xx.xxx.common.network.base.jwttoken.ConnectionJwtTokenRepository.class)
@xx.xxx.ma.abp.core.Generated()
public static final xx.xxx.common.network.base.jwttoken.ConnectionJwtTokenRepository provideConnectionJwtTokenRepository(@org.jetbrains.annotations.NotNull()
android.content.Context context) {
return null;
}
This on the other hand doesnt work:
@org.jetbrains.annotations.NotNull()
@magnet.Instance(type = CsrfTokenRepository.class)
@xx.xxx.ma.abp.core.Generated()
public static final CsrfTokenRepository providesCsrfTokenRepository(@org.jetbrains.annotations.NotNull()
android.content.Context context) {
return null;
}
Hi,
in future we'd like to replace current usage of SaveInstanceState with SavedStateHandle.
To do it we'd need to change ViewModelProvider.Factory
to AbstractSavedStateViewModelFactory(SavedStateRegistryOwner, Bundle)
as mentioned in Android docu:
https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate#savedstatehandle
When providing a custom ViewModelProvider.Factory instance, you can enable usage of SavedStateHandle by extending AbstractSavedStateViewModelFactory.
As far as I understand to achieve this we'd need to get an additional SavedStateRegistryOwner
parameter from magnet.Factory
. See example code below.
class ViewModelFactory<T : ViewModel> : Factory<T> {
override fun create(
scope: Scope,
type: Class<T>,
classifier: String,
scoping: Scoping,
instantiator: Factory.Instantiator<T>,
// As per the SavedStateRegistryOwner documentation, both Fragment, and AppCompatActivity implement SavedStateRegistryOwner
owner: SavedStateRegistryOwner
): T {
val key = scope.getOptional(String::class.java, VIEW_MODEL_KEY)
if (key != null && scoping != Scoping.UNSCOPED) {
error("ViewModel '$type' with key '$key' must be declared with Scoping.UNSCOPED")
}
val androidViewModelFactory = AndroidViewModelFactory(scope, instantiator, owner)
androidViewModelFactory
}
private class AndroidViewModelFactory<T>(
private var scope: Scope,
private var instantiator: Factory.Instantiator<T>,
private var owner: SavedStateRegistryOwner ,
private var defaultArgs: Bundle? = null
) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
TODO("Not yet implemented")
}
}
}
Version: 3.3-rc5
Scopes: scopeA { Bound1 } <- scopeB { Bound3 }
Types:
Dep1 -> [Bound1, Dep2]
Dep2 (w/ sibling Dep2Sibling) -> many(Dep3)`
Dep3 -> Bound3`
scopeB.getSingle(Dep2Sibling.class)
scopeB.getSingle(Dep1.class)
scopeA { Bound1 }
scopeB { Dep1, Dep2, Dep2Sibling, Dep3, Bound3 }
scopeA { Bound1, Dep1 }
scopeB { Dep2, Dep2Sibling, Dep3, Bound3 }
The AppExtension
from magnetx-app
should pull in dependencies for application startup, even across (library) modules. So I followed the example app's implementation closely and added in my base
(actually core
) module the said base application class. Then, in the main app module and a library module, I added two classes, similar to this:
@Instance(
type = AppExtension::class,
scoping = Scoping.UNSCOPED
)
internal class AppInitializer(private val application: Application) : AppExtension { ... }
and
@Instance(
type = AppExtension::class,
scoping = Scoping.UNSCOPED
)
internal class FeatureInitializer(private val application: Application) : AppExtension { ... }
Now, I see that Magnet creates a Factory class annotated with @FactoryIndex
(in package magnet.index
) in both cases, but at runtime neither of both instances is injected into the AppExtension.Delegate
. What am I doing wrong? How is this supposed to work?
@Implementation(forType = TypeA.class)
TypeAImpl implements TypeB { }
Current behavior. Factory gets generated, but it does not compile because TypeAImpl
does not actually implement TypeA
interface.
Expected behavior. Magnet should not allow such configuration and gracefully fail with a clear error message.
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.