Android Components & View Models
Usually wiring up android components takes a lot of boilerplate. With Codegraft, you can skip the mundane and focus on the code that really matters.
// View Model @BindViewModel class MediumViewModel @Inject constructor( val client: MediumClient ) : ViewModel() { // TODO: Implement the ViewModel } // Fragment with a view model @AndroidInject class MediumFragment : Fragment() { @Inject lateinit var viewModels: ViewModelInstanceProvider private val viewModel: MediumViewModel by ::viewModels.delegate() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return inflater.inflate(R.layout.medium_fragment, container, false) } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) viewModel.apply { Log.d("MediumFragment", "medium view model = $this, medium client = $client}") } // TODO: Use the ViewModel } companion object { fun newInstance() = MediumFragment() } }
Gradle Setup
example project structure:
project/
app/
src/
build.gradle
lib/
src/
build.gradle
build.gradle
settings.gradle
project/build.gradle
buildscript { ext { codegraftVersion = '0.8.7' } dependencies { classpath "evovetech.codegraft:inject-plugin:${codegraftVersion}" } }
app/build.gradle
apply plugin: 'com.android.application' // ... apply plugin: 'codegraft.inject.android' // optional codegraft { // turn off incremental transform incremental = false }
lib/build.gradle
apply plugin: 'com.android.library' // ... apply plugin: 'codegraft.inject.android' // optional extensions dependencies { // Crashlytics extension api "evovetech.codegraft:inject-extension-crashlytics:${codegraftVersion}" // Okhttp extension api "evovetech.codegraft:inject-extension-okhttp3:${codegraftVersion}" // Retrofit extension api "evovetech.codegraft:inject-extension-retrofit2:${codegraftVersion}" // Realm extension api "evovetech.codegraft:inject-extension-realm:${codegraftVersion}" }
Basics
Codgraft uses dagger in a way that allows you to compose multiple kotlin modules together into a generated component for use in the application and also in tests. This was built specifically for android and its needs.
// App.kt @AndroidInject class App : Application(), BootApplication<AppComponent> { @Inject lateinit var fabric: Fabric override val bootstrap = bootstrap { fabricBuilderFunction1(Fabric.Builder::bootstrap) realmConfigurationBuilderFunction1(RealmConfiguration.Builder::bootstrap) okHttpClientApplicationBuilderFunction2(OkHttpClient.Builder::bootstrap) this@App } override fun onCreate() { super.onCreate() logStartup("onCreate") } fun logStartup(tag: String) { Log.d(tag, "startup") fabric.kits.forEach { Log.d(tag, "app -- fabric kit=$it") } } } fun Fabric.Builder.bootstrap(): Fabric { return kits(Crashlytics()) .build() } fun RealmConfiguration.Builder.bootstrap(): RealmConfiguration { return name("app.realm") .schemaVersion(1) .build() } fun OkHttpClient.Builder.bootstrap(app: AndroidApplication): OkHttpClient { // TODO return build() }
Behind the scenes, this code uses annotations to either generate source code or modify bytecode so that most of the boilerplate that is usually necessary is done for you.
In the case above, the boilerplate connections are added to the bytecode so that it just works.
// App.class -- decompiled @AndroidInject public final class App extends Application implements BootApplication, HasApplicationInjector, HasActivityInjector, HasSupportFragmentInjector { @Inject @NotNull public Fabric fabric; @NotNull private final Bootstrap bootstrap = Bootstrap_GenKt.bootstrap((Function1)(new Function1() { // $FF: synthetic method // $FF: bridge method public Object invoke(Object var1) { return this.invoke((Builder)var1); } @NotNull public final App invoke(@NotNull Builder $receiver) { Intrinsics.checkParameterIsNotNull($receiver, "$receiver"); $receiver.fabricBuilderFunction1((Function1)null.INSTANCE); $receiver.realmConfigurationBuilderFunction1((Function1)null.INSTANCE); $receiver.okHttpClientApplicationBuilderFunction2((Function2)null.INSTANCE); return App.this; } })); @NotNull public final Fabric getFabric() { Fabric var10000 = this.fabric; if (this.fabric == null) { Intrinsics.throwUninitializedPropertyAccessException("fabric"); } return var10000; } public final void setFabric(@NotNull Fabric var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.fabric = var1; } @NotNull public Bootstrap getBootstrap() { return this.bootstrap; } public void onCreate() { super.onCreate(); this.logStartup("onCreate"); } public final void logStartup(@NotNull String tag) { Intrinsics.checkParameterIsNotNull(tag, "tag"); Log.d(tag, "startup"); Fabric var10000 = this.fabric; if (this.fabric == null) { Intrinsics.throwUninitializedPropertyAccessException("fabric"); } Collection var8 = var10000.getKits(); Intrinsics.checkExpressionValueIsNotNull(var8, "fabric.kits"); Iterable $receiver$iv = (Iterable)var8; Iterator var3 = $receiver$iv.iterator(); while(var3.hasNext()) { Object element$iv = var3.next(); Kit it = (Kit)element$iv; Log.d(tag, "app -- fabric kit=" + it); } } public AndroidInjector getApplicationInjector() { return BootstrapMethods.getApplicationInjector(this); } public AndroidInjector activityInjector() { return BootstrapMethods.activityInjector(this); } public AndroidInjector supportFragmentInjector() { return BootstrapMethods.supportFragmentInjector(this); } public AppComponent getComponent() { return (AppComponent)this.bootstrap.getComponent(); } }
With the help of a content provider, we are able to enforce that the application is injected before onCreate() code is called.
// Content Provider class BootstrapProvider : EmptyContentProvider() { override fun onCreate(): Boolean { val TAG = "BootstrapProvider" val app = context as Application when (app) { is BootApplication<*> -> { Log.d(TAG, "Bootstrapping!!") val component = app.bootstrap.component if (component is HasApplicationInjector) { component.applicationInjector.inject(app) } } else -> { Log.d(TAG, "NO Bootstraps :(") } } return true } }
Usage
To enable Codegraft, we need a custom component @BootstrapComponent in order to allow the a single application component to be shared. Notice that we specify a dependency on OkhttpComponent. This actually lives in a separate library but we will still be able to incorporate it into our single application component without the normal hassle and with customization.
// MediumComponent.kt @BootstrapComponent( bootstrapDependencies = [OkhttpComponent::class], applicationModules = [MediumModule::class] ) interface MediumComponent { val client: MediumClient fun newUser(): MediumUserComponent.Builder }
We can then use normal dagger to provide the functionality.
@Subcomponent(modules = [MediumUserModule::class]) interface MediumUserComponent { val user: MediumCall<User> @Subcomponent.Builder interface Builder { @BindsInstance fun username(username: String): Builder fun build(): MediumUserComponent } } @Module class MediumUserModule { @Provides fun provideUserCall( username: String, client: MediumClient ): MediumCall<User> { return client.user(username) } } // MediumModule.kt private const val AuthKey = "Bearer ${BuildConfig.API_KEY}" @Module(subcomponents = [MediumUserComponent::class]) class MediumModule { @Provides @Singleton @Named("medium") fun provideOkhttp( app: AndroidApplication, okhttpBuilder: Builder ): OkHttpClient { return okhttpBuilder .addNetworkInterceptor { chain -> val request = chain.request() .newBuilder() .header("Authorization", AuthKey) .build() chain.proceed(request) } .build() } @Provides @Named("medium") fun provideGson(): Gson { return GsonBuilder() // TODO: .create() } @Provides @Singleton @Named("medium") fun provideRetrofit( app: AndroidApplication, @Named("medium") client: OkHttpClient, @Named("medium") gson: Gson ): Retrofit { return Retrofit.Builder() .client(client) .baseUrl("https://api.medium.com/v1/") .addConverterFactory(GsonConverterFactory.create(gson)) .build() } @Provides @Singleton fun provideMediumService( @Named("medium") retrofit: Retrofit ): MediumService { return retrofit.create(MediumService::class.java) } }
Below is the okhttp library setup that enables the end application component to customize it before usage. We can then share the root okhttp configuration in an easier way than usual.
/// Okhttp extension library (example) @BootstrapComponent( applicationModules = [OkhttpModule::class], bootstrapModules = [OkhttpBootstrapModule::class], autoInclude = false ) interface OkhttpComponent { val okhttp: Okhttp } typealias OkHttpInit = OkHttpClient.Builder.(app: Application) -> OkHttpClient @Module class OkhttpBootstrapModule { @Provides @BootScope fun provideDefaultOkhttp( @BootScope app: Application, @Named("okhttp") init: OkHttpInit ): OkHttpClient { val builder = Builder() builder.init(app) return builder.build() } } @Module class OkhttpModule { @Provides fun provideDefaultOkhttpBuilder( okhttp: OkHttpClient ): Builder { return okhttp.newBuilder() } } @Singleton @BindPlugin class Okhttp @Inject constructor( private val okhttpProvider: Provider<OkHttpClient>, private val okhttpBuilderProvider: Provider<OkHttpClient.Builder> ) : Plugin { val client: OkHttpClient get() = okhttpProvider.get() val builder: OkHttpClient.Builder get() = okhttpBuilderProvider.get() }
Putting it together (codegen)
The ultimate outcome is a generated bootstrap component & builder that is intended to allow configuration (and sometime eager initialization in the case of crash framework, etc.) of the application component which will be a singleton.
(User bootstrap)
/// App.kt @AndroidInject class App : Application(), BootApplication<AppComponent> { // Run before onCreate() override val bootstrap = bootstrap { okHttpClientApplicationBuilderFunction2(OkHttpClient.Builder::bootstrap) this@App } } fun OkHttpClient.Builder.bootstrap(app: AndroidApplication): OkHttpClient { // TODO return build() }
(Generated)
/// Generated typealias BootstrapInit = BootComponent.Builder.() -> AndroidApplication fun bootstrap( init: BootstrapInit ): Bootstrap<AppComponent> = Bootstrap { DaggerBootComponent.builder() .build(init) } private fun BootComponent.Builder.build( init: BootstrapInit ): AppComponent = application(init()) .build() .appComponent
/// Generated @BootScope @Component( modules = BootModule.class ) public interface BootComponent { AppComponent getAppComponent(); @Component.Builder interface Builder { @BindsInstance Builder application(Application application); @BindsInstance Builder okHttpClientApplicationBuilderFunction2( @Named("okhttp") Function2<? super OkHttpClient.Builder, ? super Application, ? extends OkHttpClient> okHttpClientApplicationBuilderFunction2); @BindsInstance Builder okhttpModule(@Nullable OkhttpModule okhttpModule); @BindsInstance Builder mediumModule(@Nullable MediumModule mediumModule); Builder okhttpBootstrapModule(OkhttpBootstrapModule okhttpBootstrapModule); BootComponent build(); } } @Module( includes = { RealmBootstrapModule.class, OkhttpBootstrapModule.class, CrashesBootstrapModule.class, AppModule.class } ) final class BootModule { @Provides @BootScope AppComponent provideComponent(@Nullable OkhttpModule okhttpModule, @Nullable MediumModule mediumModule, AppComponent_BootData bootData) { AppComponent.Builder builder = DaggerAppComponent.builder(); if (okhttpModule != null) { builder.okhttpModule(okhttpModule); } if (mediumModule != null) { builder.mediumModule(mediumModule); } builder.bootData(bootData); return builder.build(); } } @Singleton @Component( modules = { AppComponent_BootData.class, MainActivity_Module.class, App_Module.class, MediumFragment_Module.class, MediumActivity_Module.class, MainFragment_Module.class, PlaidViewModel_Module.class, MediumViewModel_Module.class, MainViewModel_Module.class, Okhttp_Module.class } ) public interface AppComponent extends ApplicationInjectorComponent, ActivityInjectorComponent, SupportFragmentInjectorComponent, BaseComponent { @Override AndroidInjector<Application> getApplicationInjector(); @Override AndroidInjector<Activity> activityInjector(); @Override AndroidInjector<Fragment> supportFragmentInjector(); @Override Application getApplication(); @Override Plugins getPlugins(); OkhttpComponent getOkhttpComponent(); MediumComponent getMediumComponent(); ViewModelComponent getViewModelComponent(); @Component.Builder interface Builder { Builder okhttpModule(OkhttpModule okhttpModule); Builder mediumModule(MediumModule mediumModule); Builder bootData(AppComponent_BootData bootData); AppComponent build(); } } @Module( includes = { OkhttpComponent_Module.class, MediumComponent_Module.class, AndroidInjectApplicationModule.class, ViewModelComponent_Module.class, AndroidInjectActivityModule.class, AndroidInjectSupportFragmentModule.class, PluginModule.class } ) class AppComponent_BootData { private final OkhttpComponent_BootData okhttpComponent_BootData; private final BaseComponent_BootData baseComponent_BootData; @Inject AppComponent_BootData(OkhttpComponent_BootData okhttpComponent_BootData, BaseComponent_BootData baseComponent_BootData) { this.okhttpComponent_BootData = okhttpComponent_BootData; this.baseComponent_BootData = baseComponent_BootData; } @Provides @Singleton Application getApplication() { return realmComponent_BootData.getApplication(); } @Provides @Singleton OkHttpClient getOkHttpClient() { return okhttpComponent_BootData.getOkHttpClient(); } }