├── gradle-plugin
├── src
│ ├── test
│ │ ├── fixtures
│ │ │ ├── custom-lockfile
│ │ │ │ ├── base
│ │ │ │ │ ├── .gitignore
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── missing-dex
│ │ │ │ ├── base
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ ├── build.gradle
│ │ │ │ │ └── gradle.lockfile
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── library
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── kotlin
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ │ └── KotlinClass.kt
│ │ │ │ │ │ │ └── java
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ └── JavaClass.java
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ ├── build.gradle
│ │ │ │ │ └── src
│ │ │ │ │ └── main
│ │ │ │ │ └── AndroidManifest.xml
│ │ │ ├── custom-build-type
│ │ │ │ ├── base
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ ├── src
│ │ │ │ │ └── main
│ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ ├── project-dependency
│ │ │ │ ├── library
│ │ │ │ │ ├── build.gradle
│ │ │ │ │ └── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ └── kotlin
│ │ │ │ │ │ └── com
│ │ │ │ │ │ └── example
│ │ │ │ │ │ └── HelloWorld.kt
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── up-to-date
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── codegen-fixture
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── base
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── kotlin
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ │ └── ExampleFeature.kt
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── feature
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── kotlin
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ │ └── ExampleImplementation.kt
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ └── build.gradle
│ │ │ ├── has-constraints
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── out-of-date
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── same-versions
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── variant-aware
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── version-update
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── different-versions
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── lockfile-activation
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ ├── gradle.lockfile
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── resources-in-base
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── base
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── res
│ │ │ │ │ │ │ └── values
│ │ │ │ │ │ │ │ ├── styles.xml
│ │ │ │ │ │ │ │ └── strings.xml
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── feature
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── kotlin
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ │ └── HelloWorldActivity.kt
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ └── build.gradle
│ │ │ ├── resources-normal
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── library
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ └── res
│ │ │ │ │ │ │ └── values
│ │ │ │ │ │ │ └── styles.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── feature
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── kotlin
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ │ └── HelloWorldActivity.kt
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── res
│ │ │ │ │ │ │ └── values
│ │ │ │ │ │ │ │ └── strings.xml
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ └── build.gradle
│ │ │ ├── feature-compile-classpath
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── multiplatform-dependency
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── new-transitive-dependency
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── resources-overwriting
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── library
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ └── res
│ │ │ │ │ │ │ └── values
│ │ │ │ │ │ │ └── styles.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── feature
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── kotlin
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ │ └── HelloWorldActivity.kt
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── res
│ │ │ │ │ │ │ └── values
│ │ │ │ │ │ │ │ └── strings.xml
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ └── build.gradle
│ │ │ ├── transitive-dependency
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── library
│ │ │ │ │ ├── build.gradle
│ │ │ │ │ └── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ └── kotlin
│ │ │ │ │ │ └── com
│ │ │ │ │ │ └── example
│ │ │ │ │ │ └── HelloWorld.kt
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── different-versions-in-base
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── feature
│ │ │ │ │ └── build.gradle
│ │ │ │ └── base
│ │ │ │ │ └── build.gradle
│ │ │ ├── resources-correct-declaration
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── feature
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── res
│ │ │ │ │ │ │ └── values
│ │ │ │ │ │ │ │ └── styles.xml
│ │ │ │ │ │ │ ├── kotlin
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ │ └── HelloWorldActivity.kt
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── res
│ │ │ │ │ │ │ └── values
│ │ │ │ │ │ │ │ └── strings.xml
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ └── build.gradle
│ │ │ ├── resources-missing-declaration
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── feature
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── res
│ │ │ │ │ │ │ └── values
│ │ │ │ │ │ │ │ └── styles.xml
│ │ │ │ │ │ │ ├── kotlin
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ │ └── HelloWorldActivity.kt
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ ├── res
│ │ │ │ │ │ │ └── values
│ │ │ │ │ │ │ │ └── strings.xml
│ │ │ │ │ │ │ └── AndroidManifest.xml
│ │ │ │ │ └── build.gradle
│ │ │ │ └── build.gradle
│ │ │ ├── transitive-dependency-on-base
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── library
│ │ │ │ │ ├── build.gradle
│ │ │ │ │ └── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ └── kotlin
│ │ │ │ │ │ └── com
│ │ │ │ │ │ └── example
│ │ │ │ │ │ └── HelloWorld.kt
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── transitive-dependency-on-base-variant-aware
│ │ │ │ ├── gradle.properties
│ │ │ │ ├── settings.gradle
│ │ │ │ ├── library
│ │ │ │ │ ├── src
│ │ │ │ │ │ └── main
│ │ │ │ │ │ │ └── kotlin
│ │ │ │ │ │ │ └── com
│ │ │ │ │ │ │ └── example
│ │ │ │ │ │ │ └── HelloWorld.kt
│ │ │ │ │ └── build.gradle
│ │ │ │ ├── build.gradle
│ │ │ │ ├── base
│ │ │ │ │ └── build.gradle
│ │ │ │ └── feature
│ │ │ │ │ └── build.gradle
│ │ │ ├── buildscript.gradle
│ │ │ └── settings.gradle
│ │ └── java
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── better
│ │ │ └── dynamic
│ │ │ └── features
│ │ │ ├── utils
│ │ │ └── SequenceSubject.kt
│ │ │ └── TestUtils.kt
│ └── main
│ │ └── kotlin
│ │ └── app
│ │ └── cash
│ │ └── better
│ │ └── dynamic
│ │ └── features
│ │ ├── BetterDynamicFeaturesFeatureExtension.kt
│ │ ├── codegen
│ │ ├── AndroidVariant.kt
│ │ ├── TypesafeImplementationsGeneratorTask.kt
│ │ ├── TypesafeImplementationsCompilationTask.kt
│ │ └── CompileTypesafeImplementations.kt
│ │ ├── tasks
│ │ ├── LockfileEntry.kt
│ │ ├── BaseLockfileWriterTask.kt
│ │ ├── DependencyGraph.kt
│ │ ├── GenerateExternalResourcesTask.kt
│ │ ├── DependencyGraphConsumingTask.kt
│ │ ├── ResourceDumpingTask.kt
│ │ ├── CheckLockfileTask.kt
│ │ ├── DependencyGraphWriterTask.kt
│ │ └── DependencyGraphUtils.kt
│ │ └── BetterDynamicFeaturesExtension.kt
├── libs
│ └── ARSCLib-1.1.5.jar
├── gradle.properties
└── build.gradle
├── sample
├── docs
│ └── boxapp.png
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── .gitattributes
├── .gitignore
├── app
│ ├── src
│ │ └── main
│ │ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── boxapp
│ │ │ ├── install
│ │ │ ├── Module.kt
│ │ │ └── SplitInstallHelper.kt
│ │ │ ├── api
│ │ │ ├── BoxAppFeature.kt
│ │ │ ├── Navigator.kt
│ │ │ └── ServiceRegistry.kt
│ │ │ └── ui
│ │ │ └── MainActivity.kt
│ └── build.gradle
├── bigboxfeature
│ ├── api
│ │ ├── src
│ │ │ └── main
│ │ │ │ ├── AndroidManifest.xml
│ │ │ │ └── kotlin
│ │ │ │ └── app
│ │ │ │ └── cash
│ │ │ │ └── boxapp
│ │ │ │ └── bigboxfeature
│ │ │ │ └── api
│ │ │ │ └── BigBoxMainScreen.kt
│ │ └── build.gradle
│ ├── src
│ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── boxapp
│ │ │ └── bigboxfeature
│ │ │ ├── BigBoxFeature.kt
│ │ │ └── BigBoxScreen.kt
│ └── build.gradle
├── smallboxfeature
│ ├── src
│ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── boxapp
│ │ │ └── smallboxfeature
│ │ │ ├── SmallBoxWidget.kt
│ │ │ └── SmallBoxFeature.kt
│ └── build.gradle
├── extrabigboxfeature
│ ├── src
│ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── kotlin
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── boxapp
│ │ │ └── extrabigboxfeature
│ │ │ └── ExtraBigBoxFeature.kt
│ └── build.gradle
├── build.gradle
├── settings.gradle
├── README.md
└── gradlew.bat
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── license-header.txt
└── libs.versions.toml
├── agp-patch
├── gradle.properties
├── src
│ └── main
│ │ └── kotlin
│ │ └── app
│ │ └── cash
│ │ └── better
│ │ └── dynamic
│ │ └── features
│ │ ├── BetterDynamicFeaturesPatchExtension.kt
│ │ └── AgpPatchPlugin.kt
└── build.gradle
├── runtime
├── gradle.properties
├── jvm
│ ├── gradle.properties
│ ├── build.gradle
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ └── app
│ │ └── cash
│ │ └── better
│ │ └── dynamic
│ │ └── features
│ │ ├── ExperimentalDynamicFeaturesApi.kt
│ │ ├── ImplementationsContainer.kt
│ │ ├── DynamicApi.kt
│ │ └── DynamicImplementation.kt
├── build.gradle
└── src
│ └── main
│ └── kotlin
│ └── app
│ └── cash
│ └── better
│ └── dynamic
│ └── features
│ └── DynamicImplementations.kt
├── codegen
├── gradle.properties
├── ksp
│ ├── gradle.properties
│ ├── src
│ │ └── main
│ │ │ ├── resources
│ │ │ └── META-INF
│ │ │ │ └── services
│ │ │ │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider
│ │ │ └── kotlin
│ │ │ └── app
│ │ │ └── cash
│ │ │ └── better
│ │ │ └── dynamic
│ │ │ └── features
│ │ │ └── codegen
│ │ │ └── ksp
│ │ │ └── DynamicFeaturesSymbolProcessorProvider.kt
│ └── build.gradle
├── api
│ ├── gradle.properties
│ ├── build.gradle
│ └── src
│ │ └── main
│ │ └── kotlin
│ │ └── app
│ │ └── cash
│ │ └── better
│ │ └── dynamic
│ │ └── features
│ │ └── codegen
│ │ └── api
│ │ ├── FeatureApi.kt
│ │ ├── FeatureImplementation.kt
│ │ └── Constants.kt
├── build.gradle
└── src
│ ├── main
│ └── kotlin
│ │ └── app
│ │ └── cash
│ │ └── better
│ │ └── dynamic
│ │ └── features
│ │ └── codegen
│ │ ├── GenerateProguardRules.kt
│ │ └── GenerateImplementationsContainer.kt
│ └── test
│ └── kotlin
│ └── app
│ └── cash
│ └── better
│ └── dynamic
│ └── features
│ └── codegen
│ └── GenerateImplementationsContainerTest.kt
├── .gitignore
├── renovate.json
├── RELEASING.md
├── settings.gradle
├── gradle.properties
├── .github
└── workflows
│ ├── publish.yml
│ └── tests.yml
├── CHANGELOG.md
└── gradlew.bat
/gradle-plugin/src/test/fixtures/custom-lockfile/base/.gitignore:
--------------------------------------------------------------------------------
1 | custom.lockfile
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-build-type/base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/docs/boxapp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/better-dynamic-features/HEAD/sample/docs/boxapp.png
--------------------------------------------------------------------------------
/sample/gradle.properties:
--------------------------------------------------------------------------------
1 | android.useAndroidX=true
2 | org.gradle.jvmargs=-Xmx2G -XX:+HeapDumpOnOutOfMemoryError
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/project-dependency/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'org.jetbrains.kotlin.jvm'
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/better-dynamic-features/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle-plugin/libs/ARSCLib-1.1.5.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/better-dynamic-features/HEAD/gradle-plugin/libs/ARSCLib-1.1.5.jar
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/up-to-date/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/up-to-date/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/codegen-fixture/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/codegen-fixture/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-lockfile/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-lockfile/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/has-constraints/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/has-constraints/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/out-of-date/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/out-of-date/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/same-versions/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/same-versions/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/variant-aware/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/variant-aware/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/version-update/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/version-update/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/sample/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cashapp/better-dynamic-features/HEAD/sample/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-build-type/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-build-type/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/lockfile-activation/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/lockfile-activation/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/project-dependency/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/feature-compile-classpath/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/feature-compile-classpath/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/multiplatform-dependency/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/multiplatform-dependency/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/new-transitive-dependency/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/new-transitive-dependency/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/agp-patch/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=agp-patch
2 | POM_NAME=Better Dynamic Feature AGP Patch
3 | POM_DESCRIPTION=A patch of the Android Gradle Plugin
4 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions-in-base/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions-in-base/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/runtime/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=runtime
2 | POM_NAME=Better Dynamic Feature Runtime
3 | POM_DESCRIPTION=Runtime support for Better Dynamic Features
4 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 | include ':library'
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/base/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/library/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 | include ':library'
6 |
--------------------------------------------------------------------------------
/codegen/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=codegen
2 | POM_NAME=Better Dynamic Feature Code Generator
3 | POM_DESCRIPTION=Code generation logic for Better Dynamic Features
4 |
--------------------------------------------------------------------------------
/codegen/ksp/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=codegen-ksp
2 | POM_NAME=Better Dynamic Feature KSP
3 | POM_DESCRIPTION=Kotlin Sympbol Processor for Better Dynamic Features
4 |
--------------------------------------------------------------------------------
/gradle-plugin/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=gradle-plugin
2 | POM_NAME=Better Dynamic Feature Gradle Plugin
3 | POM_DESCRIPTION=Gradle Plugin for Better Dynamic Features
4 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/project-dependency/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 | include ':library'
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/library/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 | include ':library'
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base-variant-aware/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
2 | android.useAndroidX=true
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 | include ':library'
6 |
--------------------------------------------------------------------------------
/runtime/jvm/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=runtime-jvm
2 | POM_NAME=Better Dynamic Feature Runtime
3 | POM_DESCRIPTION=JVM-only components for Better Dynamic Features runtime
4 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/library/src/main/kotlin/com/example/KotlinClass.kt:
--------------------------------------------------------------------------------
1 | package com.example
2 |
3 | class KotlinClass {
4 | fun message() = "Kotlin!"
5 | }
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/feature/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/feature/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 | include ':library'
6 |
--------------------------------------------------------------------------------
/codegen/api/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=codegen-api
2 | POM_NAME=Better Dynamic Feature Codegen Api
3 | POM_DESCRIPTION=Internal APIs for the Better Dynamic Features code generator
4 |
--------------------------------------------------------------------------------
/codegen/ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider:
--------------------------------------------------------------------------------
1 | app.cash.better.dynamic.features.codegen.ksp.DynamicFeaturesSymbolProcessorProvider
2 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base-variant-aware/settings.gradle:
--------------------------------------------------------------------------------
1 | apply from: "../settings.gradle"
2 |
3 | include ':base'
4 | include ':feature'
5 | include ':library'
6 |
--------------------------------------------------------------------------------
/sample/.gitattributes:
--------------------------------------------------------------------------------
1 | #
2 | # https://help.github.com/articles/dealing-with-line-endings/
3 | #
4 | # These are explicitly windows files and should use crlf
5 | *.bat text eol=crlf
6 |
7 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'org.jetbrains.kotlin.jvm'
2 |
3 | dependencies {
4 | implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
5 | }
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'org.jetbrains.kotlin.jvm'
2 |
3 | dependencies {
4 | implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
5 | }
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'org.jetbrains.kotlin.jvm'
2 |
3 | dependencies {
4 | implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
5 | }
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/project-dependency/library/src/main/kotlin/com/example/HelloWorld.kt:
--------------------------------------------------------------------------------
1 | // Copyright Square, Inc.
2 | // Copyright Square, Inc.
3 | fun helloWorld() {
4 | println("Hello World!")
5 | }
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency/library/src/main/kotlin/com/example/HelloWorld.kt:
--------------------------------------------------------------------------------
1 | // Copyright Square, Inc.
2 | // Copyright Square, Inc.
3 | fun helloWorld() {
4 | println("Hello World!")
5 | }
6 |
--------------------------------------------------------------------------------
/runtime/jvm/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("app.cash.better-dynamic-features.convention")
3 | alias(libs.plugins.publish)
4 | alias(libs.plugins.spotless)
5 | }
6 |
7 | kotlin {
8 | explicitApi()
9 | }
10 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/codegen-fixture/base/src/main/kotlin/com/example/ExampleFeature.kt:
--------------------------------------------------------------------------------
1 | import app.cash.better.dynamic.features.DynamicApi
2 |
3 | interface ExampleFeature : DynamicApi {
4 | fun print()
5 | }
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/feature/src/main/kotlin/com/example/HelloWorldActivity.kt:
--------------------------------------------------------------------------------
1 | // Copyright Square, Inc.
2 |
3 | import android.app.Activity
4 |
5 | class HelloWorldActivity : Activity() {
6 |
7 | }
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/codegen-fixture/base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/library/src/main/java/com/example/JavaClass.java:
--------------------------------------------------------------------------------
1 | package com.example;
2 |
3 | public class JavaClass {
4 | public String message() {
5 | return "Java!";
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/feature/src/main/kotlin/com/example/HelloWorldActivity.kt:
--------------------------------------------------------------------------------
1 | // Copyright Square, Inc.
2 |
3 | import android.app.Activity
4 |
5 | class HelloWorldActivity : Activity() {
6 |
7 | }
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.library"
2 |
3 | android {
4 | namespace "app.cash.better.dynamic.features.integration.library"
5 |
6 | compileSdk 32
7 | }
8 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/feature/src/main/kotlin/com/example/HelloWorldActivity.kt:
--------------------------------------------------------------------------------
1 | // Copyright Square, Inc.
2 |
3 | import android.app.Activity
4 |
5 | class HelloWorldActivity : Activity() {
6 |
7 | }
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base/library/src/main/kotlin/com/example/HelloWorld.kt:
--------------------------------------------------------------------------------
1 | // Copyright Square, Inc.
2 | // Copyright Square, Inc.
3 | fun helloWorld() {
4 | println("Hello World!")
5 | }
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.library"
2 |
3 | android {
4 | namespace "app.cash.better.dynamic.features.integration.library"
5 |
6 | compileSdk 32
7 | }
8 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/feature/src/main/kotlin/com/example/HelloWorldActivity.kt:
--------------------------------------------------------------------------------
1 | // Copyright Square, Inc.
2 |
3 | import android.app.Activity
4 |
5 | class HelloWorldActivity : Activity() {
6 |
7 | }
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/feature/src/main/kotlin/com/example/HelloWorldActivity.kt:
--------------------------------------------------------------------------------
1 | // Copyright Square, Inc.
2 |
3 | import android.app.Activity
4 |
5 | class HelloWorldActivity : Activity() {
6 |
7 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea/
5 | .DS_Store
6 | **/build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
12 | !**/com/android/build
13 |
14 | **/fixtures/**/gradle.lockfile
15 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base-variant-aware/library/src/main/kotlin/com/example/HelloWorld.kt:
--------------------------------------------------------------------------------
1 | // Copyright Square, Inc.
2 | // Copyright Square, Inc.
3 | fun helloWorld() {
4 | println("Hello World!")
5 | }
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/base/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test
4 | Test Feature
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/base/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test
4 | Test Feature
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/base/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test
4 | Test Feature
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/base/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test
4 | Test Feature
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/base/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Test
4 | Test Feature
5 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | gen/
3 | out/
4 | .gradle/
5 | .idea/
6 | lib/
7 | build/
8 | .DS_Store
9 | local.properties
10 |
11 | # Ignore Gradle project-specific cache directory
12 | .gradle
13 |
14 | # Ignore Gradle build output directory
15 | build
16 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/base/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/codegen/api/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("app.cash.better-dynamic-features.convention")
3 | alias(libs.plugins.ksp)
4 | alias(libs.plugins.publish)
5 | alias(libs.plugins.spotless)
6 | }
7 |
8 | dependencies {
9 | ksp(libs.moshi.codegen)
10 |
11 | implementation(libs.moshi.core)
12 | }
13 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/sample/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/codegen-fixture/feature/src/main/kotlin/com/example/ExampleImplementation.kt:
--------------------------------------------------------------------------------
1 | import app.cash.better.dynamic.features.DynamicImplementation
2 |
3 | @DynamicImplementation
4 | class ExampleImplementation : ExampleFeature {
5 | override fun print() {
6 | println("Hello World")
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/sample/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Big Box Feature
3 | Extra Big Box Feature
4 | Big Box API
5 | Small Box Feature
6 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/out-of-date/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/same-versions/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/up-to-date/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/variant-aware/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/version-update/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/codegen-fixture/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-build-type/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-lockfile/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/has-constraints/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/lockfile-activation/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/project-dependency/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/multiplatform-dependency/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions-in-base/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/feature-compile-classpath/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/new-transitive-dependency/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base-variant-aware/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | apply from: "${projectDir.absolutePath}/../buildscript.gradle"
3 | }
4 |
5 | allprojects {
6 | repositories {
7 | maven {
8 | url "file://${rootDir}/../../../../build/localMaven"
9 | }
10 | mavenCentral()
11 | google()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/same-versions/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'app.cash.better.dynamic.features'
4 |
5 | android {
6 | namespace "app.cash.better.dynamic.features.integration"
7 |
8 | compileSdk 32
9 |
10 | dynamicFeatures = [':feature']
11 | }
12 |
13 | dependencies {
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | }
16 |
--------------------------------------------------------------------------------
/sample/bigboxfeature/api/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/sample/smallboxfeature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'app.cash.better.dynamic.features'
4 |
5 | android {
6 | namespace "app.cash.better.dynamic.features.integration"
7 |
8 | compileSdk 32
9 |
10 | dynamicFeatures = [':feature']
11 | }
12 |
13 | dependencies {
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | }
16 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base-variant-aware/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 |
4 | android {
5 | namespace "app.cash.better.dynamic.features.integration.library"
6 |
7 | compileSdk 32
8 | }
9 |
10 | dependencies {
11 | implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
12 | debugImplementation "com.squareup.retrofit2:retrofit:2.9.0"
13 | }
--------------------------------------------------------------------------------
/sample/extrabigboxfeature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/lockfile-activation/base/gradle.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 | com.squareup.okhttp3:okhttp:4.9.0=debugRuntimeClasspath, releaseRuntimeClasspath
5 | com.squareup.okio:okio:2.8.0=debugRuntimeClasspath
6 | org.jetbrains:annotations:13.0=debugRuntimeClasspath, releaseRuntimeClasspath
7 | empty=
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/out-of-date/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/up-to-date/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/version-update/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
--------------------------------------------------------------------------------
/sample/bigboxfeature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/lockfile-activation/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/multiplatform-dependency/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.squareup.sqldelight:runtime:1.5.4"
16 | }
17 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/new-transitive-dependency/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.google.android.material:material:1.2.0"
16 | }
17 |
--------------------------------------------------------------------------------
/codegen/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("app.cash.better-dynamic-features.convention")
3 | alias(libs.plugins.publish)
4 | alias(libs.plugins.spotless)
5 | }
6 |
7 | dependencies {
8 | implementation(projects.codegen.api)
9 |
10 | implementation(libs.ksp.api)
11 | implementation(libs.kotlinPoet.core)
12 | implementation(libs.kotlinPoet.ksp)
13 | implementation(libs.moshi.core)
14 | implementation(libs.moshi.kotlin)
15 |
16 | testImplementation(libs.junit)
17 | testImplementation(libs.truth)
18 | }
19 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/feature-compile-classpath/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | runtimeOnly "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0"
16 | }
17 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk = 24
12 | }
13 | }
14 |
15 | dependencies {
16 | implementation project(":base")
17 | implementation project(":library")
18 | }
19 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions-in-base/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | }
15 |
16 | betterDynamicFeatures {
17 | baseProject.set(project(":base"))
18 | }
19 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/same-versions/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'app.cash.better.dynamic.features'
4 |
5 | android {
6 | namespace "app.cash.better.dynamic.features.integration.feature"
7 |
8 | compileSdk 32
9 | }
10 |
11 | dependencies {
12 | implementation project(":base")
13 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
14 | }
15 |
16 | betterDynamicFeatures {
17 | baseProject.set(project(":base"))
18 | }
19 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'app.cash.better.dynamic.features'
4 |
5 | android {
6 | namespace "app.cash.better.dynamic.features.integration.feature"
7 |
8 | compileSdk 32
9 | }
10 |
11 | dependencies {
12 | implementation project(":base")
13 | implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
14 | }
15 |
16 | betterDynamicFeatures {
17 | baseProject.set(project(":base"))
18 | }
19 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/codegen-fixture/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 |
11 | defaultConfig {
12 | minSdk 24
13 | }
14 | }
15 |
16 | dependencies {
17 | implementation project(":base")
18 | implementation "com.squareup.okhttp3:okhttp:4.11.0"
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/codegen-fixture/feature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base-variant-aware/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | implementation project(":library")
17 | }
18 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/different-versions-in-base/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | debugImplementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | releaseImplementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
17 | }
18 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ],
6 | "packageRules": [
7 | {
8 | "enabled": false,
9 | "matchPackageNames": [
10 | "com.android.application**",
11 | "com.android.dynamic-feature**",
12 | "com.android.library**",
13 | "com.android.tools**"
14 | ]
15 | },
16 | {
17 | "enabled": false,
18 | "matchPackageNames": [
19 | "app.cash.better.dynamic.features**"
20 | ]
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/codegen/ksp/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("app.cash.better-dynamic-features.convention")
3 | alias(libs.plugins.publish)
4 | alias(libs.plugins.spotless)
5 | }
6 |
7 | dependencies {
8 | implementation(libs.ksp.api)
9 | implementation(libs.moshi.core)
10 | implementation(libs.moshi.kotlin)
11 |
12 | implementation(projects.codegen.api)
13 |
14 | testImplementation(libs.compiler.testing.core)
15 | testImplementation(libs.compiler.testing.ksp)
16 | testImplementation(libs.junit)
17 | testImplementation(libs.truth)
18 | testRuntimeOnly(projects.runtime.jvm)
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-lockfile/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/has-constraints/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4"
16 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"
17 | }
18 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/version-update/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-lockfile/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
18 | dependencyLocking {
19 | lockFile.set(file("$projectDir/custom.lockfile"))
20 | }
21 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/lockfile-activation/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/feature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/out-of-date/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/project-dependency/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 | apply plugin: 'org.jetbrains.kotlin.android'
6 |
7 | android {
8 | namespace "app.cash.better.dynamic.features.integration"
9 |
10 | compileSdk 32
11 |
12 | dynamicFeatures = [':feature']
13 | }
14 |
15 | dependencies {
16 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
17 | implementation project(":library")
18 | }
19 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/up-to-date/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.2"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-build-type/feature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/multiplatform-dependency/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.sqldelight:runtime:1.5.4"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 | apply plugin: 'org.jetbrains.kotlin.android'
6 |
7 | android {
8 | namespace "app.cash.better.dynamic.features.integration"
9 |
10 | compileSdk 32
11 |
12 | dynamicFeatures = [':feature']
13 | }
14 |
15 | dependencies {
16 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
17 | implementation project(":library")
18 | }
19 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/new-transitive-dependency/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.google.android.material:material:1.7.0"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/feature-compile-classpath/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | compileOnly "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency-on-base-variant-aware/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | }
16 |
17 | betterDynamicFeatures {
18 | baseProject.set(project(":base"))
19 | }
20 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-build-type/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk = 24
12 |
13 | versionCode 1
14 | }
15 |
16 | buildTypes {
17 | staging {
18 | initWith debug
19 | matchingFallbacks = ['debug']
20 | }
21 | }
22 |
23 | dynamicFeatures = [':feature']
24 | }
25 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing
2 |
3 | 1. Change the version in `gradle.properties` to a non-SNAPSHOT verson.
4 | 2. Update the `CHANGELOG.md` for the impending release.
5 | 3. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version)
6 | 4. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version)
7 | 5. Update the `gradle.properties` to the next SNAPSHOT version.
8 | 6. `git commit -am "Prepare next development version."`
9 | 7. `git push && git push --tags`
10 | 8. Wait until the "Publish a release" action completes, then visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifacts.
11 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation project(":library")
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
18 | betterDynamicFeatures {
19 | baseProject.set(project(":base"))
20 | }
21 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 | apply plugin: 'app.cash.better.dynamic.features.agp-patch'
6 |
7 | android {
8 | namespace "app.cash.better.dynamic.features.integration"
9 |
10 | compileSdk 32
11 | defaultConfig {
12 | minSdk = 24
13 |
14 | versionCode 1
15 | }
16 |
17 | dynamicFeatures = [':feature']
18 | }
19 |
20 | dependencies {
21 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
22 | }
23 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation project(":library")
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
18 | betterDynamicFeatures {
19 | baseProject.set(project(":base"))
20 | }
21 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/transitive-dependency/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | implementation project(":library")
16 | }
17 |
18 | betterDynamicFeatures {
19 | baseProject.set(project(":base"))
20 | }
21 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/variant-aware/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | dynamicFeatures = [':feature']
12 | }
13 |
14 | dependencies {
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | debugImplementation "com.jakewharton.picnic:picnic:0.4.0"
17 | debugImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0"
18 | }
19 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/project-dependency/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'org.jetbrains.kotlin.android'
4 | apply plugin: 'com.google.devtools.ksp'
5 | apply plugin: 'app.cash.better.dynamic.features'
6 |
7 | android {
8 | namespace "app.cash.better.dynamic.features.integration.feature"
9 |
10 | compileSdk 32
11 | }
12 |
13 | dependencies {
14 | implementation project(":base")
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
18 | betterDynamicFeatures {
19 | baseProject.set(project(":base"))
20 | }
21 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'org.jetbrains.kotlin.android'
4 | apply plugin: 'com.google.devtools.ksp'
5 | apply plugin: 'app.cash.better.dynamic.features'
6 |
7 | android {
8 | namespace "app.cash.better.dynamic.features.integration.feature"
9 |
10 | compileSdk 32
11 | }
12 |
13 | dependencies {
14 | implementation project(":base")
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
18 | betterDynamicFeatures {
19 | baseProject.set(project(":base"))
20 | }
21 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'org.jetbrains.kotlin.android'
4 | apply plugin: 'com.google.devtools.ksp'
5 | apply plugin: 'app.cash.better.dynamic.features'
6 |
7 | android {
8 | namespace "app.cash.better.dynamic.features.integration.feature"
9 |
10 | compileSdk 32
11 | }
12 |
13 | dependencies {
14 | implementation project(":base")
15 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
16 | }
17 |
18 | betterDynamicFeatures {
19 | baseProject.set(project(":base"))
20 | }
21 |
--------------------------------------------------------------------------------
/sample/bigboxfeature/api/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.dynamic.feature)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.ksp)
5 | alias(libs.plugins.betterDynamicFeatures)
6 | alias(libs.plugins.compose.compiler)
7 | }
8 |
9 | android {
10 | namespace "app.cash.boxapp.bigboxfeature.api"
11 |
12 | compileSdk libs.versions.compileSdk.get() as int
13 |
14 | defaultConfig {
15 | minSdk libs.versions.minSdk.get() as int
16 | }
17 |
18 | buildFeatures {
19 | compose true
20 | }
21 | }
22 |
23 | dependencies {
24 | implementation projects.app
25 |
26 | implementation libs.compose.runtime
27 | }
--------------------------------------------------------------------------------
/sample/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/codegen-fixture/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 |
11 | defaultConfig {
12 | versionCode 1
13 | minSdk 24
14 | }
15 |
16 | buildTypes {
17 | release {
18 | minifyEnabled true
19 | }
20 | }
21 |
22 | dynamicFeatures = [':feature']
23 | }
24 |
25 | dependencies {
26 | implementation "com.squareup.okhttp3:okhttp:4.11.0"
27 | }
28 |
--------------------------------------------------------------------------------
/gradle/license-header.txt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk = 24
12 | }
13 |
14 | dynamicFeatures = [':feature']
15 |
16 | lint {
17 | checkReleaseBuilds false
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
23 | implementation "androidx.appcompat:appcompat:1.5.1"
24 | }
25 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | }
15 | }
16 |
17 | rootProject.name = "better-dynamic-features"
18 |
19 | includeBuild("buildLogic")
20 |
21 | include(":agp-patch")
22 | include(":codegen")
23 | include(":codegen:api")
24 | include(":codegen:ksp")
25 | include(":gradle-plugin")
26 | include(":runtime")
27 | include(":runtime:jvm")
28 |
29 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
30 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/has-constraints/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4"
15 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.6.4"
16 | }
17 |
18 | betterDynamicFeatures {
19 | baseProject.set(project(":base"))
20 | }
21 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk = 24
12 | }
13 |
14 | dynamicFeatures = [':feature']
15 |
16 | lint {
17 | checkReleaseBuilds false
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation project(":library")
23 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
24 | implementation "androidx.appcompat:appcompat:1.5.1"
25 | }
26 |
--------------------------------------------------------------------------------
/sample/extrabigboxfeature/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.dynamic.feature)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.ksp)
5 | alias(libs.plugins.betterDynamicFeatures)
6 | alias(libs.plugins.compose.compiler)
7 | }
8 |
9 | android {
10 | namespace "app.cash.boxapp.extrabigboxfeature"
11 |
12 | compileSdk libs.versions.compileSdk.get() as int
13 |
14 | defaultConfig {
15 | minSdk libs.versions.minSdk.get() as int
16 | }
17 |
18 | buildFeatures {
19 | compose true
20 | }
21 | }
22 |
23 | dependencies {
24 | implementation platform(libs.compose.bom)
25 | implementation projects.app
26 | implementation libs.compose.material
27 | }
28 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/custom-build-type/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk = 24
12 | }
13 |
14 | buildTypes {
15 | staging {
16 | initWith debug
17 | matchingFallbacks = ['debug']
18 | }
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation project(":base")
24 | }
25 |
26 | betterDynamicFeatures {
27 | baseProject.set(project(":base"))
28 | }
29 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/variant-aware/feature/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.dynamic-feature'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration.feature"
8 |
9 | compileSdk 32
10 | }
11 |
12 | dependencies {
13 | implementation project(":base")
14 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
15 | debugImplementation "com.jakewharton.picnic:picnic:0.5.0"
16 | api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0"
17 | }
18 |
19 | betterDynamicFeatures {
20 | baseProject.set(project(":base"))
21 | }
22 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 | apply plugin: 'org.jetbrains.kotlin.android'
6 |
7 | android {
8 | namespace "app.cash.better.dynamic.features.integration"
9 |
10 | compileSdk 32
11 | defaultConfig {
12 | minSdk = 24
13 | }
14 |
15 | dynamicFeatures = [':feature']
16 |
17 | lint {
18 | checkReleaseBuilds false
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
24 | implementation "androidx.appcompat:appcompat:1.5.1"
25 | }
26 |
--------------------------------------------------------------------------------
/sample/bigboxfeature/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.dynamic.feature)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.ksp)
5 | alias(libs.plugins.betterDynamicFeatures)
6 | alias(libs.plugins.compose.compiler)
7 | }
8 |
9 | android {
10 | namespace "app.cash.boxapp.bigboxfeature"
11 |
12 | compileSdk libs.versions.compileSdk.get() as int
13 |
14 | defaultConfig {
15 | minSdk libs.versions.minSdk.get() as int
16 | }
17 |
18 | buildFeatures {
19 | compose true
20 | }
21 | }
22 |
23 | dependencies {
24 | implementation platform(libs.compose.bom)
25 | implementation projects.app
26 | implementation projects.bigboxfeature.api
27 | implementation libs.compose.material
28 | }
29 |
--------------------------------------------------------------------------------
/sample/smallboxfeature/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.dynamic.feature)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.ksp)
5 | alias(libs.plugins.betterDynamicFeatures)
6 | alias(libs.plugins.compose.compiler)
7 | }
8 |
9 | android {
10 | namespace "app.cash.boxapp.smallboxfeature"
11 |
12 | compileSdk libs.versions.compileSdk.get() as int
13 |
14 | defaultConfig {
15 | minSdk libs.versions.minSdk.get() as int
16 | }
17 |
18 | buildFeatures {
19 | compose true
20 | }
21 | }
22 |
23 | dependencies {
24 | implementation platform(libs.compose.bom)
25 | implementation projects.bigboxfeature.api
26 | implementation projects.app
27 | implementation libs.compose.material
28 | }
29 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | GROUP=app.cash.better.dynamic.features
2 | VERSION_NAME=0.5.3-SNAPSHOT
3 |
4 | POM_URL=https://github.com/cashapp/better-dynamic-features/
5 | POM_SCM_URL=https://github.com/cashapp/better-dynamic-features/
6 | POM_SCM_CONNECTION=scm:git:git://github.com/cashapp/better-dynamic-features.git
7 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/cashapp/better-dynamic-features.git
8 |
9 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
10 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
11 | POM_LICENCE_DIST=repo
12 |
13 | POM_DEVELOPER_ID=square
14 | POM_DEVELOPER_NAME=Square, Inc.
15 |
16 | SONATYPE_HOST=CENTRAL_PORTAL
17 | SONATYPE_AUTOMATIC_RELEASE=true
18 | RELEASE_SIGNING_ENABLED=true
19 |
20 | android.useAndroidX=true
21 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 |
6 | android {
7 | namespace "app.cash.better.dynamic.features.integration"
8 |
9 | compileSdk 32
10 | defaultConfig {
11 | minSdk = 24
12 | }
13 |
14 | dynamicFeatures = [':feature']
15 |
16 | lint {
17 | checkReleaseBuilds false
18 | }
19 | }
20 |
21 | dependencies {
22 | implementation project(":library")
23 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
24 | implementation "androidx.appcompat:appcompat:1.5.1"
25 | }
26 |
27 | betterDynamicFeatures {
28 | externalResources {
29 | style("MyTheme")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/base/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'org.jetbrains.kotlin.android'
3 | apply plugin: 'com.google.devtools.ksp'
4 | apply plugin: 'app.cash.better.dynamic.features'
5 | apply plugin: 'org.jetbrains.kotlin.android'
6 |
7 | android {
8 | namespace "app.cash.better.dynamic.features.integration"
9 |
10 | compileSdk 32
11 | defaultConfig {
12 | minSdk = 24
13 | }
14 |
15 | dynamicFeatures = [':feature']
16 |
17 | lint {
18 | checkReleaseBuilds false
19 | }
20 | }
21 |
22 | dependencies {
23 | implementation "com.squareup.okhttp3:okhttp:4.9.3"
24 | implementation "androidx.appcompat:appcompat:1.5.1"
25 | }
26 |
27 | betterDynamicFeatures {
28 | externalResources {
29 | style("MyTheme")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/runtime/jvm/src/main/kotlin/app/cash/better/dynamic/features/ExperimentalDynamicFeaturesApi.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | @RequiresOptIn
19 | public annotation class ExperimentalDynamicFeaturesApi
20 |
--------------------------------------------------------------------------------
/sample/app/src/main/kotlin/app/cash/boxapp/install/Module.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.install
17 |
18 | internal enum class Module(val id: String) {
19 | BigBox("bigboxfeature"),
20 | ExtraBigBox("extrabigboxfeature"),
21 | }
22 |
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application) apply false
3 | alias(libs.plugins.android.library) apply false
4 | alias(libs.plugins.android.dynamic.feature) apply false
5 | alias(libs.plugins.kotlin.android) apply false
6 | alias(libs.plugins.compose.compiler) apply false
7 | }
8 |
9 | subprojects {
10 | pluginManager.withPlugin("org.jetbrains.kotlin.android") {
11 | kotlin.jvmToolchain(17)
12 | }
13 |
14 | pluginManager.withPlugin("com.android.application") {
15 | android.compileOptions {
16 | sourceCompatibility(JavaVersion.VERSION_17)
17 | targetCompatibility(JavaVersion.VERSION_17)
18 | }
19 | }
20 | pluginManager.withPlugin("com.android.dynamic-feature") {
21 | android.compileOptions {
22 | sourceCompatibility(JavaVersion.VERSION_17)
23 | targetCompatibility(JavaVersion.VERSION_17)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-in-base/feature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-normal/feature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/runtime/jvm/src/main/kotlin/app/cash/better/dynamic/features/ImplementationsContainer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | @ExperimentalDynamicFeaturesApi
19 | public interface ImplementationsContainer {
20 | public fun buildImplementations(): List
21 | }
22 |
--------------------------------------------------------------------------------
/sample/bigboxfeature/api/src/main/kotlin/app/cash/boxapp/bigboxfeature/api/BigBoxMainScreen.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.bigboxfeature.api
17 |
18 | import androidx.compose.runtime.Composable
19 |
20 | interface BigBoxMainScreen {
21 | fun registerWidget(widget: @Composable () -> Unit)
22 | }
23 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-overwriting/feature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/sample/app/src/main/kotlin/app/cash/boxapp/api/BoxAppFeature.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.api
17 |
18 | import androidx.compose.runtime.Composable
19 | import app.cash.better.dynamic.features.DynamicApi
20 |
21 | public interface BoxAppFeature : DynamicApi {
22 | @Composable public fun Tile()
23 | }
24 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-correct-declaration/feature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/resources-missing-declaration/feature/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/runtime/build.gradle:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3 |
4 | plugins {
5 | alias(libs.plugins.android.library)
6 | alias(libs.plugins.kotlin.android)
7 | alias(libs.plugins.publish)
8 | alias(libs.plugins.spotless)
9 | }
10 |
11 | android {
12 | compileSdk libs.versions.compileSdk.get() as int
13 | namespace "app.cash.better.dynamic.features"
14 |
15 | defaultConfig {
16 | minSdk libs.versions.minSdk.get() as int
17 | }
18 | compileOptions {
19 | sourceCompatibility(JavaVersion.VERSION_11)
20 | targetCompatibility(JavaVersion.VERSION_11)
21 | }
22 | }
23 |
24 | kotlin {
25 | explicitApi()
26 | jvmToolchain(17)
27 | }
28 |
29 | tasks.withType(KotlinCompile).configureEach { task ->
30 | task.compilerOptions {
31 | jvmTarget.set(JvmTarget.JVM_11)
32 | }
33 | }
34 |
35 | dependencies {
36 | api libs.featureDelivery
37 | api projects.runtime.jvm
38 | }
39 |
--------------------------------------------------------------------------------
/codegen/api/src/main/kotlin/app/cash/better/dynamic/features/codegen/api/FeatureApi.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen.api
17 |
18 | import com.squareup.moshi.JsonClass
19 |
20 | @JsonClass(generateAdapter = true)
21 | data class FeatureApi(
22 | val packageName: String,
23 | val className: String,
24 | )
25 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/BetterDynamicFeaturesFeatureExtension.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | import org.gradle.api.Project
19 | import org.gradle.api.provider.Property
20 |
21 | abstract class BetterDynamicFeaturesFeatureExtension {
22 | abstract val baseProject: Property
23 | }
24 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/buildscript.gradle:
--------------------------------------------------------------------------------
1 | project.buildscript.repositories {
2 | mavenCentral()
3 | google()
4 | }
5 |
6 | project.buildscript.dependencies {
7 | classpath 'app.cash.better.dynamic.features:agp-patch:+'
8 | classpath 'app.cash.better.dynamic.features:gradle-plugin:+'
9 | classpath libs.kotlin.gradle
10 | classpath libs.agp
11 | classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:${libs.versions.ksp.get()}"
12 | }
13 |
14 | subprojects {
15 | extensions.findByName("kotlin")?.jvmToolchain(17)
16 |
17 | pluginManager.withPlugin("com.android.application") {
18 | android.compileOptions {
19 | sourceCompatibility(JavaVersion.VERSION_17)
20 | targetCompatibility(JavaVersion.VERSION_17)
21 | }
22 | }
23 | pluginManager.withPlugin("com.android.dynamic-feature") {
24 | android.compileOptions {
25 | sourceCompatibility(JavaVersion.VERSION_17)
26 | targetCompatibility(JavaVersion.VERSION_17)
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | versionCatalogs {
3 | libs {
4 | from(files("../../../../gradle/libs.versions.toml"))
5 | }
6 | }
7 | }
8 |
9 |
10 | includeBuild('../../../../../') {
11 | dependencySubstitution {
12 | substitute module("app.cash.better.dynamic.features:gradle-plugin") using project(":gradle-plugin")
13 | substitute module("app.cash.better.dynamic.features:agp-patch") using project(":agp-patch")
14 | substitute module('app.cash.better.dynamic.features:codegen') using project(':codegen')
15 | substitute module('app.cash.better.dynamic.features:codegen-api') using project(':codegen:api')
16 | substitute module('app.cash.better.dynamic.features:codegen-ksp') using project(':codegen:ksp')
17 | substitute module('app.cash.better.dynamic.features:runtime') using project(':runtime')
18 | substitute module('app.cash.better.dynamic.features:runtime-jvm') using project(':runtime:jvm')
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/codegen/api/src/main/kotlin/app/cash/better/dynamic/features/codegen/api/FeatureImplementation.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen.api
17 |
18 | import com.squareup.moshi.JsonClass
19 |
20 | @JsonClass(generateAdapter = true)
21 | data class FeatureImplementation(
22 | val qualifiedName: String,
23 | val parentClass: FeatureApi,
24 | )
25 |
--------------------------------------------------------------------------------
/agp-patch/src/main/kotlin/app/cash/better/dynamic/features/BetterDynamicFeaturesPatchExtension.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | open class BetterDynamicFeaturesPatchExtension {
19 | internal val ignoredVersions = mutableSetOf()
20 |
21 | fun suppressVersionCheck(version: String) {
22 | ignoredVersions += version
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/sample/app/src/main/kotlin/app/cash/boxapp/api/Navigator.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.api
17 |
18 | import androidx.compose.runtime.Composable
19 |
20 | public interface Navigator {
21 | public fun goTo(screen: @Composable () -> Unit)
22 |
23 | public companion object {
24 | // Singleton for hack week!
25 | public lateinit var INSTANCE: Navigator
26 | internal set
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/codegen/api/src/main/kotlin/app/cash/better/dynamic/features/codegen/api/Constants.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen.api
17 |
18 | const val KSP_REPORT_DIRECTORY_PREFIX = "better-dynamic-features-path"
19 |
20 | const val RUNTIME_IMPLEMENTATION_ANNOTATION = "app.cash.better.dynamic.features.DynamicImplementation"
21 | const val RUNTIME_API_INTERFACE = "app.cash.better.dynamic.features.DynamicApi"
22 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/codegen/AndroidVariant.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen
17 |
18 | import org.gradle.api.Named
19 | import org.gradle.api.attributes.Attribute
20 |
21 | interface AndroidVariant : Named {
22 | companion object {
23 | val ANDROID_VARIANT_ATTRIBUTE: Attribute = Attribute.of("androidVariant", AndroidVariant::class.java)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/runtime/jvm/src/main/kotlin/app/cash/better/dynamic/features/DynamicApi.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | /**
19 | * A base interface for classes defined in base modules with implementations that live in feature
20 | * modules.
21 | *
22 | * Your class should implement this interface, and each implementation should be annotated with
23 | * @[DynamicImplementation] to be made accessible to the base module at runtime.
24 | */
25 | public interface DynamicApi
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish a release
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | tags: [ '*' ]
7 |
8 | jobs:
9 | publish:
10 | runs-on: ubuntu-latest
11 |
12 | if: github.repository == 'cashapp/better-dynamic-features'
13 | permissions:
14 | contents: read
15 |
16 | steps:
17 | - uses: actions/checkout@v3
18 | - uses: actions/setup-java@v3.14.1
19 | with:
20 | distribution: 'zulu'
21 | java-version: 17
22 |
23 | - name: Publish Artifacts
24 | env:
25 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }}
26 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }}
27 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SECRET_KEY }}
28 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SECRET_PASSPHRASE }}
29 | run: ./gradlew publishAllPublicationsToMavenCentralRepository
30 |
31 | env:
32 | GRADLE_OPTS: -Dorg.gradle.parallel=true -Dorg.gradle.caching=true -Dorg.gradle.jvmargs="-Xmx2G -XX:+HeapDumpOnOutOfMemoryError"
33 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/tasks/LockfileEntry.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.tasks
17 |
18 | data class LockfileEntry(
19 | val artifact: String,
20 | val version: String,
21 | val configurations: Set,
22 | ) : Comparable {
23 | override fun compareTo(other: LockfileEntry): Int = this.toString().compareTo(other.toString())
24 |
25 | override fun toString(): String = "$artifact:$version=${configurations.sorted().joinToString(separator = ",")}"
26 | }
27 |
--------------------------------------------------------------------------------
/sample/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.ksp)
5 | alias(libs.plugins.betterDynamicFeatures)
6 | alias(libs.plugins.compose.compiler)
7 | }
8 |
9 | android {
10 | namespace "app.cash.boxapp"
11 |
12 | dynamicFeatures = [":bigboxfeature", ":smallboxfeature", ":bigboxfeature:api", ":extrabigboxfeature"]
13 |
14 | compileSdk libs.versions.compileSdk.get() as int
15 |
16 | defaultConfig {
17 | minSdk libs.versions.minSdk.get() as int
18 | targetSdk libs.versions.compileSdk.get() as int
19 |
20 | applicationId 'app.cash.boxapp'
21 |
22 | versionCode 1
23 | versionName "1.0"
24 | }
25 |
26 | buildTypes {
27 | release {
28 | minifyEnabled true
29 | }
30 | }
31 |
32 | buildFeatures {
33 | compose true
34 | }
35 |
36 | kotlinOptions {
37 | freeCompilerArgs += '-Xexplicit-api=strict'
38 | }
39 | }
40 |
41 | dependencies {
42 | implementation platform(libs.compose.bom)
43 | implementation libs.compose.material
44 | implementation libs.compose.activity
45 | implementation libs.featureDelivery
46 | implementation libs.kotlin.stdlib.common
47 | }
--------------------------------------------------------------------------------
/codegen/ksp/src/main/kotlin/app/cash/better/dynamic/features/codegen/ksp/DynamicFeaturesSymbolProcessorProvider.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen.ksp
17 |
18 | import com.google.devtools.ksp.processing.SymbolProcessor
19 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
20 | import com.google.devtools.ksp.processing.SymbolProcessorProvider
21 |
22 | class DynamicFeaturesSymbolProcessorProvider : SymbolProcessorProvider {
23 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor = FeatureModuleSymbolProcessor(environment)
24 | }
25 |
--------------------------------------------------------------------------------
/runtime/jvm/src/main/kotlin/app/cash/better/dynamic/features/DynamicImplementation.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | import kotlin.annotation.AnnotationTarget.CLASS
19 |
20 | /**
21 | * Classes in a dynamic-feature module marked with this annotation that also implement the
22 | * [DynamicApi] interface will have code generated to access an instance of the class from an
23 | * application's base module.
24 | *
25 | * These instances can be accessed using the [dynamicImplementations] function.
26 | */
27 | @Target(CLASS)
28 | public annotation class DynamicImplementation
29 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request: {}
7 |
8 | jobs:
9 | run_tests:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-java@v3.14.1
15 | with:
16 | distribution: 'zulu'
17 | java-version: 17
18 |
19 | - name: Run tests
20 | run: ./gradlew check --stacktrace
21 |
22 | - name: Store Test Results
23 | uses: actions/upload-artifact@v4
24 | if: failure()
25 | with:
26 | name: error-report
27 | path: "**/build/reports/tests/test/**/*.html"
28 |
29 | bundle_sample:
30 | runs-on: ubuntu-latest
31 |
32 | steps:
33 | - uses: actions/checkout@v3
34 | - uses: actions/setup-java@v3.14.1
35 | with:
36 | distribution: 'zulu'
37 | java-version: 17
38 |
39 | - name: Bundle Sample Project
40 | # TODO: Remove the extra runtime build command
41 | run: |
42 | ./gradlew :runtime:build
43 | ./gradlew -p sample app:bundleRelease
44 |
45 | env:
46 | GRADLE_OPTS: -Dorg.gradle.parallel=true -Dorg.gradle.caching=true -Dorg.gradle.jvmargs="-Xmx2G -XX:+HeapDumpOnOutOfMemoryError"
47 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/tasks/BaseLockfileWriterTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.tasks
17 |
18 | import org.gradle.api.file.RegularFileProperty
19 | import org.gradle.api.tasks.OutputFile
20 | import org.gradle.api.tasks.TaskAction
21 |
22 | abstract class BaseLockfileWriterTask : DependencyGraphConsumingTask() {
23 | @get:OutputFile
24 | abstract val outputLockfile: RegularFileProperty
25 |
26 | @TaskAction
27 | fun generateLockfile() {
28 | val entries = mergeGraphs(baseGraph(), featureGraphs())
29 | outputLockfile.get().asFile.writeText(entries.toText())
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sample/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | mavenCentral()
4 | google()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositories {
11 | mavenCentral()
12 | google()
13 | }
14 |
15 | versionCatalogs {
16 | create("libs") {
17 | from(files("../gradle/libs.versions.toml"))
18 | }
19 | }
20 | }
21 |
22 | includeBuild("../") {
23 | dependencySubstitution {
24 | substitute module('app.cash.better.dynamic.features:app.cash.better.dynamic.features.gradle.plugin') using project(':gradle-plugin')
25 | substitute module('app.cash.better.dynamic.features:runtime') using project(':runtime')
26 | substitute module('app.cash.better.dynamic.features:runtime-jvm') using project(':runtime:jvm')
27 | substitute module('app.cash.better.dynamic.features:codegen') using project(':codegen')
28 | substitute module('app.cash.better.dynamic.features:codegen-api') using project(':codegen:api')
29 | substitute module('app.cash.better.dynamic.features:codegen-ksp') using project(':codegen:ksp')
30 | }
31 | }
32 |
33 | rootProject.name = 'boxapp'
34 |
35 | enableFeaturePreview('TYPESAFE_PROJECT_ACCESSORS')
36 |
37 | include ':app'
38 | include ':bigboxfeature'
39 | include ':bigboxfeature:api'
40 | include ':extrabigboxfeature'
41 | include ':smallboxfeature'
42 |
--------------------------------------------------------------------------------
/sample/smallboxfeature/src/main/kotlin/app/cash/boxapp/smallboxfeature/SmallBoxWidget.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.smallboxfeature
17 |
18 | import androidx.compose.foundation.background
19 | import androidx.compose.foundation.layout.Box
20 | import androidx.compose.foundation.layout.size
21 | import androidx.compose.material.Text
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.graphics.Color
26 | import androidx.compose.ui.unit.dp
27 |
28 | @Composable fun SmallBoxWidget() {
29 | Box(
30 | modifier = Modifier.size(200.dp, 100.dp)
31 | .background(Color.White),
32 | contentAlignment = Alignment.Center,
33 | ) {
34 | Text(text = "Small Box Widget")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/agp-patch/build.gradle:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
2 |
3 | plugins {
4 | id("app.cash.better-dynamic-features.convention")
5 | id("java-gradle-plugin")
6 | alias(libs.plugins.publish)
7 | }
8 |
9 | gradlePlugin {
10 | plugins {
11 | agpPatch {
12 | id = "app.cash.better.dynamic.features.agp-patch"
13 | implementationClass = 'app.cash.better.dynamic.features.AgpPatchPlugin'
14 | }
15 | }
16 | }
17 |
18 | sourceSets {
19 | main.kotlin.srcDir "$buildDir/gen"
20 | }
21 |
22 | dependencies {
23 | implementation gradleApi()
24 |
25 | compileOnly(libs.agp)
26 | compileOnly(libs.google.guava)
27 | compileOnly(libs.google.gson)
28 | compileOnly(libs.android.common)
29 | compileOnly(libs.android.sdkCommon)
30 | compileOnly(libs.android.repository)
31 | }
32 |
33 | def pluginConstants = tasks.register("pluginConstants") {
34 | def outputDir = file("$buildDir/gen")
35 |
36 | outputs.dir(outputDir)
37 |
38 | doLast {
39 | def versionFile = file("$outputDir/app/cash/better/dynamic/features/Constants.kt")
40 | versionFile.parentFile.mkdirs()
41 | versionFile.text = """// Generated file. Do not edit!
42 | package app.cash.better.dynamic.features
43 |
44 | val TARGET_AGP_VERSION = "${libs.versions.agp.get()}"
45 | """
46 | }
47 | }
48 |
49 | tasks.withType(KotlinCompile).configureEach { dependsOn(pluginConstants) }
50 | tasks.withType(Jar).configureEach { dependsOn(pluginConstants) }
51 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/BetterDynamicFeaturesExtension.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | import groovy.lang.Closure
19 | import org.gradle.util.internal.ConfigureUtil
20 |
21 | open class BetterDynamicFeaturesExtension {
22 | internal val externalStyles = mutableListOf()
23 |
24 | fun externalResources(block: Closure) {
25 | val dsl = ExternalResourcesDsl()
26 | ConfigureUtil.configure(block, dsl)
27 |
28 | externalStyles += dsl.styles
29 | }
30 |
31 | fun externalResources(block: ExternalResourcesDsl.() -> Unit) {
32 | externalStyles += ExternalResourcesDsl().apply(block).styles
33 | }
34 |
35 | class ExternalResourcesDsl {
36 | internal val styles = mutableListOf()
37 |
38 | fun style(name: String) {
39 | styles += name
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/tasks/DependencyGraph.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.tasks
17 |
18 | import com.squareup.moshi.JsonClass
19 | import org.gradle.util.internal.VersionNumber
20 |
21 | @JsonClass(generateAdapter = true)
22 | data class Node(
23 | val artifact: String,
24 | val version: Version,
25 | val variants: Set,
26 | val children: List,
27 | val isProjectModule: Boolean,
28 | val type: DependencyType,
29 | )
30 |
31 | enum class DependencyType {
32 | Runtime,
33 | Compile,
34 | }
35 |
36 | @JsonClass(generateAdapter = true)
37 | data class NodeList(val nodes: List)
38 |
39 | @JsonClass(generateAdapter = true)
40 | @JvmInline
41 | value class Version(val string: String) : Comparable {
42 | override fun compareTo(other: Version): Int = VersionNumber.parse(string).compareTo(VersionNumber.parse(other.string))
43 |
44 | override fun toString(): String = string
45 | }
46 |
--------------------------------------------------------------------------------
/codegen/src/main/kotlin/app/cash/better/dynamic/features/codegen/GenerateProguardRules.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen
17 |
18 | import app.cash.better.dynamic.features.codegen.api.FeatureApi
19 | import app.cash.better.dynamic.features.codegen.api.FeatureImplementation
20 | import com.squareup.kotlinpoet.ClassName
21 |
22 | fun generateProguardRules(
23 | forApi: FeatureApi,
24 | implementations: List,
25 | ): String = buildString {
26 | val forApiClass = ClassName(forApi.packageName, forApi.className)
27 | // Keep feature API interface itself
28 | appendLine("-keepnames class ${forApiClass.canonicalName}")
29 | // Keep generated implementations container looked up by reflection
30 | appendLine("-keep class ${forApiClass.canonicalName}ImplementationsContainer { *; }")
31 |
32 | // Keep each implementation class and its empty constructor
33 | implementations.forEach { implementation ->
34 | appendLine("-keep class ${implementation.qualifiedName} {")
35 | appendLine(" public ();")
36 | appendLine("}")
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/tasks/GenerateExternalResourcesTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.tasks
17 |
18 | import org.gradle.api.DefaultTask
19 | import org.gradle.api.file.DirectoryProperty
20 | import org.gradle.api.provider.ListProperty
21 | import org.gradle.api.tasks.Input
22 | import org.gradle.api.tasks.OutputDirectory
23 | import org.gradle.api.tasks.TaskAction
24 | import java.io.File
25 |
26 | abstract class GenerateExternalResourcesTask : DefaultTask() {
27 | @get:OutputDirectory
28 | abstract val outputDirectory: DirectoryProperty
29 |
30 | @get:Input
31 | abstract val declarations: ListProperty
32 |
33 | @TaskAction
34 | fun generateResources() {
35 | val outputFile = File(outputDirectory.asFile.get(), "values/externals.xml")
36 | outputFile.parentFile.mkdirs()
37 | outputFile.writeText(
38 | """
39 | |
40 | |
41 | |
42 | |${declarations.get().joinToString(separator = "\n") { """ """ }}
43 | |
44 | """.trimMargin(),
45 | )
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/sample/bigboxfeature/src/main/kotlin/app/cash/boxapp/bigboxfeature/BigBoxFeature.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.bigboxfeature
17 |
18 | import androidx.compose.foundation.background
19 | import androidx.compose.foundation.clickable
20 | import androidx.compose.foundation.layout.Box
21 | import androidx.compose.foundation.layout.padding
22 | import androidx.compose.foundation.layout.size
23 | import androidx.compose.material.Text
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.graphics.Color
27 | import androidx.compose.ui.unit.dp
28 | import app.cash.better.dynamic.features.DynamicImplementation
29 | import app.cash.boxapp.api.BoxAppFeature
30 | import app.cash.boxapp.api.Navigator
31 |
32 | @DynamicImplementation
33 | class BigBoxFeature : BoxAppFeature {
34 | private val navigator get() = Navigator.INSTANCE
35 |
36 | @Composable override fun Tile() {
37 | Box(
38 | Modifier
39 | .size(width = 200.dp, height = 60.dp)
40 | .background(Color(255, 222, 133))
41 | .padding(20.dp)
42 | .clickable { navigator.goTo { BigBoxScreen() } },
43 | ) {
44 | Text("Big Box Feature!")
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/tasks/DependencyGraphConsumingTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.tasks
17 |
18 | import com.squareup.moshi.Moshi
19 | import org.gradle.api.DefaultTask
20 | import org.gradle.api.file.ConfigurableFileCollection
21 | import org.gradle.api.tasks.InputFiles
22 |
23 | private val moshi = Moshi.Builder().build()
24 | private val adapter = moshi.adapter(NodeList::class.java)
25 |
26 | abstract class DependencyGraphConsumingTask : DefaultTask() {
27 | /**
28 | * Dependency graph files for each AGP variant of every feature module.
29 | */
30 | @get:InputFiles
31 | abstract val featureDependencyGraphFiles: ConfigurableFileCollection
32 |
33 | /**
34 | * Dependency graph files for every AGP variant of the base module.
35 | */
36 | @get:InputFiles
37 | abstract val baseDependencyGraphFiles: ConfigurableFileCollection
38 |
39 | protected fun baseGraph() = baseDependencyGraphFiles.map {
40 | adapter.fromJson(it.readText())?.nodes ?: error("Could not read graph from $it")
41 | }.flatten()
42 |
43 | protected fun featureGraphs() = featureDependencyGraphFiles.map {
44 | adapter.fromJson(it.readText())?.nodes ?: error("Could not read graph from $it")
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/agp-patch/src/main/kotlin/app/cash/better/dynamic/features/AgpPatchPlugin.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | import com.android.Version
19 | import com.android.build.gradle.internal.crash.afterEvaluate
20 | import org.gradle.api.Plugin
21 | import org.gradle.api.Project
22 |
23 | class AgpPatchPlugin : Plugin {
24 | override fun apply(target: Project) {
25 | val extension = target.extensions.create("betterDynamicFeaturesPatch", BetterDynamicFeaturesPatchExtension::class.java)
26 |
27 | target.plugins.withId("com.android.application") {
28 | target.afterEvaluate {
29 | val agpVersion = Version.ANDROID_GRADLE_PLUGIN_VERSION
30 | // Skip the version check if we ignored the current AGP version!
31 | if (agpVersion in extension.ignoredVersions) {
32 | target.logger.info("Version compatibility check for Android Gradle Plugin $agpVersion skipped.")
33 | return@afterEvaluate
34 | }
35 |
36 | // Do a strict version check to ensure that our monkey-patching will work correctly.
37 | check(agpVersion == TARGET_AGP_VERSION) {
38 | "This version of the Android Gradle Plugin ($agpVersion) is not supported by the better-dynamic-features plugin. Only version $TARGET_AGP_VERSION is supported."
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/tasks/ResourceDumpingTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.tasks
17 |
18 | import org.gradle.api.DefaultTask
19 | import org.gradle.api.artifacts.ArtifactCollection
20 | import org.gradle.api.artifacts.PublishArtifactSet
21 | import org.gradle.api.file.RegularFileProperty
22 | import org.gradle.api.tasks.InputFile
23 | import org.gradle.api.tasks.Optional
24 | import org.gradle.api.tasks.TaskAction
25 |
26 | abstract class ResourceDumpingTask : DefaultTask() {
27 |
28 | private lateinit var dependencyArtifacts: ArtifactCollection
29 |
30 | fun setDependencyArtifacts(collection: ArtifactCollection) {
31 | this.dependencyArtifacts = collection
32 | }
33 |
34 | private lateinit var publishedArtifacts: PublishArtifactSet
35 | fun setPublishArtifacts(artifacts: PublishArtifactSet) {
36 | this.publishedArtifacts = artifacts
37 | }
38 |
39 | @get:[Optional InputFile]
40 | abstract val localArtifacts: RegularFileProperty
41 |
42 | @TaskAction
43 | fun dump() {
44 | println("Dump: ${dependencyArtifacts.artifacts.size}")
45 | dependencyArtifacts.artifactFiles.forEach {
46 | println(it)
47 | }
48 | println("Published: ${publishedArtifacts.size}")
49 | publishedArtifacts.files.forEach {
50 | println(it)
51 | }
52 | println(localArtifacts.orNull?.asFile)
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/sample/extrabigboxfeature/src/main/kotlin/app/cash/boxapp/extrabigboxfeature/ExtraBigBoxFeature.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.extrabigboxfeature
17 |
18 | import androidx.compose.foundation.background
19 | import androidx.compose.foundation.layout.Box
20 | import androidx.compose.foundation.layout.padding
21 | import androidx.compose.foundation.layout.size
22 | import androidx.compose.material.Text
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.graphics.Color
26 | import androidx.compose.ui.text.SpanStyle
27 | import androidx.compose.ui.text.buildAnnotatedString
28 | import androidx.compose.ui.text.font.FontWeight
29 | import androidx.compose.ui.text.withStyle
30 | import androidx.compose.ui.unit.dp
31 | import app.cash.better.dynamic.features.DynamicImplementation
32 | import app.cash.boxapp.api.BoxAppFeature
33 |
34 | @DynamicImplementation
35 | class ExtraBigBoxFeature : BoxAppFeature {
36 | @Composable override fun Tile() {
37 | Box(
38 | Modifier
39 | .size(width = 500.dp, height = 100.dp)
40 | .background(Color(0, 222, 133))
41 | .padding(20.dp),
42 | ) {
43 | Text(
44 | buildAnnotatedString {
45 | withStyle(SpanStyle(fontWeight = FontWeight.Black)) {
46 | append("EXTRA")
47 | }
48 | append(" Big Box Feature!")
49 | },
50 | )
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/sample/app/src/main/kotlin/app/cash/boxapp/api/ServiceRegistry.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.api
17 |
18 | import kotlin.reflect.KType
19 | import kotlin.reflect.typeOf
20 |
21 | public interface ServiceRegistry {
22 | public fun install(instance: R)
23 | public fun whenInstalled(withService: (T) -> Unit)
24 |
25 | public companion object {
26 | private val services = linkedMapOf>()
27 |
28 | public fun of(type: KType): ServiceRegistry {
29 | return services.getOrPut(type) {
30 | object : ServiceRegistry {
31 | private var value: T? = null
32 | private val runWhenInstalledQueue = ArrayDeque<(T) -> Unit>()
33 |
34 | override fun install(instance: R) {
35 | value = instance
36 | exhaustRunWhenInstalledQueue()
37 | }
38 |
39 | override fun whenInstalled(withService: (T) -> Unit) {
40 | runWhenInstalledQueue.add(withService)
41 | exhaustRunWhenInstalledQueue()
42 | }
43 |
44 | private fun exhaustRunWhenInstalledQueue() {
45 | val instance = value ?: return
46 | runWhenInstalledQueue.removeAll {
47 | it(instance)
48 | true
49 | }
50 | }
51 | }
52 | } as ServiceRegistry
53 | }
54 |
55 | public inline fun of(): ServiceRegistry = of(typeOf())
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/sample/README.md:
--------------------------------------------------------------------------------
1 | # Box App
2 |
3 | This is a sample app demonstrating the use of Better Dynamic Features.
4 |
5 | ## Building and Installing
6 |
7 | You will need [bundletool] installed.
8 |
9 | ```shell
10 | ./gradlew :app:bundleDebug
11 |
12 | pushd app/build/outputs/bundle/debug
13 |
14 | # The --local-testing flag is required to simulate feature module installation
15 | bundletool build-apks --local-testing --bundle app-debug.aab --output app-debug.apks
16 | bundletool install-apks --apks app-debug.apks
17 |
18 | popd
19 | ```
20 |
21 | This will bundle and install the apps, and configure support for `FakeSplitInstallManager` to
22 | simulate the installation of on-demand modules.
23 |
24 | ## Box App Structure
25 |
26 | The sample app feature two install-time feature modules (`smallboxfeature` and `bigboxfeature`), and
27 | one on-demand feature module (`extrabigboxfeature`). The sample demonstrates the code generation
28 | capabilities of Better Dynamic Features to access interface implementations from feature modules.
29 |
30 | The `:app` module defines a [`BoxAppFeature`] interface with a composable method that allows a
31 | feature implementation to draw something on the app's `HomeScreen`. The "small" and "big" box
32 | features are included by default, while the "extra big" box feature can be installed on demand.
33 | These features are accessed in the `:app` module using the `dynamicImplementations()` method which
34 | returns a `Flow` of the feature implementations.
35 |
36 | After launching the app, the "big" and "small" box features will appear on the home screen (they are
37 | just yellow rectangles).
38 | Clicking the button at the bottom of the screen will install the "extra big" box feature. This will
39 | install the `extrabigboxfeature` module, the `dynamicImplementations()` flow will automatically
40 | update with the newly installed feature, and the "extra big" box feature will be populated onto the
41 | home screen.
42 |
43 |
44 |
45 | [bundletool]: https://github.com/google/bundletool
46 | [`BoxAppFeature`]: app/src/main/kotlin/app/cash/boxapp/api/BoxAppFeature.kt
47 |
--------------------------------------------------------------------------------
/sample/bigboxfeature/src/main/kotlin/app/cash/boxapp/bigboxfeature/BigBoxScreen.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.bigboxfeature
17 |
18 | import androidx.compose.foundation.background
19 | import androidx.compose.foundation.layout.Column
20 | import androidx.compose.foundation.layout.Spacer
21 | import androidx.compose.foundation.layout.fillMaxSize
22 | import androidx.compose.material.Text
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.graphics.Color
27 | import app.cash.boxapp.api.ServiceRegistry
28 | import app.cash.boxapp.bigboxfeature.api.BigBoxMainScreen
29 |
30 | private object BigBoxScreenWidgets : BigBoxMainScreen {
31 | val widgets = mutableListOf<@Composable () -> Unit>()
32 |
33 | init {
34 | ServiceRegistry.of().install(this)
35 | }
36 |
37 | override fun registerWidget(widget: @Composable () -> Unit) {
38 | widgets.add(widget)
39 | }
40 | }
41 |
42 | @Composable fun BigBoxScreen() {
43 | Column(
44 | modifier = Modifier
45 | .fillMaxSize()
46 | .background(Color(255, 222, 133)),
47 | horizontalAlignment = Alignment.CenterHorizontally,
48 | ) {
49 | Spacer(modifier = Modifier.weight(1f))
50 | Text(text = "Big Box Main Screen!")
51 | Spacer(modifier = Modifier.weight(1f))
52 |
53 | BigBoxScreenWidgets.widgets.forEach {
54 | it()
55 | Spacer(modifier = Modifier.weight(1f))
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/tasks/CheckLockfileTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.tasks
17 |
18 | import org.gradle.api.file.RegularFileProperty
19 | import org.gradle.api.provider.Property
20 | import org.gradle.api.tasks.Input
21 | import org.gradle.api.tasks.InputFile
22 | import org.gradle.api.tasks.Internal
23 | import org.gradle.api.tasks.Optional
24 | import org.gradle.api.tasks.OutputFile
25 | import org.gradle.api.tasks.TaskAction
26 | import java.io.File
27 |
28 | abstract class CheckLockfileTask : DependencyGraphConsumingTask() {
29 | @get:Internal
30 | abstract val currentLockfilePath: RegularFileProperty
31 |
32 | @get:[Optional InputFile]
33 | val currentLockfile: File?
34 | get() = currentLockfilePath.get().asFile.takeIf { it.exists() }
35 |
36 | // Used for caching
37 | @get:OutputFile
38 | abstract val outputFile: RegularFileProperty
39 |
40 | @get:Input
41 | abstract val projectPath: Property
42 |
43 | @TaskAction
44 | fun checkLockfiles() {
45 | val currentEntries = mergeGraphs(baseGraph(), featureGraphs())
46 | val areTheyEqual = currentEntries.toText() == currentLockfile?.readText()
47 |
48 | outputFile.asFile.get().writeText(areTheyEqual.toString())
49 | if (!areTheyEqual) {
50 | currentLockfilePath.asFile.get().writeText(currentEntries.toText())
51 | throw IllegalStateException("The lockfile was out of date and has been updated. Rerun your build.")
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/sample/smallboxfeature/src/main/kotlin/app/cash/boxapp/smallboxfeature/SmallBoxFeature.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.smallboxfeature
17 |
18 | import androidx.compose.foundation.background
19 | import androidx.compose.foundation.layout.Box
20 | import androidx.compose.foundation.layout.padding
21 | import androidx.compose.foundation.layout.size
22 | import androidx.compose.material.Text
23 | import androidx.compose.runtime.Composable
24 | import androidx.compose.runtime.LaunchedEffect
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.unit.dp
29 | import app.cash.better.dynamic.features.DynamicImplementation
30 | import app.cash.boxapp.api.BoxAppFeature
31 | import app.cash.boxapp.api.ServiceRegistry
32 | import app.cash.boxapp.bigboxfeature.api.BigBoxMainScreen
33 |
34 | @DynamicImplementation
35 | class SmallBoxFeature : BoxAppFeature {
36 | @Composable override fun Tile() {
37 | Box(
38 | modifier = Modifier
39 | .size(width = 100.dp, height = 40.dp)
40 | .background(Color(255, 222, 133))
41 | .padding(start = 10.dp, end = 10.dp),
42 | contentAlignment = Alignment.CenterStart,
43 | ) {
44 | Text("Small Box Feature!")
45 | }
46 |
47 | LaunchedEffect(null) {
48 | ServiceRegistry.of().whenInstalled { bigBoxMainScreen ->
49 | bigBoxMainScreen.registerWidget { SmallBoxWidget() }
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/java/app/cash/better/dynamic/features/utils/SequenceSubject.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.utils
17 |
18 | import com.google.common.truth.Fact.simpleFact
19 | import com.google.common.truth.FailureMetadata
20 | import com.google.common.truth.Subject
21 | import com.google.common.truth.Truth.assertAbout
22 |
23 | class SequenceSubject(metadata: FailureMetadata, private val actual: Sequence) : Subject(metadata, actual) {
24 | fun containsInConsecutiveOrder(vararg values: T) {
25 | var matchCounter = 0
26 | actual.forEach {
27 | if (matchCounter == values.size) return@forEach
28 | if (it == values[matchCounter]) {
29 | matchCounter++
30 | } else {
31 | matchCounter = 0
32 | }
33 | }
34 |
35 | if (matchCounter != values.size) {
36 | failWithoutActual(
37 | simpleFact(
38 | "Expected to find ${
39 | values.joinToString(
40 | prefix = "[",
41 | postfix = "]",
42 | )
43 | } in consecutive order, but they were not found.",
44 | ),
45 | )
46 | }
47 | }
48 |
49 | companion object {
50 | @JvmStatic
51 | fun sequences(): Factory, Sequence> {
52 | return Factory { metadata, actual ->
53 | SequenceSubject(metadata, actual as Sequence)
54 | }
55 | }
56 | }
57 | }
58 |
59 | fun assertThat(actual: Sequence): SequenceSubject = assertAbout(SequenceSubject.sequences()).that(actual)
60 |
--------------------------------------------------------------------------------
/sample/app/src/main/kotlin/app/cash/boxapp/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.ui
17 |
18 | import android.os.Bundle
19 | import androidx.activity.ComponentActivity
20 | import androidx.activity.compose.setContent
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import app.cash.boxapp.api.Navigator
27 | import java.util.Stack
28 |
29 | internal class MainActivity :
30 | ComponentActivity(),
31 | Navigator {
32 | init {
33 | Navigator.INSTANCE = this
34 | }
35 |
36 | private var goToListener: (@Composable () -> Unit) -> Unit = { }
37 | private var backStack = Stack<@Composable () -> Unit>().apply {
38 | push { HomeScreen() }
39 | }
40 |
41 | override fun goTo(screen: @Composable () -> Unit) {
42 | backStack.push(screen)
43 | goToListener(screen)
44 | }
45 |
46 | override fun onBackPressed() {
47 | if (backStack.size == 1) {
48 | super.onBackPressed()
49 | } else {
50 | backStack.pop() // Current screen.
51 | goToListener(backStack.peek())
52 | }
53 | }
54 |
55 | override fun onCreate(savedInstanceState: Bundle?) {
56 | super.onCreate(savedInstanceState)
57 | setContent {
58 | var screen: @Composable () -> Unit by remember { mutableStateOf({ HomeScreen() }) }
59 |
60 | goToListener = {
61 | screen = it
62 | }
63 |
64 | screen()
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/runtime/src/main/kotlin/app/cash/better/dynamic/features/DynamicImplementations.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | import com.google.android.play.core.ktx.status
19 | import com.google.android.play.core.splitinstall.SplitInstallManager
20 | import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
21 | import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus
22 | import kotlinx.coroutines.ExperimentalCoroutinesApi
23 | import kotlinx.coroutines.channels.awaitClose
24 | import kotlinx.coroutines.flow.Flow
25 | import kotlinx.coroutines.flow.callbackFlow
26 |
27 | /**
28 | * Obtain a list of instances of [T] that were implemented in feature modules.
29 | *
30 | * TODO: This API is not finalized and may change drastically!
31 | */
32 | @OptIn(ExperimentalCoroutinesApi::class)
33 | @ExperimentalDynamicFeaturesApi
34 | @Suppress("UNCHECKED_CAST")
35 | public inline fun SplitInstallManager.dynamicImplementations(): Flow> {
36 | val apiClassName = T::class.java.name
37 |
38 | val container = Class.forName("${apiClassName}ImplementationsContainer")
39 | .getDeclaredField("INSTANCE")
40 | .get(null) as ImplementationsContainer
41 |
42 | return callbackFlow {
43 | send(container.buildImplementations())
44 | val listener = SplitInstallStateUpdatedListener {
45 | if (it.status == SplitInstallSessionStatus.INSTALLED) {
46 | trySend(container.buildImplementations())
47 | }
48 | }
49 |
50 | this@dynamicImplementations.registerListener(listener)
51 | awaitClose {
52 | this@dynamicImplementations.unregisterListener(listener)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/gradle-plugin/build.gradle:
--------------------------------------------------------------------------------
1 | import com.google.devtools.ksp.gradle.KspTaskJvm
2 | import com.squareup.wire.gradle.WireTask
3 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
4 |
5 | plugins {
6 | id("app.cash.better-dynamic-features.convention")
7 | alias(libs.plugins.ksp)
8 | id("java-gradle-plugin")
9 | alias(libs.plugins.publish)
10 | alias(libs.plugins.spotless)
11 | alias(libs.plugins.wire)
12 | }
13 |
14 | gradlePlugin {
15 | plugins {
16 | betterDynamicFeatures {
17 | id = 'app.cash.better.dynamic.features'
18 | implementationClass = 'app.cash.better.dynamic.features.BetterDynamicFeaturesPlugin'
19 | }
20 | }
21 | }
22 |
23 | sourceSets {
24 | main.kotlin.srcDir "$buildDir/gen"
25 | }
26 |
27 | dependencies {
28 | ksp(libs.moshi.codegen)
29 |
30 | compileOnly(libs.kotlin.compiler)
31 |
32 | implementation(projects.codegen)
33 | implementation(projects.codegen.api)
34 |
35 | implementation gradleApi()
36 | implementation(libs.agp)
37 | implementation(libs.javassist)
38 | implementation(libs.kotlin.gradle)
39 | implementation(libs.kotlinPoet.core)
40 | implementation(libs.ksp.gradlePlugin)
41 | implementation(libs.moshi.core)
42 | implementation(libs.moshi.kotlin)
43 | implementation(files("libs/ARSCLib-1.1.5.jar"))
44 |
45 | testImplementation(libs.kotlin.compiler)
46 | testImplementation(libs.junit)
47 | testImplementation(libs.truth)
48 | }
49 |
50 | def pluginVersion = tasks.register("pluginVersion") {
51 | def outputDir = file("$buildDir/gen")
52 | def rootPropertiesFile = file("$rootProject.projectDir/gradle.properties")
53 |
54 | inputs.file rootPropertiesFile
55 | outputs.dir outputDir
56 |
57 | doLast {
58 | def rootProperties = new Properties()
59 | rootPropertiesFile.withInputStream {
60 | rootProperties.load(it)
61 | }
62 |
63 | def versionFile = file("$outputDir/app/cash/better/dynamic/features/Version.kt")
64 | versionFile.parentFile.mkdirs()
65 | versionFile.text = """// Generated file. Do not edit!
66 | package app.cash.better.dynamic.features
67 |
68 | val VERSION = "${rootProperties.getProperty("VERSION_NAME")}"
69 | val KOTLIN_VERSION = "${libs.versions.kotlin.get()}"
70 | """
71 | }
72 | }
73 |
74 | tasks.withType(KotlinCompile).configureEach { dependsOn(pluginVersion) }
75 | tasks.withType(KspTaskJvm).configureEach { dependsOn(pluginVersion) }
76 |
77 | wire {
78 | kotlin {}
79 | }
80 |
81 | // https://github.com/square/wire/issues/2335
82 | afterEvaluate {
83 | tasks.withType(KspTaskJvm).configureEach {
84 | dependsOn(tasks.withType(WireTask))
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/codegen/src/test/kotlin/app/cash/better/dynamic/features/codegen/GenerateImplementationsContainerTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen
17 |
18 | import app.cash.better.dynamic.features.codegen.api.FeatureApi
19 | import app.cash.better.dynamic.features.codegen.api.FeatureImplementation
20 | import com.google.common.truth.Truth.assertThat
21 | import org.junit.Test
22 |
23 | class GenerateImplementationsContainerTest {
24 | @Test
25 | fun `codegen for an api and implementations`() {
26 | val api = FeatureApi("test", "TestApi")
27 | val implementations = listOf(
28 | FeatureImplementation("test.TestImplementation1", api),
29 | FeatureImplementation("test.TestImplementation2", api),
30 | )
31 |
32 | val spec = generateImplementationsContainer(forApi = api, implementations)
33 | assertThat(spec.toString())
34 | .isEqualTo(
35 | """
36 | package test
37 |
38 | import app.cash.better.`dynamic`.features.ExperimentalDynamicFeaturesApi
39 | import app.cash.better.`dynamic`.features.ImplementationsContainer
40 | import java.lang.ClassNotFoundException
41 | import kotlin.OptIn
42 | import kotlin.collections.List
43 | import kotlin.collections.buildList
44 |
45 | @OptIn(ExperimentalDynamicFeaturesApi::class)
46 | public object TestApiImplementationsContainer : ImplementationsContainer {
47 | public override fun buildImplementations(): List = buildList {
48 | try {
49 | add(Class.forName("test.TestImplementation1").getDeclaredConstructor().newInstance() as
50 | TestApi)
51 | } catch(e: ClassNotFoundException) {
52 | }
53 | try {
54 | add(Class.forName("test.TestImplementation2").getDeclaredConstructor().newInstance() as
55 | TestApi)
56 | } catch(e: ClassNotFoundException) {
57 | }
58 | }
59 | }
60 |
61 | """.trimIndent(),
62 | )
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/sample/app/src/main/kotlin/app/cash/boxapp/install/SplitInstallHelper.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.boxapp.install
17 |
18 | import android.content.Context
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.ui.platform.LocalContext
22 | import com.google.android.play.core.ktx.requestDeferredUninstall
23 | import com.google.android.play.core.ktx.requestInstall
24 | import com.google.android.play.core.ktx.status
25 | import com.google.android.play.core.splitinstall.SplitInstallManager
26 | import com.google.android.play.core.splitinstall.SplitInstallManagerFactory
27 | import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener
28 | import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus
29 | import kotlinx.coroutines.channels.awaitClose
30 | import kotlinx.coroutines.flow.Flow
31 | import kotlinx.coroutines.flow.callbackFlow
32 |
33 | @Composable
34 | internal fun rememberSplitInstallHelper(): SplitInstallHelper {
35 | val context = LocalContext.current
36 | return remember { SplitInstallHelper(context) }
37 | }
38 |
39 | internal class SplitInstallHelper(
40 | context: Context,
41 | val splitInstallManager: SplitInstallManager = SplitInstallManagerFactory.create(context),
42 | ) {
43 | val isInstalling: Flow = callbackFlow {
44 | send(false)
45 | val listener = SplitInstallStateUpdatedListener {
46 | when (it.status) {
47 | SplitInstallSessionStatus.PENDING,
48 | SplitInstallSessionStatus.DOWNLOADING,
49 | SplitInstallSessionStatus.DOWNLOADED,
50 | SplitInstallSessionStatus.INSTALLING,
51 | -> trySend(true)
52 |
53 | else -> trySend(false)
54 | }
55 | }
56 |
57 | splitInstallManager.registerListener(listener)
58 | awaitClose {
59 | splitInstallManager.unregisterListener(listener)
60 | }
61 | }
62 |
63 | suspend fun requestInstall(module: Module) {
64 | splitInstallManager.requestInstall(modules = listOf(module.id))
65 | }
66 |
67 | suspend fun requestUninstall(module: Module) {
68 | splitInstallManager.requestDeferredUninstall(moduleNames = listOf(module.id))
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/codegen/TypesafeImplementationsGeneratorTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen
17 |
18 | import app.cash.better.dynamic.features.codegen.api.FeatureImplementation
19 | import com.squareup.moshi.Moshi
20 | import okio.buffer
21 | import okio.source
22 | import org.gradle.api.DefaultTask
23 | import org.gradle.api.file.ConfigurableFileCollection
24 | import org.gradle.api.file.DirectoryProperty
25 | import org.gradle.api.file.RegularFileProperty
26 | import org.gradle.api.tasks.InputFiles
27 | import org.gradle.api.tasks.OutputDirectory
28 | import org.gradle.api.tasks.OutputFile
29 | import org.gradle.api.tasks.TaskAction
30 |
31 | private val moshi = Moshi.Builder().build()
32 | private val featureAdapter = moshi.adapter(FeatureImplementation::class.java)
33 |
34 | abstract class TypesafeImplementationsGeneratorTask : DefaultTask() {
35 | @get:InputFiles
36 | abstract val featureImplementationReports: ConfigurableFileCollection
37 |
38 | @get:OutputFile
39 | abstract val generatedProguardFile: RegularFileProperty
40 |
41 | @get:OutputDirectory
42 | abstract val generatedFilesDirectory: DirectoryProperty
43 |
44 | private val generatedSourcesDirectory get() = generatedFilesDirectory.asFile.get()
45 |
46 | @TaskAction
47 | fun generate() {
48 | generatedSourcesDirectory.mkdirs()
49 |
50 | val collectedFeatures = featureImplementationReports.files
51 | .flatMap { dir -> buildList { dir.walk().forEach { if (it.isFile) add(it) } } }
52 | .mapNotNull { it.source().buffer().use { buffer -> featureAdapter.fromJson(buffer) } }
53 | .groupBy { it.parentClass }
54 |
55 | collectedFeatures.forEach { (api, implementations) ->
56 | val fileSpec =
57 | generateImplementationsContainer(forApi = api, implementations = implementations)
58 | fileSpec.writeTo(directory = generatedSourcesDirectory)
59 | }
60 |
61 | val proguardFile = generatedProguardFile.asFile.get()
62 | if (proguardFile.exists()) {
63 | proguardFile.delete()
64 | proguardFile.createNewFile()
65 | }
66 | proguardFile.bufferedWriter().use { writer ->
67 | collectedFeatures.forEach { (api, implementations) ->
68 | writer.append(generateProguardRules(forApi = api, implementations))
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/codegen/TypesafeImplementationsCompilationTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen
17 |
18 | import org.gradle.api.DefaultTask
19 | import org.gradle.api.file.ConfigurableFileCollection
20 | import org.gradle.api.file.Directory
21 | import org.gradle.api.file.DirectoryProperty
22 | import org.gradle.api.file.RegularFile
23 | import org.gradle.api.file.RegularFileProperty
24 | import org.gradle.api.provider.ListProperty
25 | import org.gradle.api.tasks.Classpath
26 | import org.gradle.api.tasks.InputDirectory
27 | import org.gradle.api.tasks.InputFiles
28 | import org.gradle.api.tasks.OutputFile
29 | import org.gradle.api.tasks.TaskAction
30 | import org.gradle.workers.WorkParameters
31 | import org.gradle.workers.WorkerExecutor
32 | import javax.inject.Inject
33 |
34 | abstract class TypesafeImplementationsCompilationTask : DefaultTask() {
35 | @get:InputFiles
36 | abstract val projectJars: ListProperty
37 |
38 | @get:InputFiles
39 | abstract val projectClasses: ListProperty
40 |
41 | @get:OutputFile
42 | abstract val output: RegularFileProperty
43 |
44 | @get:InputDirectory
45 | abstract val generatedSources: DirectoryProperty
46 |
47 | @get:Classpath
48 | abstract val compileClasspath: ConfigurableFileCollection
49 |
50 | @get:Classpath
51 | abstract val kotlinCompiler: ConfigurableFileCollection
52 |
53 | @get:Inject
54 | abstract val workerExecutor: WorkerExecutor
55 |
56 | interface TypesafeImplementationsCompilationWorkParameters : WorkParameters {
57 | val projectJars: ListProperty
58 | val projectClasses: ListProperty
59 | val output: RegularFileProperty
60 | val generatedSources: DirectoryProperty
61 | val compileClasspath: ConfigurableFileCollection
62 | val temporaryDir: DirectoryProperty
63 | }
64 |
65 | @TaskAction
66 | fun processImplementations() {
67 | val tempClassDirectory = temporaryDir.resolve("classes").also { it.mkdirs() }
68 |
69 | val workQueue = workerExecutor.classLoaderIsolation {
70 | it.classpath.from(kotlinCompiler)
71 | }
72 |
73 | workQueue.submit(CompileTypesafeImplementations::class.java) { parameters ->
74 | parameters.projectJars.set(projectJars)
75 | parameters.projectClasses.set(projectClasses)
76 | parameters.output.set(output)
77 | parameters.generatedSources.set(generatedSources)
78 | parameters.compileClasspath.setFrom(compileClasspath)
79 | parameters.temporaryDir.set(tempClassDirectory)
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/java/app/cash/better/dynamic/features/TestUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features
17 |
18 | import org.gradle.testkit.runner.GradleRunner
19 | import java.io.File
20 | import java.nio.file.Files
21 | import java.nio.file.Path
22 | import java.util.Properties
23 | import kotlin.io.path.Path
24 |
25 | internal fun GradleRunner.withCommonConfiguration(projectRoot: File): GradleRunner {
26 | File(projectRoot, "gradle.properties").writeText(
27 | """
28 | |org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
29 | |android.useAndroidX=true
30 | |
31 | """.trimMargin(),
32 | )
33 | File(projectRoot, "local.properties").apply {
34 | if (!exists()) writeText("sdk.dir=${androidHome()}\n")
35 | }
36 | return withProjectDir(projectRoot)
37 | }
38 |
39 | internal fun androidHome(): String {
40 | val env = System.getenv("ANDROID_HOME")
41 | if (env != null) {
42 | return env.withInvariantPathSeparators()
43 | }
44 | val localProp = File(File(System.getProperty("user.dir")).parentFile, "local.properties")
45 | if (localProp.exists()) {
46 | val prop = Properties()
47 | localProp.inputStream().use {
48 | prop.load(it)
49 | }
50 | val sdkHome = prop.getProperty("sdk.dir")
51 | if (sdkHome != null) {
52 | return sdkHome.withInvariantPathSeparators()
53 | }
54 | }
55 | throw IllegalStateException(
56 | "Missing 'ANDROID_HOME' environment variable or local.properties with 'sdk.dir'",
57 | )
58 | }
59 |
60 | internal fun String.withInvariantPathSeparators() = replace("\\", "/")
61 |
62 | fun GradleRunner.withFreshLockfile(module: String = "base"): GradleRunner {
63 | withArguments("$module:writeLockfile").build()
64 | return this
65 | }
66 |
67 | fun GradleRunner.cleaned(module: String? = null): GradleRunner {
68 | withArguments(if (module == null) "clean" else "$module:clean").build()
69 | return this
70 | }
71 |
72 | fun dexdump(file: File): Path {
73 | val outputPath = Files.createTempFile("dexdump", "txt")
74 |
75 | val buildToolsVersion = Class.forName("com.android.SdkConstants").getDeclaredField("CURRENT_BUILD_TOOLS_VERSION").get(null)
76 | val dexdumpPath = "${System.getenv("ANDROID_SDK_ROOT")}/build-tools/$buildToolsVersion/dexdump"
77 | check(Files.exists(Path(dexdumpPath))) { "Could not find 'dexdump' binary. Checked $dexdumpPath" }
78 |
79 | val process = ProcessBuilder()
80 | .apply {
81 | command(dexdumpPath, file.absolutePath)
82 | redirectOutput(outputPath.toFile())
83 | }
84 | .start()
85 |
86 | check(process.waitFor() == 0) { "dexdump failed" }
87 | return outputPath
88 | }
89 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [0.5.2-agp8.13.1] - 2025-11-14
4 |
5 | * Bump to AGP 8.13.1 (#229)
6 | * Bump to Gradle 9.2 (#228)
7 |
8 | ## [0.5.2-agp8.13.0] - 2025-09-25
9 |
10 | * Bump to AGP 8.13.0 (#219)
11 |
12 | ## [0.5.2-agp8.12.2] - 2025-09-02
13 |
14 | * Bump to AGP 8.12.2 (#216)
15 |
16 | ## [0.5.2-agp8.12.1] - 2025-08-29
17 |
18 | * Bump to AGP 8.12.1 (#212)
19 |
20 | ## [0.5.2-agp8.12.0] - 2025-08-14
21 |
22 | * Bump to AGP 8.12.0 (#207)
23 |
24 | ## [0.5.2-agp8.11.0] - 2025-07-23
25 |
26 | * Bump to AGP 8.11.1 (#202)
27 |
28 | ## [0.5.2-agp8.11.0] - 2025-06-25
29 |
30 | * Bump to AGP 8.11.0 (#195)
31 |
32 | ## [0.5.2-agp8.10.1] - 2025-05-07
33 |
34 | * Bump to AGP 8.10.1 (#196)
35 | * Bump compileSdk and targetSdk to 36 (#192)
36 | * Gradle cleanup (#191, #190)
37 | * Fix compile error in tests (#186)
38 | * Bump Wire to 5.3.1 (#185)
39 | * Switch to Compose BOM (#184)
40 |
41 | ## [0.5.1-agp8.10.0] - 2025-05-07
42 |
43 | * Bump to AGP 8.10.0 (#148)
44 | * Bump Kotlin to 2.1.20 (#164)
45 |
46 | ## [0.5.1-agp8.9.2] - 2025-04-25
47 |
48 | * Bump to AGP 8.9.2 (#168)
49 | * Support KSP integration for custom build types (#146)
50 |
51 | ## [0.5.0-agp8.9.0] - 2025-03-04
52 |
53 | * Bump to AGP 8.9.0 and Gradle 8.12.1 (#138)
54 |
55 | ## [0.5.0-agp8.8.1] - 2025-01-14
56 |
57 | * Bump to AGP 8.8.1 (#140)
58 |
59 | ## [0.5.0-agp8.8.0] - 2025-01-14
60 |
61 | * Bump to AGP 8.8.0 and Gradle 8.10.2 (#131)
62 |
63 | ## [0.5.0-agp8.7.3] - 2024-12-11
64 |
65 | * Bump to AGP 8.7.3 (#135)
66 |
67 | ## [0.5.0-agp8.7.2] - 2024-11-20
68 |
69 | * Bump to AGP 8.7.2 (#133)
70 |
71 | ## [0.5.0-agp8.7.1] - 2024-10-18
72 |
73 | * Bump to AGP 8.7.1 (#132)
74 |
75 | ## [0.5.0-agp8.6.1] - 2024-09-18
76 |
77 | * Bump to AGP 8.6.1 (#130)
78 |
79 | ## [0.5.0-agp8.5.1] - 2024-07-24
80 |
81 | * Bump to AGP 8.5.1 (#128)
82 |
83 | ## [0.5.0-agp8.5.0] - 2024-07-03
84 |
85 | * Bump to AGP 8.5.0 (#123)
86 | * Change to new versioning format
87 |
88 | ## [0.4.1] - 2024-06-20
89 |
90 | * Bump to AGP 8.4.2 (#124)
91 |
92 | ## [0.4.0] - 2024-05-29
93 |
94 | * Bump to AGP 8.4.1 (#121)
95 |
96 | ## [0.3.1] - 2024-05-01
97 |
98 | * Put the gradle styles in quotes in the error message (#118)
99 | * Bump to AGP 8.3.2 (#120)
100 |
101 | ## [0.3.0] - 2024-03-14
102 |
103 | * Upgrade AGP to 8.3.0 (#117)
104 | * Recursively search for `DynamicApi` supertype (#114)
105 | * Fixed KSP configuration for flavored builds (#115)
106 |
107 | ## [0.2.2] - 2024-01-24
108 |
109 | * Upgrade AGP to 8.2.2
110 |
111 | ## [0.2.1] - 2024-01-22
112 |
113 | * Upgrade AGP to 8.2.1 (#108)
114 |
115 | ## [0.2.0] - 2023-12-11
116 |
117 | * Upgrade AGP to 8.2.0 and Gradle 8.2 (#106)
118 |
119 | ## [0.1.3] - 2023-11-17
120 |
121 | * Upgrade AGP to 8.1.4
122 |
123 | ## [0.1.2] - 2023-09-22
124 |
125 | * Upgrade AGP to 8.1.1 (#104)
126 | * Fix gradle plugin applying `unspecified` version of KSP and runtime dependencies to projects. (#103)
127 |
128 | ## [0.1.1] - 2023-07-31
129 |
130 | * Remove an unnecessary println message
131 |
132 | ## [0.1.0] - 2023-07-25
133 |
134 | Initial non-snapshot release.
135 |
136 | * `agp-patch` targeting Android Gradle Plugin 8.1.0
137 | * Initial experimental code generation functionality.
138 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/sample/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/codegen/src/main/kotlin/app/cash/better/dynamic/features/codegen/GenerateImplementationsContainer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | @file:Suppress("PrivatePropertyName")
17 |
18 | package app.cash.better.dynamic.features.codegen
19 |
20 | import app.cash.better.dynamic.features.codegen.api.FeatureApi
21 | import app.cash.better.dynamic.features.codegen.api.FeatureImplementation
22 | import com.squareup.kotlinpoet.AnnotationSpec
23 | import com.squareup.kotlinpoet.ClassName
24 | import com.squareup.kotlinpoet.CodeBlock
25 | import com.squareup.kotlinpoet.FileSpec
26 | import com.squareup.kotlinpoet.FunSpec
27 | import com.squareup.kotlinpoet.KModifier.OVERRIDE
28 | import com.squareup.kotlinpoet.MemberName
29 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
30 | import com.squareup.kotlinpoet.TypeSpec
31 | import com.squareup.kotlinpoet.asTypeName
32 |
33 | private val TYPE_IMPLEMENTATIONS_CONTAINER =
34 | ClassName("app.cash.better.dynamic.features", "ImplementationsContainer")
35 |
36 | fun generateImplementationsContainer(
37 | forApi: FeatureApi,
38 | implementations: List,
39 | ): FileSpec {
40 | val apiTypeName = ClassName(forApi.packageName, forApi.className)
41 | val fileSpec = FileSpec.builder(forApi.packageName, "${forApi.className}ImplementationsContainer")
42 |
43 | val implementationsFunction = FunSpec.builder("buildImplementations")
44 | .returns(List::class.asTypeName().parameterizedBy(apiTypeName))
45 | .addModifiers(OVERRIDE)
46 | .addCode(
47 | CodeBlock.builder()
48 | .beginControlFlow("return %M", MemberName("kotlin.collections", "buildList"))
49 | .apply {
50 | implementations.forEach { implementation ->
51 | beginControlFlow("try")
52 | addStatement(
53 | "add(Class.forName(%S).getDeclaredConstructor().newInstance() as %T)",
54 | implementation.qualifiedName,
55 | apiTypeName,
56 | )
57 | nextControlFlow("catch(e: %T)", ClassName("java.lang", "ClassNotFoundException"))
58 | endControlFlow()
59 | }
60 | }
61 | .endControlFlow().build(),
62 | )
63 | .build()
64 |
65 | val objectSpec = TypeSpec.objectBuilder("${forApi.className}ImplementationsContainer")
66 | .addSuperinterface(TYPE_IMPLEMENTATIONS_CONTAINER.parameterizedBy(apiTypeName))
67 | .addAnnotation(
68 | AnnotationSpec.builder(ClassName.bestGuess("kotlin.OptIn"))
69 | .addMember(
70 | "%T::class",
71 | ClassName("app.cash.better.dynamic.features", "ExperimentalDynamicFeaturesApi"),
72 | ).build(),
73 | )
74 | .addFunction(implementationsFunction)
75 | .build()
76 |
77 | return fileSpec.addType(objectSpec).build()
78 | }
79 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.13.1" # keep in sync with android-tools
3 | android-tools = "31.13.1" # = 23.0.0 + agp
4 | compilerTesting = "0.7.1"
5 | kotlin = "2.1.20"
6 | kotlinPoet = "1.13.1"
7 | ksp = "2.1.20-1.0.32"
8 | ktlint = "1.7.1"
9 | moshi = "1.15.2"
10 |
11 | # Used by the sample app
12 | compileSdk = "36"
13 | minSdk = "24"
14 |
15 |
16 | [libraries]
17 | agp = { module = "com.android.tools.build:gradle", version.ref = "agp" }
18 | javassist = { module = "org.javassist:javassist", version = "3.30.2-GA" }
19 | compiler-testing-core = { module = "dev.zacsweers.kctfork:core", version.ref = "compilerTesting" }
20 | compiler-testing-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "compilerTesting" }
21 | featureDelivery = { module = "com.google.android.play:feature-delivery-ktx", version = "2.1.0"}
22 | junit = { module = "junit:junit", version = "4.13.2" }
23 | kotlin-stdlib-common = { module = "org.jetbrains.kotlin:kotlin-stdlib-common", version.ref = "kotlin" }
24 | kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
25 | kotlin-compiler = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" }
26 | kotlinPoet-core = { module = "com.squareup:kotlinpoet", version.ref = "kotlinPoet" }
27 | kotlinPoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinPoet" }
28 | ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }
29 | ksp-gradlePlugin = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" }
30 | moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
31 | moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
32 | moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
33 | truth = { module = "com.google.truth:truth", version = "1.4.5" }
34 |
35 | # Used by the sample app
36 | compose-bom = { module = "androidx.compose:compose-bom", version = "2025.05.01" }
37 | compose-material = { module = "androidx.compose.material:material" }
38 | compose-runtime = { module = "androidx.compose.runtime:runtime" }
39 |
40 | compose-activity = { module = "androidx.activity:activity-compose", version = "1.10.1" }
41 |
42 | # AGP things
43 | google-guava = "com.google.guava:guava:33.5.0-jre"
44 | google-gson = "com.google.code.gson:gson:2.13.2"
45 | android-common = { module = "com.android.tools:common", version.ref = "android-tools" }
46 | android-repository = { module = "com.android.tools:repository", version.ref = "android-tools" }
47 | android-sdkCommon = { module = "com.android.tools:sdk-common", version.ref = "android-tools" }
48 |
49 | [plugins]
50 | android-application = { id = "com.android.application", version.ref = "agp" }
51 | android-dynamic-feature = { id = "com.android.dynamic-feature", version.ref = "agp" }
52 | android-library = { id = "com.android.library", version.ref = "agp" }
53 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
54 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
55 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
56 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
57 | publish = { id = "com.vanniktech.maven.publish", version = "0.34.0" }
58 | spotless = { id = "com.diffplug.spotless", version = "8.0.0" }
59 | wire = { id = "com.squareup.wire", version = "5.4.0" }
60 |
61 | # Used by the sample app
62 | # This is just a dummy version to make the verison catalog work. The real version is subsituted in the composite build.
63 | betterDynamicFeatures = { id = "app.cash.better.dynamic.features", version = "0.0.0" }
64 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/tasks/DependencyGraphWriterTask.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.tasks
17 |
18 | import com.squareup.moshi.Moshi
19 | import org.gradle.api.DefaultTask
20 | import org.gradle.api.artifacts.component.ProjectComponentIdentifier
21 | import org.gradle.api.artifacts.result.DependencyResult
22 | import org.gradle.api.artifacts.result.ResolvedComponentResult
23 | import org.gradle.api.artifacts.result.ResolvedDependencyResult
24 | import org.gradle.api.file.ConfigurableFileCollection
25 | import org.gradle.api.file.RegularFileProperty
26 | import org.gradle.api.provider.Provider
27 | import org.gradle.api.tasks.InputFiles
28 | import org.gradle.api.tasks.OutputFile
29 | import org.gradle.api.tasks.TaskAction
30 |
31 | abstract class DependencyGraphWriterTask : DefaultTask() {
32 | /**
33 | * This is only used to trick Gradle into recognizing that this task is no longer up-to-date
34 | * For actual processing, [graph] is used.
35 | */
36 | @get:InputFiles
37 | abstract val dependencyFileCollection: ConfigurableFileCollection
38 |
39 | private lateinit var graph: Provider>
40 |
41 | fun setResolvedLockfileEntriesProvider(provider: Provider, variant: String) {
42 | graph = provider.map { (runtime, compile) ->
43 | val runtimeNodes = if (runtime.variants.isEmpty()) {
44 | emptyList()
45 | } else {
46 | buildDependencyGraph(
47 | runtime.getDependenciesForVariant(runtime.variants.first()),
48 | variant,
49 | mutableSetOf(),
50 | type = DependencyType.Runtime,
51 | )
52 | }
53 | val compileNodes = if (compile.variants.isEmpty()) {
54 | emptyList()
55 | } else {
56 | buildDependencyGraph(
57 | compile.getDependenciesForVariant(compile.variants.first()),
58 | variant,
59 | mutableSetOf(),
60 | type = DependencyType.Compile,
61 | )
62 | }
63 |
64 | runtimeNodes + compileNodes
65 | }
66 | }
67 |
68 | @get:OutputFile
69 | abstract val partialLockFile: RegularFileProperty
70 |
71 | @TaskAction
72 | fun printList() {
73 | val moshi = Moshi.Builder().build()
74 | partialLockFile.asFile.get().writeText(moshi.adapter(NodeList::class.java).toJson(NodeList(graph.get())))
75 | }
76 |
77 | private val ResolvedDependencyResult.key: String
78 | get() = selected.moduleVersion?.let { info -> "${info.group}:${info.name}" } ?: ""
79 |
80 | private fun buildDependencyGraph(topLevel: List, variant: String, visited: MutableSet, type: DependencyType): List = topLevel
81 | .asSequence()
82 | .filterIsInstance()
83 | .filter { !it.isConstraint && it.key !in visited }
84 | .onEach { visited += it.key }
85 | .map {
86 | val info = it.selected.moduleVersion!!
87 | Node(
88 | "${info.group}:${info.name}",
89 | Version(info.version),
90 | mutableSetOf(variant),
91 | children = buildDependencyGraph(it.selected.getDependenciesForVariant(it.resolvedVariant), variant, visited, type),
92 | isProjectModule = it.resolvedVariant.owner is ProjectComponentIdentifier,
93 | type = type,
94 | )
95 | }
96 | .toList()
97 |
98 | data class ResolvedComponentResultPair(val runtime: ResolvedComponentResult, val compile: ResolvedComponentResult)
99 | }
100 |
--------------------------------------------------------------------------------
/gradle-plugin/src/test/fixtures/missing-dex/base/gradle.lockfile:
--------------------------------------------------------------------------------
1 | # This is a Gradle generated file for dependency locking.
2 | # Manual edits can break the build and are not advised.
3 | # This file is expected to be part of source control.
4 | androidx.activity:activity:1.0.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
5 | androidx.annotation:annotation:1.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
6 | androidx.arch.core:core-common:2.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
7 | androidx.arch.core:core-runtime:2.0.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
8 | androidx.collection:collection:1.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
9 | androidx.core:core:1.2.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
10 | androidx.customview:customview:1.0.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
11 | androidx.fragment:fragment:1.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
12 | androidx.lifecycle:lifecycle-common:2.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
13 | androidx.lifecycle:lifecycle-livedata-core:2.0.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
14 | androidx.lifecycle:lifecycle-livedata:2.0.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
15 | androidx.lifecycle:lifecycle-runtime:2.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
16 | androidx.lifecycle:lifecycle-viewmodel:2.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
17 | androidx.loader:loader:1.0.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
18 | androidx.savedstate:savedstate:1.0.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
19 | androidx.versionedparcelable:versionedparcelable:1.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
20 | androidx.viewpager:viewpager:1.0.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
21 | com.google.android.gms:play-services-basement:18.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
22 | com.google.android.gms:play-services-tasks:18.0.2=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
23 | com.google.android.play:core-common:2.0.3=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
24 | com.google.android.play:feature-delivery-ktx:2.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
25 | com.google.android.play:feature-delivery:2.1.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
26 | com.squareup.okhttp3:okhttp:5.0.0-alpha.2=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
27 | com.squareup.okio:okio:2.9.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
28 | org.jetbrains.kotlin:kotlin-stdlib-common:2.1.20=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
29 | org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
30 | org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
31 | org.jetbrains.kotlin:kotlin-stdlib:2.1.20=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
32 | org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
33 | org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
34 | org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
35 | org.jetbrains:annotations:13.0=debugCompileClasspath,debugRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath
36 | empty=
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/tasks/DependencyGraphUtils.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.tasks
17 |
18 | import app.cash.better.dynamic.features.tasks.DependencyType.Compile
19 | import app.cash.better.dynamic.features.tasks.DependencyType.Runtime
20 |
21 | fun List.toText(): String = """
22 | |# This is a Gradle generated file for dependency locking.
23 | |# Manual edits can break the build and are not advised.
24 | |# This file is expected to be part of source control.
25 | |${sorted().joinToString(separator = "\n")}
26 | |empty=
27 | """.trimMargin()
28 |
29 | private val Node.configurationNames
30 | get() = variants.mapTo(mutableSetOf()) {
31 | when (type) {
32 | Compile -> "${it}CompileClasspath"
33 | Runtime -> "${it}RuntimeClasspath"
34 | }
35 | }
36 |
37 | private fun mergeSingleTypeGraphs(
38 | base: List,
39 | others: List>,
40 | ): Map {
41 | val graphMap = mutableMapOf()
42 |
43 | fun registerNodes(nodes: List) {
44 | nodes.walkAll { node ->
45 | when (val existing = graphMap[node.artifact]) {
46 | null -> graphMap[node.artifact] = node
47 | else -> {
48 | if (node.version > existing.version) {
49 | graphMap[node.artifact] = node.copy(variants = node.variants + existing.variants)
50 | } else {
51 | graphMap[existing.artifact] = existing.copy(variants = existing.variants + node.variants)
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
58 | // Register the initial set of dependencies
59 | registerNodes(base)
60 |
61 | others.flatten().walkAll { node ->
62 | val existing = graphMap[node.artifact]
63 | // We only care about the conflicting dependencies that actually exist in the base
64 | if (existing != null && node.variants.single() in existing.variants) {
65 | if (node.version > existing.version) {
66 | graphMap[node.artifact] = node.copy(variants = node.variants + existing.variants)
67 | } else {
68 | graphMap[existing.artifact] = existing.copy(variants = existing.variants + node.variants)
69 | }
70 |
71 | // Register any new transitive dependencies
72 | registerNodes(node.children)
73 | }
74 | }
75 |
76 | return graphMap
77 | .filter { (_, entry) -> !entry.isProjectModule }
78 | .map { (_, entry) ->
79 | LockfileEntry(
80 | entry.artifact,
81 | entry.version.toString(),
82 | entry.configurationNames,
83 | )
84 | }
85 | .associateBy { it.artifact }
86 | }
87 |
88 | fun mergeGraphs(base: List, others: List>): List {
89 | val runtimeMap = mergeSingleTypeGraphs(
90 | base.filter { it.type == Runtime },
91 | others.map { list -> list.filter { it.type == Runtime } },
92 | )
93 | val compileMap = mergeSingleTypeGraphs(
94 | base.filter { it.type == Compile },
95 | others.map { list -> list.filter { it.type == Compile } },
96 | ).toMutableMap()
97 |
98 | // Merge the runtime and compile graphs
99 | val mergedMap = mutableMapOf()
100 | runtimeMap.forEach { (artifact, entry) ->
101 | mergedMap[artifact] = entry
102 | val compileEntry = compileMap[artifact] ?: return@forEach
103 |
104 | mergedMap[artifact] = entry.copy(
105 | version = maxOf(entry.version, compileEntry.version),
106 | configurations = entry.configurations + compileEntry.configurations,
107 | )
108 |
109 | compileMap.remove(artifact)
110 | }
111 | compileMap.forEach { (artifact, entry) ->
112 | mergedMap[artifact] = entry
113 | }
114 |
115 | return mergedMap.values.toList()
116 | }
117 |
118 | private tailrec fun List.walkAll(callback: (Node) -> Unit) {
119 | forEach(callback)
120 | val nextLevel = flatMap { it.children }
121 |
122 | if (nextLevel.isNotEmpty()) {
123 | nextLevel.walkAll(callback)
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/gradle-plugin/src/main/kotlin/app/cash/better/dynamic/features/codegen/CompileTypesafeImplementations.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2023 Square, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package app.cash.better.dynamic.features.codegen
17 |
18 | import org.gradle.workers.WorkAction
19 | import org.jetbrains.kotlin.cli.common.ExitCode
20 | import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
21 | import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
22 | import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
23 | import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
24 | import org.jetbrains.kotlin.config.Services
25 | import org.slf4j.LoggerFactory
26 | import java.io.File
27 | import java.util.jar.JarEntry
28 | import java.util.jar.JarFile
29 | import java.util.jar.JarOutputStream
30 |
31 | abstract class CompileTypesafeImplementations : WorkAction {
32 | private val logger = LoggerFactory.getLogger("CompileTypesafeImplementations")
33 |
34 | override fun execute() {
35 | val tempClassDirectory = parameters.temporaryDir.asFile.get()
36 |
37 | compile(tempClassDirectory)
38 | mergeClasses(tempClassDirectory)
39 | }
40 |
41 | private fun compile(classOutputDirectory: File) {
42 | val combinedClasspath = (
43 | parameters.projectClasses.get().map { it.asFile } +
44 | parameters.compileClasspath.files.flatMap { it.walk().filter(File::isFile) }
45 | )
46 |
47 | val generatedSourceFiles = parameters.generatedSources.asFile.get()
48 | .walk()
49 | .filter(File::isFile)
50 | .map { it.absolutePath }
51 | .toList()
52 |
53 | val arguments = K2JVMCompilerArguments().apply {
54 | classpath = combinedClasspath.joinToString(separator = File.pathSeparator) { it.absolutePath }
55 | destination = classOutputDirectory.absolutePath
56 | freeArgs = generatedSourceFiles
57 |
58 | // We supply the stdlib JAR(s) via the classpath, this just suppresses a warning message
59 | noStdlib = true
60 | }
61 |
62 | // We can't use @SkipWhenEmpty on the sources directory because AGP always needs an output from
63 | // this task, but if we try to pass zero source files to the compiler it crashes
64 | if (generatedSourceFiles.isNotEmpty()) {
65 | val result = K2JVMCompiler().exec(
66 | messageCollector = PrintingMessageCollector(
67 | System.err,
68 | MessageRenderer.GRADLE_STYLE,
69 | false,
70 | ),
71 | services = Services.EMPTY,
72 | arguments,
73 | )
74 |
75 | require(result == ExitCode.OK) { "Kotlin Compiler Error while compiling dynamic feature implementations." }
76 | } else {
77 | logger.debug("No generated sources to compile. Skipping.")
78 | }
79 | }
80 |
81 | private fun mergeClasses(compiledGeneratedClasses: File) {
82 | val target = parameters.output.asFile.get()
83 |
84 | JarOutputStream(target.outputStream()).use { jar ->
85 | // Copy existing jar entries over
86 | parameters.projectJars.get().map { JarFile(it.asFile) }.forEach { inputJar ->
87 | inputJar.entries().iterator().forEach { jarEntry ->
88 | jar.putNextEntry(JarEntry(jarEntry.name))
89 | inputJar.getInputStream(jarEntry).use { it.copyTo(jar) }
90 | jar.closeEntry()
91 | }
92 | }
93 |
94 | // Copy existing classes over
95 | parameters.projectClasses.get().map { it.asFile }.forEach { root ->
96 | root.walk().filter(File::isFile).forEach { file ->
97 | val relative = file.relativeTo(root)
98 | jar.putEntry(relative.path, file)
99 | }
100 | }
101 |
102 | // Copy compiled generated classes over
103 | compiledGeneratedClasses.walk().filter(File::isFile).forEach { file ->
104 | val relative = file.relativeTo(compiledGeneratedClasses)
105 | jar.putEntry(relative.path, file)
106 | }
107 | }
108 | }
109 |
110 | private fun JarOutputStream.putEntry(name: String, source: File) {
111 | putNextEntry(JarEntry(name))
112 | source.inputStream().copyTo(this)
113 | closeEntry()
114 | }
115 | }
116 |
--------------------------------------------------------------------------------