diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..072ce38
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,233 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 120
+# Uncomment if you want to show non-strict recommended guideline
+#ij_visual_guides = 100
+
+# General
+ij_continuation_indent_size = 8
+ij_smart_tabs = false
+ij_wrap_on_typing = false
+ij_any_keep_indents_on_empty_lines = false
+
+# Formatter
+ij_formatter_tags_enabled = true
+ij_formatter_on_tag = @formatter:on
+ij_formatter_off_tag = @formatter:off
+
+[{*.kt,*.kts}]
+# Tabs and Indents
+# continuation_indent_size = 4 to match ktlint settings
+ij_kotlin_continuation_indent_size = 4
+ij_kotlin_keep_indents_on_empty_lines = unset
+
+# Spaces
+## Before parentheses
+ij_kotlin_space_before_if_parentheses = true
+ij_kotlin_space_before_for_parentheses = true
+ij_kotlin_space_before_while_parentheses = true
+ij_kotlin_space_before_catch_parentheses = true
+ij_kotlin_space_before_when_parentheses = true
+## Around operators
+ij_kotlin_spaces_around_assignment_operators = true
+ij_kotlin_spaces_around_logical_operators = true
+ij_kotlin_spaces_around_equality_operators = true
+ij_kotlin_spaces_around_relational_operators = true
+ij_kotlin_spaces_around_additive_operators = true
+ij_kotlin_spaces_around_multiplicative_operators = true
+ij_kotlin_spaces_around_unary_operator = false
+ij_kotlin_spaces_around_range = false
+## Other
+ij_kotlin_space_before_comma = false
+ij_kotlin_space_after_comma = true
+ij_kotlin_space_before_type_colon = false
+ij_kotlin_space_after_type_colon = true
+ij_kotlin_space_before_extend_colon = true
+ij_kotlin_space_after_extend_colon = true
+ij_kotlin_insert_whitespaces_in_simple_one_line_method = true
+ij_kotlin_spaces_around_function_type_arrow = true
+ij_kotlin_spaces_around_when_arrow = true
+ij_kotlin_space_before_lambda_arrow = true
+
+# Wrapping and Braces
+## Keep when reformatting
+ij_kotlin_keep_line_breaks = true
+ij_kotlin_keep_first_column_comment = true
+## Extends/implements list
+ij_kotlin_extends_list_wrap = normal
+ij_kotlin_align_multiline_extends_list = false
+ij_kotlin_continuation_indent_in_supertype_lists = false
+## Function declaration parameters
+ij_kotlin_method_parameters_wrap = on_every_item
+ij_kotlin_align_multiline_parameters = true
+ij_kotlin_method_parameters_new_line_after_left_paren = true
+ij_kotlin_method_parameters_right_paren_on_new_line = true
+ij_kotlin_continuation_indent_in_parameter_lists = false
+## Function call arguments
+ij_kotlin_call_parameters_wrap = on_every_item
+ij_kotlin_align_multiline_parameters_in_calls = false
+ij_kotlin_call_parameters_new_line_after_left_paren = true
+ij_kotlin_call_parameters_right_paren_on_new_line = true
+ij_kotlin_continuation_indent_in_argument_lists = false
+## Function parentheses
+ij_kotlin_align_multiline_method_parentheses = false
+## Chained function calls
+ij_kotlin_method_call_chain_wrap = normal
+ij_kotlin_wrap_first_method_in_call_chain = false
+ij_kotlin_continuation_indent_for_chained_calls = false
+## 'if()' statement
+ij_kotlin_else_on_new_line = false
+ij_kotlin_if_rparen_on_new_line = true
+ij_kotlin_continuation_indent_in_if_conditions = false
+## 'do ... while()' statement
+ij_kotlin_while_on_new_line = false
+## 'try' statement
+ij_kotlin_catch_on_new_line = false
+ij_kotlin_finally_on_new_line = false
+## Binary expressions
+ij_kotlin_align_multiline_binary_operation = false
+## Wraps
+ij_kotlin_assignment_wrap = normal
+ij_kotlin_enum_constants_wrap = off
+ij_kotlin_class_annotation_wrap = split_into_lines
+ij_kotlin_method_annotation_wrap = split_into_lines
+ij_kotlin_field_annotation_wrap = split_into_lines
+ij_kotlin_parameter_annotation_wrap = off
+ij_kotlin_variable_annotation_wrap = off
+## 'when' statements
+ij_kotlin_align_in_columns_case_branch = false
+ij_kotlin_line_break_after_multiline_when_entry = true
+## Braces placement
+ij_kotlin_lbrace_on_next_line = false
+## Expression body functions
+ij_kotlin_wrap_expression_body_functions = 1
+ij_kotlin_continuation_indent_for_expression_bodies = false
+## Elvis expressions
+ij_kotlin_wrap_elvis_expressions = 1
+ij_kotlin_continuation_indent_in_elvis = false
+
+# Blank Lines
+## Keep maximum blank lines
+ij_kotlin_keep_blank_lines_in_declarations = 1
+ij_kotlin_keep_blank_lines_in_code = 1
+ij_kotlin_keep_blank_lines_before_right_brace = 0
+## Minimum blank lines
+ij_kotlin_blank_lines_after_class_header = 0
+ij_kotlin_blank_lines_around_block_when_branches = 1
+ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1
+
+# Imports
+ij_kotlin_name_count_to_use_star_import = 5
+ij_kotlin_name_count_to_use_star_import_for_members = 3
+ij_kotlin_import_nested_classes = false
+ij_kotlin_packages_to_use_import_on_demand = java.util.*,kotlinx.android.synthetic.**,io.ktor.**
+ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^
+
+# Other
+## Trailing comma
+ij_kotlin_allow_trailing_comma = true
+ij_kotlin_allow_trailing_comma_on_call_site = false
+
+# Code generation
+## Comment code
+ij_kotlin_line_comment_at_first_column = true
+ij_kotlin_line_comment_add_space = false
+ij_kotlin_line_comment_add_space_on_reformat = false
+ij_kotlin_block_comment_at_first_column = true
+ij_kotlin_block_comment_add_space = false
+
+# Compose
+ij_kotlin_use_custom_formatting_for_modifiers = true
+
+# Load/Save
+ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
+
+[*.kts]
+# Always use wildcard imports in scripts
+ij_kotlin_name_count_to_use_star_import = 2
+
+# EditorConfig can not set some of XML code style options.
+# Remember to set default Android XML code style: Editor > Code Style > XML > Set from... -> Android
+[{**/res/**.xml,**/AndroidManifest.xml}]
+# Tabs and Indents
+ij_xml_continuation_indent_size = 4
+ij_xml_keep_indents_on_empty_lines = unset
+
+# Other
+ij_xml_keep_line_breaks = false
+ij_xml_keep_line_breaks_in_text = true
+ij_xml_keep_blank_lines = 2
+ij_xml_attribute_wrap = normal
+ij_xml_text_wrap = normal
+ij_xml_align_text = false
+ij_xml_align_attributes = false
+ij_xml_keep_whitespaces = false
+## Spaces
+ij_xml_space_around_equals_in_attribute = false
+ij_xml_space_after_tag_name = false
+ij_xml_space_inside_empty_tag = true
+## CDATA
+ij_xml_keep_whitespaces_around_cdata = preserve
+ij_xml_keep_whitespaces_inside_cdata = false
+
+# Code Generation
+ij_xml_line_comment_at_first_column = true
+ij_xml_block_comment_at_first_column = true
+ij_xml_block_comment_add_space = false
+
+# Android
+ij_xml_use_custom_settings = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+# Wrapping and Braces
+ij_markdown_wrap_text_if_long = false
+ij_markdown_wrap_text_inside_blockquotes = false
+## When reformatting
+ij_markdown_keep_line_breaks_inside_text_blocks = true
+ij_markdown_insert_quote_arrows_on_wrap = true
+ij_markdown_format_tables = true
+
+# Tabs and Indents
+ij_markdown_keep_indents_on_empty_lines = unset
+
+# Blank Lines
+## Keep maximum blank lines
+ij_markdown_max_lines_around_header = 1
+ij_markdown_max_lines_around_block_elements = 1
+ij_markdown_max_lines_between_paragraphs = 1
+## Minimum blank lines
+ij_markdown_min_lines_around_header = 1
+ij_markdown_min_lines_around_block_elements = 1
+ij_markdown_min_lines_between_paragraphs = 1
+
+# Spaces
+## Force one space
+ij_markdown_force_one_space_between_words = true
+ij_markdown_force_one_space_after_header_symbol = true
+ij_markdown_force_one_space_after_list_bullet = true
+ij_markdown_force_one_space_after_blockquote_symbol = true
+
+[{*.yaml,*.yml}]
+indent_size = 2
+ij_yaml_keep_indents_on_empty_lines = unset
+ij_yaml_keep_line_breaks = true
+ij_yaml_spaces_within_brackets = false
+
+[{*.bash,*.sh,*.zsh}]
+indent_size = 2
+tab_width = 2
+
+[*.bat]
+end_of_line = crlf
+
+[*.properties]
+ij_properties_keep_blank_lines = true
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..00a51af
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,6 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# These are explicitly windows files and should use crlf
+*.bat text eol=crlf
+
diff --git a/.github/workflows/cleanup.yml b/.github/workflows/cleanup.yml
new file mode 100644
index 0000000..a6789ae
--- /dev/null
+++ b/.github/workflows/cleanup.yml
@@ -0,0 +1,41 @@
+# GitHub Actions Workflow responsible for cleaning up the Template repository from
+# the template-specific files and configurations. This workflow is supposed to be triggered automatically
+# when a new template-based repository has been created.
+
+name: Template Cleanup
+on:
+ push:
+ branches: [main]
+
+jobs:
+ # Run cleaning process only if workflow is triggered by the non-"android-library-template" repository.
+ template-cleanup:
+ name: Template Cleanup
+ runs-on: ubuntu-latest
+ if: github.event.repository.name != 'android-library-template'
+ permissions:
+ contents: write
+ steps:
+ # Check out current repository
+ - name: Fetch Sources
+ uses: actions/checkout@v4
+
+ # Cleanup project
+ - name: Cleanup
+ run: |
+ ./cleanup.sh "${GITHUB_REPOSITORY##*/}"
+
+ # Commit modified files
+ - name: Commit files
+ run: |
+ git config --local user.email "action@github.com"
+ git config --local user.name "GitHub Action"
+ git add .
+ git commit -m "Template cleanup"
+
+ # Push changes
+ - name: Push changes
+ uses: ad-m/github-push-action@master
+ with:
+ branch: main
+ github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..a66378a
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,63 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ # Release tag format is v[version]
+ # For example: v1.3.5
+ tags: ["v*"]
+ pull_request:
+ branches: [main]
+
+jobs:
+ check:
+ name: Check
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: 17
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v3
+ - name: Cleanup project
+ run: ./cleanup.sh TestTemplate
+ - name: Run Check
+ run: ./gradlew check detektAll detektReleaseAll
+
+ publish:
+ name: Publish
+ needs: check
+ runs-on: ubuntu-latest
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
+
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v4
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: "temurin"
+ java-version: 17
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@v3
+ - name: Run Publish
+ run: ./gradlew publish
+ env:
+ ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
+ ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
+ ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
+ ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSWORD }}
+ ORG_GRADLE_PROJECT_githubPackagesUsername: ${{ github.actor }}
+ ORG_GRADLE_PROJECT_githubPackagesPassword: ${{ secrets.GITHUB_TOKEN }}
+ - name: Extract release notes
+ uses: ffurrer2/extract-release-notes@v2
+ with:
+ release_notes_file: RELEASE_NOTES.md
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ body_path: RELEASE_NOTES.md
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fd68970
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+.DS_Store
+
+## JetBrains IDEs
+/.idea/**
+*.iml
+
+# Keep required plugins
+!.idea/externalDependencies.xml
+
+# Keep detekt plugin config
+!.idea/detekt.xml
+
+# Keep VCS config
+!.idea/vcs.xml
+
+## Gradle
+# Ignore Gradle project-specific cache directory
+.gradle
+
+# Ignore Gradle build output directory
+**/build/
+!**/src/**/build/
+
+# Ignore local properties
+local.properties
diff --git a/.idea/detekt.xml b/.idea/detekt.xml
new file mode 100644
index 0000000..d9c69a3
--- /dev/null
+++ b/.idea/detekt.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/externalDependencies.xml b/.idea/externalDependencies.xml
new file mode 100644
index 0000000..b439ce7
--- /dev/null
+++ b/.idea/externalDependencies.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..45aea96
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1aef7af
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+## [Unreleased]
+
+### Changed
+
+- *No changes*
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..bd971d5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 red_mad_robot
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8472f07
--- /dev/null
+++ b/README.md
@@ -0,0 +1,44 @@
+# %Stub%
+
+[![License](https://img.shields.io/github/license/RedMadRobot/%Stub%?style=flat-square)][license]
+
+%Stub% and no more spoilers.
+
+---
+
+
+
+- [Installation](#installation)
+- [Usage](#usage)
+- [Contributing](#contributing)
+
+
+
+## Installation
+
+Add the dependency:
+
+```groovy
+repositories {
+ mavenCentral()
+ google()
+}
+
+dependencies {
+ implementation("com.redmadrobot.%stub%:%stub%:")
+}
+```
+
+## Usage
+
+## Contributing
+
+Merge requests are welcome.
+For major changes, please open an issue first to discuss what you would like to change.
+
+## Checklist after repository creation (remove after checked)
+
+- Update developers in [publishing plugin](buildSrc/src/main/kotlin/convention.publishing.gradle.kts)
+- remove `Cleanup` step from [main.yml](.github/workflows/main.yml)
+
+[license]: ../LICENSE
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..c9bcd66
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,24 @@
+# Releasing
+
+1. Run the script `release.sh`:
+ ```bash
+ ./release.sh
+ ````
+2. The script will ask you if you want to push the changes and create a release tag.
+3. Ensure `CHANGELOG.md` looks good and is ready to be published.
+ Delete sections that don't contain changes.
+4. Type "yes" to console if everything is okay.
+
+Tag push will trigger GitHub Actions workflow, which will publish the release artifacts to Maven Central and create a GitHub release.
+
+## Manual release preparation
+
+To prepare a release manually, follow the steps the script does:
+
+1. Ensure the repository is up-to-date, and the main branch is checked out.
+2. Update the version in `gradle.properties` and `README.md` ("Usage" section) using the current date as a version.
+3. Update the `CHANGELOG.md`:
+ 1. Replace `Unreleased` section with the release version
+ 2. Add a link to the diff between the previous and the new version
+ 3. Add a new empty `Unreleased` section on the top
+4. Commit the changes, create a tag on the commit and push it to the remote repository
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..2922a30
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ alias(libs.plugins.infrastructure.detekt)
+ alias(libs.plugins.versions)
+ convention.detekt
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..55229e2
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ `kotlin-dsl`
+}
+
+tasks.withType {
+ kotlinOptions.jvmTarget = JavaVersion.VERSION_11.toString()
+}
+
+java {
+ targetCompatibility = JavaVersion.VERSION_11
+ sourceCompatibility = JavaVersion.VERSION_11
+}
+
+dependencies {
+ implementation(libs.infrastructure.publish)
+ implementation(libs.infrastructure.android)
+ implementation(libs.publish.gradlePlugin)
+ implementation(libs.gradle.android.cacheFixGradlePlugin)
+ implementation(libs.kotlin.gradlePlugin)
+ implementation(libs.detekt.gradlePlugin)
+ implementation(libs.android.gradlePlugin)
+}
diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts
new file mode 100644
index 0000000..ec83ce2
--- /dev/null
+++ b/buildSrc/settings.gradle.kts
@@ -0,0 +1,37 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google {
+ content {
+ includeGroupAndSubgroups("com.android")
+ includeGroupAndSubgroups("com.google")
+ includeGroupAndSubgroups("androidx")
+ }
+ }
+ mavenCentral()
+ }
+}
+
+@Suppress("UnstableApiUsage")
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+
+ repositories {
+ google {
+ content {
+ includeGroupAndSubgroups("com.android")
+ includeGroupAndSubgroups("com.google")
+ includeGroupAndSubgroups("androidx")
+ }
+ }
+
+ mavenCentral()
+ gradlePluginPortal()
+ }
+
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
diff --git a/buildSrc/src/main/kotlin/convention.detekt.gradle.kts b/buildSrc/src/main/kotlin/convention.detekt.gradle.kts
new file mode 100644
index 0000000..5b28665
--- /dev/null
+++ b/buildSrc/src/main/kotlin/convention.detekt.gradle.kts
@@ -0,0 +1,19 @@
+import io.gitlab.arturbosch.detekt.*
+
+plugins {
+ id("io.gitlab.arturbosch.detekt")
+}
+
+tasks.withType().configureEach {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+}
+tasks.withType().configureEach {
+ jvmTarget = JavaVersion.VERSION_11.toString()
+}
+
+dependencies {
+ //noinspection UseTomlInstead
+ detektPlugins("io.gitlab.arturbosch.detekt:detekt-rules-libraries:1.23.6")
+ //noinspection UseTomlInstead
+ detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.6")
+}
diff --git a/buildSrc/src/main/kotlin/convention.library.android.gradle.kts b/buildSrc/src/main/kotlin/convention.library.android.gradle.kts
new file mode 100644
index 0000000..a7afd44
--- /dev/null
+++ b/buildSrc/src/main/kotlin/convention.library.android.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id("com.redmadrobot.android-library")
+ id("convention.publishing")
+ id("convention.detekt")
+}
+
+redmadrobot {
+ android.minSdk = 21
+}
diff --git a/buildSrc/src/main/kotlin/convention.library.kotlin.gradle.kts b/buildSrc/src/main/kotlin/convention.library.kotlin.gradle.kts
new file mode 100644
index 0000000..ff378c7
--- /dev/null
+++ b/buildSrc/src/main/kotlin/convention.library.kotlin.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ id("com.redmadrobot.kotlin-library")
+ id("convention.publishing")
+ id("convention.detekt")
+}
diff --git a/buildSrc/src/main/kotlin/convention.publishing.gradle.kts b/buildSrc/src/main/kotlin/convention.publishing.gradle.kts
new file mode 100644
index 0000000..3dfa848
--- /dev/null
+++ b/buildSrc/src/main/kotlin/convention.publishing.gradle.kts
@@ -0,0 +1,32 @@
+import com.redmadrobot.build.dsl.*
+import com.vanniktech.maven.publish.SonatypeHost
+
+plugins {
+ id("com.vanniktech.maven.publish")
+}
+
+mavenPublishing {
+ publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true)
+ signAllPublications()
+
+ pom {
+ name.convention(project.name)
+ description.convention(project.description)
+
+ licenses {
+ mit()
+ }
+
+ developers {
+// developer(id = "coolest id", name = "the best name", email = "awesome email")
+ }
+
+ setGitHubProject("RedMadRobot/%Stub%")
+ }
+}
+
+publishing {
+ repositories {
+ if (isRunningOnCi) githubPackages("RedMadRobot/%Stub%")
+ }
+}
diff --git a/cleanup.sh b/cleanup.sh
new file mode 100755
index 0000000..eb9c339
--- /dev/null
+++ b/cleanup.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+
+set -e
+
+RESET="\033[0m"
+F_BOLD="\033[1m"
+F_BOLD_RESET="\033[22m"
+C_RED="\033[31m"
+C_GREEN="\033[32m"
+
+function log_success() { printf -- "\n${F_BOLD}${C_GREEN}%s${RESET}\n" "$*"; }
+function log_error() { printf -- "\n${F_BOLD}${C_RED}ERROR:${F_BOLD_RESET} %s${RESET}\n" "$*"; }
+
+if [[ $# -ne 1 ]]; then
+ log_error "Please specify the actual repository name. Usage: ./cleanup.sh MyRepositoryName"
+ exit 1
+fi
+
+NAME="$1"
+SAFE_NAME="$(echo $NAME | sed 's/[^a-zA-Z0-9]//g' | tr '[:upper:]' '[:lower:]')"
+
+STUB="%Stub%"
+SAFE_STUB="%stub%"
+
+# Replace
+sed -i "s/$SAFE_STUB/$SAFE_NAME/g" $(find . -type f -not -path "**/build/**" -not -path "**/.**")
+sed -i "s/$STUB/$NAME/g" $(find . -type f -not -path "**/build/**" -not -path "**/.**")
+
+# Move stub
+mv stub/src/main/kotlin/Stub.kt stub/src/main/kotlin/$NAME.kt
+mv stub/ $SAFE_NAME
+
+# Cleanup
+if [[ $GITHUB_ACTIONS ]]; then
+ rm .github/workflows/cleanup.yml
+ rm -- "$0"
+fi
+
+# Remove leftover empty directories
+find . -type d -empty -delete
diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml
new file mode 100644
index 0000000..d6ec68b
--- /dev/null
+++ b/config/detekt/detekt.yml
@@ -0,0 +1,787 @@
+build:
+ maxIssues: 0
+ excludeCorrectable: false
+ weights:
+ complexity: 2
+ style: 1
+ comments: 1
+
+config:
+ validation: true
+ warningsAsErrors: true
+ # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
+ excludes: ''
+
+processors:
+ active: true
+ exclude:
+ - 'DetektProgressListener'
+ # - 'KtFileCountProcessor'
+ # - 'PackageCountProcessor'
+ # - 'ClassCountProcessor'
+ # - 'FunctionCountProcessor'
+ # - 'PropertyCountProcessor'
+ # - 'ProjectComplexityProcessor'
+ # - 'ProjectCognitiveComplexityProcessor'
+ # - 'ProjectLLOCProcessor'
+ # - 'ProjectCLOCProcessor'
+ # - 'ProjectLOCProcessor'
+ # - 'ProjectSLOCProcessor'
+ # - 'LicenseHeaderLoaderExtension'
+
+console-reports:
+ active: true
+ exclude:
+ - 'ProjectStatisticsReport'
+ - 'ComplexityReport'
+ - 'NotificationReport'
+ # - 'FindingsReport'
+ - 'FileBasedFindingsReport'
+ - 'LiteFindingsReport'
+
+output-reports:
+ active: true
+ exclude: []
+ # - 'TxtOutputReport'
+ # - 'XmlOutputReport'
+ # - 'HtmlOutputReport'
+
+comments:
+ active: true
+ AbsentOrWrongFileLicense:
+ active: false
+ licenseTemplateFile: 'license.template'
+ licenseTemplateIsRegex: false
+ CommentOverPrivateFunction:
+ active: false
+ CommentOverPrivateProperty:
+ active: false
+ DeprecatedBlockTag:
+ active: false
+ EndOfSentenceFormat:
+ active: false
+ endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
+ OutdatedDocumentation:
+ active: false
+ matchTypeParameters: true
+ matchDeclarationsOrder: true
+ UndocumentedPublicClass:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ searchInNestedClass: true
+ searchInInnerClass: true
+ searchInInnerObject: true
+ searchInInnerInterface: true
+ UndocumentedPublicFunction:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ UndocumentedPublicProperty:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+
+complexity:
+ active: true
+ ComplexCondition:
+ active: true
+ threshold: 4
+ ComplexInterface:
+ active: false
+ threshold: 10
+ includeStaticDeclarations: false
+ includePrivateDeclarations: false
+ CognitiveComplexMethod:
+ active: true
+ threshold: 15
+ LabeledExpression:
+ active: false
+ ignoredLabels: []
+ LargeClass:
+ active: true
+ threshold: 600
+ LongMethod:
+ active: true
+ threshold: 60
+ LongParameterList:
+ active: true
+ functionThreshold: 6
+ constructorThreshold: 7
+ ignoreDefaultParameters: true
+ ignoreDataClasses: true
+ ignoreAnnotatedParameter: []
+ MethodOverloading:
+ active: false
+ threshold: 6
+ NamedArguments:
+ active: true
+ threshold: 3
+ NestedBlockDepth:
+ active: true
+ threshold: 4
+ ReplaceSafeCallChainWithRun:
+ active: true
+ StringLiteralDuplication:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ threshold: 3
+ ignoreAnnotation: true
+ excludeStringsWithLessThan5Characters: true
+ ignoreStringsRegex: '^(boolean|false)$'
+ TooManyFunctions:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ thresholdInFiles: 99
+ thresholdInClasses: 11
+ thresholdInInterfaces: 11
+ thresholdInObjects: 11
+ thresholdInEnums: 11
+ ignoreDeprecated: false
+ ignorePrivate: true
+ ignoreOverridden: true
+
+coroutines:
+ active: true
+ GlobalCoroutineUsage:
+ active: true
+ InjectDispatcher:
+ active: true
+ dispatcherNames:
+ - 'IO'
+ - 'Default'
+ - 'Unconfined'
+ RedundantSuspendModifier:
+ active: true
+ SleepInsteadOfDelay:
+ active: true
+ SuspendFunWithFlowReturnType:
+ active: true
+
+empty-blocks:
+ active: true
+ EmptyCatchBlock:
+ active: true
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ EmptyClassBlock:
+ active: true
+ EmptyDefaultConstructor:
+ active: true
+ EmptyDoWhileBlock:
+ active: true
+ EmptyElseBlock:
+ active: true
+ EmptyFinallyBlock:
+ active: true
+ EmptyForBlock:
+ active: true
+ EmptyFunctionBlock:
+ active: true
+ ignoreOverridden: false
+ EmptyIfBlock:
+ active: true
+ EmptyInitBlock:
+ active: true
+ EmptyKtFile:
+ active: true
+ EmptySecondaryConstructor:
+ active: true
+ EmptyTryBlock:
+ active: true
+ EmptyWhenBlock:
+ active: true
+ EmptyWhileBlock:
+ active: true
+
+exceptions:
+ active: true
+ ExceptionRaisedInUnexpectedLocation:
+ active: true
+ methodNames:
+ - 'equals'
+ - 'finalize'
+ - 'hashCode'
+ - 'toString'
+ InstanceOfCheckForException:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ NotImplementedDeclaration:
+ active: true
+ ObjectExtendsThrowable:
+ active: true
+ PrintStackTrace:
+ active: true
+ RethrowCaughtException:
+ active: true
+ ReturnFromFinally:
+ active: true
+ ignoreLabeled: false
+ SwallowedException:
+ active: true
+ ignoredExceptionTypes:
+ - 'InterruptedException'
+ - 'MalformedURLException'
+ - 'NumberFormatException'
+ - 'ParseException'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ ThrowingExceptionFromFinally:
+ active: true
+ ThrowingExceptionInMain:
+ active: false
+ ThrowingExceptionsWithoutMessageOrCause:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptions:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Exception'
+ - 'IllegalArgumentException'
+ - 'IllegalMonitorStateException'
+ - 'IllegalStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ ThrowingNewInstanceOfSameException:
+ active: true
+ TooGenericExceptionCaught:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ exceptionNames:
+ - 'ArrayIndexOutOfBoundsException'
+ - 'Error'
+ - 'Exception'
+ - 'IllegalMonitorStateException'
+ - 'IndexOutOfBoundsException'
+ - 'NullPointerException'
+ - 'RuntimeException'
+ - 'Throwable'
+ allowedExceptionNameRegex: '_|(ignore|expected).*'
+ TooGenericExceptionThrown:
+ active: true
+ exceptionNames:
+ - 'Error'
+ - 'Exception'
+ - 'RuntimeException'
+ - 'Throwable'
+
+formatting:
+ active: true
+ android: true
+ autoCorrect: true
+ AnnotationOnSeparateLine:
+ active: false
+ autoCorrect: true
+ AnnotationSpacing:
+ active: true
+ autoCorrect: true
+ ArgumentListWrapping:
+ # TODO: Enable after update to ktlint 0.42+
+ # Issue: https://github.com/pinterest/ktlint/issues/1159
+ active: false
+ autoCorrect: true
+ indentSize: 4
+ maxLineLength: 120
+ ChainWrapping:
+ active: true
+ autoCorrect: true
+ CommentSpacing:
+ active: true
+ autoCorrect: true
+ EnumEntryNameCase:
+ active: false
+ autoCorrect: true
+ Filename:
+ active: true
+ FinalNewline:
+ active: true
+ autoCorrect: true
+ insertFinalNewLine: true
+ ImportOrdering:
+ active: true
+ autoCorrect: true
+ layout: '*,java.**,javax.**,kotlin.**,^'
+ Indentation:
+ active: true
+ autoCorrect: true
+ indentSize: 4
+ MaximumLineLength:
+ active: false # See style.MaxLineLength
+ maxLineLength: 120
+ ignoreBackTickedIdentifier: true
+ ModifierOrdering:
+ active: true
+ autoCorrect: true
+ MultiLineIfElse:
+ active: true
+ autoCorrect: true
+ NoBlankLineBeforeRbrace:
+ active: true
+ autoCorrect: true
+ NoConsecutiveBlankLines:
+ active: true
+ autoCorrect: true
+ NoEmptyClassBody:
+ active: true
+ autoCorrect: true
+ NoEmptyFirstLineInMethodBlock:
+ active: true
+ autoCorrect: true
+ NoLineBreakAfterElse:
+ active: true
+ autoCorrect: true
+ NoLineBreakBeforeAssignment:
+ active: true
+ autoCorrect: true
+ NoMultipleSpaces:
+ active: true
+ autoCorrect: true
+ NoSemicolons:
+ active: true
+ autoCorrect: true
+ NoTrailingSpaces:
+ active: true
+ autoCorrect: true
+ NoUnitReturn:
+ active: true
+ autoCorrect: true
+ NoUnusedImports:
+ active: true
+ autoCorrect: true
+ NoWildcardImports:
+ active: false
+ PackageName:
+ active: true
+ autoCorrect: true
+ ParameterListWrapping:
+ active: true
+ autoCorrect: true
+ indentSize: 4
+ maxLineLength: 120
+ SpacingAroundAngleBrackets:
+ active: true
+ autoCorrect: true
+ SpacingAroundColon:
+ active: true
+ autoCorrect: true
+ SpacingAroundComma:
+ active: true
+ autoCorrect: true
+ SpacingAroundCurly:
+ active: true
+ autoCorrect: true
+ SpacingAroundDot:
+ active: true
+ autoCorrect: true
+ SpacingAroundDoubleColon:
+ active: true
+ autoCorrect: true
+ SpacingAroundKeyword:
+ active: true
+ autoCorrect: true
+ SpacingAroundOperators:
+ active: true
+ autoCorrect: true
+ SpacingAroundParens:
+ active: true
+ autoCorrect: true
+ SpacingAroundRangeOperator:
+ active: true
+ autoCorrect: true
+ SpacingAroundUnaryOperator:
+ active: true
+ autoCorrect: true
+ SpacingBetweenDeclarationsWithAnnotations:
+ active: true
+ autoCorrect: true
+ SpacingBetweenDeclarationsWithComments:
+ active: true
+ autoCorrect: true
+ StringTemplate:
+ active: true
+ autoCorrect: true
+
+naming:
+ active: true
+ BooleanPropertyNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ allowedPattern: '^(is|has|are)'
+ ClassNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ classPattern: '[A-Z][a-zA-Z0-9]*'
+ ConstructorParameterNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ privateParameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ EnumNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
+ ForbiddenClassName:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ forbiddenName: []
+ FunctionMaxLength:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ maximumFunctionNameLength: 30
+ FunctionMinLength:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ minimumFunctionNameLength: 3
+ FunctionNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
+ ignoreAnnotated: ['Composable']
+ excludeClassPattern: '$^'
+ FunctionParameterNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ parameterPattern: '[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+ InvalidPackageDeclaration:
+ active: false
+ rootPackage: ''
+ LambdaParameterNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ parameterPattern: '[a-z][A-Za-z0-9]*|_'
+ MatchingDeclarationName:
+ active: true
+ mustBeFirst: true
+ MemberNameEqualsClassName:
+ active: true
+ ignoreOverridden: true
+ NoNameShadowing:
+ active: true
+ NonBooleanPropertyPrefixedWithIs:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ ObjectPropertyNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ constantPattern: '[A-Za-z][_A-Za-z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
+ PackageNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
+ TopLevelPropertyNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ constantPattern: '[A-Z][_A-Z0-9]*'
+ propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
+ privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
+ VariableMaxLength:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ maximumVariableNameLength: 64
+ VariableMinLength:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ minimumVariableNameLength: 1
+ VariableNaming:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ variablePattern: '[a-z][A-Za-z0-9]*'
+ privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
+ excludeClassPattern: '$^'
+
+performance:
+ active: true
+ ArrayPrimitive:
+ active: true
+ ForEachOnRange:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ SpreadOperator:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ UnnecessaryTemporaryInstantiation:
+ active: true
+
+potential-bugs:
+ active: true
+ AvoidReferentialEquality:
+ active: true
+ forbiddenTypePatterns:
+ - 'kotlin.String'
+ CastToNullableType:
+ active: true
+ Deprecation:
+ active: false
+ DontDowncastCollectionTypes:
+ active: true
+ DoubleMutabilityForCollection:
+ active: true
+ EqualsAlwaysReturnsTrueOrFalse:
+ active: true
+ EqualsWithHashCodeExist:
+ active: true
+ ExitOutsideMain:
+ active: true
+ ExplicitGarbageCollectionCall:
+ active: true
+ HasPlatformType:
+ active: true
+ IgnoredReturnValue:
+ active: true
+ restrictToConfig: true
+ returnValueAnnotations:
+ - '*.CheckResult'
+ - '*.CheckReturnValue'
+ ignoreReturnValueAnnotations:
+ - '*.CanIgnoreReturnValue'
+ ImplicitDefaultLocale:
+ active: true
+ ImplicitUnitReturnType:
+ active: false
+ allowExplicitReturnType: true
+ InvalidRange:
+ active: true
+ IteratorHasNextCallsNextMethod:
+ active: true
+ IteratorNotThrowingNoSuchElementException:
+ active: true
+ LateinitUsage:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ ignoreOnClassesPattern: ''
+ MapGetWithNotNullAssertionOperator:
+ active: true
+ MissingPackageDeclaration:
+ active: true
+ excludes: ['**/*.kts']
+ NullableToStringCall:
+ active: true
+ UnconditionalJumpStatementInLoop:
+ active: true
+ UnnecessaryNotNullOperator:
+ active: true
+ UnnecessarySafeCall:
+ active: true
+ UnreachableCatchBlock:
+ active: true
+ UnreachableCode:
+ active: true
+ UnsafeCallOnNullableType:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ UnsafeCast:
+ active: true
+ UnusedUnaryOperator:
+ active: true
+ UselessPostfixExpression:
+ active: true
+ WrongEqualsTypeParameter:
+ active: true
+
+style:
+ active: true
+ BracesOnWhenStatements:
+ active: false
+ ClassOrdering:
+ active: true
+ CollapsibleIfStatements:
+ active: true
+ DataClassContainsFunctions:
+ active: false
+ conversionFunctionPrefix: ['to']
+ DataClassShouldBeImmutable:
+ active: true
+ DestructuringDeclarationWithTooManyEntries:
+ active: true
+ maxDestructuringEntries: 3
+ EqualsNullCall:
+ active: true
+ EqualsOnSignatureLine:
+ active: true
+ ExplicitCollectionElementAccessMethod:
+ active: true
+ ExplicitItLambdaParameter:
+ active: true
+ # TODO: https://github.com/detekt/detekt/issues/950#issuecomment-401575701
+ ExpressionBodySyntax:
+ active: false
+ includeLineWrapping: false
+ ForbiddenComment:
+ active: true
+ comments:
+ - reason: 'Forbidden FIXME todo marker in comment, please fix the problem.'
+ value: 'FIXME:'
+ - reason: 'Forbidden STOPSHIP todo marker in comment, please address the problem before shipping the code.'
+ value: 'STOPSHIP:'
+ allowedPatterns: ''
+ ForbiddenImport:
+ active: false
+ imports: []
+ forbiddenPatterns: ''
+ ForbiddenMethodCall:
+ active: true
+ methods:
+ - 'kotlin.io.print'
+ - 'kotlin.io.println'
+ ForbiddenVoid:
+ active: true
+ ignoreOverridden: false
+ ignoreUsageInGenerics: false
+ FunctionOnlyReturningConstant:
+ active: true
+ ignoreOverridableFunction: true
+ ignoreActualFunction: true
+ excludedFunctions: ['']
+ LoopWithTooManyJumpStatements:
+ active: true
+ maxJumpCount: 1
+ MagicNumber:
+ active: true
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**', '**/*.kts']
+ ignoreNumbers:
+ - '-1'
+ - '0'
+ - '0.5'
+ - '1'
+ - '2'
+ ignoreHashCodeFunction: true
+ ignorePropertyDeclaration: true
+ ignoreLocalVariableDeclaration: false
+ ignoreConstantDeclaration: true
+ ignoreCompanionObjectPropertyDeclaration: true
+ ignoreAnnotation: false
+ ignoreNamedArgument: true
+ ignoreEnums: false
+ ignoreRanges: false
+ ignoreExtensionFunctions: true
+ MandatoryBracesLoops:
+ active: true
+ MaxLineLength:
+ active: true
+ maxLineLength: 120
+ excludePackageStatements: true
+ excludeImportStatements: true
+ excludeCommentStatements: true
+ MayBeConst:
+ active: true
+ ModifierOrder:
+ active: true
+ MultilineLambdaItParameter:
+ active: true
+ NestedClassesVisibility:
+ active: true
+ NewLineAtEndOfFile:
+ active: true
+ NoTabs:
+ active: true
+ ObjectLiteralToLambda:
+ active: true
+ OptionalAbstractKeyword:
+ active: true
+ OptionalUnit:
+ active: true
+ PreferToOverPairSyntax:
+ active: true
+ ProtectedMemberInFinalClass:
+ active: true
+ RedundantExplicitType:
+ active: true
+ RedundantHigherOrderMapUsage:
+ active: true
+ RedundantVisibilityModifierRule:
+ active: false # Conflicts with explicit API mode
+ ReturnCount:
+ active: true
+ max: 2
+ excludedFunctions: ['equals']
+ excludeLabeled: false
+ excludeReturnFromLambda: true
+ excludeGuardClauses: false
+ SafeCast:
+ active: true
+ SerialVersionUIDInSerializableClass:
+ active: true
+ SpacingBetweenPackageAndImports:
+ active: false # See formatting.NoConsecutiveBlankLines
+ ThrowsCount:
+ active: true
+ max: 2
+ excludeGuardClauses: false
+ TrailingWhitespace:
+ active: false # See formatting.NoTrailingWhitespace
+ UnderscoresInNumericLiterals:
+ active: true
+ acceptableLength: 5
+ UnnecessaryAbstractClass:
+ active: true
+ UnnecessaryAnnotationUseSiteTarget:
+ active: true
+ UnnecessaryApply:
+ active: true
+ UnnecessaryFilter:
+ active: true
+ UnnecessaryInheritance:
+ active: true
+ UnnecessaryLet:
+ active: true
+ UnnecessaryParentheses:
+ active: true
+ UntilInsteadOfRangeTo:
+ active: true
+ UnusedImports:
+ active: false # See formatting.NoUnusedImports
+ UnusedPrivateClass:
+ active: true
+ UnusedPrivateMember:
+ active: true
+ allowedNames: '(_|ignored|expected|serialVersionUID)'
+ UseAnyOrNoneInsteadOfFind:
+ active: true
+ UseArrayLiteralsInAnnotations:
+ active: true
+ UseCheckNotNull:
+ active: true
+ UseCheckOrError:
+ active: true
+ UseDataClass:
+ active: false
+ allowVars: false
+ UseEmptyCounterpart:
+ active: true
+ UseIfEmptyOrIfBlank:
+ active: true
+ UseIfInsteadOfWhen:
+ active: true
+ UseIsNullOrEmpty:
+ active: true
+ UseOrEmpty:
+ active: true
+ UseRequire:
+ active: true
+ UseRequireNotNull:
+ active: true
+ UselessCallOnNotNull:
+ active: true
+ UtilityClassWithPublicConstructor:
+ active: true
+ VarCouldBeVal:
+ active: true
+ WildcardImport:
+ active: false
+ excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
+ excludeImports:
+ - 'java.util.*'
+
+libraries:
+ ForbiddenPublicDataClass:
+ active: true
+ excludes: [ '**' ]
+ ignorePackages:
+ - '*.internal'
+ - '*.internal.*'
+ LibraryCodeMustSpecifyReturnType:
+ active: true
+ excludes: [ '**' ]
+ LibraryEntitiesShouldNotBePublic:
+ active: true
+ excludes: [ '**' ]
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..36f71f7
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,41 @@
+# Project-wide Gradle settings.
+group=com.redmadrobot.%stub%
+version=1.0.0
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx1024m -XX:MaxPermSize=256m
+org.gradle.jvmargs=-Xmx1536m -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+org.gradle.parallel=true
+
+# Gradle will try to reuse outputs from previous builds for all builds, unless
+# explicitly disabled with --no-build-cache.
+# https://docs.gradle.org/current/userguide/build_cache.html
+org.gradle.caching=true
+
+# To detect changes on the file-system, and to calculate what needs to be rebuilt, Gradle collects
+# information about the file-system in-memory during every build (aka Virtual File-System).
+# https://docs.gradle.org/current/userguide/gradle_daemon.html#sec:daemon_watch_fs
+org.gradle.vfs.watch=true
+#org.gradle.vfs.verbose=true
+
+# Notify Android Gradle Plugin that we use AndroidX and not use Jetifier
+android.useAndroidX=true
+android.enableJetifier=false
+
+# Disable Android Studio version check to be able to use Intellij IDEA.
+android.injected.studio.version.check=false
+
+# Use official kotlin code style
+kotlin.code.style=official
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..e337b1f
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,24 @@
+# For an example of how to maintain dependencies in version catalogs,
+# see https://github.com/RedMadRobot/gradle-version-catalogs.
+[versions]
+activity = "1.9.0"
+android-gradle-plugin = "8.4.0"
+detekt = "1.23.6"
+gradle-android-cacheFix = "3.0.1"
+gradle-infrastructure = "0.18.1"
+kotlin = "1.9.23"
+versionsPlugin = "0.51.0"
+publish-plugin = "0.28.0"
+
+[libraries]
+android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
+detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
+gradle-android-cacheFixGradlePlugin = { module = "gradle.plugin.org.gradle.android:android-cache-fix-gradle-plugin", version.ref = "gradle-android-cacheFix" }
+infrastructure-android = { module = "com.redmadrobot.build:infrastructure-android", version.ref = "gradle-infrastructure" }
+infrastructure-publish = { module = "com.redmadrobot.build:infrastructure-publish", version.ref = "gradle-infrastructure" }
+publish-gradlePlugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publish-plugin" }
+kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+
+[plugins]
+infrastructure-detekt = { id = "com.redmadrobot.detekt", version.ref = "gradle-infrastructure" }
+versions = { id = "com.github.ben-manes.versions", version.ref = "versionsPlugin" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e644113
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..a441313
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..b740cf1
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..25da30d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/release.sh b/release.sh
new file mode 100755
index 0000000..7f52f81
--- /dev/null
+++ b/release.sh
@@ -0,0 +1,93 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+# The script could be run from any directory.
+cd "$(dirname "$0")"
+
+# Configure the script
+properties="gradle.properties"
+changelog="CHANGELOG.md"
+readme="README.md"
+files_to_update_version=("$properties" "$readme")
+github_repository_url="https://github.com/RedMadRobot/%Stub%"
+
+#region Utils
+function property {
+ grep "^${1}=" "$properties" | cut -d'=' -f2
+}
+
+function replace() {
+ # Escape linebreaks
+ local replacement=${2//$'\n'/\\\n}
+ # Portable in-place edit.
+ # See: https://stackoverflow.com/a/4247319
+ sed -i".bak" -E "s~$1~$replacement~g" "$3"
+ rm "$3.bak"
+}
+
+function diff_link() {
+ echo -n "$github_repository_url/compare/${1}...${2}"
+}
+#endregion
+
+# 0. Fetch remote changes
+echo "️⏳ Updating local repository..."
+git fetch --quiet -p origin
+git switch --quiet main
+git pull --quiet --rebase origin
+echo "✅ Repository updated."
+echo
+
+# 1. Calculate version values for later
+last_version=$(property "version")
+version=$(date "+%Y.%m.%d")
+if [[ "$last_version" == "$version" ]]; then
+ echo "🤔 Version $version is already set."
+ exit 0
+fi
+echo "🚀 Update $last_version -> $version"
+echo
+
+# 2. Update version everywhere
+for file in "${files_to_update_version[@]}" ; do
+ replace "$last_version" "$version" "$file"
+ echo "✅ Updated version in $file"
+done
+
+# 3. Update header in CHANGELOG.md
+header_replacement=\
+"## [Unreleased]
+
+### Changes
+
+- *No changes*
+
+## [$version]"
+replace "^## \[Unreleased\].*" "$header_replacement" "$changelog"
+echo "✅ Updated CHANGELOG.md header"
+
+# 4. Add link to version diff
+unreleased_diff_link="[unreleased]: $(diff_link "$version" "main")"
+version_diff_link="[$version]: $(diff_link "$last_version" "$version")"
+replace "^\[unreleased\]:.*" "$unreleased_diff_link\n$version_diff_link" "$changelog"
+echo "✅ Added a diff link to CHANGELOG.md"
+
+# 5. Ask if the changes should be pushed to remote branch
+echo
+echo "Do you want to commit the changes and create a release tag?"
+echo "The release tag push will trigger a release workflow on CI."
+read -p " Enter 'yes' to continue: " -r input
+if [[ "$input" != "yes" ]]; then
+ echo "👌 DONE."
+ exit 0
+fi
+
+# 6. Push changes and trigger release on CI
+echo
+echo "⏳ Pushing the changes to the remote repository..."
+git add "$readme" "$changelog" "$properties"
+git commit --quiet --message "version: $version"
+git tag "$version"
+git push --quiet origin HEAD "$version"
+echo "🎉 DONE."
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..6b77f68
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:recommended",
+ ":semanticCommitScopeDisabled"
+ ]
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..2f5acdd
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,27 @@
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupAndSubgroups("com.android")
+ includeGroupAndSubgroups("com.google")
+ includeGroupAndSubgroups("androidx")
+ }
+ }
+ gradlePluginPortal()
+ }
+}
+
+@Suppress("UnstableApiUsage")
+dependencyResolutionManagement {
+ repositories {
+ mavenCentral()
+ }
+}
+
+rootProject.name = "%stub%-root"
+
+include(
+ "%stub%",
+)
diff --git a/stub/build.gradle.kts b/stub/build.gradle.kts
new file mode 100644
index 0000000..6b81a51
--- /dev/null
+++ b/stub/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ id("com.redmadrobot.kotlin-library")
+ convention.publishing
+ convention.detekt
+}
+
+description = "%stub%"
+
+dependencies {
+ api(kotlin("stdlib"))
+}
diff --git a/stub/src/main/kotlin/Stub.kt b/stub/src/main/kotlin/Stub.kt
new file mode 100644
index 0000000..901400f
--- /dev/null
+++ b/stub/src/main/kotlin/Stub.kt
@@ -0,0 +1,3 @@
+package com.redmadrobot.%stub%
+
+public interface %Stub%