Asynchronous DI Container for Unity
Please install the packages in the following order:
https://github.com/mewlist/MewCore.git
https://github.com/mewlist/Doinject.git
Doinject is an asynchronous Dependency Injection (DI) framework for Unity.
It is built around the concept of an asynchronous DI container. Supports Unity 2022.3 LTS / Unity 6 and later.
Traditional DI containers typically create instances synchronously. However, this approach doesn't easily support Unity's asynchronous operations, such as loading assets or fetching data before an object can be fully initialized.
Doinject provides a DI container designed for asynchronous workflows. It supports creating and disposing of instances through asynchronous processes. This allows for more flexible dependency management with a clear API, enabling scenarios like:
- Instantiating objects after asynchronously loading their prefabs or assets via the Addressables system.
- Initializing services with data fetched asynchronously from a network or file. Custom factories can be created to handle any asynchronous instantiation logic.
Doinject defines context scopes that naturally align with Unity's object lifecycle:
- Project Context: Lives for the entire application duration.
- Scene Context: Tied to a specific scene's lifetime. When the scene is unloaded, the context and its associated instances are disposed.
- GameObject Context: Tied to a specific GameObject's lifetime. When the GameObject is destroyed, the context and its instances are disposed. These contexts automatically form parent-child relationships (e.g., Scene Context inherits from Project Context), allowing dependencies to be resolved hierarchically.
Doinject seamlessly integrates with Unity's Addressable Asset System. You can bind Addressable assets directly, and Doinject will automatically handle loading the asset asynchronously when needed and releasing its handle when the associated context is disposed. This simplifies resource management significantly compared to manual handle tracking.
Doinject simplifies the implementation of common dependency management patterns:
- Factory Pattern: Easily bind factories for creating instances on demand.
- Singleton Pattern: Bind objects as singletons scoped to their context (Project, Scene, or GameObject).
- Service Locator: While DI is generally preferred, Doinject can be used to manage globally or locally accessible services. Custom factories and resolvers offer further flexibility for complex instantiation logic.
Code | Resolver Behavior | Type |
---|---|---|
container.Bind<SomeClass>(); |
new SomeClass() |
cached |
container.Bind<SomeClass>().AsSingleton(); |
new SomeClass() |
singleton |
container.Bind<SomeClass>().AsTransient(); |
new SomeClass() |
transient |
container.Bind<SomeClass>().Args(123,"ABC"); |
new SomeClass(123, "abc") |
cached |
container.Bind<ISomeInterface>().To<SomeClass>(); |
new SomeClass() as ISomeInterface |
cached |
container.Bind<ISomeInterface, SomeClass>(); |
new SomeClass() as ISomeInterface |
cached |
container.Bind<SomeClass>() .FromInstance(instance); |
instance |
instance |
container.BindInstance(instance); |
instance |
instance |
Binding Lifetimes:
cached
(Default): Creates an instance on the first resolution within its container and reuses that same instance for subsequent requests within that container. The instance is disposed (ifIDisposable
orIAsyncDisposable
) when the container is disposed.singleton
: Creates a single instance within the context scope where it was first resolved. This instance persists for the lifetime of that context and is reused for all requests within that context and its child contexts. It is disposed when the context is disposed.transient
: Creates a new instance every time it is resolved. Doinject does not manage the lifecycle (creation/disposal) of transient instances beyond initial creation; manual disposal might be necessary.instance
: Binds a pre-existing instance to the container. Doinject does not manage the lifecycle (creation/disposal) of this instance.
Code | Resolver Behavior |
---|---|
container.Bind<SomeComponent>(); |
new GameObject().AddComponent<SomeComponent>() |
container .Bind<SomeComponent>() .Under(transform); |
var instance = new GameObject().AddComponent<SomeComponent>(); instance.transform.SetParent(transform); |
container .Bind<SomeComponent>() .On(gameObject); |
gameObject.AddComponent<SomeComponent>() |
container .BindPrefab<SomeComponent>(somePrefab); |
Instantiate(somePrefab).GetComponent<SomeComponent>() |
Code | Resolver Behavior |
---|---|
container .BindAssetReference<SomeAddressableObject>(assetReference); |
var handle = Addressables .LoadAssetAsync<GameObject>(assetReference) await handle.Task |
container .BindPrefabAssetReference<SomeComponent>(prefabAssetReference); |
var handle = Addressables .LoadAssetAsync<GameObject>(prefabAssetReference) var prefab = await handle.Task Instantiate(prefab).GetComponent<SomeComponent>() |
container .BindAssetRuntimeKey<SomeAddressableObject>("guid or path"); |
var handle = Addressables .LoadAssetAsync<GameObject>("guid or path") await handle.Task |
container .BindPrefabAssetRuntimeKey<SomeComponent>("guid or path"); |
var handle = Addressables .LoadAssetAsync<GameObject>("guid or path") var prefab = await handle.Task Instantiate(prefab).GetComponent<SomeComponent>() |
Code | Resolver Behavior |
---|---|
container .Bind<SomeClass>() .AsFactory(); |
var resolver = new TypeResolver<SomeClass>() new Factory<SomeClass>(resolver) as IFactory<SomeClass> |
container .Bind<SomeComponent>() .AsFactory(); |
var resolver = new MonoBehaviourResolver<SomeComponent>() new Factory<SomeComponent>(resolver)) as IFactory<SomeComponent> |
container .Bind<SomeClass>() .AsCustomFactory<MyFactory>(); |
new CustomFactoryResolver<MyFactory>() as IFactory<SomeClass> |
Factory bindings can also be combined with Addressables. For example, you can create a factory that asynchronously loads a prefab via Addressables, instantiates it, and returns a specific component:
container
.BindAssetReference<SomeComponentOnAddressalbesPrefab>(assetReference)
.AsFactory<SomeComponentOnAddressalbesPrefab>();
[Inject]
void Construct(IFactory<SomeComponentOnAddressalbesPrefab> factory)
{
var instance = await factory.CreateAsync();
}
public class SomeInstaller : BindingInstallerScriptableObject
{
public override void Install(DIContainer container, IContextArg contextArg)
{
container.Bind<SomeClass>();
}
}
class ExampleClass
{
// Constructor Injection
public ExampleClass(SomeClass someClass)
{ ... }
}
class ExampleClass
{
// Method Injection
[Inject]
public Construct(SomeClass someClass)
{ ... }
}
To enable injection into a MonoBehaviour
, it must implement the IInjectableComponent
interface. Dependencies can then be injected via constructor (if applicable) or method injection.
using UnityEngine;
using Doinject;
// Inherit from MonoBehaviour and implement IInjectableComponent
public class ExampleComponent : MonoBehaviour, IInjectableComponent
{
private SomeClass _someClassDependency;
// Method Injection using [Inject] attribute
[Inject]
public void Construct(SomeClass someClass)
{
_someClassDependency = someClass;
// ... use the dependency
}
}
The container automatically finds and calls methods marked with [Inject]
on components that implement IInjectableComponent
within its context scope after the component is enabled.