In general, the library enables components with longer lifecycles to interact with those that have shorter lifecycles, without causing memory leaks.
The most common use case is sending one-off events from Android ViewModels to the UI.
With this library, there's no need for SharedFlow, Channel, or extra event-related
properties in your state classes.
Supported DI Frameworks:
- Hilt
- Koin
- Projects without any DI Framework
- Basic Example (one-off event with Hilt)
- More Complex Example (suspend functions and Flow)
- Example Projects
- Prerequisites
- Installation (Single-module Projects)
- Installation for Multi-Module Projects
- Migration from Version 1.x to 2.x
- Key Concepts (regardless of used DI framework)
- Detailed Tutorials
- Advanced Details
If you are using Koin, check out this page for more details.
Let's say you want to:
- Trigger navigation commands
- Show an alert dialog and capture the user's choice
- Display toasts, snackbars, etc.
- Subscribe to onClick events
- Safely access an Activity from anywhere in your code
-
Define an interface:
interface Toasts { fun show(message: String) }
-
Inject the interface to a view-model constructor or to any other component that has longer lifecycle:
@HiltViewModel class CatsViewModel @Inject constructor( // inject your interface here: private val toasts: Toasts, ): ViewModel() { fun removeCat(cat: Cat) { // execute removal logic here; // ... // and initiate one-off event: toasts.show("Cat $cat has been removed") } }
-
Implement the interface and annotate the implementation with
@HiltEffect. As a result, you can safely use an activity reference or any other UI-related stuff in the implementation:@HiltEffect // <-- do not forget this annotation class ToastsImpl( // you can add any UI-related stuff to the constructor without memory leaks private val activity: ComponentActivity, ): Toasts { override fun show(message: String) { Toasts.makeText(activity, message, Toast.LENGTH_SHORT).show() } }
-
Use the implementation either in @Composable functions or in Activity/Fragment classes:
-
for projects with Jetpack Compose:
@AndroidEntryPoint class MainActivity: AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate() setContent { // create an effect implementation val toastsImpl = remember { ToastsImpl(this) } // connect the effect implementation to an interface injected to // a view-model constructor: EffectProvider(toastsImpl) { MyApp() } } } } @Composable fun MyApp() { // optionally you can use getEffect() call to get an instance // of the effect implementation class val toastsImpl = getEffect<ToastsImpl>() // or: val toasts = getEffect<Toasts>() }
-
for projects without Jetpack Compose:
@AndroidEntryPoint class MainActivity: AppCompatActivity() { // option 1 (if you need an access to ToastsImpl instance): private val toasts by lazyEffect { ToastsImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // option 2 (if you don't need an access to ToastImpl instance): initEffect { ToastsImpl(this) } } }
-
-
Define an interface that contains
suspendfunctions and methods returning aFlow:interface Effects { // suspend functions can return results of effect processing: suspend fun showConfirmDialog(message: String): Boolean // non-suspend functions can return a Flow: fun listenActions(): Flow<Action> }
-
Inject the interface into a ViewModel constructor:
@HiltViewModel class MyViewModel @Inject constructor( private val effects: Effects ) : ViewModel() { init { // example of using a Flow: effects.listenActions() .onEach { action -> when (action) { is SignIn -> signIn(action.email, action.password) else -> TODO("other actions if needed") } } .launchIn(viewModelScope) } fun deleteEverything() { // example of using a suspend function: viewModelScope.launch { val confirmed = effects.showConfirmDialog("Remove system dir?") if (confirmed) { TODO("let's play a game") } } } }
-
Create an implementation of the
Effectsinterface:@HiltEffect // <-- add this annotation class EffectsImpl(private val context: Context) : Effects { private val eventsFlow = MutableSharedFlow<Action>( extraBufferCapacity = 8, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) override suspend fun showConfirmDialog(message: String): Boolean { // the suspend function is cancelled on stop and restarted after device rotation return suspendCancellableCoroutine { continuation -> val dialog = TODO() // build an alert dialog using context and message // use continuation.resume(true/false) to return the result to the ViewModel dialog.show() continuation.invokeOnCancellation { dialog.dismiss() } } } // collecting the Flow is cancelled on stop and restarted after device rotation override fun listenActions(): Flow<Action> = eventsFlow fun onAction(action: Action) { eventsFlow.tryEmit(action) } }
-
Use the implementation in
@Composablefunctions or inActivity/Fragmentclasses. Example for Jetpack Compose:@AndroidEntryPoint class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate() setContent { val effects = remember { EffectsImpl(this) } EffectProvider(effects) { // now, EffectsImpl is connected globally for each screen within MyApp MyApp() } } } } @Composable fun MyApp() { // use getEffect() if you need to interact with `EffectsImpl` in // composable functions: val effectsImpl = getEffect<EffectsImpl>() Button( onClick = { effectsImpl.onAction(Action.TestButtonClicked) } ) { Text("Click Me") } }
-
Real-world scenario #1: Define a
Routerinterface and inject it into your ViewModel. Its implementation can safely accept a NavController in its constructor without memory leaks. Usage example in a ViewModel:@HiltViewModel class MyViewModel @Inject constructor( private val signInUseCase: SignInUseCase, private val router: Router, private val toaster: Toaster, ) : ViewModel() { fun signIn(credentials: Credentials) { viewModelScope.launch { try { signInUseCase.invoke() router.launchMainScreen() } catch (_: SignInException) { toaster.toast("Oops, can't sign in :)") } } } }
-
Real-world scenario #2: Open dialogs, request permissions, etc., via your own interface, and wait for results using suspend functions. A Context or Activity instance can be safely used inside your PermissionRequester implementation. Usage example in a ViewModel:
@HiltViewModel class MyViewModel @Inject constructor( private val startTrackingUseCase: StartTrackUseCase, private val permissionRequester: PermissionRequester, ) : ViewModel() { fun letsDrive() { viewModelScope.launch { if (permissionRequester.requestAccessFineLocation() == PermissionResult.Granted) { startTrackingUseCase.invoke() } } } }
- Single-module apps:
- Multi-module apps:
-
Use the latest version of Android Studio.
-
Use Kotlin v2.0 or above.
-
Ensure the KSP plugin is added to your project. How to install KSP?.
-
The library can also be used without KSP. In that case, it behaves similarly to Retrofit, by creating proxies at runtime. This approach has its own pros and cons, but if you're really interested in using the library without KSP, check out this guide for more details.
-
β οΈ Note: Hilt requires KSP anyway.
-
-
Make sure your chosen DI framework is properly set up in your project. Guides for the most popular DI setups:
Installation steps may vary depending on the DI framework you're using.
-
Add Hilt and KSP to your Android project.
-
Add the following dependencies:
// annotation processor (required): ksp("com.uandcode:effects2-hilt-compiler:2.0.0") // for projects without Jetpack Compose: implementation("com.uandcode:effects2-hilt:2.0.0") // for projects with Jetpack Compose: implementation("com.uandcode:effects2-hilt-compose:2.0.0")
For more details, check out the single-module Hilt example app.
-
Add KSP to your project (recommended).
-
Add Koin to the project.
-
Add the following dependencies:
// annotation processor: ksp("com.uandcode:effects2-koin-compiler:2.0.0") // for projects without Jetpack Compose: implementation("com.uandcode:effects2-koin:2.0.0") // for projects with Jetpack Compose: implementation("com.uandcode:effects2-koin-compose:2.0.0")
Check out single-module Koin example app for more details.
While the library supports projects without a DI framework, we strongly recommend using one, especially for medium to large codebases, as it can greatly improve scalability and maintainability.
-
Add KSP to your project (recommended).
-
Add the following dependencies:
// annotation processor: ksp("com.uandcode:effects2-core-compiler:2.0.0") // for projects without Jetpack Compose: implementation("com.uandcode:effects2-core-lifecycle:2.0.0") // for projects with Jetpack Compose: implementation("com.uandcode:effects2-core-compose:2.0.0")
Check out the single-module No-DI example app for a working setup.
-
Dependencies for your application module remain the same:
// Hilt Integration: ksp("com.uandcode:effects2-hilt-compiler:2.0.0") implementation("com.uandcode:effects2-hilt:2.0.0") // without Jetpack Compose implementation("com.uandcode:effects2-hilt-compose:2.0.0") // with Jetpack Compose // Koin Integration: ksp("com.uandcode:effects2-koin-compiler:2.0.0") implementation("com.uandcode:effects2-koin:2.0.0") // without Jetpack Compose implementation("com.uandcode:effects2-koin-compose:2.0.0") // with Jetpack Compose // No DI: ksp("com.uandcode:effects2-core-compiler:2.0.0") implementation("com.uandcode:effects2-core-lifecycle:2.0.0") // without Jetpack Compose implementation("com.uandcode:effects2-core-compose:2.0.0") // with Jetpack Compose
-
Additional configuration is required for your Library modules, if you plan to use any of the following annotations within library code:
@HiltEffect(Hilt extension)@KoinEffect(Koin extension)@EffectClass
-
Steps for Library Modules:
-
Ensure KSP is added and configured in the library module.
-
Add the following KSP argument:
β οΈ This is required only in library modules, not in the application module// my-android-lib/build.gradle.kts: ksp { arg("effects.processor.metadata", "generate") }
-
-
Explore example projects based on your DI setup:
The previous major version of the library supported only the Hilt DI Framework, so this guide is relevant only for projects using Hilt.
-
In your
build.gradlefile, update the library dependencies:ksp("com.uandcode:effects2-hilt-compiler:2.0.0") implementation("com.uandcode:effects2-hilt:2.0.0") // without Jetpack Compose implementation("com.uandcode:effects2-hilt-compose:2.0.0") // with Jetpack Compose
-
Update relevant import statements and package references:
// @HiltEffect annotation: import com.uandcode.effects.hilt.annotations.HiltEffect // lazyEffect delegate: import com.uandcode.effects.hilt.lazyEffect // EffectProvider: import com.uandcode.effects.hilt.compose.EffectProvider // EffectController: import com.uandcode.effects.core.EffectController // BoundEffectController: import com.uandcode.effects.core.BoundEffectController
-
Configure KSP in Multi-module projects.
For library modules only (not the application module), add the following KSP configuration in each
build.gradle.ktsfile::// my-android-lib/build.gradle.kts: ksp { arg("effects.processor.metadata", "generate") }
Learn the fundamentals of how the library works:
π Key Concepts
The library offers a consistent API across supported DI frameworks. However, there may be small differences, such as package names, annotations, or setup steps.
Read more about integrating Effects with Hilt:
π Effects + Hilt guide
Read more about integrating Effects with Koin:
π Effects + Koin guide
Using the library without any DI framework? Start here:
π Pure Effects (No DI) Guide
Get ready to learn more? Here is a dedicated page for you:
π Advanced Details