Skip to content

Whetstone is a dependency injection framework for Android that greatly simplifies working with Dagger 2 using all the powers of Anvil

License

Notifications You must be signed in to change notification settings

deliveryhero/whetstone

Repository files navigation

Maven Central

Whetstone

"An Anvil forges a Dagger. A Whetstone sharpens it. And when you're not planning on using your Dagger, you may keep it in something that rhymes with kilt." — Tiago Cunha.

Whetstone provides a simplified way to incorporate Dagger and Anvil into an Android application.

The goals of Whetstone are:

  • To simplify Dagger-related infrastructure for Android apps.
  • To create a standard set of components and scopes to ease setup, but allowing customizations.

Why would you use Whetstone instead of Hilt?

  • All generated code is in Kotlin, which can have significant benefits in a Kotlin only codebase
  • Whetstone avoids KAPT completely for performance reasons by taking advantage of Anvil compiler.
  • Whetstone is extensible by using the powers of Dagger and Anvil.
  • Whetstone significantly reduces boiler plate.
  • Whetstone doesn't do bytecode manipulation for complementing classes. Hilt does.
  • Summarily, while philosophies are similar, whetstone is relatively easier to work with ;).

Getting Started

First you must apply whetstone plugin in the build.gradle file of any module that requires dependency injection:

plugins {
    id("com.deliveryhero.whetstone").version("<latest version>")
}

Or you can use the old way to apply a plugin:

// In root build.gradle.kts
buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath("com.deliveryhero.whetstone:whetstone-gradle-plugin:${latest_version}")
  }
}

// In individual modules
apply(plugin = "com.deliveryhero.whetstone")

This automatically configures Dagger and Anvil, and also adds the necessary whetstone dependencies for you.

Basic Usage

To use whetstone, you must initialize it in your Application class.

@ContributesAppInjector(generateAppComponent = true)
class MyApplication : Application(), ApplicationComponentOwner {

    override val applicationComponent by lazy {
        GeneratedApplicationComponent.create(this)
    }
}

Note: For more sophisticated use cases, the generated app component might not be sufficient for you. In such scenario, you can disable automatic generation of app component, and create your own instead. An example may look like this:

@Singleton // Optional. Can be omitted if you never use this annotation
@SingleIn(ApplicationScope::class)
@MergeComponent(ApplicationScope::class)
interface MyApplicationComponent : ApplicationComponent {

    @Component.Factory
    interface Factory {
        fun create(
            @BindsInstance application: Application, // this is necessary for whetstone to set things up properly
            // ...
        ): MyApplicationComponent
    }
}

After that, you can easily inject into any Android class (see below).

Guide

Unlike traditional Dagger, you do not need to define or instantiate Dagger components directly. Instead, we offer predefined components that are generated for you. Whetstone comes with a built-in set of components (and corresponding scope annotations) that are automatically integrated to the Android Framework. As expected, a binding in a child component can have dependencies on any binding in an ancestor component.

Component Lifecycle

Component lifetimes are generally bounded by the creation and destruction of a corresponding instance of an important event. The table below lists the scope annotation and bounded lifetime for each component.

Component Scope Created At Destroyed At
ApplicationComponent @ApplicationScope Application#onCreate Application#onTerminate
ActivityComponent @ActivityScope Activity#onCreate Activity#onDestroy
FragmentComponent @FragmentScope FragmentFactory#instantiate Fragment#onDestroy
ViewModelComponent @ViewModelScope ViewModelProvider.Factory#create ViewModel#onCleared
ViewComponent @ViewScope View#init View#finalize

whetstone-scopes

Application

Applications support field/method injection with Whetstone. Constructor injection is not supported here because the instantiation of applications is completely managed by the system

@ContributesAppInjector
class MyApplication : Application(), ApplicationComponentOwner {

    override val applicationComponent by lazy {
        TODO("Create application component.")
    }

    @Inject
    lateinit var dependency: MyDependency

    fun onCreate() {
        Whetstone.inject(this)
        super.onCreate()
    }
}

Activity

Similar to applications, activities only support field/method injection

@ContributesActivityInjector
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var dependency: MyDependency

    // Get the contributed ViewModel
    // We automatically handle process death and saved state handle wiring
    private val viewModel by injectedViewModel<MyViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        Whetstone.inject(this)
        super.onCreate(savedInstanceState)
    }
}

Service

Services should be generally avoided when possible. For most cases, work manager can be a great alternative and is highly recommended. See the workmanager section for more details about how to use it with Whetstone

@ContributesServiceInjector
class MyService : Service() {

    @Inject
    lateinit var dependency: MyDependency

    override fun onCreate() {
        Whetstone.inject(this)
        super.onCreate()
    }
}

View

Disclaimer: View injection should be avoided by all means. This provision is considered legacy and may be completely removed in a later version of Whetstone.

@ContributesViewInjector
class MyView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
) : View(context, attrs) {

    @Inject
    lateinit var dependency: MainDependency

    init {
        if (!isInEditMode) {
            Whetstone.inject(this)
        }
    }
}

Fragments

Fragments support only construction injection, exclusively. This is possible because we are able to hook into the system to influence exactly how fragments should be created. To achieve this, the activity hosting the fragment must install Whetstone's fragment factory.

class MyActivity : Activity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        Whetstone.inject(this)
        super.onCreate(savedInstanceState)
    }
}

Then you're able to use the injected fragments with as many constructor arguments as necessary, as long as all these dependencies can be satisfied by dependency injection.

@ContributesFragment
class MyFragment @Inject constructor(
    private val dependency: MyDependency,
    private val anotherDependency: AnotherDependency,
): Fragment() {

    // Get the contributed ViewModel
    // We automatically handle process death and saved state handle wiring
    private val viewModel by injectedViewModel<MyViewModel>()
    private val activityViewModel by injectedActivityViewModel<ActivityViewModel>()
}

Note that all injected fragments must be created via the fragment manager. For example:

val myFragment = fragmentManager.instantiate<MyFragment>()

For fragments that don't require any external dependencies, the simple no-arg constructor can still be used, and we gracefully fallback to the default behavior

Important: A Fragment should NEVER be scoped. The Android Framework controls the Lifecycle of ALL Fragments.

ViewModels

Like fragments, view models also support full constructor injection

@ContributesViewModel
class MyViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
): ViewModel()

Important: A ViewModel should NEVER be scoped. The Android Framework controls the Lifecycle of ALL ViewModels.

WorkManager

Workmanager integration is an extra add-on, and must be enabled explicitly in your build.gradle file before use:

whetstone {
    addOns.workManager.set(true)
}

This will automatically install Whetstone's worker factory (replacing the default factory), so that you can immediately start taking advantage of injected workers

@ContributesWorker
class UploadWorker @Inject constructor(
    @ForScope(WorkerScope::class) context: Context,
    workerParameters: WorkerParameters,
    private val dependency: MyDependency,
): Worker(appContext, workerParameters)

To disable automatic initialization, you can remove the initializer from your AndroidManifest.xml

<provider
   android:name="androidx.startup.InitializationProvider"
   android:authorities="${applicationId}.androidx-startup"
   android:exported="false"
   tools:node="merge">
   <meta-data
       android:name="com.deliveryhero.whetstone.worker.WhetstoneWorkerInitializer"
       android:value="androidx.startup"
       tools:node="remove" />
</provider>

However, you must make sure to install Whetstone's worker factory before the first call to WorkManager#getInstance to avoid breaking the integration. Whetstone provides an injectable WorkerFactory that can be used to configure the work manager. For example, you can update your application class to implement work manager's Configuration.Provider and supply Whetstone's WorkerFactory to the configuration builder See the official documentation for more details

Compose

Compose integration is an extra add-on, and must be enabled explicitly in your build.gradle file before use:

whetstone {
    addOns.compose.set(true)
}

Currently, this artefact only exposes APIs for injecting ViewModels that have been contributed to Whetstone

@Composable
fun MyScreen(viewModel: MyViewModel = injectedViewModel()) {
    // injectedViewModel takes care of providing the VM instance directly to this function
}

License

Copyright 2021 Delivery Hero, GmbH.

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.