diff --git a/android/activity/build.gradle b/android/activity/build.gradle
new file mode 100644
index 00000000..c22d7dbb
--- /dev/null
+++ b/android/activity/build.gradle
@@ -0,0 +1,28 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ compileSdk = 30
+ defaultConfig {
+ minSdk = 14
+ }
+ buildFeatures {
+ buildConfig = false
+ resValues = false
+ }
+}
+
+dependencies {
+ api("androidx.activity:activity-ktx:1.2.2")
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions.jvmTarget = "1.8"
+}
+
+ext.groupId = 'me.tatarka.inject.android'
+ext.artifactId = 'kotlin-inject-viewmodel'
+
+//apply from: "$rootProject.projectDir/publish.gradle"
\ No newline at end of file
diff --git a/android/activity/src/main/AndroidManifest.xml b/android/activity/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..3d893557
--- /dev/null
+++ b/android/activity/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/android/activity/src/main/java/me/tatarka/inject/android/activity/ActivityViewModelLazy.kt b/android/activity/src/main/java/me/tatarka/inject/android/activity/ActivityViewModelLazy.kt
new file mode 100644
index 00000000..09f543f2
--- /dev/null
+++ b/android/activity/src/main/java/me/tatarka/inject/android/activity/ActivityViewModelLazy.kt
@@ -0,0 +1,71 @@
+package me.tatarka.inject.android.activity
+
+import androidx.activity.ComponentActivity
+import androidx.annotation.MainThread
+import androidx.lifecycle.AbstractSavedStateViewModelFactory
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelLazy
+import androidx.lifecycle.ViewModelProvider
+
+/**
+ * Returns a [Lazy] delegate to access the ComponentActivity's ViewModel, the instance returned from [factory] will be
+ * used to create [ViewModel] first time.
+ *
+ * ```
+ * @Inject class MyViewModel
+ *
+ * @Component interface ActivityComponent {
+ * val myViewModel: () -> MyViewModel
+ * }
+ *
+ * class MyComponentActivity : ComponentActivity() {
+ * val component by lazy { ActivityComponent::class.create() }
+ * val viewModel by viewModels { component.myViewModel() }
+ * }
+ * ```
+ *
+ * This property can be accessed only after the Activity is attached to the Application,
+ * and access prior to that will result in IllegalArgumentException.
+ */
+@MainThread
+inline fun ComponentActivity.viewModels(crossinline factory: () -> VM): Lazy =
+ ViewModelLazy(VM::class, { viewModelStore }, {
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T = factory() as T
+ }
+ })
+
+/**
+ * Returns a [Lazy] delegate to access the ComponentActivity's ViewModel, the instance returned from [factory] will be
+ * used to create [ViewModel] first time.
+ *
+ * ```
+ * @Inject class MyViewModel(handle: SavedStateHandle)
+ *
+ * @Component interface ActivityComponent {
+ * val myViewModel: (SavedStateHandle) -> MyViewModel
+ * }
+ *
+ * class MyComponentActivity : ComponentActivity() {
+ * val component by lazy { ActivityComponent::class.create() }
+ * val viewModel by viewModels { handle -> component.myViewModel(handle) }
+ * }
+ * ```
+ *
+ * This property can be accessed only after the Activity is attached to the Application,
+ * and access prior to that will result in IllegalArgumentException.
+ */
+@MainThread
+inline fun ComponentActivity.viewModels(crossinline factory: (SavedStateHandle) -> VM): Lazy =
+ ViewModelLazy(VM::class, { viewModelStore }, {
+ object : AbstractSavedStateViewModelFactory(this, intent?.extras) {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(
+ key: String,
+ modelClass: Class,
+ handle: SavedStateHandle
+ ): T = factory(handle) as T
+ }
+ })
\ No newline at end of file
diff --git a/android/fragment-factory-compiler/build.gradle b/android/fragment-factory-compiler/build.gradle
new file mode 100644
index 00000000..9e6d269c
--- /dev/null
+++ b/android/fragment-factory-compiler/build.gradle
@@ -0,0 +1,24 @@
+plugins {
+ id("org.jetbrains.kotlin.jvm")
+}
+
+dependencies {
+ implementation(project(":kotlin-inject-compiler:ksp"))
+ implementation("org.jetbrains.kotlin:kotlin-stdlib")
+ implementation("com.squareup:kotlinpoet:1.5.0")
+ // for access to CompilationErrorException until ksp properly fails on errors
+ implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlin_version")
+ implementation("com.google.devtools.ksp:symbol-processing-api:$ksp_version")
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions.jvmTarget = "1.8"
+}
+
+ext.groupId = 'me.tatarka.inject.android'
+ext.artifactId = 'kotlin-inject-fragment-factory'
+
+apply from: "$rootProject.projectDir/publish.gradle"
\ No newline at end of file
diff --git a/android/fragment-factory-compiler/src/main/kotlin/me/tatarka/inject/android/fragment/compiler/FragmentFactoryProcessor.kt b/android/fragment-factory-compiler/src/main/kotlin/me/tatarka/inject/android/fragment/compiler/FragmentFactoryProcessor.kt
new file mode 100644
index 00000000..e6e5eb30
--- /dev/null
+++ b/android/fragment-factory-compiler/src/main/kotlin/me/tatarka/inject/android/fragment/compiler/FragmentFactoryProcessor.kt
@@ -0,0 +1,120 @@
+package me.tatarka.inject.android.fragment.compiler
+
+import com.google.devtools.ksp.getAllSuperTypes
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.symbol.KSAnnotated
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSDeclaration
+import com.squareup.kotlinpoet.ClassName
+import com.squareup.kotlinpoet.FileSpec
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.LambdaTypeName
+import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
+import com.squareup.kotlinpoet.PropertySpec
+import com.squareup.kotlinpoet.TypeSpec
+import me.tatarka.inject.compiler.ksp.asClassName
+import me.tatarka.inject.compiler.ksp.findAnnotation
+import java.util.Locale
+
+class FragmentFactoryProcessor : SymbolProcessor {
+ private lateinit var codeGenerator: CodeGenerator
+ private lateinit var logger: KSPLogger
+
+ override fun init(
+ options: Map,
+ kotlinVersion: KotlinVersion,
+ codeGenerator: CodeGenerator,
+ logger: KSPLogger
+ ) {
+ this.codeGenerator = codeGenerator
+ this.logger = logger
+ }
+
+ override fun process(resolver: Resolver): List {
+ val injectFragments = resolver.getSymbolsWithAnnotation(INJECT.canonicalName).mapNotNull {
+ if (it is KSClassDeclaration && it.extendsFragment()) it else null
+ }.groupBy { fragment ->
+ val intoComponent = fragment.findAnnotation(INTO_COMPONENT.packageName, INTO_COMPONENT.simpleName)
+ intoComponent?.arguments?.first { it.name?.asString() == "value" }?.value
+ }
+ for (element in resolver.getSymbolsWithAnnotation(GENERATE_FRAGMENT_FACTORY.canonicalName)) {
+ if (element !is KSDeclaration) continue
+ val generateFragmentFactory =
+ element.findAnnotation(GENERATE_FRAGMENT_FACTORY.packageName, GENERATE_FRAGMENT_FACTORY.simpleName)!!
+ val isDefault =
+ generateFragmentFactory.arguments.first { it.name?.asString() == "default" }.value as Boolean
+ val key = if (isDefault) null else TODO()
+ val fragments = injectFragments[key] ?: emptyList()
+ val generatedClassName = "FragmentFactory${element.simpleName.asString()}"
+ val file = element.containingFile!!
+ val fileSpec = FileSpec.builder(element.packageName.asString(), generatedClassName)
+ .addType(
+ TypeSpec.interfaceBuilder(generatedClassName)
+ .apply {
+ if (fragments.isNotEmpty()) {
+ for (fragment in fragments) {
+ val name = fragment.simpleName.asString().decapitalize(Locale.US)
+ val type = fragment.asClassName()
+ addProperty(
+ PropertySpec.builder(name, FRAGMENT_ENTRY)
+ .receiver(LambdaTypeName.get(returnType = type))
+ .getter(
+ FunSpec.getterBuilder()
+ .addAnnotation(PROVIDES)
+ .addAnnotation(INTO_MAP)
+ .addStatement("return %N(this)", FRAGMENT_ENTRY)
+ .build()
+ ).build()
+ )
+ addProperty(name, FRAGMENT_ENTRY)
+ }
+ } else {
+ addProperty(
+ PropertySpec.builder(
+ "noFragments", ClassName("kotlin.collections", "Map").parameterizedBy(
+ ClassName("kotlin", "String"),
+ LambdaTypeName.get(returnType = FRAGMENT)
+ )
+ ).getter(
+ FunSpec.getterBuilder()
+ .addAnnotation(PROVIDES)
+ .addStatement("return emptyMap()")
+ .build()
+ ).build()
+ )
+ }
+ }
+ .build()
+ ).build()
+ codeGenerator.createNewFile(
+ Dependencies(true, file),
+ fileSpec.packageName,
+ fileSpec.name
+ ).bufferedWriter().use { fileSpec.writeTo(it) }
+ }
+ return emptyList()
+ }
+
+ private fun KSClassDeclaration.extendsFragment(): Boolean {
+ return getAllSuperTypes().any {
+ val decl = it.declaration
+ decl.packageName.asString() == FRAGMENT.packageName && decl.simpleName.asString() == FRAGMENT.simpleName
+ }
+ }
+
+ companion object {
+ private const val ANNOTATION_PACKAGE_NAME = "me.tatarka.inject.annotations"
+ private const val ANDROID_ANNOTATION_PACKAGE_NAME = "me.tatarka.inject.android.annotations"
+ val PROVIDES = ClassName(ANNOTATION_PACKAGE_NAME, "Provides")
+ val INTO_MAP = ClassName(ANNOTATION_PACKAGE_NAME, "IntoMap")
+ val FRAGMENT = ClassName("androidx.fragment.app", "Fragment")
+ val INJECT = ClassName(ANDROID_ANNOTATION_PACKAGE_NAME, "Inject")
+ val GENERATE_FRAGMENT_FACTORY = ClassName(ANDROID_ANNOTATION_PACKAGE_NAME, "GenerateFragmentFactory")
+ val INTO_COMPONENT = ClassName(ANDROID_ANNOTATION_PACKAGE_NAME, "IntoComponent")
+ val FRAGMENT_ENTRY = (ClassName("me.tatarka.inject.android.fragment", "FragmentEntry"))
+ }
+}
\ No newline at end of file
diff --git a/android/fragment-factory-compiler/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessor b/android/fragment-factory-compiler/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessor
new file mode 100644
index 00000000..ba4704b3
--- /dev/null
+++ b/android/fragment-factory-compiler/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessor
@@ -0,0 +1 @@
+me.tatarka.inject.android.fragment.compiler.FragmentFactoryProcessor
\ No newline at end of file
diff --git a/android/fragment/build.gradle b/android/fragment/build.gradle
new file mode 100644
index 00000000..fc6ab921
--- /dev/null
+++ b/android/fragment/build.gradle
@@ -0,0 +1,29 @@
+plugins {
+ id("com.android.library")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ compileSdk = 30
+ defaultConfig {
+ minSdk = 14
+ }
+ buildFeatures {
+ buildConfig = false
+ resValues = false
+ }
+}
+
+dependencies {
+ api(project(":kotlin-inject-runtime"))
+ api("androidx.fragment:fragment-ktx:1.3.3")
+}
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions.jvmTarget = "1.8"
+}
+
+ext.groupId = 'me.tatarka.inject.android'
+ext.artifactId = 'kotlin-inject-fragment'
+
+//apply from: "$rootProject.projectDir/publish.gradle"
\ No newline at end of file
diff --git a/android/fragment/src/main/AndroidManifest.xml b/android/fragment/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..cc145b34
--- /dev/null
+++ b/android/fragment/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/android/fragment/src/main/java/me/tatarka/inject/android/annotations/Annotations.kt b/android/fragment/src/main/java/me/tatarka/inject/android/annotations/Annotations.kt
new file mode 100644
index 00000000..ff2ea859
--- /dev/null
+++ b/android/fragment/src/main/java/me/tatarka/inject/android/annotations/Annotations.kt
@@ -0,0 +1,11 @@
+package me.tatarka.inject.android.annotations
+
+import kotlin.reflect.KClass
+
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.CLASS)
+annotation class GenerateFragmentFactory(val default: Boolean = false)
+
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.CLASS)
+annotation class IntoComponent(val value: KClass)
\ No newline at end of file
diff --git a/android/fragment/src/main/java/me/tatarka/inject/android/fragment/FragmentViewModelLazy.kt b/android/fragment/src/main/java/me/tatarka/inject/android/fragment/FragmentViewModelLazy.kt
new file mode 100644
index 00000000..fae4b954
--- /dev/null
+++ b/android/fragment/src/main/java/me/tatarka/inject/android/fragment/FragmentViewModelLazy.kt
@@ -0,0 +1,160 @@
+package me.tatarka.inject.android.fragment
+
+import androidx.annotation.MainThread
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.AbstractSavedStateViewModelFactory
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelLazy
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelStoreOwner
+
+/**
+ * Returns a property delegate to access [ViewModel] by **default** scoped to this [Fragment]:
+ * ```
+ * @Inject
+ * class MyFragment(myViewModel: () -> MyViewModel) : Fragment() {
+ * val viewModel = viewModels(myViewModel)
+ * }
+ * ```
+ *
+ * This property can be accessed only after this Fragment is attached i.e., after
+ * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
+ */
+@MainThread
+inline fun Fragment.viewModels(
+ crossinline factory: () -> VM
+): Lazy = viewModels({ this }, factory)
+
+/**
+ * Returns a property delegate to access [ViewModel] by **default** scoped to this [Fragment]:
+ * ```
+ * @Inject
+ * class MyFragment(myViewModel: () -> MyViewModel) : Fragment() {
+ * val viewModel = viewModels(myViewModel)
+ * }
+ * ```
+ *
+ * Default scope may be overridden with parameter [ownerProducer]:
+ * ```
+ * class MyFragment(myParentViewModel: () -> MyParentViewModel) : Fragment() {
+ * val viewModel = viewModels({requireParentFragment()}, myParentViewModel)
+ * }
+ * ```
+ *
+ * This property can be accessed only after this Fragment is attached i.e., after
+ * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
+ */
+@MainThread
+inline fun Fragment.viewModels(
+ noinline ownerProducer: () -> ViewModelStoreOwner,
+ crossinline factory: () -> VM
+): Lazy = ViewModelLazy(VM::class, { ownerProducer().viewModelStore }, {
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T = factory() as T
+ }
+})
+
+/**
+ * Returns a property delegate to access [ViewModel] by **default** scoped to this [Fragment]:
+ * ```
+ * @Inject
+ * class MyFragment(myViewModel: (SavedStateHandle) -> MyViewModel) : Fragment() {
+ * val viewModel = viewModels(myViewModel)
+ * }
+ * ```
+ *
+ * This property can be accessed only after this Fragment is attached i.e., after
+ * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
+ */
+@MainThread
+inline fun Fragment.viewModels(
+ crossinline factory: (SavedStateHandle) -> VM
+): Lazy = viewModels({ this }, factory)
+
+/**
+ * Returns a property delegate to access [ViewModel] by **default** scoped to this [Fragment]:
+ * ```
+ * @Inject
+ * class MyFragment(myViewModel: (SavedStateHandle) -> MyViewModel) : Fragment() {
+ * val viewModel = viewModels(myViewModel)
+ * }
+ * ```
+ *
+ * Default scope may be overridden with parameter [ownerProducer]:
+ * ```
+ * class MyFragment(parentViewModel: (SavedStateHandle) -> ParentViewModel) : Fragment() {
+ * val viewModel = viewModels({requireParentFragment()}, parentViewModel)
+ * }
+ * ```
+ *
+ * This property can be accessed only after this Fragment is attached i.e., after
+ * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
+ */
+@MainThread
+inline fun Fragment.viewModels(
+ noinline ownerProducer: () -> ViewModelStoreOwner,
+ crossinline factory: (SavedStateHandle) -> VM,
+): Lazy = ViewModelLazy(VM::class, { ownerProducer().viewModelStore }, {
+ object : AbstractSavedStateViewModelFactory(this, arguments) {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(
+ key: String,
+ modelClass: Class,
+ handle: SavedStateHandle
+ ): T = factory(handle) as T
+ }
+})
+
+/**
+ * Returns a property delegate to access parent activity's [ViewModel], the [factory] will be used to create [ViewModel]
+ * first time.
+ *
+ * ```
+ * @Inject
+ * class MyFragment(myActivityViewModel: () -> MyActivityViewModel) : Fragment() {
+ * val viewModel by activityViewModels(myActivityViewModel)
+ * }
+ * ```
+ *
+ * This property can be accessed only after this Fragment is attached i.e., after
+ * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
+ */
+@MainThread
+inline fun Fragment.activityViewModels(
+ crossinline factory: () -> VM
+): Lazy = ViewModelLazy(VM::class, { requireActivity().viewModelStore }, {
+ object : ViewModelProvider.Factory {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T = factory() as T
+ }
+})
+
+/**
+ * Returns a property delegate to access parent activity's [ViewModel], the [factory] will be used to create [ViewModel]
+ * first time.
+ *
+ * ```
+ * @Inject
+ * class MyFragment(myActivityViewModel: (SavedStateHandel) -> MyActivityViewModel) : Fragment() {
+ * val viewModel by activityViewModels(myActivityViewModel)
+ * }
+ * ```
+ *
+ * This property can be accessed only after this Fragment is attached i.e., after
+ * [Fragment.onAttach()], and access prior to that will result in IllegalArgumentException.
+ */
+@MainThread
+inline fun Fragment.activityViewModels(
+ crossinline factory: (SavedStateHandle) -> VM
+): Lazy = ViewModelLazy(VM::class, { requireActivity().viewModelStore }, {
+ object : AbstractSavedStateViewModelFactory(this, arguments) {
+ @Suppress("UNCHECKED_CAST")
+ override fun create(
+ key: String,
+ modelClass: Class,
+ handle: SavedStateHandle
+ ): T = factory(handle) as T
+ }
+})
\ No newline at end of file
diff --git a/android/fragment/src/main/java/me/tatarka/inject/android/fragment/InjectFragmentFactory.kt b/android/fragment/src/main/java/me/tatarka/inject/android/fragment/InjectFragmentFactory.kt
new file mode 100644
index 00000000..dbfe694d
--- /dev/null
+++ b/android/fragment/src/main/java/me/tatarka/inject/android/fragment/InjectFragmentFactory.kt
@@ -0,0 +1,39 @@
+package me.tatarka.inject.android.fragment
+
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentFactory
+import me.tatarka.inject.annotations.Inject
+
+/**
+ * The return type you should return from a `@Provides @IntoMap` function to inject it into [InjectFragmentFactory].
+ */
+typealias FragmentEntry = Pair Fragment>
+
+/**
+ * Creates a [FragmentEntry] from the given fragment [factory].
+ */
+inline fun FragmentEntry(noinline factory: () -> F): FragmentEntry =
+ F::class.java.canonicalName!! to factory
+
+/**
+ * A [FragmentFactory] that allows fragment constructor injection. You can provide the fragments to inject with
+ * `@Provides @IntoMap`.
+ *
+ * ```
+ * @Component abstract class FragmentsComponent {
+ * abstract val fragmentFactory: InjectFragmentFactory
+ *
+ * val (() -> MyFragment1).myFragment1: FragmentEntry
+ * @Provides @IntoMap get() = FragmentEntry(this)
+ *
+ * val (() -> MyFragment2).myFragment2: FragmentEntry
+ * @Provides @IntoMap get() = FragmentEntry(this)
+ * }
+ * ```
+ */
+@Inject
+class InjectFragmentFactory(private val fragmentFactories: Map Fragment>) : FragmentFactory() {
+ override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+ return fragmentFactories[className]?.invoke() ?: super.instantiate(classLoader, className)
+ }
+}
\ No newline at end of file
diff --git a/android/integration-tests/build.gradle b/android/integration-tests/build.gradle
new file mode 100644
index 00000000..ac60af0c
--- /dev/null
+++ b/android/integration-tests/build.gradle
@@ -0,0 +1,64 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+ id("com.google.devtools.ksp")
+}
+
+android {
+ compileSdk = 30
+
+ defaultConfig {
+ applicationId = "me.tatarka.inject.android.test"
+ minSdk = 21
+ targetSdk = 30
+ versionCode = 1
+ versionName = "1.0"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ sourceSets {
+ // allows android studio to see ksp generated code
+ debug.java {
+ srcDir("build/generated/ksp/debug/kotlin")
+ }
+ release.java {
+ srcDir("build/generated/ksp/release/kotlin")
+ }
+ testDebug.java {
+ srcDir("build/generated/ksp/debugUnitTest/kotlin")
+ }
+ androidTestDebug.java {
+ srcDir("build/generated/ksp/debugAndroidTest/kotlin")
+ }
+ // share code between unit and android tests
+ test.java {
+ srcDir("src/commonTest/java")
+ }
+ androidTest.java {
+ srcDir("src/commonTest/java")
+ }
+ }
+}
+
+dependencies {
+ implementation(project(":android:activity"))
+ implementation(project(":android:fragment"))
+
+ ksp(project(":kotlin-inject-compiler:ksp"))
+ ksp(project(":android:fragment-factory-compiler"))
+
+ androidTestImplementation("org.jetbrains.kotlin:kotlin-test")
+ androidTestImplementation("junit:junit:4.13.2")
+ androidTestImplementation("androidx.test:core-ktx:1.3.0")
+ debugImplementation("androidx.fragment:fragment-testing:1.3.3")
+ androidTestImplementation("androidx.test.ext:junit:1.1.2")
+ androidTestImplementation("androidx.test:runner:1.3.0")
+ androidTestImplementation("com.willowtreeapps.assertk:assertk-jvm:0.22")
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
+ kotlinOptions.jvmTarget = "1.8"
+}
\ No newline at end of file
diff --git a/android/integration-tests/src/androidTest/java/me/tatarka/inject/android/test/ActivityViewModelTest.kt b/android/integration-tests/src/androidTest/java/me/tatarka/inject/android/test/ActivityViewModelTest.kt
new file mode 100644
index 00000000..8876c9ca
--- /dev/null
+++ b/android/integration-tests/src/androidTest/java/me/tatarka/inject/android/test/ActivityViewModelTest.kt
@@ -0,0 +1,115 @@
+package me.tatarka.inject.android.test
+
+import androidx.activity.viewModels
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentFactory
+import androidx.fragment.app.testing.launchFragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.SavedStateHandle
+import androidx.test.core.app.launchActivity
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import assertk.assertThat
+import assertk.assertions.isSameAs
+import me.tatarka.inject.android.TestChildViewModelFragment
+import me.tatarka.inject.android.TestParentViewModelFragment
+import me.tatarka.inject.android.TestSavedStateViewModel
+import me.tatarka.inject.android.TestViewModel
+import me.tatarka.inject.android.TestViewModelActivity
+import me.tatarka.inject.android.TestViewModelFragment
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ActivityViewModelTest {
+
+ @Test
+ fun constructs_and_retains_a_view_model_for_an_activity() {
+ val scenario = launchActivity()
+
+ lateinit var viewModel: TestViewModel
+ lateinit var savedStateViewModel: TestSavedStateViewModel
+ scenario.onActivity {
+ viewModel = it.viewModel
+ savedStateViewModel = it.savedStateViewModel
+ }
+ scenario.recreate()
+ scenario.onActivity {
+ assertThat(it.viewModel).isSameAs(viewModel)
+ assertThat(it.savedStateViewModel).isSameAs(savedStateViewModel)
+ }
+ }
+
+ @Test
+ fun constructs_and_retains_a_view_model_for_a_fragment() {
+ val vmFactory = { TestViewModel() }
+ val vmSavedStateFactory = { handle: SavedStateHandle -> TestSavedStateViewModel(handle) }
+ val scenario = launchFragment { TestViewModelFragment(vmFactory, vmSavedStateFactory) }
+
+ lateinit var viewModel: TestViewModel
+ lateinit var savedStateViewModel: TestSavedStateViewModel
+ scenario.onFragment {
+ viewModel = it.viewModel
+ savedStateViewModel = it.savedStateViewModel
+ }
+ scenario.recreate()
+ scenario.onFragment {
+ assertThat(it.viewModel).isSameAs(viewModel)
+ assertThat(it.savedStateViewModel).isSameAs(savedStateViewModel)
+ }
+ }
+
+ @Test
+ fun constructs_and_retains_a_view_model_from_the_parent_activity() {
+ val vmFactory = { TestViewModel() }
+ val vmSavedStateFactory = { handle: SavedStateHandle -> TestSavedStateViewModel(handle) }
+ val scenario = launchFragment { TestViewModelFragment(vmFactory, vmSavedStateFactory) }
+
+ lateinit var viewModel: TestViewModel
+ lateinit var savedStateViewModel: TestSavedStateViewModel
+ scenario.onFragment {
+ viewModel = it.activityViewModel
+ savedStateViewModel = it.savedStateActivityViewModel
+ }
+ scenario.recreate()
+ scenario.onFragment {
+ val activityViewModel by it.requireActivity().viewModels()
+ val savedStateActivityViewModel by it.requireActivity().viewModels()
+
+ assertThat(activityViewModel).isSameAs(viewModel)
+ assertThat(savedStateActivityViewModel).isSameAs(savedStateViewModel)
+ }
+ }
+
+ @Test
+ fun constructs_and_retains_a_view_model_from_the_parent_fragment() {
+ val vmFactory = { TestViewModel() }
+ val vmSavedStateFactory = { handle: SavedStateHandle -> TestSavedStateViewModel(handle) }
+ val scenario = launchFragment(factory = object : FragmentFactory() {
+ override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+ return when (className) {
+ TestParentViewModelFragment::class.java.name -> TestParentViewModelFragment()
+ TestChildViewModelFragment::class.java.name -> TestChildViewModelFragment(
+ vmFactory,
+ vmSavedStateFactory
+ )
+ else -> super.instantiate(classLoader, className)
+ }
+ }
+ })
+
+ lateinit var viewModel: TestViewModel
+ lateinit var savedStateViewModel: TestSavedStateViewModel
+ scenario.onFragment {
+ viewModel = it.childFragment.parentViewModel
+ savedStateViewModel = it.childFragment.savedStateParentViewModel
+ }
+ scenario.recreate()
+ scenario.onFragment {
+ val parentViewModel by it.viewModels()
+ val savedStateParentViewModel by it.viewModels()
+
+ assertThat(parentViewModel).isSameAs(viewModel)
+ assertThat(savedStateParentViewModel).isSameAs(savedStateViewModel)
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/integration-tests/src/main/AndroidManifest.xml b/android/integration-tests/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..abad3115
--- /dev/null
+++ b/android/integration-tests/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/android/integration-tests/src/main/java/me/tatarka/inject/android/TestFragmentFactoryActivity.kt b/android/integration-tests/src/main/java/me/tatarka/inject/android/TestFragmentFactoryActivity.kt
new file mode 100644
index 00000000..761f205e
--- /dev/null
+++ b/android/integration-tests/src/main/java/me/tatarka/inject/android/TestFragmentFactoryActivity.kt
@@ -0,0 +1,28 @@
+package me.tatarka.inject.android
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import me.tatarka.inject.android.annotations.GenerateFragmentFactory
+import me.tatarka.inject.android.fragment.InjectFragmentFactory
+import me.tatarka.inject.annotations.Component
+import me.tatarka.inject.annotations.Inject
+
+@Component
+@GenerateFragmentFactory(default = true)
+abstract class ActivityComponent : FragmentFactoryActivityComponent {
+ abstract val fragmentFactory: InjectFragmentFactory
+}
+
+class TestFragmentFactoryActivity : FragmentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ supportFragmentManager.fragmentFactory = ActivityComponent::class.create().fragmentFactory
+ super.onCreate(savedInstanceState)
+ }
+}
+
+@Inject
+class Fragment1 : Fragment()
+
+@Inject
+class Fragment2 : Fragment()
\ No newline at end of file
diff --git a/android/integration-tests/src/main/java/me/tatarka/inject/android/TestViewModelActivity.kt b/android/integration-tests/src/main/java/me/tatarka/inject/android/TestViewModelActivity.kt
new file mode 100644
index 00000000..d42c5c87
--- /dev/null
+++ b/android/integration-tests/src/main/java/me/tatarka/inject/android/TestViewModelActivity.kt
@@ -0,0 +1,19 @@
+package me.tatarka.inject.android
+
+import androidx.activity.ComponentActivity
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import me.tatarka.inject.android.activity.viewModels
+
+class TestViewModelActivity : ComponentActivity() {
+ val vmFactory = { TestViewModel() }
+ val vmSavedStateFactory = { handle: SavedStateHandle -> TestSavedStateViewModel(handle) }
+
+ val viewModel by viewModels(vmFactory)
+ val savedStateViewModel by viewModels(vmSavedStateFactory)
+}
+
+class TestViewModel : ViewModel() {
+}
+
+class TestSavedStateViewModel(val handle: SavedStateHandle) : ViewModel()
\ No newline at end of file
diff --git a/android/integration-tests/src/main/java/me/tatarka/inject/android/TestViewModelFragment.kt b/android/integration-tests/src/main/java/me/tatarka/inject/android/TestViewModelFragment.kt
new file mode 100644
index 00000000..b47db3b1
--- /dev/null
+++ b/android/integration-tests/src/main/java/me/tatarka/inject/android/TestViewModelFragment.kt
@@ -0,0 +1,43 @@
+package me.tatarka.inject.android
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.commit
+import androidx.lifecycle.SavedStateHandle
+import me.tatarka.inject.android.fragment.activityViewModels
+import me.tatarka.inject.android.fragment.viewModels
+
+class TestViewModelFragment(
+ vmFactory: () -> TestViewModel,
+ vmSavedStateFactory: (SavedStateHandle) -> TestSavedStateViewModel
+) : Fragment() {
+
+ val viewModel by viewModels(vmFactory)
+ val savedStateViewModel by viewModels(vmSavedStateFactory)
+
+ val activityViewModel by activityViewModels(vmFactory)
+ val savedStateActivityViewModel by activityViewModels(vmSavedStateFactory)
+}
+
+class TestParentViewModelFragment : Fragment() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (savedInstanceState == null) {
+ childFragmentManager.commit {
+ add(TestChildViewModelFragment::class.java, null, "test")
+ }
+ }
+ }
+
+ val childFragment: TestChildViewModelFragment
+ get() = childFragmentManager.findFragmentByTag("test") as TestChildViewModelFragment
+}
+
+class TestChildViewModelFragment(
+ vmFactory: () -> TestViewModel,
+ vmSavedStateFactory: (SavedStateHandle) -> TestSavedStateViewModel
+) : Fragment() {
+
+ val parentViewModel by viewModels({ requireParentFragment() }, vmFactory)
+ val savedStateParentViewModel by viewModels({ requireParentFragment() }, vmSavedStateFactory)
+}
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 2ee2e1cd..ec46e801 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,6 +1,8 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version "$kotlin_version" apply false
+ id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
id 'com.google.devtools.ksp' version "$ksp_version" apply false
+ id 'com.android.library' version '4.1.0' apply false
id "de.marcphilipp.nexus-publish" version "0.4.0" apply false
id "io.gitlab.arturbosch.detekt" version "1.15.0"
}
diff --git a/gradle.properties b/gradle.properties
index 9613f6d3..84624233 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,5 @@
kotlin_version=1.4.30
ksp_version=1.4.30-1.0.0-alpha05
org.gradle.parallel=true
-ksp.incremental=true
\ No newline at end of file
+ksp.incremental=true
+android.useAndroidX=true
\ No newline at end of file
diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/Ast.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/Ast.kt
index 9eb1d07a..faafbf3a 100644
--- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/Ast.kt
+++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/Ast.kt
@@ -47,6 +47,8 @@ abstract class AstClass : AstElement(), AstHasModifiers {
abstract val name: String
+ abstract val isError: Boolean
+
abstract val companion: AstClass?
abstract val isObject: Boolean
diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/FailedToGenerateException.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/FailedToGenerateException.kt
index b14340ff..98beed56 100644
--- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/FailedToGenerateException.kt
+++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/FailedToGenerateException.kt
@@ -2,3 +2,6 @@ package me.tatarka.inject.compiler
class FailedToGenerateException(message: String, val element: AstElement? = null) :
Exception(message)
+
+
+class ErrorTypeException: Exception()
\ No newline at end of file
diff --git a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt
index 27097139..574f0ce5 100644
--- a/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt
+++ b/kotlin-inject-compiler/core/src/main/kotlin/me/tatarka/inject/compiler/TypeCollector.kt
@@ -117,6 +117,9 @@ class TypeCollector private constructor(private val provider: AstProvider, priva
var elementScope: AstType? = null
astClass.visitInheritanceChain { parentClass ->
+ if (parentClass.isError) {
+ throw ErrorTypeException()
+ }
val parentScope = parentClass.scopeType(options)
if (parentScope != null) {
if (scopeClass == null) {
diff --git a/kotlin-inject-compiler/kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ModelAst.kt b/kotlin-inject-compiler/kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ModelAst.kt
index f1e565f3..eac96833 100644
--- a/kotlin-inject-compiler/kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ModelAst.kt
+++ b/kotlin-inject-compiler/kapt/src/main/kotlin/me/tatarka/inject/compiler/kapt/ModelAst.kt
@@ -215,6 +215,7 @@ private class PrimitiveModelAstClass(
override val packageName: String = "kotlin"
override val name: String = type.toString()
+ override val isError: Boolean = false
override val visibility: AstVisibility = AstVisibility.PUBLIC
override val isAbstract: Boolean = false
override val isInterface: Boolean = false
@@ -257,6 +258,8 @@ private class ModelAstClass(
override val name: String get() = element.simpleName.toString()
+ override val isError: Boolean = false
+
override val visibility: AstVisibility
get() = astVisibility(element, kmClass?.flags)
diff --git a/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/InjectProcessor.kt b/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/InjectProcessor.kt
index c5d1c8bf..d5661930 100644
--- a/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/InjectProcessor.kt
+++ b/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/InjectProcessor.kt
@@ -5,7 +5,10 @@ import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.symbol.KSAnnotated
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.validate
import me.tatarka.inject.compiler.COMPONENT
+import me.tatarka.inject.compiler.ErrorTypeException
import me.tatarka.inject.compiler.FailedToGenerateException
import me.tatarka.inject.compiler.InjectGenerator
import me.tatarka.inject.compiler.Options
@@ -37,7 +40,12 @@ class InjectProcessor(private val profiler: Profiler? = null) : SymbolProcessor,
val generator = InjectGenerator(this, options)
- for (element in resolver.getSymbolsWithClassAnnotation(COMPONENT.packageName, COMPONENT.simpleName)) {
+ val (elements, failedToValidate) = resolver.getSymbolsWithClassAnnotation(
+ COMPONENT.packageName,
+ COMPONENT.simpleName
+ ).partition { it.validate() }
+
+ for (element in elements) {
val astClass = element.toAstClass()
try {
@@ -52,7 +60,9 @@ class InjectProcessor(private val profiler: Profiler? = null) : SymbolProcessor,
profiler?.onStop()
- return emptyList()
+ logger.warn("failed to process: ${failedToValidate.joinToString(", ")}")
+
+ return failedToValidate
}
override fun finish() {
diff --git a/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/KSAst.kt b/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/KSAst.kt
index 747fadd0..53cd28a4 100644
--- a/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/KSAst.kt
+++ b/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/KSAst.kt
@@ -26,6 +26,7 @@ import com.google.devtools.ksp.symbol.Modifier
import com.google.devtools.ksp.symbol.Nullability
import com.google.devtools.ksp.symbol.Variance
import com.google.devtools.ksp.symbol.Visibility
+import com.google.devtools.ksp.validate
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.MemberName
@@ -125,7 +126,7 @@ object KSAstMessenger : Messenger {
}
}
-private interface KSAstAnnotated : AstAnnotated, KSAstProvider {
+interface KSAstAnnotated : AstAnnotated, KSAstProvider {
val declaration: KSAnnotated
override fun hasAnnotation(packageName: String, simpleName: String): Boolean {
@@ -158,6 +159,9 @@ private class KSAstClass(provider: KSAstProvider, override val declaration: KSCl
override val name: String
get() = declaration.simpleName.asString()
+ override val isError: Boolean
+ get() = !declaration.validate()
+
override val visibility: AstVisibility
get() = declaration.getVisibility().astVisibility()
diff --git a/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/Util.kt b/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/Util.kt
index cd801322..bc9f1d84 100644
--- a/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/Util.kt
+++ b/kotlin-inject-compiler/ksp/src/main/kotlin/me/tatarka/inject/compiler/ksp/Util.kt
@@ -34,6 +34,10 @@ fun KSAnnotated.annotationAnnotatedWith(packageName: String, simpleName: String)
return null
}
+fun KSAnnotated.findAnnotation(packageName: String, simpleName: String): KSAnnotation? {
+ return annotations.first { it.hasName(packageName, simpleName) }
+}
+
fun KSAnnotated.hasAnnotation(packageName: String, simpleName: String): Boolean {
return annotations.any { it.hasName(packageName, simpleName) }
}
diff --git a/kotlin-inject-runtime/build.gradle b/kotlin-inject-runtime/build.gradle
index 5d801919..ddd7d5d8 100644
--- a/kotlin-inject-runtime/build.gradle
+++ b/kotlin-inject-runtime/build.gradle
@@ -13,4 +13,4 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions.jvmTarget = "1.8"
}
-apply from: '../publish.gradle'
+apply from: "$rootProject.projectDir/publish.gradle"
\ No newline at end of file
diff --git a/local.properties b/local.properties
new file mode 100644
index 00000000..ff7f1c05
--- /dev/null
+++ b/local.properties
@@ -0,0 +1,10 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file should *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/Users/evantatarka/Library/Android/sdk
\ No newline at end of file
diff --git a/publish.gradle b/publish.gradle
index 673b77d1..12ec13b7 100644
--- a/publish.gradle
+++ b/publish.gradle
@@ -23,11 +23,15 @@ nexusPublishing {
connectTimeout.set(Duration.ofMinutes(5))
}
+def customGroupId = project.ext.find("groupId")
def customArtifactId = project.ext.find("artifactId")
publishing {
publications {
lib(MavenPublication) {
+ if (customGroupId != null) {
+ groupId customGroupId
+ }
if (customArtifactId != null) {
artifactId customArtifactId
}
diff --git a/settings.gradle b/settings.gradle
index 0463ad12..4686fe0a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -3,6 +3,13 @@ pluginManagement {
gradlePluginPortal()
google()
}
+ resolutionStrategy {
+ eachPlugin {
+ if (requested.id.id.startsWith("com.android")) {
+ useModule("com.android.tools.build:gradle:${requested.version}")
+ }
+ }
+ }
}
rootProject.name='kotlin-inject'
@@ -16,4 +23,8 @@ include ':integration-tests:kapt'
include ':integration-tests:ksp'
include ':integration-tests:module'
include ':integration-tests:kapt-companion'
-include ':integration-tests:ksp-companion'
\ No newline at end of file
+include ':integration-tests:ksp-companion'
+include ':android:activity'
+include ':android:fragment'
+include ':android:fragment-factory-compiler'
+include ':android:integration-tests'
\ No newline at end of file