$30 off During Our Annual Pro Sale. View Details »

Droidcon Italy: Working with Dagger and Kotlin

Ash Davies
October 05, 2020

Droidcon Italy: Working with Dagger and Kotlin

Dagger has become one of the most widely used libraries in Android development, and is often one of the first to be thought of when bootstrapping a new project, but there are still many nuances and caveats that often get overlooked!

Many questions arose after the adoption of Kotlin on how to continue following best practices from Java, so whether you’re using dagger-android or vanilla Dagger, I’ll go through some tips and tricks to make sure you’re using Dagger to its best capacity.

Ash Davies

October 05, 2020
Tweet

More Decks by Ash Davies

Other Decks in Programming

Transcript

  1. // Car.kt class Car { private val engine = Engine()

    fun start() { engine.start() } } fun main() { val car = Car() car.start() }
  2. // Car.kt class Car(private val engine: Engine) { fun start()

    { engine.start() } } fun main() { val engine = Engine() val car = Car(engine) car.start() }
  3. // ServiceLocator.kt object ServiceLocator { val engine: Engine get() =

    Engine() } // Car.kt class Car { private val engine = ServiceLocator.engine fun start() { engine.start() } }
  4. // CarModule.kt val solarEngine get() = SolarEngine() // Car.kt class

    Car(private val engine = solarEngine) { fun start() { engine.start() } }
  5. @Singleton javax.inject Annotate an @Provides method or injectable class with

    @Singleton. The graph will use a single instance of the value for all of its clients. ⚠ Tangent Alert
  6. @Singleton class Singleton { private Singleton instance; public Helper getInstance()

    { if (instance == null) { synchronized (this) { if (instance == null) { instance = new Singleton(); } } } return instance; } } Double-checked locking ⚠ Tangent Alert
  7. Kotlin private val viewModel by lazy(NONE) { LazyViewModel() } fun

    <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> = when (mode) { LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) } Lazy ⚠ Tangent Alert
  8. Favour @Reusable Use @Scope for stateful dependencies ⚠ Tangent Alert

    • Great for expensive dependencies • Work great in single thread environments • Not guaranteed same instance in multiple threads • Prefer to keep your Dagger graph stateless
  9. Constructor Injection javax.inject.Named public final class Game { private final

    Player player1; private final Player player2; @Inject public Game( @Named("P1") Player player1, @Named("P2") Player player2) { super(); this.player1 = player1; this.player2 = player2; } }
  10. Field Injection: lateinit var javax.inject.Named class Game @Inject constructor() {

    @Inject @Named("P1") private val player1: Player, @Inject @Named("P2") private val player2: Player )
  11. • Mutable (lateinit) properties • Annotation target unclear ‍♂ •

    Difficult to configure tests Field vs Constructor Injection • Immutable • Easy to use • Reliable injection • Compilation safety
  12. • instantiateApplication(ClassLoader, String) • instantiateActivity(ClassLoader, String, Intent) • instantiateClassLoader(ClassLoader, ApplicationInfo)

    • instantiateService(ClassLoader, String, Intent) • instantiateReceiver(ClassLoader, String, Intent) • instantiateProvider(ClassLoader, String) Android 9 (API 28) AppComponentFactory
  13. class GameFragmentFactory : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className:

    String): Fragment { return super.instantiate(classLoader, className) } } FragmentFactory androidx.fragment.app
  14. LayoutInflater.Factory2 github.com/square/AssistedInject class ImageLoaderView @AssistedInject constructor( @Assisted context: Context, @Assisted

    attrs: AttributeSet, private val loader: ImageLoader ) : ImageView(context, attrs) { @AssistedInject.Factory interface Factory { fun create(context: Context, attrs: AttributeSet): ImageLoaderView } }
  15. LayoutInflater.Factory2 github.com/square/AssistedInject class ApplicationLayoutInflaterFactory @Inject constructor( private val imageLoaderFactory: ImageLoaderView.Factory

    ) : LayoutInflater.Factory { override fun onCreateView(name: String, context: Context,attrs: AttributeSet): View? { if (name == ImageLoaderView::class.java.name) { return imageLoaderFactory.create(context, attrs) } return null } }
  16. Field Injection: lateinit var javax.inject.Named class Game @Inject constructor() {

    @Inject @Named("P1") private val player1: Player, @Inject @Named("P2") private val player2: Player )
  17. Field Injection: lateinit var javax.inject.Named public final class Game {

    @Inject public Player player1; @Inject public Player player2; @Named("P1") public static void player1$annotations() {} public final Player getPlayer1() { ... } public final void setPlayer1(Player var1) {...} ... }
  18. Field Injection: lateinit var javax.inject.Named class Game @Inject constructor() {

    @Inject @field:Named("P1") lateinit var player1: Player @Inject @field:Named("P2") lateinit var player2: Player } @field: @set: @get: @param: @property: @setparam: @receiver: @delegate:
  19. Dagger 2.25.2 Released 23rd Oct 2019 • Qualifier annotations on

    fields can now be understood without the need for @field:MyQualifier (646e033) • @Module object classes no longer need @JvmStatic on the provides methods. (0da2180)
  20. Dagger 2.28 Released 10th Jun 2020 • Using a redundant

    field: site target in injected properties • Using a redundant @JvmStatic in object modules. • Annotating companion objects with @Module
  21. @Module Why can’t @Binds and instance @Provides methods go in

    the same module? @Module abstract class GameModule { @Binds abstract GameController playerOne(PlayerOne playerOne); @Provides static InputChannel inputChannel(GameController gameController) { return ControllerInputChannel(gameController); } } dagger.dev/dev-guide/faq.html#why-cant-binds-and-instance-provides-methods-go-in-the-same-module
  22. @Module Why can’t @Binds and instance @Provides methods go in

    the same module? @Module abstract class GameModule { @Binds abstract fun gameController(playerOne: PlayerOne): GameController @Module companion object { @Provides @JvmStatic fun inputChannel(gameController: GameController): InputChannel { return ControllerInputChannel(gameController) } } }
  23. @Module Dagger 2.25.2 - 2.26 @Module interface GameModule { @Binds

    fun gameController(playerOne: PlayerOne): GameController companion object { @Provides fun inputChannel(gameController: GameController): InputChannel { return ControllerInputChannel(gameController) } } }
  24. @Module Inline Method Body’s @Module object GameModule { @Provides fun

    gameController() = PlayerOne() } @Module object GameModule { @Provides fun gameController(): GameController = PlayerOne() }
  25. Generics<T> Java Interoperability • Wildcard types not available in Kotlin*

    • Declaration-site variance • Type projections
  26. interface Collection<E> extends Iterable<E> { boolean addAll(Collection<? extends E> collection);

    } https://kotlinlang.org/docs/reference/generics.html#variance
  27. List<String> strings = new ArrayList<String>(); List<Object> objs = strings; objs.add(1);

    String string = strings.get(0); // https://kotlinlang.org/docs/reference/generics.html#variance
  28. interface Collection<E> extends Iterable<E> { boolean addAll(Collection<? extends E> collection);

    } https://kotlinlang.org/docs/reference/generics.html#variance
  29. // ListAdapter.kt class ListAdapter @Inject constructor(strings: Set<String>)} // ListModule.kt @Module

    object ListModule { @IntoSet @Provides fun hello(): String = "Hello" @IntoSet @Provides fun world(): String = "World" }
  30. public final class ListAdapter { @Inject public ListAdapter(@NotNull List<? extends

    String> strings) { Intrinsics.checkParameterIsNotNull(strings, "strings"); super(); } } Build Failed…
  31. • Wrapping types can introduce runtime overhead • Performance worse

    for primitive types • Initialised with single backing property • Inline classes represented by backing field at runtime • Sometimes represented as boxed type... Kotlin Inline classes
  32. • Dagger recognises inline class as it's backing type •

    Module @Provide not complex enough to require wrapper • @Inject sites not complex enough to require wrapper • Can cause problems if backing type not qualified • Operates the same for typealias Kotlin ❌ Inline classes
  33. • Introduced at Google IO Dev Keynote 2018 • Bootstrap

    Modern Android Development • Opinionated implementations • Break up support libraries • androidx namespace Jetpack ViewModel ⚠ Tangent Alert
  34. ⚠ Caution: A ViewModel must never reference a View, Lifecycle,

    or any class that may hold a reference to the activity Context.
  35. class GameViewModel @Inject constructor() : ViewModel() { } @MapKey @Retention(RUNTIME)

    annotation class ViewModelKey(val value: KClass<out ViewModel>) @Module interface GameViewModelModule { @Binds @IntoMap @ViewModelKey(GameViewModel::class) fun model(model: GameViewModel): ViewModel } I’ll cover @ViewModelInject soon :)
  36. class ViewModelFactory @Inject constructor( private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards

    Provider<ViewModel>> ) : ViewModelProvider.Factory { override fun <T : ViewModel> create(kls: Class<T>): T { var creator: Provider<out ViewModel>? = creators[kls] creator ?: creators.keys .firstOrNull(kls::isAssignableFrom) ?.apply { creator = creators[this] } creator ?: throw IllegalArgumentException("Unrecognised class $kls") return creator.get() as T } }
  37. class GameActivity : DaggerAppCompatActivity { private val model: GameViewModel by

    viewModels { factory } @Inject internal lateinit var factory: ViewModelFactory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... } } androidx.activity:activity-ktx:1.0.0
  38. internal class ViewModelFactory( private val provider: Provider<out ViewModel> ) :

    ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T = try { provider.get() as T } catch (exception: ClassCastException) { throw IllegalArgumentException( "Class ${modelClass.name} is not supported by this factory", exception ) } }
  39. class GameActivity : DaggerAppCompatActivity { private val model: GameViewModel by

    viewModels { factory } @Inject internal lateinit var factory: ViewModelFactory<GameViewModel> override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... } }
  40. class Thermosiphon implements Pump { private final Heater heater; @Inject

    Thermosiphon(Heater heater) { this.heater = heater; } ... } class CoffeeMaker { @Inject Heater heater; @Inject Pump pump; ... } @Module interface HeaterModule { @Binds abstract Heater bindHeater( ElectricHeater impl ); }
  41. @Module @InstallIn(SingletonComponent::class) object class FooModule { // SingletonComponent has access

    to Application binding @Provides fun provideBar(app: Application): Bar {...} }
  42. @HiltAndroidApp class GameApplication : BaseApplication() { @Inject lateinit var game:

    Game override fun onCreate() { super.onCreate() // Injection happens here // Do you want to play a $game? } }
  43. class GameViewModel @ViewModelInject constructor( @Assisted handle: SavedStateHandle, listing: GameListing )

    @AndroidEntryPoint class GameActivity : AppCompatActivity { private val viewModel: GameViewModel by viewModels() }
  44. Manuel Vivo: An Opinionated Guide to Dependency Injection on Android

    youtube.com/watch?v=o-ins1nvbDg Jake Wharton: Helping Dagger Help You jakewharton.com/helping-dagger-help-you/ Dagger: Kotlin Dagger Best Practices github.com/google/dagger/issues/900 Fred Porciúncula: Dagger 2 Official Guidelines proandroiddev.com/dagger-2-on-android-the-official-guidelines-you-should-be-following-2607fd6c002e Zac Sweers: Dagger Party tricks zacsweers.dev/dagger-party-tricks-deferred-okhttp-init/ Further Reading