# Using an Activity from a Hilt ViewModel

*👋 Hi, this is P.Y., I work as an Android Engineer at* [*Block*](https://block.xyz/)*. This blog shares a bit of hackery to be able to access an activity instance within a Hilt ViewModel. If you come up with other interesting ways to do this, let me know* [*on Twitter*](https://twitter.com/Piwai)*! If you're mad because you think I'm encouraging bad practices, try yoga.*

# I need that god object

I've been playing with Hilt's support for view models in a small app, and needed my view model to start a sharing activity:

```kotlin
@HiltViewModel
class MyCuteLittleViewModel @Inject constructor(
) : ViewModel() {

  // ... some code that invokes share()

  private fun share(content: String) {
    val intent = Intent(Intent.ACTION_SEND).apply {
      type = "text/plain"
      putExtra(Intent.EXTRA_TEXT, content)
    }
    val chooserIntent = Intent.createChooser(intent, "Share with…")

    val activity = TODO("Need an activity here!")
    activity.startActivity(chooserIntent)
  }
}
```

View models are retained across activity config changes, so the activity isn't injectable, which makes total sense: injecting the activity in a view model would lead to leaks on config changes.

Unfortunately, the `Activity` class provides a lot of utility so it's fairly common to need access to it (see [God object](https://en.wikipedia.org/wiki/God_object)).

Most online resources recommend moving the code to the activity or a collaborator that has access to it, have that listen to events that indicate the action to perform, then send the events from the ViewModel.

# You're not the boss of me.

I don't care for these "best" practices. I want that code right there where it's used, and I don't want unnecessary decoupling (also I pinky swear I'll write unit tests tomorrow).

Anyway, here's a little bit of Hilt hackery to support this without changing any `Activity` code.

First, let's create a `CurrentActivityProvider` scoped to `@ActivityRetainedScoped`, which will be in charge of holding the current activity instance:

```kotlin
@ActivityRetainedScoped
class CurrentActivityProvider @Inject constructor() {

  // TODO Set and clear currentActivity
  private var currentActivity: Activity? = null

  fun <T> withActivity(block: Activity.() -> T) : T {
    checkMainThread()
    val activity = currentActivity
    check(activity != null) {
      "Don't call this after the activity is finished!"
    }
    return activity.block()
  }
}
```

Then we can use it as needed. Notice that `withActivity()` makes it slightly harder to store the activity instance in the wrong place accidentally:

```kotlin
@HiltViewModel
class MyCuteLittleViewModel @Inject constructor(
  private val activityProvider: CurrentActivityProvider
) : ViewModel() {

  private fun share(content: String) {
    // ...
    activityProvider.withActivity {
      startActivity(chooserIntent)
    }
  }
}
```

Now we need to set up `CurrentActivityProvider.currentActivity` for each `ActivityRetainedComponent` scope. For that, we create an entry point scoped to the activity (`ActivityComponent`) which will provide access to the `CurrentActivityProvider` (which lives in a parent `ActivityRetainedComponent` scope). The entry point:

```kotlin
@EntryPoint
@InstallIn(ActivityComponent::class)
interface ActivityProviderEntryPoint {
  val activityProvider: CurrentActivityProvider
}
```

Now we can retrieve the scoped activity provider from an activity instance with:

```kotlin
val entryPoint: ActivityProviderEntryPoint =
  EntryPointAccessors.fromActivity(this)
val activityProvider = entryPoint.activityProvider
```

This only works if the activity is Hilt-aware, so let's check that it implements `GeneratedComponentManagerHolder` (🤫 it's in Hilt's internal package but it's also public so 🤷‍♂️) and let's make a small `Activity.withProvider()` utility for that:

```kotlin
activity.withProvider { activityProvider ->
  // TODO
}

    private fun Activity.withProvider(
      block: CurrentActivityProvider.() -> Unit
    ) {
      if (this is GeneratedComponentManagerHolder) {
        val entryPoint: ActivityProviderEntryPoint =
          EntryPointAccessors.fromActivity(this)
        val provider = entryPoint.activityProvider
        provider.block()
      }
    }
```

> Note: Android apps can have multiple activities in created state at the same time. The code here supports that by relying on the `ActivityRetainedComponent` scope which will give us a new component for each activity in the stack, but still return the same logical component when an activity is recreated through a config change.

Now let's add methods to update the activity reference on lifecycle changes:

```kotlin
@ActivityRetainedScoped
class CurrentActivityProvider @Inject constructor() {

  private var currentActivity: Activity? = null

  fun <T> withActivity(block: Activity.() -> T) : T { /* ... */  }

  companion object {
    private fun Activity.withProvider(
      block: CurrentActivityProvider.() -> Unit
    ) { /* ... */ }

    fun onActivityCreated(activity: Activity) {
      activity.withProvider {
        currentActivity = activity
      }
    }

    fun onActivityDestroyed(activity: Activity) {
      activity.withProvider {
        if (currentActivity === activity) {
          currentActivity = null
        }
      }
    }
  }
}
```

And finally let's hook the lifecycle callbacks from my `Application` class:

```kotlin
@HiltAndroidApp
class MyCuteLittleApp : Application() {

  override fun onCreate() {
    super.onCreate()
    
    registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
      override fun onActivityCreated(
        activity: Activity,
        savedInstanceState: Bundle?
      ) {
        CurrentActivityProvider.onActivityCreated(activity)
      }

      override fun onActivityDestroyed(activity: Activity) {
        CurrentActivityProvider.onActivityDestroyed(activity)
      }
    })
  }
}
```

With this, we can now inject `CurrentActivityProvider` in any `ActivityRetainedComponent` scope (as well as lower scopes) and easily access the activity with `activityProvider.withActivity()`.

# Testing testing 1 2 3 🎤

To make `MyCuteLittleViewModel` easier to test we can move the sharing responsibility to an injected collaborator, e.g. `Sharer`:

```kotlin
interface Sharer {
  fun share(content: String)
}

class ActivitySharer @Inject constructor(
  private val activityProvider: CurrentActivityProvider
) : Sharer {
  override fun share(content: String) {
    // ...
    activityProvider.withActivity {
      startActivity(chooserIntent)
    }
  }
}

@Module
@InstallIn(ActivityRetainedComponent::class)
interface SharerModule {
  @Binds fun bindSharer(sharer: ActivitySharer): Sharer
}
```

> Header image generated by DALL-E, prompt: "sword held by an Android with glasses, 3d render"
