Giter VIP home page Giter VIP logo

magnet's Introduction

Build Magnet Kotlin version badge Maven Central License

‼️ 27.01.2023: This project is not actively developed anymore ‼️

🧲 Magnet

Magnet is a concise, scope tree based Dependency Injection (DI) library designed for highly modular Android applications. It consists of two parts: an annotation processor (Kotlin) and a reflection free runtime library (Java + Kotlin).

Design

Magnet defines and opetates on two core concepts: Scopes and Instances.

Scope is a container for instances. Scopes can be combined into a hierarchical tree by referencing parent scopes. The most top scope of the tree hierarchy, which has no parent scope, is called the root scope.

Instance is a concrete occurrence of an injected type. Instances can be allocated in scopes (scoped instances) or outside of scopes (unscoped instances).

The Dependency Rule

Scopes depend on each other using the strong dependency rule - scope dependency can only point towards its parent scope. The dependency direction between two scopes enforces the direction of dependencies between instances allocated in those scopes. Instances allocated in a parent scope can know nothing about instances allocated in its child scopes. This simple design rule helps preventing memory leaks and allows safe disposal of child scopes and garbage collecting instances allocated there.

Getting Started

In the example below we will compose a very naive MediaPlayer which loads media using a MediaLoader and then plays the media.

fun main() {
   val rootScope = MagnetScope.createRootScope()
   val playerScope = rootScope.createSubscope {
      bind(Uri.parse("https://my-media-file"))
   }
   
   // mark 1
   val mediaPlayer = playerScope.getSingle<MediaPlayer>()
   mediaPlayer.playWhenReady()
   
   // mark 2
   Thread.sleep(5000)
   playerScope.dispose()
   
   // mark 3
}

// MediaPlayer.kt
interface MediaPlayer {
   fun playWhenReady()
}

@Instance(type = MediaPlayer::class, disposer = "dispose")
internal class DefaultMediaPlayer(
   private val assetId: Uri,
   private val mediaLoader: MediaLoader
) : MediaPlayer {
   override fun playWhenReady() { ... }
   fun dispose() { ... }
}

// MediaLoader.kt
interface MediaLoader {
   fun load(mediaUri: Uri): Single<Media>
}

@Instance(type = MediaLoader::class)
internal class DefaultMediaLoader() : MediaLoader {
   override fun load(mediaUri: Uri): Single<Media> { ... }
}

The diagram below shows how Magnet manages the scope hierarchy when different marks of the main function are reached.

At Mark 1, two scopes are created and the Uri instance gets bound into the playerScope.

At Mark 2, mediaPlayer and mediaLoader instances get allocated in respective scopes. mediaPlayer is allocated in the playerScope because one of its dependencies, the Uri, is located in playerScope. Magnet cannot move mediaPlayer up to the rootScope because this would break the dependency rule described above. mediaLoader has no dependencies, that's why it is allocated in the rootScope. This instance allocation logic is specific to Magnet DI and is called auto-scoping. See developer documentation for more detail.

At Mark 3, the playerScope gets disposed and all its instances are garbage collected.

For more information refer to Magnet documentation.

Documentation

  1. Developer Guide
  2. Dependency auto-scoping
  3. Scope Inspection
  4. How to Inject Android ViewModels
  5. Blog: Magnet - an alternative to Dagger
  6. Co2Monitor sample app
  7. Another sample app

Features

  • Minimalistic API
  • Auto-scoping of instances
  • Hierarchical, disposable scopes
  • Kotlin friendly annotation
  • Injection into Kotlin constructors with default arguments
  • Injection from binary libraries
  • Dependency inversion
  • No direct references to Magnet generated code
  • No reflection for injection, apt generated factory classes
  • Extensible - some magnetx extensions are available
  • Customizable - custom factories and instance selectors

Why Magnet?

Magnet was crafted with simplicity and development speed in mind. It lets developers spend less time on DI configuration and do more other stuff, also more mistakes when used inattentively. Magnet motivates you writing highly modular apps because it makes DI so simple. It can even inject instances from the libraries added in build scripts without necessity to adapt source code. Magnet could be interesting for those, who needs an easy to configure and simple DI with more runtime control.

Why not Magnet?

If compile time consistency validation is your highest priority, I recommend using awesome Dagger2 instead. You will spend slightly more time on DI configuration but Dagger2 lets you keep it highly consistent and error prone (in most cases) very early in the development cycle - at compile time.

Peace ✌️ and have fun.

Gradle

Kotlin

repositories {
   mavenCentral()
}
dependencies {
   api 'de.halfbit:magnet-kotlin:<version>'
   kapt 'de.halfbit:magnet-processor:<version>'
}

Java

repositories {
   mavenCentral()
}
dependencies {
   api 'de.halfbit:magnet:<version>'
   annotationProcessor 'de.halfbit:magnet-processor:<version>'
}

Compatibility

Kotlin Version Magnet Version
1.8.x 3.8
1.7.x 3.7
1.6.x 3.6
1.5.x 3.5
1.4.x 3.4
1.3.x 3.4

Proguard & R8

-keep class magnet.internal.MagnetIndexer { *; }

Build from Sources

  1. Set JAVA_HOME variable to JDK 11.
  2. Import project into Android Studio.
  3. Set Gradle → Gradle Settings... → Gradle JDK to JDK 11.
  4. To build the project run ./gradlew build
  5. To release the project run ./gradlew publish

Maven repository configurations

Repository Configuration
Central mavenCentral()
Snapshot maven { url = "https://oss.sonatype.org/content/repositories/snapshots/" }
Local mavenLocal()

License

Copyright 2018-2023 Sergej Shafarenka, www.halfbit.de

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

magnet's People

Contributors

realdadfish avatar sergejsha 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

magnet's Issues

Logo offer for magnet

Hi sir. I'm graphic designer. I have studied your work and I think a new design will make you more visible. I would like to offer logo. I can give you all the formats of the design free. If you want I send you PR or a change please specify.

Please choose what you like.

untitled-1

Best Regards
Famil Qasimov

Kotlin source generation

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.

Implementation must actually implement interface mentioned in `forType`

@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.

3.3-rc3 packaging issue

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!

@Classifier not applicable on factory methods

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()
    ^

Type binding

Android scope hierarchy

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:

  • Lifespan of any parent scope is bigger than the lifespan of any of its children scopes
  • Proper bottom-up dependencies ensure no instance and memory leaking (e.g. Fragment instance injected into ViewModel scope)
  • Scopes gets destroyed when their "host" components are destroyed

Type binding

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?

Broken factory gets generated for "inject into list" with generic classes

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

Lazy injection in Kotlin

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 :)

Jacoco coverage

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

Injection overrides

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?

Unsupported KotlinClassMetadata of type null after update to Kotlin 1.5.20

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 (...))

ViewModel injection

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 ViewModels 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 for Kotlin 1.8.

Will magnet support Kotlin 1.8?
After first tests I get in logs Unexoected compilation error with message: Unsupported KotlinClassMetadata of type null.

java.lang.IllegalStateException: Single instance requested, while many instances are stored

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)

Logo Proposal for magnet

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.

Generics

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 :))?

Class annotated with @Implementation cannot have constructor with argument of parametrised type.

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);
  }
}

SelectorAttributeParser is treating nots as delimiters

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 !.

AppExtensions

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?

Cannot verify type declaration

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;
     }

Lazy construction

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? :)

`@Instance(types)` must accept a classifier per type

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 {}

No stetho-enabled processes running - with Magnet 3.6-rc1

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"
        ],

Processor fails to generate Factory when custom scope name is used

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

Determine placement of instances

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:

  1. The root scope gets too large and includes many stateless instances that could have been removed in the meantime, because they're not actually used anymore.
  2. In case an instance holds state that is specific for a child scope, this state, together with it's instance holding a reference to it, will not be removed when the child scope is removed.

Now I could think of several possibilities how to "fix" this problem:

  1. Eventually annotate all those child instances with 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.
  2. Have a single thing that is explicitely bound to the particular child scope and inject this in every other instance (even if it is unused), just as a "handle" to identify this particular instance.
  3. Have a way to debug-print the contents of the scopes so that one sees exactly what instance lives where and apply the particular 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?

Provide SavedStateRegistryOwner in a magnet.Factory interface

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")
        }
    }
}

Wrong placement of instances when "getMany" and "sibling types" are used

Version: 3.3-rc5

Given

Scopes: scopeA { Bound1 } <- scopeB { Bound3 }
Types:

Dep1 -> [Bound1, Dep2]
Dep2 (w/ sibling Dep2Sibling) -> many(Dep3)`
Dep3 -> Bound3`

When

scopeB.getSingle(Dep2Sibling.class)
scopeB.getSingle(Dep1.class)

Expected

scopeA { Bound1 }
scopeB { Dep1, Dep2, Dep2Sibling, Dep3, Bound3 }

Actual

scopeA { Bound1, Dep1 }
scopeB { Dep2, Dep2Sibling, Dep3, Bound3 }

Classes annotated with interface magnet.Instance must have exactly one constructor

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.

Implement scope visitor

Create scope visitor capable of visiting all scope instances and sub-scopes recursively.

  • Visitor must be configurable to visit either instances, or sub-scopes, or both.
  • Visitor must be able to stop visiting next instance or scope after exiting from previous visit function.

Avoid "MagnetFactory.java uses unchecked or unsafe operations" warning

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

Enhancement: chaining of `DependencyScope::register()` calls.

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)
        );

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.