Browse Source

Merge pull request #260 from vector-im/feature/fga/module_templates

Feature/fga/module templates
test/jme/compound-poc
ganfra 2 years ago committed by GitHub
parent
commit
aafe90d165
  1. 2
      app/build.gradle.kts
  2. 2
      appnav/build.gradle.kts
  3. 38
      docs/_developer_onboarding.md
  4. 1
      features/template/.gitignore
  5. 56
      features/template/build.gradle.kts
  6. 0
      features/template/consumer-rules.pro
  7. 21
      features/template/proguard-rules.pro
  8. 20
      features/template/src/main/AndroidManifest.xml
  9. 22
      features/template/src/main/kotlin/io/element/android/features/template/TemplateEvents.kt
  10. 45
      features/template/src/main/kotlin/io/element/android/features/template/TemplateNode.kt
  11. 38
      features/template/src/main/kotlin/io/element/android/features/template/TemplatePresenter.kt
  12. 23
      features/template/src/main/kotlin/io/element/android/features/template/TemplateState.kt
  13. 31
      features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt
  14. 58
      features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt
  15. 52
      features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt
  16. 45
      plugins/src/main/kotlin/extension/DependencyHandleScope.kt
  17. 39
      settings.gradle.kts
  18. 2
      tests/uitests/build.gradle.kts
  19. 1
      tools/templates/FeatureModule.json
  20. BIN
      tools/templates/file_templates.zip

2
app/build.gradle.kts

@ -203,7 +203,7 @@ knit { @@ -203,7 +203,7 @@ knit {
dependencies {
allLibrariesImpl()
allServicesImpl()
allFeaturesImpl()
allFeaturesImpl(rootDir)
implementation(projects.tests.uitests)
implementation(projects.anvilannotations)
implementation(projects.appnav)

2
appnav/build.gradle.kts

@ -38,7 +38,7 @@ dependencies { @@ -38,7 +38,7 @@ dependencies {
implementation(libs.dagger)
kapt(libs.dagger.compiler)
allFeaturesApi()
allFeaturesApi(rootDir)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)

38
docs/_developer_onboarding.md

@ -269,10 +269,42 @@ Here are the main points: @@ -269,10 +269,42 @@ Here are the main points:
#### Template and naming
There is a template module to easily start a new feature. When creating a new module, you can just copy paste the template. It is
located [here](../features/template).
This documentation provides you with the steps to install and use the AS plugin for generating modules in your project.
The plugin and templates will help you quickly create new features with a standardized structure.
A. Installation
Follow these steps to install and configure the plugin and templates:
1. Install the AS plugin for generating modules :
[Generate Module from Template](https://plugins.jetbrains.com/plugin/13586-generate-module-from-template)
2. Import file templates in AS :
- Navigate to File/Manage IDE Settings/Import Settings
- Pick the `tools/templates/file_templates.zip` files
- Click on OK
3. Configure generate-module-from-template plugin :
- Navigate to AS/Settings/Tools/Module Template Settings
- Click on + / Import From File
- Pick the `tools/templates/FeatureModule.json`
Everything should be ready to use.
B. Usage
Example for a new feature called RoomDetails:
1. Right-click on the features package and click on Create Module from Template
2. Fill the 2 text fields like so:
- MODULE_NAME = roomdetails
- FEATURE_NAME = RoomDetails
3. Click on Next
4. Verify that the structure looks ok and click on Finish
5. The modules api/impl should be created under `features/roomdetails` directory.
6. Sync project with Gradle so the modules are recognized (no need to add them to settings.gradle).
7. You can now add more Presentation classes (Events, State, StateProvider, View, Presenter) in the impl module with the `Template Presentation Classes`.
To use it, just right click on the package where you want to generate classes, and click on `Template Presentation Classes`.
Fill the text field with the base name of the classes, ie `RootRoomDetails` in the `root` package.
For the naming rules, please follow what is being currently used in the template module.
Note that naming of files and classes is important, since those names are used to set up code coverage rules. For instance, presenters MUST have a
suffix `Presenter`,states MUST have a suffix `State`, etc. Also we want to have a common naming along all the modules.

1
features/template/.gitignore vendored

@ -1 +0,0 @@ @@ -1 +0,0 @@
/build

56
features/template/build.gradle.kts

@ -1,56 +0,0 @@ @@ -1,56 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* 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
*
* http://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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
}
android {
// TODO change the namespace (and your classes package)
namespace = "io.element.android.features.template"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
anvil(projects.anvilcodegen)
implementation(projects.anvilannotations)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.elementresources)
implementation(projects.libraries.uiStrings)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor)
}

0
features/template/consumer-rules.pro

21
features/template/proguard-rules.pro vendored

@ -1,21 +0,0 @@ @@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

20
features/template/src/main/AndroidManifest.xml

@ -1,20 +0,0 @@ @@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2023 New Vector Ltd
~
~ 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
~
~ http://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.
-->
<manifest>
</manifest>

22
features/template/src/main/kotlin/io/element/android/features/template/TemplateEvents.kt

@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* 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
*
* http://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.
*/
package io.element.android.features.template
// TODO Add your events or remove the file completely if no events
sealed interface TemplateEvents {
object MyEvent: TemplateEvents
}

45
features/template/src/main/kotlin/io/element/android/features/template/TemplateNode.kt

@ -1,45 +0,0 @@ @@ -1,45 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* 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
*
* http://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.
*/
package io.element.android.features.template
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.AppScope
// TODO Change to use the right Scope for your feature. For now it can be AppScope, SessionScope or RoomScope
@ContributesNode(AppScope::class)
class TemplateNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: TemplatePresenter,
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
TemplateView(
state = state,
modifier = modifier
)
}
}

38
features/template/src/main/kotlin/io/element/android/features/template/TemplatePresenter.kt

@ -1,38 +0,0 @@ @@ -1,38 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* 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
*
* http://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.
*/
package io.element.android.features.template
import androidx.compose.runtime.Composable
import io.element.android.libraries.architecture.Presenter
import javax.inject.Inject
class TemplatePresenter @Inject constructor() : Presenter<TemplateState> {
@Composable
override fun present(): TemplateState {
fun handleEvents(event: TemplateEvents) {
when (event) {
TemplateEvents.MyEvent -> Unit
}
}
return TemplateState(
eventSink = ::handleEvents
)
}
}

23
features/template/src/main/kotlin/io/element/android/features/template/TemplateState.kt

@ -1,23 +0,0 @@ @@ -1,23 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* 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
*
* http://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.
*/
package io.element.android.features.template
// TODO add your ui models. Remove the eventSink if you don't have events.
// Do not use default value, so no member get forgotten in the presenters.
data class TemplateState(
val eventSink: (TemplateEvents) -> Unit
)

31
features/template/src/main/kotlin/io/element/android/features/template/TemplateStateProvider.kt

@ -1,31 +0,0 @@ @@ -1,31 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* 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
*
* http://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.
*/
package io.element.android.features.template
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
open class TemplateStateProvider : PreviewParameterProvider<TemplateState> {
override val values: Sequence<TemplateState>
get() = sequenceOf(
aTemplateState(),
// Add other state here
)
}
fun aTemplateState() = TemplateState(
eventSink = {}
)

58
features/template/src/main/kotlin/io/element/android/features/template/TemplateView.kt

@ -1,58 +0,0 @@ @@ -1,58 +0,0 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* 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
*
* http://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.
*/
package io.element.android.features.template
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun TemplateView(
state: TemplateState,
modifier: Modifier = Modifier,
) {
Box(modifier, contentAlignment = Alignment.Center) {
Text(
"Template feature view",
color = MaterialTheme.colorScheme.primary,
)
}
}
@Preview
@Composable
fun TemplateViewLightPreview(@PreviewParameter(TemplateStateProvider::class) state: TemplateState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
fun TemplateViewDarkPreview(@PreviewParameter(TemplateStateProvider::class) state: TemplateState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: TemplateState) {
TemplateView(
state = state,
)
}

52
features/template/src/test/kotlin/io/element/android/features/template/TemplatePresenterTests.kt

@ -1,52 +0,0 @@ @@ -1,52 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* 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
*
* http://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.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.template
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
class TemplatePresenterTests {
@Test
fun `present - initial state`() = runTest {
val presenter = TemplatePresenter()
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState)
}
}
@Test
fun `present - send event`() = runTest {
val presenter = TemplatePresenter()
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink.invoke(TemplateEvents.MyEvent)
}
}
}

45
plugins/src/main/kotlin/extension/DependencyHandleScope.kt

@ -22,6 +22,7 @@ import gradle.kotlin.dsl.accessors._71f190358cebd46a469f2989484fd643.implementat @@ -22,6 +22,7 @@ import gradle.kotlin.dsl.accessors._71f190358cebd46a469f2989484fd643.implementat
import org.gradle.accessors.dm.LibrariesForLibs
import org.gradle.kotlin.dsl.DependencyHandlerScope
import org.gradle.kotlin.dsl.project
import java.io.File
/**
* Dependencies used by all the modules
@ -51,6 +52,21 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) { @@ -51,6 +52,21 @@ fun DependencyHandlerScope.composeDependencies(libs: LibrariesForLibs) {
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
}
private fun DependencyHandlerScope.addImplementationProjects(directory: File, path: String, nameFilter: String) {
directory.listFiles().orEmpty().forEach { file ->
if (file.isDirectory) {
val newPath = "$path:${file.name}"
val buildFile = File(file, "build.gradle.kts")
if (buildFile.exists() && file.name == nameFilter) {
implementation(project(newPath))
println("Added implementation(project($newPath))")
} else {
addImplementationProjects(file, newPath, nameFilter)
}
}
}
}
fun DependencyHandlerScope.allLibrariesImpl() {
implementation(project(":libraries:designsystem"))
implementation(project(":libraries:matrix:impl"))
@ -71,28 +87,11 @@ fun DependencyHandlerScope.allServicesImpl() { @@ -71,28 +87,11 @@ fun DependencyHandlerScope.allServicesImpl() {
implementation(project(":services:toolbox:impl"))
}
fun DependencyHandlerScope.allFeaturesApi() {
implementation(project(":features:onboarding:api"))
implementation(project(":features:login:api"))
implementation(project(":features:logout:api"))
implementation(project(":features:roomlist:api"))
implementation(project(":features:messages:api"))
implementation(project(":features:rageshake:api"))
implementation(project(":features:preferences:api"))
implementation(project(":features:createroom:api"))
implementation(project(":features:verifysession:api"))
implementation(project(":features:selectusers:api"))
fun DependencyHandlerScope.allFeaturesApi(rootDir: File) {
val featuresDir = File(rootDir, "features")
addImplementationProjects(featuresDir, ":features", "api")
}
fun DependencyHandlerScope.allFeaturesImpl() {
implementation(project(":features:onboarding:impl"))
implementation(project(":features:login:impl"))
implementation(project(":features:logout:impl"))
implementation(project(":features:roomlist:impl"))
implementation(project(":features:messages:impl"))
implementation(project(":features:rageshake:impl"))
implementation(project(":features:preferences:impl"))
implementation(project(":features:createroom:impl"))
implementation(project(":features:verifysession:impl"))
implementation(project(":features:selectusers:impl"))
fun DependencyHandlerScope.allFeaturesImpl(rootDir: File) {
val featuresDir = File(rootDir, "features")
addImplementationProjects(featuresDir, ":features", "impl")
}

39
settings.gradle.kts

@ -58,7 +58,6 @@ include(":tests:uitests") @@ -58,7 +58,6 @@ include(":tests:uitests")
include(":anvilannotations")
include(":anvilcodegen")
include(":libraries:architecture")
include(":features:template")
include(":libraries:androidutils")
include(":samples:minimal")
include(":libraries:encrypted-db")
@ -74,24 +73,20 @@ include(":services:appnavstate:impl") @@ -74,24 +73,20 @@ include(":services:appnavstate:impl")
include(":services:toolbox:api")
include(":services:toolbox:impl")
include(":features:onboarding:api")
include(":features:onboarding:impl")
include(":features:logout:api")
include(":features:logout:impl")
include(":features:roomlist:api")
include(":features:roomlist:impl")
include(":features:rageshake:api")
include(":features:rageshake:impl")
include(":features:rageshake:test")
include(":features:preferences:api")
include(":features:preferences:impl")
include(":features:messages:api")
include(":features:messages:impl")
include(":features:login:api")
include(":features:login:impl")
include(":features:createroom:api")
include(":features:createroom:impl")
include(":features:verifysession:api")
include(":features:verifysession:impl")
include(":features:selectusers:api")
include(":features:selectusers:impl")
fun includeProjects(directory: File, path: String) {
directory.listFiles().orEmpty().forEach { file ->
if (file.isDirectory) {
val newPath = "$path:${file.name}"
val buildFile = File(file, "build.gradle.kts")
if (buildFile.exists()) {
include(newPath)
println("Included project: $newPath")
} else {
includeProjects(file, newPath)
}
}
}
}
val featuresDir = File(rootDir, "features")
includeProjects(featuresDir, ":features")

2
tests/uitests/build.gradle.kts

@ -40,5 +40,5 @@ dependencies { @@ -40,5 +40,5 @@ dependencies {
ksp(libs.showkase.processor)
allLibrariesImpl()
allFeaturesImpl()
allFeaturesImpl(rootDir)
}

1
tools/templates/FeatureModule.json

@ -0,0 +1 @@ @@ -0,0 +1 @@
{"template":{"name":"","isDir":true,"placeholders":{"MODULE_NAME":"","FEATURE_NAME":"","BUILD_GRADLE_API":"build.gradle.kts","BUILD_GRADLE_IMPL":"build.gradle.kts"},"fileTemplates":{"${FEATURE_NAME}EntryPoint":"Template Module Feature Entry Point API","Default${FEATURE_NAME}EntryPoint":"Template Module Feature Entry Point Flow Impl","${BUILD_GRADLE_API}":"Template Module Feature Build Gradle API","${BUILD_GRADLE_IMPL}":"Template Module Feature Build Gradle Impl","${FEATURE_NAME}FlowNode":"Template Module Feature Node Flow Impl"},"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"api","isDir":true,"realChildren":[{"name":"src","isDir":true,"realChildren":[{"name":"main","isDir":true,"realChildren":[{"name":"kotlin","isDir":true,"realChildren":[{"name":"io","isDir":true,"realChildren":[{"name":"element","isDir":true,"realChildren":[{"name":"android","isDir":true,"realChildren":[{"name":"features","isDir":true,"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"api","isDir":true,"realChildren":[{"name":"${FEATURE_NAME}EntryPoint","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]}]}]}]}]}]}]}]}]},{"name":"${BUILD_GRADLE_API}","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]},{"name":"impl","isDir":true,"realChildren":[{"name":"src","isDir":true,"realChildren":[{"name":"main","isDir":true,"realChildren":[{"name":"kotlin","isDir":true,"realChildren":[{"name":"io","isDir":true,"realChildren":[{"name":"element","isDir":true,"realChildren":[{"name":"android","isDir":true,"realChildren":[{"name":"features","isDir":true,"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"impl","isDir":true,"realChildren":[{"name":"Default${FEATURE_NAME}EntryPoint","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]},{"name":"${FEATURE_NAME}FlowNode","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]}]}]}]}]}]}]}]},{"name":"test","isDir":true,"realChildren":[{"name":"kotlin","isDir":true,"realChildren":[{"name":"io","isDir":true,"realChildren":[{"name":"element","isDir":true,"realChildren":[{"name":"android","isDir":true,"realChildren":[{"name":"features","isDir":true,"realChildren":[{"name":"${MODULE_NAME}","isDir":true,"realChildren":[{"name":"impl","isDir":true,"realChildren":[]}]}]}]}]}]}]}]}]},{"name":"${BUILD_GRADLE_IMPL}","isDir":false,"placeholders":{},"fileTemplates":{},"realChildren":[]}]}]}]},"language":"java","templateName":"FeatureModule","lowercaseDir":true,"capitalizeFile":false,"packageNameToDir":false}

BIN
tools/templates/file_templates.zip

Binary file not shown.
Loading…
Cancel
Save