{"id":19521836,"url":"https://github.com/smarttoolfactory/android-daggerhilt-dynamicfeturemodule-boilerplate","last_synced_at":"2025-07-01T12:33:33.976Z","repository":{"id":48493295,"uuid":"283968237","full_name":"SmartToolFactory/Android-DaggerHilt-DynamicFetureModule-Boilerplate","owner":"SmartToolFactory","description":"Boilerplate to create empty project with Dagger Hilt, Dynamic Feature modules, MVVM, RxJava, Coroutines with modularization","archived":false,"fork":false,"pushed_at":"2021-08-27T16:47:53.000Z","size":495,"stargazers_count":18,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-04T10:28:12.178Z","etag":null,"topics":["android","boiler-template","boilerplate","coroutines","dagger-hilt","detekt","dynamic-feature-module","flow","git-hooks","gradle","gradle-kotlin-dsl","jetpack-compose","ktlint","modularization","navigation","navigation-component-kotlin","navigation-components","rxjava3"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SmartToolFactory.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-07-31T07:11:02.000Z","updated_at":"2024-09-05T21:14:15.000Z","dependencies_parsed_at":"2022-08-31T13:54:09.076Z","dependency_job_id":null,"html_url":"https://github.com/SmartToolFactory/Android-DaggerHilt-DynamicFetureModule-Boilerplate","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FAndroid-DaggerHilt-DynamicFetureModule-Boilerplate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FAndroid-DaggerHilt-DynamicFetureModule-Boilerplate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FAndroid-DaggerHilt-DynamicFetureModule-Boilerplate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FAndroid-DaggerHilt-DynamicFetureModule-Boilerplate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SmartToolFactory","download_url":"https://codeload.github.com/SmartToolFactory/Android-DaggerHilt-DynamicFetureModule-Boilerplate/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250967149,"owners_count":21515547,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","boiler-template","boilerplate","coroutines","dagger-hilt","detekt","dynamic-feature-module","flow","git-hooks","gradle","gradle-kotlin-dsl","jetpack-compose","ktlint","modularization","navigation","navigation-component-kotlin","navigation-components","rxjava3"],"created_at":"2024-11-11T00:35:20.085Z","updated_at":"2025-04-26T09:31:59.783Z","avatar_url":"https://github.com/SmartToolFactory.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Boilerplate for creating project with MVVM, Jetpack Compose, Dagger Hilt, Dynamic Feature Modules\n\n[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)\n[![Kotlin Version](https://img.shields.io/badge/kotlin-1.4.0-blue.svg)](https://kotlinlang.org)\n[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21)\n\n\nThis is a boilerplate code to use not to repeat same process over again when starting a new project. Folders are created, dependencies and libraries are set up.\n\nMain branch has no DataSource, Repository or ViewModel classes. Checkout coroutines-flow\nor compose branch. Data module in these branches\ncontain fake dao, local and remote data source, repository, mapper.\n\n\n* Gradle Kotlin DSL is used for setting up gradle files with ```buildSrc``` folder and extensions.\n* KtLint, Detekt, and Git Hooks is used for checking, and formatting code and validating code before commits.\n* Dagger Hilt, Dynamic Feature Modules with Navigation Components, ViewModel, Retrofit, Room, Coroutines libraries adn dependencies are set up.\n* ```features``` and ```libraries``` folders are used to include android libraries and dynamic feature modules\n* In core module dagger hilt dependencies and ```@EntryPoint``` is created\n* test-utils module for shared folder for tes and androidTest folders, LiveDataObserver and FlowObserver.\n* Jetpack Compose dependencies added, app runs ```@Composeable``` as project is run\n### Note\nChange ```applicationId```in ```Version.AndroidVersion```\n\n```\nobject AndroidVersion {\n    const val APPLICATION_ID = \"com....\"\n}\n```\n\n## Gradle Kotlin DSL, ktLint, detekt And Git Hooks Integration\nKotlin DSL, ktlint, detekt and Git Hooks to app on app, library and dynamic feature module step by step with each commit.\nAlso added git hooks using script below in ```.git\\hooks``` folder with a excutable **pre-commit.sh** to turn simple text to excutable call ```chmod a+x pre-commit```\n\n```\n#!/bin/sh\n\n\necho \"Running static analysis...\"\n\n\n# Inspect code using KtLint, and Detekt\n\n# Run KtLint only\n#./gradlew app:ktlintCheck --daemon\n\n# Format code using KtLint, then run Detekt and KtLint static analysis\n./gradlew app:ktlintFormat app:detekt app:ktlintCheck --daemon\n\nstatus=$?\n\n\nif [ \"$status\" = 0 ] ; then\n\n    echo \"Static analysis found no problems.\"\n\n    exit 0\n\nelse\n\n    echo 1\u003e\u00262 \"Static analysis found violations it could not fix.\"\n\n    exit 1\n\nfi\n```\n\nwhich calls in order\n\n1. ktLint to format code before commit\n2. runs detekt for static code analysis\n3. ktlintCheck to check code,\n\nif after these tasks status is 0 then commit is successful, otherwise you need to check out console for links to your code and fix those errors.\n\n\nSteps taken to migrate to Gradle with Kotlin\n\n* Rename settings.gradle to settings.gradle.kts\n* Rename project level build.gradle to build.gradle.kts\n* Add plugins using the snippet below\n```\nplugins {\n    id(Plugins.KTLINT) version PluginVersion.KTLINT_VERSION\n    id(Plugins.DETEKT) version PluginVersion.DETEKT_VERSION\n}\n```\n\nThen us the snippet below to add **ktlint** and **detekt** to each module used in project\n\n```\nsubprojects {\n\n    // KtLint\n    apply(plugin = Plugins.KTLINT) // Version should be inherited from parent\n\n    // Optionally configure plugin\n    ktlint {\n        debug.set(true)\n    }\n\n    // Detekt\n    apply(plugin = Plugins.DETEKT)\n\n    detekt {\n        config = files(\"${project.rootDir}/config/detekt/detekt.yml\")\n        parallel = true\n\n        reports {\n            xml {\n                enabled = true\n                destination = file(\"${project.rootDir}/build/reports/detekt_report.xml\")\n            }\n            html {\n                enabled = true\n                destination = file(\"${project.rootDir}/build/reports/detekt_report.html\")\n            }\n            txt {\n                enabled = true\n                destination = file(\"${project.rootDir}/build/reports/detekt_report.txt\")\n            }\n        }\n    }\n}\n```\n* Create a **buildSrc** folder and add it's build.gradle.kts with\n```\nplugins {\n    `kotlin-dsl`\n}\n```\nto turn it a dsl folder\n\nCreated [DependencyHandler](buildSrc/src/main/java/extension/DependencyHandlerExtension.kt) extension functions to not repeat adding same dependencies to different modules over and over again\n\nFor instance\n\n```\nfun DependencyHandler.addCoreModuleDependencies() {\n\n    implementation(Deps.KOTLIN)\n    implementation(Deps.ANDROIDX_CORE_KTX)\n\n    ...\n\n}\n```\n\n## Modules\n\n*Module heierarchy*\n```\n     --\u003e feature module(dynamic featature module)\n     |    |\n     |  app module \u003c-\u003c----\n     |    |          |   |\n     -core module    |   |\n                     |   |\n      domain module\u003c------\n           |             |\n      data module\u003c--------\n                         |\n      test-utils module--\n\n```\n\n### Data Module\nData module is empty to create repository, database and REST apis, or cache implementations.\n\n### Domain Module\nDomain module is for UseCase or Interactor to contain business logic.\n\n### Core Module\nCore module can be used for containing libraries and other android related stuff. It only contains *CoreModule* and **CoreModuleDependencies**\nfor adding *dependent components* to dynamic feature modules or app module if required\n\n### App Module\nMain app with Application that uses @DaggerHiltApp, you can build your app in this module\n\n### Dynamic Feature Modules\nModules for separating feature from app to prevent app being monolithic and having parallel gradle builds\n\n### test-utils Module\nModule ready for unit-testing with libraries and extensions to be used in every module\nadd build.gradle file with ```testImplementation(project(Modules.AndroidLibrary.TEST_UTILS))```\nIf needed put your json data into response.json which is located in test/resources in test-utils module\n\n## Dynamic Feature Navigation with Navigation Components\n\nTo navigate with Dynamic Feature Modules main layout should contain a ```DynamicNavHostFragment```,\n```\n    \u003candroidx.fragment.app.FragmentContainerView\n        android:id=\"@+id/nav_host_fragment\"\n        android:name=\"androidx.navigation.dynamicfeatures.fragment.DynamicNavHostFragment\"\n        app:defaultNavHost=\"true\"\n        app:navGraph=\"@navigation/nav_graph\" /\u003e\n```\n\n### Main navigation graph\nAnd navigation folder should contain navigation graph with\n\n    \u003c!-- dynamic feature module--\u003e\n    \u003cinclude-dynamic\n        android:id=\"@+id/nav_graph_feature\"\n        android:name=\"com.smarttoolfactory.feature\"\n        app:graphResName=\"nav_graph_feature\"\n        app:moduleName=\"feature\"\u003e\n\n        \u003cargument\n            android:name=\"count\"\n            android:defaultValue=\"0\"\n            app:argType=\"integer\" /\u003e\n    \u003c/include-dynamic\u003e\n\nThere are 3 important properties that should be carefully added to main graph for not receiving error\n\n1. id of the navigation,``` android:id=\"@+id/nav_graph_feature\"```, should be same with the dynamic feature id\n2. ```graphResName``` is the name of the navigation folder which is nav_graph_feature.xml for this boilerplate\n3. module name should be exactly same name dynamic feature module is named.\n\n\n### Dynamic Feature Module navigation graph\nIn nav_graph_feature.xml\n\n```\n\u003cnavigation xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@id/nav_graph_feature\"\n    app:moduleName=\"feature\"\n    app:startDestination=\"@id/featureFragment\"\u003e\n    \u003cfragment\n        android:id=\"@+id/featureFragment\"\n        android:name=\"com.smarttoolfactory.feature.FeatureFragment\"\n        android:label=\"FeatureFragment\"\n        tools:layout=\"@layout/fragment_feature\" /\u003e\n\u003c/navigation\u003e\n```\n\nMake sure that\n\n1. id is same with the where this navigation is called except do not use *@+id/* since it creates new resource id, use *@id/*\n2. Add module name as in other navigation graph\n3. And start destination for first fragment to be displayed after navigating to this fragment.\n\n\n### Navigating with navController and InstallMonitor\n\nFor navigation i used ```DynamicInstallFragment``` fragment which is inside *core module* since it provides\n\n```private val installMonitor = DynamicInstallMonitor()``` to track installation state of the dynamic feature module\nwe navigate.\n\nNavigate to a dynamic feature module using\n\n```\nfun navigateWithInstallMonitor(navController: NavController, @IdRes destinationId: Int) {\n\n        navController.navigate(\n            destinationId,\n            null,\n            null,\n            DynamicExtras(installMonitor)\n        )\n}\n```\nwhich checks if the dynamic feature we wish to navigate is installed already ```installMonitor.isInstallRequired```\nand. If the dynamic feature is not installed returns states for status where you can take action.\n\n## Dagger Hilt and Dynamic Feature Module injections\n\n### Application\nApplication class only need to use  ```@HiltAndroidApp``` annotation\n\n### Activity\nActivity should use ```@AndroidEntryPoint```annotation\n\n### Core Module\nCore module should have provision methods for dependencies to be injected in core module to any other dependent module\n\n```\n@EntryPoint\n@InstallIn(SingletonComponent::class)\ninterface CoreModuleDependencies {\n\n    /*\n       Provision methods to provide dependencies to components that depend on this component\n     */\n    fun coreDependency(): CoreDependency\n\n}\n```\n\n### Dynamic Feature Modules\n\nModules that depend on core module should create a component, that depends on core module, with\n\n```\n@Component(\n    dependencies = [CoreModuleDependencies::class],\n    modules = [FeatureModule::class]\n)\ninterface FeatureComponent {\n\n    fun inject(featureFragment: FeatureFragment)\n\n    @Component.Factory\n    interface Factory {\n        fun create(\n            coreModuleDependencies: CoreModuleDependencies,\n            @BindsInstance application: Application\n        ): FeatureComponent\n    }\n}\n```\n\nAnd create this component in a ```Fragment``` or ```Activity``` using\n\n```\n        val coreModuleDependencies = EntryPointAccessors.fromApplication(\n            requireActivity().applicationContext,\n            CoreModuleDependencies::class.java\n        )\n\n        DaggerFeatureComponent.factory().create(\n            coreModuleDependencies,\n            requireActivity().application\n        )\n            .inject(this)\n```\n\n🔥 ```EntryPointAccessors.fromApplication``` depends on which component ```CoreModule``` uses ```@InstallIn``` with\n\n## Testing\n\n\n### test-utils\n ```test-shared``` folder contains common rules, and utilities for both ```test``` and ```androidTest``` for using\n both with unit tests and integration test.\n\n```\n    sourceSets {\n\n        val sharedTestDir = \"src/test-shared/java\"\n\n        getByName(\"test\") {\n            java.srcDir(sharedTestDir)\n        }\n\n        getByName(\"androidTest\") {\n            java.srcDir(sharedTestDir)\n            resources.srcDir(\"src/test/resources\")\n        }\n    }\n```\n\n### LiveDataObserver\n\nThis class is observer for testing ```LiveData``` that emits more than one values and similar to RxJava ```TestObserver```.\n\n```\nclass LiveDataTestObserver\u003cT\u003e constructor(\n    private val liveData: LiveData\u003cT\u003e\n) : Observer\u003cT\u003e {\n\n    init {\n        liveData.observeForever(this)\n    }\n\n    private val testValues = mutableListOf\u003cT\u003e()\n\n    override fun onChanged(t: T) {\n        if (t != null) testValues.add(t)\n    }\n\n    fun assertNoValues(): LiveDataTestObserver\u003cT\u003e {\n        if (testValues.isNotEmpty()) throw AssertionError(\n            \"Assertion error with actual size ${testValues.size}\"\n        )\n        return this\n    }\n\n    fun assertValueCount(count: Int): LiveDataTestObserver\u003cT\u003e {\n        if (count \u003c 0) throw AssertionError(\n            \"Assertion error! value count cannot be smaller than zero\"\n        )\n        if (count != testValues.size) throw AssertionError(\n            \"Assertion error! with expected $count while actual ${testValues.size}\"\n        )\n        return this\n    }\n\n    fun assertValues(vararg predicates: T): LiveDataTestObserver\u003cT\u003e {\n        if (!testValues.containsAll(predicates.asList())) throw  AssertionError(\"Assertion error!\")\n        return this\n    }\n\n    fun assertValues(predicate: (List\u003cT\u003e) -\u003e Boolean): LiveDataTestObserver\u003cT\u003e {\n        predicate(testValues)\n        return this\n    }\n\n    fun values(predicate: (List\u003cT\u003e) -\u003e Unit): LiveDataTestObserver\u003cT\u003e {\n        predicate(testValues)\n        return this\n    }\n\n    fun values(): List\u003cT\u003e {\n        return testValues\n    }\n\n    /**\n     * Removes this observer from the [LiveData] which was observing\n     */\n    fun dispose() {\n        liveData.removeObserver(this)\n    }\n\n    /**\n     * Clears data available in this observer and removes this observer from the [LiveData] which was observing\n     */\n    fun clear() {\n        testValues.clear()\n        dispose()\n    }\n}\n\nfun \u003cT\u003e LiveData\u003cT\u003e.test(): LiveDataTestObserver\u003cT\u003e {\n\n    val testObserver = LiveDataTestObserver(this)\n\n    // Remove this testObserver that is added in init block of TestObserver, and clears previous data\n    testObserver.clear()\n    observeForever(testObserver)\n\n    return testObserver\n}\n```\n\n### FlowTestObserver\n\nTestObserver with declarative assertion methods to test more than multiple states and values sequentaially.\n\n```\nclass FlowTestObserver\u003cT\u003e(\n    private val coroutineScope: CoroutineScope,\n    private val flow: Flow\u003cT\u003e,\n    private val waitForDelay: Boolean = false\n) {\n    private val testValues = mutableListOf\u003cT\u003e()\n    private var error: Throwable? = null\n\n    private var isInitialized = false\n\n    private var isCompleted = false\n\n    private lateinit var job: Job\n\n\n    private suspend fun initializeAndJoin() {\n        job = createJob(coroutineScope)\n    }\n\n\n    private suspend fun initialize() {\n\n        if (!isInitialized) {\n            isInitialized = true\n\n            if (waitForDelay) {\n                try {\n                    withTimeout(Long.MAX_VALUE) {\n                        job = createJob(this)\n                    }\n                } catch (e: Exception) {\n                    isCompleted = false\n                }\n            } else {\n                initializeAndJoin()\n            }\n        }\n    }\n\n    private fun createJob(scope: CoroutineScope): Job {\n\n        val job = flow\n            .onStart {\n            }\n            .onCompletion {\n                isCompleted = true\n            }\n            .catch { throwable -\u003e\n                error = throwable\n            }\n            .onEach {\n                testValues.add(it)\n            }\n            .launchIn(scope)\n\n        return job\n    }\n\n\n    suspend fun assertNoValues(): FlowTestObserver\u003cT\u003e {\n        initialize()\n        if (testValues.isNotEmpty()) throw AssertionError(\n            \"Assertion error! Actual size ${testValues.size}\"\n        )\n        return this\n    }\n\n    suspend fun assertValueCount(count: Int): FlowTestObserver\u003cT\u003e {\n        initialize()\n        if (count \u003c 0) throw AssertionError(\n            \"Assertion error! Value count cannot be smaller than zero\"\n        )\n        if (count != testValues.size) throw AssertionError(\n            \"Assertion error! Expected $count while actual ${testValues.size}\"\n        )\n        return this\n    }\n\n    suspend fun assertValues(vararg values: T): FlowTestObserver\u003cT\u003e {\n        initialize()\n        if (!testValues.containsAll(values.asList()))\n            throw  AssertionError(\"Assertion error! At least one value does not match\")\n        return this\n    }\n\n    suspend fun assertValues(predicate: (List\u003cT\u003e) -\u003e Boolean): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (!predicate(testValues))\n            throw  AssertionError(\"Assertion error! At least one value does not match\")\n        return this\n    }\n\n    suspend fun assertError(throwable: Throwable): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        val errorNotNull = exceptionNotNull()\n\n        if (!(errorNotNull::class.java == throwable::class.java \u0026\u0026\n                    errorNotNull.message == throwable.message)\n        )\n            throw AssertionError(\"Assertion Error! throwable: $throwable does not match $errorNotNull\")\n        return this\n    }\n\n    suspend fun assertError(errorClass: Class\u003cThrowable\u003e): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        val errorNotNull = exceptionNotNull()\n\n        if (errorNotNull::class.java != errorClass)\n            throw  AssertionError(\"Assertion Error! errorClass $errorClass does not match ${errorNotNull::class.java}\")\n        return this\n    }\n\n    suspend fun assertError(predicate: (Throwable) -\u003e Boolean): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        val errorNotNull = exceptionNotNull()\n\n        if (!predicate(errorNotNull))\n            throw AssertionError(\"Assertion Error! Exception for $errorNotNull\")\n        return this\n    }\n\n    suspend fun assertNoError(): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (error != null)\n            throw AssertionError(\"Assertion Error! Exception occurred $error\")\n\n        return this\n    }\n\n    suspend fun assertNull(): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        testValues.forEach {\n            if (it != null) throw AssertionError(\"Assertion Error! There are more than one item that is not null\")\n        }\n\n        return this\n    }\n\n    suspend fun assertComplete(): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (!isCompleted) throw AssertionError(\"Assertion Error! Job is not completed yet!\")\n        return this\n    }\n\n    suspend fun assertNotComplete(): FlowTestObserver\u003cT\u003e {\n\n        initialize()\n\n        if (isCompleted) throw AssertionError(\"Assertion Error! Job is completed!\")\n        return this\n    }\n\n    suspend fun values(predicate: (List\u003cT\u003e) -\u003e Unit): FlowTestObserver\u003cT\u003e {\n        predicate(testValues)\n        return this\n    }\n\n    suspend fun values(): List\u003cT\u003e {\n\n        initialize()\n\n        return testValues\n    }\n\n\n    private fun exceptionNotNull(): Throwable {\n\n        if (error == null)\n            throw  AssertionError(\"There is no exception\")\n\n        return error!!\n    }\n\n    fun dispose() {\n        job.cancel()\n    }\n}\n\n/**\n * Creates a RxJava2 style test observer that uses `onStart`, `onEach`, `onCompletion`\n *\n * * Set waitForDelay true for testing delay.\n *\n * ###  Note: waiting for delay with a channel that sends values throw TimeoutCancellationException, don't use timeout with channel\n * TODO Fix channel issue\n */\nsuspend fun \u003cT\u003e Flow\u003cT\u003e.test(\n    scope: CoroutineScope,\n    waitForDelay: Boolean = false\n): FlowTestObserver\u003cT\u003e {\n    return FlowTestObserver(scope, this@test, waitForDelay)\n}\n\n/**\n * Test function that awaits with time out until each delay method is run and then since\n * it takes a predicate that runs after a timeout.\n */\nsuspend fun \u003cT\u003e Flow\u003cT\u003e.testAfterDelay(\n    scope: CoroutineScope,\n    predicate: suspend FlowTestObserver\u003cT\u003e.() -\u003e Unit\n\n): Job {\n    return scope.launch(coroutineContext) {\n        FlowTestObserver(this, this@testAfterDelay, true).predicate()\n    }\n}\n\n\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmarttoolfactory%2Fandroid-daggerhilt-dynamicfeturemodule-boilerplate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmarttoolfactory%2Fandroid-daggerhilt-dynamicfeturemodule-boilerplate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmarttoolfactory%2Fandroid-daggerhilt-dynamicfeturemodule-boilerplate/lists"}