diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d02eb1f..8c581ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +# 0.14.1 + +### 🐛 Bug Fixes + +- update Anvil to 2.3.8 ([#379](https://github.com/rbusarow/Tangle/pull/379)) +- remove inferred nullability for `TangleViewModelFactory.create` ([#378](https://github.com/rbusarow/Tangle/pull/378)) + +### 🧰 Maintenance + +- Bump activity-ktx from 1.3.1 to 1.4.0 ([#372](https://github.com/rbusarow/Tangle/pull/372)) +- Bump navigation-ui-ktx from 2.4.0-alpha10 to 2.4.0-beta01 ([#371](https://github.com/rbusarow/Tangle/pull/371)) +- Bump auto-common from 1.1.2 to 1.2 ([#364](https://github.com/rbusarow/Tangle/pull/364)) +- Bump room-compiler from 2.4.0-alpha04 to 2.4.0-alpha05 ([#357](https://github.com/rbusarow/Tangle/pull/357)) +- Bump prismjs from 1.24.1 to 1.25.0 in /website ([#363](https://github.com/rbusarow/Tangle/pull/363)) +- Bump axios from 0.21.1 to 0.21.4 in /website ([#362](https://github.com/rbusarow/Tangle/pull/362)) + # 0.14.0 ### 🐛 Bug Fixes diff --git a/gradle.properties b/gradle.properties index de12ba37..68343b3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,7 +27,7 @@ kotlin.incremental=true # Maven GROUP=com.rickbusarow.tangle -VERSION_NAME=0.14.0 +VERSION_NAME=0.14.1 POM_DESCRIPTION=Android dependency injection using Anvil POM_INCEPTION_YEAR=2021 POM_URL=https://github.com/rbusarow/Tangle diff --git a/website/docs/configuration.mdx b/website/docs/configuration.mdx index 78d1b5b8..d7f354f7 100644 --- a/website/docs/configuration.mdx +++ b/website/docs/configuration.mdx @@ -35,25 +35,25 @@ plugins { dependencies { // Fragments - api("com.rickbusarow.tangle:tangle-fragment-api:0.14.0") - anvil("com.rickbusarow.tangle:tangle-fragment-compiler:0.14.0") + api("com.rickbusarow.tangle:tangle-fragment-api:0.14.1") + anvil("com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1") // ViewModels - api("com.rickbusarow.tangle:tangle-viewmodel-api:0.14.0") - anvil("com.rickbusarow.tangle:tangle-viewmodel-compiler:0.14.0") + api("com.rickbusarow.tangle:tangle-viewmodel-api:0.14.1") + anvil("com.rickbusarow.tangle:tangle-viewmodel-compiler:0.14.1") // optional Activity ViewModel support - implementation("com.rickbusarow.tangle:tangle-viewmodel-activity:0.14.0") + implementation("com.rickbusarow.tangle:tangle-viewmodel-activity:0.14.1") // optional Compose ViewModel support - implementation("com.rickbusarow.tangle:tangle-viewmodel-compose:0.14.0") + implementation("com.rickbusarow.tangle:tangle-viewmodel-compose:0.14.1") // optional Fragment ViewModel support - implementation("com.rickbusarow.tangle:tangle-viewmodel-fragment:0.14.0") + implementation("com.rickbusarow.tangle:tangle-viewmodel-fragment:0.14.1") // WorkManager - api("com.rickbusarow.tangle:tangle-work-api:0.14.0") - anvil("com.rickbusarow.tangle:tangle-work-compiler:0.14.0") + api("com.rickbusarow.tangle:tangle-work-api:0.14.1") + anvil("com.rickbusarow.tangle:tangle-work-compiler:0.14.1") } ``` @@ -73,25 +73,25 @@ plugins { dependencies { // Fragments - api 'com.rickbusarow.tangle:tangle-fragment-api:0.14.0' - anvil 'com.rickbusarow.tangle:tangle-fragment-compiler:0.14.0' + api 'com.rickbusarow.tangle:tangle-fragment-api:0.14.1' + anvil 'com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1' // ViewModels - api 'com.rickbusarow.tangle:tangle-viewmodel-api:0.14.0' - anvil 'com.rickbusarow.tangle:tangle-viewmodel-compiler:0.14.0' + api 'com.rickbusarow.tangle:tangle-viewmodel-api:0.14.1' + anvil 'com.rickbusarow.tangle:tangle-viewmodel-compiler:0.14.1' // optional Activity ViewModel support - implementation 'com.rickbusarow.tangle:tangle-viewmodel-activity:0.14.0' + implementation 'com.rickbusarow.tangle:tangle-viewmodel-activity:0.14.1' // optional Compose ViewModel support - implementation 'com.rickbusarow.tangle:tangle-viewmodel-compose:0.14.0' + implementation 'com.rickbusarow.tangle:tangle-viewmodel-compose:0.14.1' // optional Fragment ViewModel support - implementation 'com.rickbusarow.tangle:tangle-viewmodel-fragment:0.14.0' + implementation 'com.rickbusarow.tangle:tangle-viewmodel-fragment:0.14.1' // WorkManager - api 'com.rickbusarow.tangle:tangle-work-api:0.14.0' - anvil 'com.rickbusarow.tangle:tangle-work-compiler:0.14.0' + api 'com.rickbusarow.tangle:tangle-work-api:0.14.1' + anvil 'com.rickbusarow.tangle:tangle-work-compiler:0.14.1' } ``` diff --git a/website/docs/fragments/fragments.mdx b/website/docs/fragments/fragments.mdx index d7a51bf9..0a27eb2a 100644 --- a/website/docs/fragments/fragments.mdx +++ b/website/docs/fragments/fragments.mdx @@ -68,8 +68,8 @@ plugins { } dependencies { - api("com.rickbusarow.tangle:tangle-fragment-api:0.14.0") - anvil("com.rickbusarow.tangle:tangle-fragment-compiler:0.14.0") + api("com.rickbusarow.tangle:tangle-fragment-api:0.14.1") + anvil("com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1") } ``` @@ -86,8 +86,8 @@ plugins { } dependencies { - api 'com.rickbusarow.tangle:tangle-fragment-api:0.14.0' - anvil 'com.rickbusarow.tangle:tangle-fragment-compiler:0.14.0' + api 'com.rickbusarow.tangle:tangle-fragment-api:0.14.1' + anvil 'com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1' } ``` diff --git a/website/docs/gradle-plugin.mdx b/website/docs/gradle-plugin.mdx index 1cd6e6de..d6bcc4c0 100644 --- a/website/docs/gradle-plugin.mdx +++ b/website/docs/gradle-plugin.mdx @@ -23,8 +23,8 @@ dependencies { ``` Then Tangle will add the tangle-fragment dependencies: -- com.rickbusarow.tangle:tangle-fragment-api:0.14.0 -- com.rickbusarow.tangle:tangle-fragment-compiler:0.14.0 +- com.rickbusarow.tangle:tangle-fragment-api:0.14.1 +- com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1 apply false - id("com.rickbusarow.tangle") version "0.14.0" apply false + id("com.rickbusarow.tangle") version "0.14.1" apply false } ``` @@ -62,7 +62,7 @@ plugins { plugins { id("android-library") // or application, etc. kotlin("android") - id("com.rickbusarow.tangle") version "0.14.0" + id("com.rickbusarow.tangle") version "0.14.1" } ``` @@ -86,7 +86,7 @@ pluginManagement { plugins { // add Tangle and Anvil versions to the project's classpath id 'com.squareup.anvil' version apply false - id 'com.rickbusarow.tangle' version "0.14.0" apply false + id 'com.rickbusarow.tangle' version "0.14.1" apply false } ``` @@ -128,7 +128,7 @@ Tangle functionality. plugins { id("android-library") // or application, etc. kotlin("android") - id("com.rickbusarow.tangle") version "0.14.0" + id("com.rickbusarow.tangle") version "0.14.1" } // optional diff --git a/website/package.json b/website/package.json index 7a31f3a3..2e70b7e2 100644 --- a/website/package.json +++ b/website/package.json @@ -1,6 +1,6 @@ { "name": "tangle", - "version": "0.14.0", + "version": "0.14.1", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/website/src/pages/CHANGELOG.md b/website/src/pages/CHANGELOG.md index 4022456d..251fe3c5 100644 --- a/website/src/pages/CHANGELOG.md +++ b/website/src/pages/CHANGELOG.md @@ -1,3 +1,19 @@ +## 0.14.1 + +#### 🐛 Bug Fixes + +- update Anvil to 2.3.8 ([#379](https://github.com/rbusarow/Tangle/pull/379)) +- remove inferred nullability for `TangleViewModelFactory.create` ([#378](https://github.com/rbusarow/Tangle/pull/378)) + +#### 🧰 Maintenance + +- Bump activity-ktx from 1.3.1 to 1.4.0 ([#372](https://github.com/rbusarow/Tangle/pull/372)) +- Bump navigation-ui-ktx from 2.4.0-alpha10 to 2.4.0-beta01 ([#371](https://github.com/rbusarow/Tangle/pull/371)) +- Bump auto-common from 1.1.2 to 1.2 ([#364](https://github.com/rbusarow/Tangle/pull/364)) +- Bump room-compiler from 2.4.0-alpha04 to 2.4.0-alpha05 ([#357](https://github.com/rbusarow/Tangle/pull/357)) +- Bump prismjs from 1.24.1 to 1.25.0 in /website ([#363](https://github.com/rbusarow/Tangle/pull/363)) +- Bump axios from 0.21.1 to 0.21.4 in /website ([#362](https://github.com/rbusarow/Tangle/pull/362)) + ## 0.14.0 #### 🐛 Bug Fixes diff --git a/website/versioned_docs/version-0.14.1/benchmarks.md b/website/versioned_docs/version-0.14.1/benchmarks.md new file mode 100644 index 00000000..2cc7ac5a --- /dev/null +++ b/website/versioned_docs/version-0.14.1/benchmarks.md @@ -0,0 +1,55 @@ +--- +id: benchmarks + +sidebar_label: Benchmarks + +title: Benchmarks +--- + +The Tangle project has the ability to generate test projects and run synthetic benchmarks against +it, using [Gradle-Profiler](https://github.com/gradle/gradle-profiler). + +For the time being, the intent of these benchmarks is to provide a hermetic comparison between Hilt +and Tangle's build times, with as few variables as possible. + +The generated test projects represent best-case scenarios, in that no library module depends upon +any other library module, and each library module only has a single empty `Fragment` and +empty `ViewModel`. The build speed percentage gain from using Tangle is most likely higher than +anything which could be observed in a real world application. + +To run these tests yourself, [check out the Tangle project](https://github.com/RBusarow/tangle) and +run `./gradlew profile`. The generated code is in `$rootDir/build/benchmark-project`. + +The generated benchmark project is also hosted on +GitHub [here](https://github.com/RBusarow/tangle-benchmark-project), with different branches for +different project sizes. + +## The results + +These tests were all run on a water-cooled 12-core 4.3GHz hackintosh with 32GB of ram. I chose that +machine because it has excellent cooling. A MacBook Pro will start overheating and thermal +throttling during prolonged benchmarking, skewing the results. + +### 100 modules + +Tangle's mean execution time was a 20.23% reduction from Hilt's mean. + +[full results from Gradle Profile here](@site/static/benchmark/benchmark_100.html) + +![Hilt vs Tangle results, 100 modules](/img/benchmark_100.png "Hilt vs Tangle results, 100 modules") + +### 10 modules + +Tangle's mean execution time was an 11.67% reduction from Hilt's mean. This is less significant +because the Tangle test project still needs to generate a Component using Kapt/Dagger, and that cost +is relatively static regardless of benchmark size. It's also comparable to the static cost of +component generation in a Hilt project. In a real world project with a much more complicated Dagger +graph, component generation should take longer. + +It's also worth noting that an "11.67% reduction" in this case really just means that the build took +18 seconds instead of 20 seconds. For a project of this size, it's safe to say that the decision +should be made based upon API surface rather than build performance. + +[full results from Gradle Profile here](@site/static/benchmark/benchmark_10.html) + +![Hilt vs Tangle results, 10 modules](/img/benchmark_10.png "Hilt vs Tangle results, 100 modules") diff --git a/website/versioned_docs/version-0.14.1/configuration.mdx b/website/versioned_docs/version-0.14.1/configuration.mdx new file mode 100644 index 00000000..d7f354f7 --- /dev/null +++ b/website/versioned_docs/version-0.14.1/configuration.mdx @@ -0,0 +1,129 @@ +--- +id: configuration +sidebar_label: Configuration +title: Configuration + +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +## Gradle + +The simple way to apply Tangle is to just [apply the Gradle plugin](gradle-plugin). + +You can also just add dependencies yourself, without applying the plugin: + + + + + +```kotlin +// any Android module's build.gradle.kts + +plugins { + id("android-library") // or application, etc. + kotlin("android") + id("com.squareup.anvil") +} + +dependencies { + + // Fragments + api("com.rickbusarow.tangle:tangle-fragment-api:0.14.1") + anvil("com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1") + + // ViewModels + api("com.rickbusarow.tangle:tangle-viewmodel-api:0.14.1") + anvil("com.rickbusarow.tangle:tangle-viewmodel-compiler:0.14.1") + + // optional Activity ViewModel support + implementation("com.rickbusarow.tangle:tangle-viewmodel-activity:0.14.1") + + // optional Compose ViewModel support + implementation("com.rickbusarow.tangle:tangle-viewmodel-compose:0.14.1") + + // optional Fragment ViewModel support + implementation("com.rickbusarow.tangle:tangle-viewmodel-fragment:0.14.1") + + // WorkManager + api("com.rickbusarow.tangle:tangle-work-api:0.14.1") + anvil("com.rickbusarow.tangle:tangle-work-compiler:0.14.1") +} +``` + + + + + +```groovy +// any Android module's build.gradle + +plugins { + id 'android-library' // or application, etc. + kotlin("android") + id 'com.squareup.anvil' +} + +dependencies { + + // Fragments + api 'com.rickbusarow.tangle:tangle-fragment-api:0.14.1' + anvil 'com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1' + + // ViewModels + api 'com.rickbusarow.tangle:tangle-viewmodel-api:0.14.1' + anvil 'com.rickbusarow.tangle:tangle-viewmodel-compiler:0.14.1' + + // optional Activity ViewModel support + implementation 'com.rickbusarow.tangle:tangle-viewmodel-activity:0.14.1' + + // optional Compose ViewModel support + implementation 'com.rickbusarow.tangle:tangle-viewmodel-compose:0.14.1' + + // optional Fragment ViewModel support + implementation 'com.rickbusarow.tangle:tangle-viewmodel-fragment:0.14.1' + + // WorkManager + api 'com.rickbusarow.tangle:tangle-work-api:0.14.1' + anvil 'com.rickbusarow.tangle:tangle-work-compiler:0.14.1' +} +``` + + + + +## Setting up the Tangle graph + +In order to connect Tangle to your application-scoped Dagger component, +call `TangleGraph.add(...)` immediately after creating the component. + +```kotlin +import android.app.Application +import tangle.inject.TangleGraph + +class MyApplication : Application() { + + override fun onCreate() { + super.onCreate() + + val myAppComponent = DaggerMyAppComponent.factory() + .create(this) + + TangleGraph.add(myAppComponent) + } +} +``` +## Next steps + +Tangle is now able to generate its code and hook in to Dagger. + +Check out these features to start using Tangle in your project: +- [Fragments](fragments/fragments.mdx) +- [ViewModels](viewModels/viewModels.md) +- [WorkManager](workManager/workManager.md) diff --git a/website/versioned_docs/version-0.14.1/extending-anvil.md b/website/versioned_docs/version-0.14.1/extending-anvil.md new file mode 100644 index 00000000..9980ccd4 --- /dev/null +++ b/website/versioned_docs/version-0.14.1/extending-anvil.md @@ -0,0 +1,87 @@ +--- +id: extending-anvil + +sidebar_label: Extending Anvil + +title: Extending Anvil +--- + +[Anvil] is a Kotlin compiler plugin which simplifies how we use [Dagger]. Anvil's documentation is +definitely worth a read. Its benefits can be broken down into three categories: + +1. It can dramatically simplify Dagger development by automating most of the "plumbing". +2. It can **reduce build times** by [generating dagger factories], meaning that you can remove + the `dagger-compiler` kapt dependency (and probably remove kapt entirely) from most of your + project. +3. It allows us to write our own code generators. This is what Tangle does. We're able to generate + our own code, which is then integrated into the final graph by Anvil. + +## Generating code from generated code + +Some of the code generated by Tangle requires additional processing. + +For instance, given this human-written code: + +```kotlin +class MyViewModel @VMInject constructor( + @TangleParam("name") + val name: String +) : ViewModel() +``` + +Tangle will generate this: + +```kotlin +// generated by Tangle +public class MyViewModel_Factory @Inject constructor( + internal val savedStateHandleProvider: Provider +) { + public fun create(): MyViewModel { + val name = savedStateHandleProvider.get().get("name") + checkNotNull(name) { + "Required parameter with name `name` and type `kotlin.String` is missing from SavedStateHandle." + } + return MyViewModel(name) + } +} +``` + +Notice that it uses `@Inject constructor`. This code was created *after* compilation started, but it +needs to be analyzed as part of the overall Kotlin compilation task. + +This is trivial if the project is using Dagger to generate dagger factories, because the kapt +compilation stage hasn't started yet. When kapt starts, it will scan the files and see the generated +MyViewModel_Factory.kt as though it's just another human-written file. + +For Anvil, however, things are more difficult. The Kotlin compiler scans files once and passes that +collection of files to the Anvil plugin. So in order for Anvil to generate code for Tangle, the two +libraries need to be able to talk to one another. As Tangle creates new files, those files are +passed along to Anvil for additional processing. For the MyViewModel_Factory.kt file above, Anvil +will generate the following: + +```kotlin +// generated by Anvil +public class MyViewModel_Factory_Factory( + private val param0: Provider<@JvmSuppressWildcards SavedStateHandle> +) : Factory { + public override fun `get`(): MyViewModel_Factory = newInstance(param0) + + public companion object { + @JvmStatic + public fun create(param0: Provider<@JvmSuppressWildcards SavedStateHandle>): + MyViewModel_Factory_Factory = MyViewModel_Factory_Factory(param0) + + @JvmStatic + public fun newInstance(param0: Provider<@JvmSuppressWildcards SavedStateHandle>): + MyViewModel_Factory = MyViewModel_Factory(param0) + } +} +``` + +[Anvil]: https://github.com/square/anvil + +[compiler-api]: https://github.com/square/anvil/tree/main/compiler-api + +[Dagger]: https://dagger.dev + +[generating dagger factories]: https://github.com/square/anvil#dagger-factory-generation diff --git a/website/versioned_docs/version-0.14.1/fragments/bundles.md b/website/versioned_docs/version-0.14.1/fragments/bundles.md new file mode 100644 index 00000000..95cddd63 --- /dev/null +++ b/website/versioned_docs/version-0.14.1/fragments/bundles.md @@ -0,0 +1,168 @@ +--- +title: Bundle Injection + +sidebar_label: Bundle Injection +--- + +### The goal + +Fragment runtime arguments must be passed via a `Bundle` in order for the arguments to be present +if the Fragment is recreated by a [FragmentManager]. For those of us who don't want to rely upon +[Androidx Navigation], there's still quite a lot of boilerplate involved in passing these arguments +and ensuring that it's compile-time safe. + +Tangle removes as much of that boilerplate as possible, +while using some Dagger tricks to prevent creating new instances without their arguments. + +:::note +Use `@FragmentInject` instead of `@Inject` +::: + +```kotlin +import androidx.fragment.app.Fragment +import com.example.AppScope +import tangle.fragment.ContributesFragment +import tangle.fragment.FragmentInject +import tangle.fragment.FragmentInjectFactory +import tangle.fragment.arg +import tangle.inject.TangleParam + +@ContributesFragment(AppScope::class) +class MyFragment @FragmentInject constructor() : Fragment() { + + val name by arg("name") + + @FragmentInjectFactory + interface Factory { + fun create(@TangleParam("name") name: String): MyFragment + } +} + +val myFragmentFactory: MyFragment.Factory = TODO("use your favorite Dagger pattern here") + +val fragment = myFragmentFactory.create(name = "Bigyan") +``` + +### Background + +Since long before [FragmentFactory] and [Androidx Navigation], +[it has long been common practice](https://stackoverflow.com/a/9245510/7537239) to create static +`newInstance` functions which take the deconstructed Bundle parameters and return +a Fragment instance which already has those arguments injected as a Bundle. + +Here's what it may look like in Kotlin: + +```kotlin +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment + +class MyFragment : Fragment() { + + companion object { + fun newInstance(name: String): MyFragment { + val myFragment = MyFragment() + + myFragment.arguments = bundleOf("name" to name) + return myFragment + } + } +} +``` + +### Tangle's generated factories + +For the `MyFragment` definition above, Tangle will generate the following: + +```kotlin +import androidx.core.os.bundleOf +import dagger.internal.InstanceFactory +import javax.inject.Provider + +public class MyFragment_Factory_Impl( + public val delegateFactory: MyFragment_Factory +) : MyFragment.Factory { + public override fun create(name: String): MyFragment { + val bundle = bundleOf( + "name" to name + ) + return delegateFactory.get().apply { + this@apply.arguments = bundle + } + } + + public companion object { + @JvmStatic + public fun create(delegateFactory: MyFragment_Factory): Provider = + InstanceFactory.create(MyFragment_Factory_Impl(delegateFactory)) + } +} +``` + +It will then create a Dagger binding for `MyFragment_Factory_Impl` to `MyFragment.Factory`, +which allows us to use it in our code: + +```kotlin +import javax.inject.Inject +import javax.inject.Provider + +class MyNavigationImpl @Inject constructor( + // fragments without bundle arguments can be injected in a Provider + val myListFragmentProvider: Provider, + // fragments with a factory must be injected this way + val myFragmentFactory: MyFragment.Factory +) : MyNavigation { + + override fun goToMyListFragment(name: String){ + val fragment = myFragmentFactory.create(name) + // actual navigation logic would go here + } + override fun goToMyFragment(name: String){ + val fragment = myFragmentFactory.create(name) + // actual navigation logic would go here + } +} +``` + +These factories are essentially an "entry point" to the [TangleFragmentFactory]. Once the factory +has initialized its Fragment, the arguments are established and cached by the Android framework. +If the Fragment needs to be recreated by the [TangleFragmentFactory], the new instance will be +created using a `Provider` and just invoking the constructor, without recreating the `Bundle`. + +### Limiting access + +If a Fragment requires a custom factory for bundle arguments, +Tangle _does_ create a `@Provides`-annotated function, but it's hidden behind a qualifier: + +```kotlin +@Provides +@TangleFragmentProviderMap +public fun provideMyFragment(): MyFragment = MyFragment_Factory.newInstance() +``` + +This means that if anyone attempts to inject it like a normal Dagger dependency: +```kotlin +class SomeClass @Inject constructor( + val myFragmentProvider: Provider +) +``` +...Dagger will fail the build with a very familiar error message: +> [Dagger/MissingBinding] com.example.MyFragment cannot be provided without an @Inject constructor or an @Provides-annotated method. + + + + + +[Anvil]: https://github.com/square/anvil +[MergeComponent]: https://github.com/square/anvil#scopes + +[Dagger]: https://dagger.dev +[AssistedInject]: https://dagger.dev/dev-guide/assisted-injection +[Hilt]: https://dagger.dev/hilt/view-model.html + +[Androidx Navigation]: https://developer.android.com/guide/navigation/navigation-getting-started +[FragmentManager]: https://developer.android.com/reference/kotlin/androidx/fragment/app/FragmentManager +[SavedStateHandle]: https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate + +[ContributesFragment]: https://rbusarow.github.io/Tangle/api/tangle-fragment-api/tangle.fragment/-contributes-fragment/index.html +[TangleFragmentFactory]: https://rbusarow.github.io/Tangle/api/tangle-fragment-api/tangle.fragment/-tangle-fragment-factory +[FragmentFactory]: https://developer.android.com/reference/kotlin/androidx/fragment/app/FragmentFactory diff --git a/website/versioned_docs/version-0.14.1/fragments/fragments.mdx b/website/versioned_docs/version-0.14.1/fragments/fragments.mdx new file mode 100644 index 00000000..0a27eb2a --- /dev/null +++ b/website/versioned_docs/version-0.14.1/fragments/fragments.mdx @@ -0,0 +1,289 @@ +--- +title: Fragments + +sidebar_label: Fragments +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Tangle performs Fragment injection using **constructor** injection, just like the rest of a +typical Dagger/Anvil graph. There are several steps to configuration, +with two different paths at the end. + +### 1. Set up Gradle + + + + + +```kotlin +// any Android module's build.gradle.kts +plugins { + id("android-library") // or application, etc. + kotlin("android") + id("com.rickbusarow.tangle") +} + +tangle { + fragmentsEnabled = true // default is null +} +``` + + + + + +```groovy +// any Android module's build.gradle +plugins { + id 'android-library' // or application, etc. + kotlin("android") + id 'com.rickbusarow.tangle' +} + +// optional +tangle { + fragmentsEnabled true // default is null +} +``` + + + + + +```kotlin +// any Android module's build.gradle.kts +plugins { + id("android-library") // or application, etc. + kotlin("android") + id("com.squareup.anvil") +} + +dependencies { + api("com.rickbusarow.tangle:tangle-fragment-api:0.14.1") + anvil("com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1") +} +``` + + + + + +```groovy +// any Android module's build.gradle +plugins { + id 'android-library' // or application, etc. + kotlin("android") + id 'com.squareup.anvil' +} + +dependencies { + api 'com.rickbusarow.tangle:tangle-fragment-api:0.14.1' + anvil 'com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1' +} +``` + + + + +### 2. Use Anvil for the app-scoped Component + +Tangle uses the [MergeComponent] annotation from [Anvil] to identify the application's Component +and add its own dependencies to the Dagger graph. + +For anyone already using Anvil, there's probably nothing to be done here. + +Anvil uses `KClass` references to define scopes. A common pattern is to define an `AppScope` +class specifically for this purpose in a low-level shared (Gradle) module: + +```kotlin +package myApp.core.anvil + +abstract class AppScope private constructor() +``` + +Then at your application Component, use `MergeComponent` with this scope: + +```kotlin +package myApp.app + +import com.squareup.anvil.annotations.MergeComponent +import myApp.core.anvil.AppScope + +@MergeComponent(AppScope::class) +interface MyAppComponent +``` + +### 3. Set the custom FragmentFactory + +New Fragment instances are provided by [TangleFragmentFactory]. This custom factory +is automatically added to any Dagger graph for any `@MergeComponent`-annotated Component. + +:::note + +If a requested Fragment is not contained within Tangle's bindings, `TangleFragmentFactory` will +fall back to using the default initialization with an empty constructor. This means that large +projects can be migrated gradually. + +If a project was already doing Fragment constructor injection using vanilla Dagger, they were +probably already binding into a +`Map, Provider<@JvmSuppressWildcards Fragment>>`. That is what Tangle uses, +so existing multi-bound graphs will often support gradual migrations as well. + +::: + +Any [FragmentManager] used within the application will need to have its `fragmentFactory` +property set to a `TangleFragmentFactory` instance. This means the +`AppCompatActivity.supportFragmentManager`, and possibly `Fragment.childFragmentManager` as well. +This is easiest if your application uses an abstract base class. + +```kotlin +abstract class BaseActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + supportFragmentManager.fragmentFactory = Components.get() + .tangleFragmentFactory + super.onCreate(savedInstanceState) + } +} +``` + +
+ Click to see how Components works + +In a core module, define this singleton. +```kotlin +package myApp.core.anvil + +object Components { + @PublishedApi + internal val _components = mutableSetOf() + + /** Set by Application class after creating app component */ + fun add(component: Any) { + _components.add(component) + } + + inline fun get(): T = _components + .filterIsInstance() + .single() +} +``` + +In your application, save off the AppComponent instance. + +```kotlin +package myApp.core.anvil + +class MyApplication : Application() { + + override fun onCreate() { + val component = DaggerMyAppComponent.factory() + .create(/*...*/) + + Components.add(component) + + super.onCreate() + } +} +``` + +Anywhere you need to, create a "component" interface with whatever dependency properties you need, +and annotate it with `@ContributesTo()`. Your AppComponent will +automatically implement this interface, +which means that an implementation of it will be stored in `Components`. + +```kotlin +import com.squareup.anvil.annotations.ContributesTo + +@ContributesTo(AppScope::class) +interface BaseActivityComponent { + val tangleFragmentFactory: TangleFragmentFactory +} +``` +Now, `Components.get()` will return `MyAppComponent` +safely cast to `BaseActivityComponent`, and you can access its properties. + +```kotlin +val fragmentFactory = Components.get() + .tangleFragmentFactory +``` +
+ +### 4. Contribute Fragments to the graph + +Finally, add the Fragments themselves. For basic injection, the only difference +from any other constructor-injected class is that you must add the [ContributesFragment] annotation. +This will ensure that the Fragment is included in the [TangleFragmentFactory]. + +```kotlin +import tangle.fragment.ContributesFragment + +@ContributesFragment(AppScope::class) +class MyFragment @Inject constructor( + val myRepository: MyRepository +) : Fragment() { + // ... +} +``` + + +### 5. Create Fragments with the FragmentManager + +All the pieces are now in place, and your FragmentManagers are able to create Fragments with Dagger +dependencies. + +```kotlin +class MyActivity: BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportFragmentManager.beginTransaction() + .add(R.id.fragmentContainer) + .commit() + } +} +``` + +### Next step -- "Assisted" Bundle injection + +Tangle is able to generate type-safe factories for Bundle arguments, similar to [AssistedInject]. +Read about this more in [bundle injection](bundles). + +```kotlin +@ContributesFragment(AppScope::class) +class MyFragment @FragmentInject constructor() : Fragment() { + + val name by arg("name") + + @FragmentInjectFactory + interface Factory { + fun create(@TangleParam("name") name: String): MyFragment + } +} +``` + + + + + +[Anvil]: https://github.com/square/anvil +[MergeComponent]: https://github.com/square/anvil#scopes + +[Dagger]: https://dagger.dev +[AssistedInject]: https://dagger.dev/dev-guide/assisted-injection +[Hilt]: https://dagger.dev/hilt/view-model.html + +[FragmentManager]: https://developer.android.com/reference/kotlin/androidx/fragment/app/FragmentManager +[SavedStateHandle]: https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate + +[ContributesFragment]: https://rbusarow.github.io/Tangle/api/tangle-fragment-api/tangle.fragment/-contributes-fragment/index.html +[TangleFragmentFactory]: https://rbusarow.github.io/Tangle/api/tangle-fragment-api/tangle.fragment/-tangle-fragment-factory diff --git a/website/versioned_docs/version-0.14.1/gradle-plugin.mdx b/website/versioned_docs/version-0.14.1/gradle-plugin.mdx new file mode 100644 index 00000000..d6bcc4c0 --- /dev/null +++ b/website/versioned_docs/version-0.14.1/gradle-plugin.mdx @@ -0,0 +1,184 @@ +--- +id: gradle-plugin +sidebar_label: Gradle Plugin +title: Gradle Plugin + +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +The simplest way to apply Tangle is via the Gradle plugin. + +The plugin will automatically apply the Anvil compiler plugin and all required Tangle extensions. +By default, the plugin will automatically determine which Tangle dependencies to add by inspecting +the module's Androidx dependencies, and adding the corresponding Tangle features. + +For example, if a project has declared a Fragments dependency like so: + +```kotlin +dependencies { + api("androidx.fragment:fragment") +} +``` + +Then Tangle will add the tangle-fragment dependencies: +- com.rickbusarow.tangle:tangle-fragment-api:0.14.1 +- com.rickbusarow.tangle:tangle-fragment-compiler:0.14.1 + + + + + + +```kotlin +// settings.gradle.kts + +pluginManagement { + repositories { + gradlePluginPortal() + } +} +``` + +```kotlin +// root project build.gradle.kts + +plugins { + // add Tangle and Anvil versions to the project's classpath + id("com.squareup.anvil") version apply false + id("com.rickbusarow.tangle") version "0.14.1" apply false +} +``` + +```kotlin +// any Android module's build.gradle.kts + +plugins { + id("android-library") // or application, etc. + kotlin("android") + id("com.rickbusarow.tangle") version "0.14.1" +} +``` + + + + + +```groovy +// settings.gradle + +pluginManagement { + repositories { + gradlePluginPortal() + } +} +``` + +```groovy +// root project build.gradle + +plugins { + // add Tangle and Anvil versions to the project's classpath + id 'com.squareup.anvil' version apply false + id 'com.rickbusarow.tangle' version "0.14.1" apply false +} +``` + +```groovy +// any Android module's build.gradle + +plugins { + id 'android-library' // or application, etc. + kotlin("android") + id 'com.rickbusarow.tangle' +} +``` + + + + + +## Explicitly defining behavior + +This automatic behavior may be overridden by using the `tangle { ... }` configuration block. + +These settings are prioritized ahead of the automatic configuration. Note that explicitly setting +a feature to `true` (enabled) will force the plugin to add dependencies and compiler extensions +which probably aren't needed. This functionality mostly exists for its ability to *disable* the +Tangle functionality. + + + + + +```kotlin +// any Android module's build.gradle.kts + +plugins { + id("android-library") // or application, etc. + kotlin("android") + id("com.rickbusarow.tangle") version "0.14.1" +} + +// optional +tangle { + // enables the Fragments feature regardless of the project's dependencies + fragmentsEnabled = true // default is null + + // disables the Work/WorkManager feature regardless of the project's dependencies + workEnabled = false // default is null + + viewModelOptions { + enabled = true // default is null + activitiesEnabled = true // default is null + composeEnabled = true // default is null + fragmentsEnabled = true // default is null + } +} +``` + + + + + +```groovy +// any Android module's build.gradle + +plugins { + id 'android-library' // or application, etc. + kotlin("android") + id 'com.rickbusarow.tangle' +} + +// optional +tangle { + // enables the Fragments feature regardless of the project's dependencies + fragmentsEnabled = true // default is null + + // disables the Work/WorkManager feature regardless of the project's dependencies + workEnabled = false // default is null + + viewModelOptions { + enabled true // default is null + activitiesEnabled true // default is null + composeEnabled true // default is null + fragmentsEnabled true // default is null + } +} +``` + + + + + diff --git a/website/versioned_docs/version-0.14.1/member-injection.md b/website/versioned_docs/version-0.14.1/member-injection.md new file mode 100644 index 00000000..1860800d --- /dev/null +++ b/website/versioned_docs/version-0.14.1/member-injection.md @@ -0,0 +1,116 @@ +--- +id: member-injection + +sidebar_label: Member Injection + +title: Member Injection +--- + + +The Android framework has a number of classes which are initialized automatically for us: + +- `Application` +- `Activity` +- `View` +- `Service` +- `BroadcastReceiver` +- `Fragment` (these are a special case. See [fragments] for more info) + +Because we don't control their initialization, we can't use Dagger's constructor injection to +provide their dependencies. Instead, we often choose to get our dependencies +using [member injection]. + +```kotlin +import android.app.Activity +import android.os.Bundle +import tangle.inject.TangleGraph +import tangle.inject.TangleScope +import javax.inject.Inject + +@TangleScope(UserScope::class) // Dependencies will be provided by the UserScope +class UserActivity : Activity() { + + @Inject + lateinit var logger: MyLogger + + override fun onCreate(savedInstanceState: Bundle?) { + // inject MyLogger + TangleGraph.inject(this) + + super.onCreate(savedInstanceState) + + logger.log("started UserActivity") + } +} +``` + +Tangle's member injection is simple to implement. + +1. Define your dependencies using `@Inject lateinit var` +2. Annotate your class with `@TangleScope(::class)` +3. Call `TangleGraph.inject(this)` in your class's `onCreate(...)`. + +The `TangleGraph.inject(...)` function uses the target's class in order to find the appropriate +scoped MemberInjector. + +## TangleScope adds scope to target classes + +In order to perform member injection with `TangleGraph.inject(target)`, the target of the injection +must be annotated with `@TangleScope(...)`. This is how Tangle determines where the dependencies are +coming from. For instance, your application may have an `AppScope` and a `UserScope`. For those two +scopes, you would use `@TangleScope(AppScope::class)` or `@TangleScope(UserScope::class)` +respectively. + +Once a target class has an assigned scope, its dependencies will be validated at compile time. For +example, if you scope an activity to `AppScope` but it requires a dependency which is only available +in `UserScope`, the build will fail with a standard Dagger "MissingBinding" error message. + +"Base" classes do not need a TangleScope annotation. The will be injected using the scope of their +subclass. + +## Base classes + +Large projects frequently have abstract base classes like a `BaseActivity`. These base classes may +have dependencies of their own. Injecting from a base class is supported in Tangle. + +```kotlin +@TangleScope(UserScope::class) // Dependencies will be provided by the UserScope +class UserActivity : BaseActivity() { + + @Inject + lateinit var logger: MyLogger + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + logger.log("started UserActivity") + } +} + +class BaseActivity : Activity() { + + @Inject + lateinit var fragmentFactory: TangleFragmentFactory + + override fun onCreate(savedInstanceState: Bundle?) { + // inject this class and the subclass + TangleGraph.inject(this) + + super.onCreate(savedInstanceState) + } +} +``` + +## Components must be added to TangleGraph + +Tangle must know about your Component instances in order to inject your classes. + +See [setting up the TangleGraph][setting-up-the-tangle-graph] for a simple example. + + + +[member injection]: https://dagger.dev/members-injection.html + +[fragments]: fragments/fragments.mdx + +[setting-up-the-tangle-graph]: configuration#setting-up-the-tangle-graph diff --git a/website/versioned_docs/version-0.14.1/viewModels/compose.md b/website/versioned_docs/version-0.14.1/viewModels/compose.md new file mode 100644 index 00000000..292680aa --- /dev/null +++ b/website/versioned_docs/version-0.14.1/viewModels/compose.md @@ -0,0 +1,31 @@ +--- +title: Compose +sidebar_label: Compose +--- + +Tangle supports ViewModel "injection" in composables in a manner very similar to Hilt's +navigation/viewModel artifact. It will scope the ViewModel to the composable's `NavBackStackEntry`. + +The viewModels are still able to make use of automatic [SavedStateHandle injection](savedStateHandle.md), +including arguments annotated with `@TangleParam`. + +```kotlin +import androidx.compose.runtime.Composable +import androidx.navigation.NavController +import tangle.viewmodel.compose.tangleViewModel + +@Composable +fun MyComposable( + navController: NavController, + viewModel: MyViewModel = tangleViewModel() +) { /* ... */ } +``` + + +[Anvil]: https://github.com/square/anvil + +[Dagger]: https://dagger.dev + +[Hilt]: https://dagger.dev/hilt/view-model.html + +[SavedStateHandle]: https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate diff --git a/website/versioned_docs/version-0.14.1/viewModels/savedStateHandle.md b/website/versioned_docs/version-0.14.1/viewModels/savedStateHandle.md new file mode 100644 index 00000000..f0de38c4 --- /dev/null +++ b/website/versioned_docs/version-0.14.1/viewModels/savedStateHandle.md @@ -0,0 +1,75 @@ +--- +title: SavedStateHandle injection +sidebar_label: SavedStateHandle Injection +--- + +When using the `tangleViewModel` delegate function, a scoped subcomponent is created +with a binding for [SavedStateHandle]. This `SavedStateHandle` is provided +by the ViewModel's owning `Fragment`, `Activity`, or `NavBackStackEntry`. + +This `SavedStateHandle` may then be included as a dependency in injected constructors, +just as it can in [Hilt]. + +```kotlin +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import tangle.viewmodel.VMInject + +class MyViewModel @VMInject constructor( + val savedState: SavedStateHandle +) : ViewModel() +``` + +In addition, Tangle can automatically extract arguments from the `SavedStateHandle` +and inject them into the constructor, through use of the `TangleParam` annotation. + +If the constructor argument's type is not nullable, then Tangle will assert that the argument is in +the bundle while creating the ViewModel. + +If the argument is marked as nullable, then Tangle will gracefully handle a missing argument and +just inject `null`. + +Given this code: + +```kotlin +import androidx.lifecycle.ViewModel +import tangle.inject.TangleParam +import tangle.viewmodel.VMInject + +class MyViewModel @VMInject constructor( + @TangleParam("userId") + val userId: String, // must be present in the SavedStateHandle + @TangleParam("address") + val addressOrNull: String? // can safely be null +) : ViewModel() +``` + +Tangle will generate the following: + +```kotlin +import androidx.lifecycle.SavedStateHandle +import javax.inject.Inject +import javax.inject.Provider + +public class MyViewModel_Factory @Inject constructor( + internal val savedStateHandleProvider: Provider +) { + public fun create(): MyViewModel { + val userId = savedStateHandleProvider.get().get("userId") + checkNotNull(userId) { + "Required parameter with name `userId` " + + "and type `kotlin.String` is missing from SavedStateHandle." + } + val addressOrNull = savedStateHandleProvider.get().get("address") + return MyViewModel(userId, addressOrNull) + } +} +``` + +[Anvil]: https://github.com/square/anvil + +[Dagger]: https://dagger.dev + +[Hilt]: https://dagger.dev/hilt/view-model.html + +[SavedStateHandle]: https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate diff --git a/website/versioned_docs/version-0.14.1/viewModels/viewModels.md b/website/versioned_docs/version-0.14.1/viewModels/viewModels.md new file mode 100644 index 00000000..bda1c6ce --- /dev/null +++ b/website/versioned_docs/version-0.14.1/viewModels/viewModels.md @@ -0,0 +1,56 @@ +--- +title: ViewModels + +sidebar_label: ViewModels +--- + +Once you've added Tangle as a dependency, implementing [ViewModel] injection is easy. + +### 1. Annotate your ViewModels + +`ViewModel` injection is done through the `@VMInject` constructor annotation. + +```kotlin +import androidx.lifecycle.ViewModel +import com.example.MyRepository +import tangle.viewmodel.VMInject + +class MyViewModel @VMInject constructor( + val repository: MyRepository +) : ViewModel() +``` + +### 2. Tell Tangle about the AppComponent + +`TangleGraph` must be initialized as early as possible -- typically in `Application.onCreate()`. + +```kotlin +import android.app.Application +import tangle.inject.TangleGraph + +class MyApplication : Application() { + + override fun onCreate() { + super.onCreate() + + val myAppComponent = DaggerAppComponent.factory() + .create(this) + + TangleGraph.add(myAppComponent) + } +} +``` + +### 3. Use the `tangleViewModel` delegate + +```kotlin +import androidx.fragment.app.Fragment +import tangle.viewmodel.tangleViewModel + +class MyFragment : Fragment() { + val viewModel: MyViewModel by tangleViewModel() +} +``` + + +[ViewModel]: https://developer.android.com/topic/libraries/architecture/viewmodel diff --git a/website/versioned_docs/version-0.14.1/workManager/workManager.md b/website/versioned_docs/version-0.14.1/workManager/workManager.md new file mode 100644 index 00000000..c0e211c1 --- /dev/null +++ b/website/versioned_docs/version-0.14.1/workManager/workManager.md @@ -0,0 +1,68 @@ +--- +title: Worker Injection + +sidebar_label: Worker +--- + +Tangle is able to leverage Dagger's [AssistedInject] functionality to perform constructor injection +on your [Workers][Worker]. The `@TangleWorker` annotation will automatically multi-bind any Worker, +allowing you to create it via the `TangleWorkerFactory`. + +```kotlin +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import tangle.work.TangleWorker + +@TangleWorker +class MyWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + val repository: MyRepository +) : CoroutineWorker(context,params){ + override suspend fun doWork(): Result { + // ... + } +} +``` + +`TangleGraph` must be initialized as early as possible -- typically in `Application.onCreate()`. + +`TangleWorkerFactory` will then be automatically added to the application-scoped component. +Use an instance of `TangleWorkerFactory` in your `WorkManager` configuration. + +```kotlin +import android.app.Application +import androidx.work.Configuration +import tangle.inject.TangleGraph +import tangle.work.TangleWorkerFactory +import javax.inject.Inject + +class MyApplication : Application(), Configuration.Provider { + + @Inject lateinit var workerFactory: TangleWorkerFactory + + override fun onCreate() { + super.onCreate() + + val myAppComponent = DaggerAppComponent.factory() + .create(this) + + TangleGraph.add(myAppComponent) + + // inject your application class after initializing TangleGraph + (myAppComponent as MyApplicationComponent).inject(this) + } + + override fun getWorkManagerConfiguration(): Configuration { + return Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + } +} +``` + +[AssistedInject]: https://dagger.dev/dev-guide/assisted-injection +[Worker]: https://developer.android.com/reference/androidx/work/ListenableWorker diff --git a/website/versioned_sidebars/version-0.14.1-sidebars.json b/website/versioned_sidebars/version-0.14.1-sidebars.json new file mode 100644 index 00000000..6a958922 --- /dev/null +++ b/website/versioned_sidebars/version-0.14.1-sidebars.json @@ -0,0 +1,72 @@ +{ + "version-0.14.1/Docs": [ + { + "type": "doc", + "id": "version-0.14.1/configuration" + }, + { + "type": "doc", + "id": "version-0.14.1/gradle-plugin" + }, + { + "type": "doc", + "id": "version-0.14.1/extending-anvil" + }, + { + "type": "doc", + "id": "version-0.14.1/benchmarks" + }, + { + "type": "doc", + "id": "version-0.14.1/member-injection" + }, + { + "type": "category", + "label": "ViewModels", + "collapsed": false, + "items": [ + { + "type": "doc", + "id": "version-0.14.1/viewModels/viewModels" + }, + { + "type": "doc", + "id": "version-0.14.1/viewModels/savedStateHandle" + }, + { + "type": "doc", + "id": "version-0.14.1/viewModels/compose" + } + ], + "collapsible": true + }, + { + "type": "category", + "label": "Fragments", + "collapsed": false, + "items": [ + { + "type": "doc", + "id": "version-0.14.1/fragments/fragments" + }, + { + "type": "doc", + "id": "version-0.14.1/fragments/bundles" + } + ], + "collapsible": true + }, + { + "type": "category", + "label": "WorkManager", + "collapsed": false, + "items": [ + { + "type": "doc", + "id": "version-0.14.1/workManager/workManager" + } + ], + "collapsible": true + } + ] +} diff --git a/website/versions.json b/website/versions.json index 2fab24c2..df66e8c8 100644 --- a/website/versions.json +++ b/website/versions.json @@ -1,4 +1,5 @@ [ + "0.14.1", "0.14.0", "0.13.2", "0.13.1",