create-droid 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/bin/cli.js +7 -0
- package/dist/env/checkEnv.js +42 -0
- package/dist/gradle/setupGradle.js +232 -0
- package/dist/index.js +60 -0
- package/dist/sdk/installSdk.js +112 -0
- package/dist/template/generateProject.js +109 -0
- package/dist/utils/constants.js +24 -0
- package/dist/utils/logger.js +14 -0
- package/package.json +50 -0
- package/templates/base/app/proguard-rules.pro +3 -0
- package/templates/base/build.gradle.kts +5 -0
- package/templates/base/gradle/libs.versions.toml +34 -0
- package/templates/base/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/templates/base/gradle.properties +4 -0
- package/templates/base/settings.gradle.kts +23 -0
- package/templates/ui-compose/app/build.gradle.kts +65 -0
- package/templates/ui-compose/app/src/main/AndroidManifest.xml +28 -0
- package/templates/ui-compose/app/src/main/java/com/example/template/MainActivity.kt +46 -0
- package/templates/ui-views/app/build.gradle.kts +44 -0
- package/templates/ui-views/app/src/main/AndroidManifest.xml +28 -0
- package/templates/ui-views/app/src/main/java/com/example/template/MainActivity.kt +19 -0
- package/templates/ui-views/app/src/main/res/layout/activity_main.xml +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# create-droid 🤖
|
|
2
|
+
|
|
3
|
+
The fastest way to start an Android project. No Studio required.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/create-droid)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Why?
|
|
9
|
+
|
|
10
|
+
Most Android tutorials assume you want to download a 2GB IDE just to write "Hello World".
|
|
11
|
+
Modern web developers are used to tools like `create-react-app` or `vite` – simple, fast, and CLI-first.
|
|
12
|
+
|
|
13
|
+
**`create-droid` brings that DX to Android.**
|
|
14
|
+
|
|
15
|
+
* **⚡️ Fast:** Scaffolds a project in seconds.
|
|
16
|
+
* **🚫 No Studio Required:** Fully functional Gradle builds out of the box.
|
|
17
|
+
* **🛠 Local SDK Management:** Auto-downloads and configures the Android SDK locally (no global pollution).
|
|
18
|
+
* **💎 Modern Stack:** Kotlin DSL, Version Catalogs (`libs.versions.toml`), Jetpack Compose, and Material 3 by default.
|
|
19
|
+
* **🐧 Linux & Mac First:** Designed for terminal-centric workflows.
|
|
20
|
+
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
* **Node.js**: >= 18.0.0
|
|
24
|
+
* **Java (JDK)**: >= 17 (Run `java -version` to check)
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Simply run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm create droid my-app
|
|
32
|
+
# or
|
|
33
|
+
npx create-droid my-app
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Follow the interactive prompts:
|
|
37
|
+
1. **Project Name**: Defaults to directory name.
|
|
38
|
+
2. **UI Framework**: Choose **Jetpack Compose** (Modern declarative UI) or **XML Views** (Classic).
|
|
39
|
+
|
|
40
|
+
### After Scaffolding
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd my-app
|
|
44
|
+
|
|
45
|
+
# Build and verify the project
|
|
46
|
+
./gradlew build
|
|
47
|
+
|
|
48
|
+
# Install on a connected device/emulator
|
|
49
|
+
./gradlew installDebug
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## What's Inside?
|
|
53
|
+
|
|
54
|
+
The generated project is **clean** and follows modern best practices:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
my-app/
|
|
58
|
+
├── app/
|
|
59
|
+
│ ├── src/main/java/com/example/ # Your Kotlin source code
|
|
60
|
+
│ └── build.gradle.kts # App module configuration
|
|
61
|
+
├── gradle/
|
|
62
|
+
│ └── libs.versions.toml # Central dependency management
|
|
63
|
+
├── build.gradle.kts # Root project configuration
|
|
64
|
+
├── settings.gradle.kts # Module inclusion
|
|
65
|
+
├── gradlew # The Gradle wrapper (runs builds)
|
|
66
|
+
└── local.properties # SDK location (auto-generated)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Advanced
|
|
70
|
+
|
|
71
|
+
### Customizing the SDK Location
|
|
72
|
+
|
|
73
|
+
By default, the SDK is installed to `~/.local/share/create-android-app/sdk`.
|
|
74
|
+
If you already have an SDK installed, simply set the environment variable:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
export ANDROID_HOME=/path/to/existing/sdk
|
|
78
|
+
npm create droid my-app
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT © YELrhilassi
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { CONSTANTS } from '../utils/constants.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
export async function checkEnv() {
|
|
5
|
+
// 1. Check Node
|
|
6
|
+
const nodeVersion = process.version;
|
|
7
|
+
if (!nodeVersion.startsWith('v' + CONSTANTS.NODE_VERSION_REQ) && parseInt(nodeVersion.substring(1)) < CONSTANTS.NODE_VERSION_REQ) {
|
|
8
|
+
logger.error(`Node.js ${CONSTANTS.NODE_VERSION_REQ}+ required. Found ${nodeVersion}`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
logger.success(`Node.js ${nodeVersion}`);
|
|
12
|
+
// 2. Check Java (Robust Regex)
|
|
13
|
+
try {
|
|
14
|
+
const { stdout, stderr } = await execa('java', ['-version']);
|
|
15
|
+
const output = stdout || stderr; // Java version often prints to stderr
|
|
16
|
+
// Regex to capture major version: "version \"17.0.1\"" or "17.0.1"
|
|
17
|
+
const versionMatch = output.match(/version\s+"?(\d+)/i);
|
|
18
|
+
if (versionMatch && versionMatch[1]) {
|
|
19
|
+
const majorVersion = parseInt(versionMatch[1], 10);
|
|
20
|
+
if (majorVersion >= CONSTANTS.JAVA_VERSION_REQ) {
|
|
21
|
+
logger.success(`Java version ${majorVersion} detected.`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
logger.error(`Java ${CONSTANTS.JAVA_VERSION_REQ}+ required. Found version ${majorVersion}.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
logger.warn(`Could not parse Java version from output: ${output}`);
|
|
30
|
+
logger.warn(`Assuming compatible if "17" or "21" is present...`);
|
|
31
|
+
if (!output.includes('17') && !output.includes('21')) {
|
|
32
|
+
logger.error(`Java 17+ required. Please verify your installation.`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
logger.error(`Java runtime not found. Please install JDK 17+.`);
|
|
39
|
+
logger.info(`Download: https://adoptium.net/temurin/releases/`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { pipeline } from 'stream/promises';
|
|
6
|
+
import { createWriteStream } from 'fs';
|
|
7
|
+
export async function setupGradle(projectPath) {
|
|
8
|
+
const wrapperDir = path.join(projectPath, 'gradle', 'wrapper');
|
|
9
|
+
await fs.ensureDir(wrapperDir);
|
|
10
|
+
const wrapperJarPath = path.join(wrapperDir, 'gradle-wrapper.jar');
|
|
11
|
+
const gradlewPath = path.join(projectPath, 'gradlew');
|
|
12
|
+
// 1. Ensure gradle-wrapper.jar exists
|
|
13
|
+
if (!fs.existsSync(wrapperJarPath)) {
|
|
14
|
+
await ensureWrapperJar(wrapperJarPath);
|
|
15
|
+
}
|
|
16
|
+
// 2. Write gradlew script (Shell) - Full standard script
|
|
17
|
+
if (!fs.existsSync(gradlewPath)) {
|
|
18
|
+
await fs.writeFile(gradlewPath, GRADLEW_SCRIPT.trim(), { mode: 0o755 });
|
|
19
|
+
}
|
|
20
|
+
// 3. Make executable
|
|
21
|
+
await fs.chmod(gradlewPath, 0o755);
|
|
22
|
+
// 4. Verify Wrapper (Dry Run)
|
|
23
|
+
logger.info(`Verifying Gradle setup...`);
|
|
24
|
+
try {
|
|
25
|
+
await execa(gradlewPath, ['--version'], { cwd: projectPath, stdio: 'inherit' });
|
|
26
|
+
logger.success('Gradle wrapper verified.');
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
logger.warn('Gradle wrapper verification failed. You may need to run it manually.');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function ensureWrapperJar(destPath) {
|
|
33
|
+
logger.step('Downloading gradle-wrapper.jar...');
|
|
34
|
+
// Reliable source from Google's sample
|
|
35
|
+
const WRAPPER_URL = 'https://raw.githubusercontent.com/android/nowinandroid/main/gradle/wrapper/gradle-wrapper.jar';
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(WRAPPER_URL);
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
throw new Error(`Failed to fetch wrapper jar: ${response.statusText}`);
|
|
40
|
+
if (!response.body)
|
|
41
|
+
throw new Error('No body in response');
|
|
42
|
+
await pipeline(response.body, createWriteStream(destPath));
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
throw new Error(`Failed to download gradle-wrapper.jar. Error: ${e.message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Standard POSIX gradlew script (simplified but fully functional variant)
|
|
49
|
+
const GRADLEW_SCRIPT = `#!/bin/sh
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
# Copyright 2015 the original author or authors.
|
|
53
|
+
#
|
|
54
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
55
|
+
# you may not use this file except in compliance with the License.
|
|
56
|
+
# You may obtain a copy of the License at
|
|
57
|
+
#
|
|
58
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
59
|
+
#
|
|
60
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
61
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
62
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
63
|
+
# See the License for the specific language governing permissions and
|
|
64
|
+
# limitations under the License.
|
|
65
|
+
#
|
|
66
|
+
|
|
67
|
+
##############################################################################
|
|
68
|
+
#
|
|
69
|
+
# Gradle start up script for POSIX generated by Gradle.
|
|
70
|
+
#
|
|
71
|
+
##############################################################################
|
|
72
|
+
|
|
73
|
+
# Attempt to set APP_HOME
|
|
74
|
+
# Resolve links: $0 may be a link
|
|
75
|
+
PRG="$0"
|
|
76
|
+
# Need this for relative symlinks.
|
|
77
|
+
while [ -h "$PRG" ] ; do
|
|
78
|
+
ls=\`ls -ld "$PRG"\`
|
|
79
|
+
link=\`expr "$ls" : '.*-> \\(.*\\)$'\`
|
|
80
|
+
if expr "$link" : '/.*' > /dev/null; then
|
|
81
|
+
PRG="$link"
|
|
82
|
+
else
|
|
83
|
+
PRG=\`dirname "$PRG"\`"/$link"
|
|
84
|
+
fi
|
|
85
|
+
done
|
|
86
|
+
SAVED="$\`pwd\`"
|
|
87
|
+
cd "\`dirname \\"$PRG\\"\`/" >/dev/null
|
|
88
|
+
APP_HOME="\`pwd -P\`"
|
|
89
|
+
cd "$SAVED" >/dev/null
|
|
90
|
+
|
|
91
|
+
APP_NAME="Gradle"
|
|
92
|
+
APP_BASE_NAME=\`basename "$0"\`
|
|
93
|
+
|
|
94
|
+
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
95
|
+
DEFAULT_JVM_OPTS=""
|
|
96
|
+
|
|
97
|
+
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
98
|
+
MAX_FD="maximum"
|
|
99
|
+
|
|
100
|
+
warn () {
|
|
101
|
+
echo "$*"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
die () {
|
|
105
|
+
echo
|
|
106
|
+
echo "$*"
|
|
107
|
+
echo
|
|
108
|
+
exit 1
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# OS specific support (must be 'true' or 'false').
|
|
112
|
+
cygwin=false
|
|
113
|
+
msys=false
|
|
114
|
+
darwin=false
|
|
115
|
+
nonstop=false
|
|
116
|
+
case "\`uname\`" in
|
|
117
|
+
CYGWIN* )
|
|
118
|
+
cygwin=true
|
|
119
|
+
;;
|
|
120
|
+
Darwin* )
|
|
121
|
+
darwin=true
|
|
122
|
+
;;
|
|
123
|
+
MINGW* )
|
|
124
|
+
msys=true
|
|
125
|
+
;;
|
|
126
|
+
NONSTOP* )
|
|
127
|
+
nonstop=true
|
|
128
|
+
;;
|
|
129
|
+
esac
|
|
130
|
+
|
|
131
|
+
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
132
|
+
|
|
133
|
+
# Determine the Java command to use to start the JVM.
|
|
134
|
+
if [ -n "$JAVA_HOME" ] ; then
|
|
135
|
+
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
136
|
+
# IBM's JDK on AIX uses strange locations for the executables
|
|
137
|
+
JAVACMD="$JAVA_HOME/jre/sh/java"
|
|
138
|
+
else
|
|
139
|
+
JAVACMD="$JAVA_HOME/bin/java"
|
|
140
|
+
fi
|
|
141
|
+
if [ ! -x "$JAVACMD" ] ; then
|
|
142
|
+
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
143
|
+
|
|
144
|
+
Please set the JAVA_HOME variable in your environment to match the
|
|
145
|
+
location of your Java installation."
|
|
146
|
+
fi
|
|
147
|
+
else
|
|
148
|
+
JAVACMD="java"
|
|
149
|
+
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
150
|
+
|
|
151
|
+
Please set the JAVA_HOME variable in your environment to match the
|
|
152
|
+
location of your Java installation."
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
# Increase the maximum file descriptors if we can.
|
|
156
|
+
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
|
157
|
+
MAX_FD_LIMIT=\`ulimit -H -n\`
|
|
158
|
+
if [ $? -eq 0 ] ; then
|
|
159
|
+
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
|
160
|
+
MAX_FD="$MAX_FD_LIMIT"
|
|
161
|
+
fi
|
|
162
|
+
ulimit -n $MAX_FD
|
|
163
|
+
if [ $? -ne 0 ] ; then
|
|
164
|
+
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
|
165
|
+
fi
|
|
166
|
+
else
|
|
167
|
+
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
|
168
|
+
fi
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# For Darwin, add options to specify how the application appears in the dock
|
|
172
|
+
if $darwin; then
|
|
173
|
+
GRADLE_OPTS="$GRADLE_OPTS \\"-Xdock:name=$APP_NAME\\" \\"-Xdock:icon=$APP_HOME/media/gradle.icns\\""
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# For Cygwin, switch paths to Windows format before running java
|
|
177
|
+
if $cygwin ; then
|
|
178
|
+
APP_HOME=\`cygpath --path --mixed "$APP_HOME"\`
|
|
179
|
+
CLASSPATH=\`cygpath --path --mixed "$CLASSPATH"\`
|
|
180
|
+
JAVACMD=\`cygpath --unix "$JAVACMD"\`
|
|
181
|
+
|
|
182
|
+
# We build the pattern for arguments to be converted via cygpath
|
|
183
|
+
ROOTDIRSRAW=\`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null\`
|
|
184
|
+
SEP=""
|
|
185
|
+
for dir in $ROOTDIRSRAW ; do
|
|
186
|
+
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
|
187
|
+
SEP="|"
|
|
188
|
+
done
|
|
189
|
+
OURCYGPATTERN="(^($ROOTDIRS))"
|
|
190
|
+
# Add a user-defined pattern to the cygpath arguments
|
|
191
|
+
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
|
192
|
+
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
|
193
|
+
fi
|
|
194
|
+
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
195
|
+
i=0
|
|
196
|
+
for arg in "$@" ; do
|
|
197
|
+
CHECK=\`echo "$arg"|egrep -c "$OURCYGPATTERN" - \`
|
|
198
|
+
CHECK2=\`echo "$arg"|egrep -c "^-"\` ### Determine if an option
|
|
199
|
+
|
|
200
|
+
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
|
201
|
+
eval \`echo args$i\`=\`cygpath --path --ignore --mixed "$arg"\`
|
|
202
|
+
else
|
|
203
|
+
eval \`echo args$i\`="\\"$arg\\""
|
|
204
|
+
fi
|
|
205
|
+
i=$((i+1))
|
|
206
|
+
done
|
|
207
|
+
case $i in
|
|
208
|
+
(0) set -- ;;
|
|
209
|
+
(1) set -- "$args0" ;;
|
|
210
|
+
(2) set -- "$args0" "$args1" ;;
|
|
211
|
+
(3) set -- "$args0" "$args1" "$args2" ;;
|
|
212
|
+
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
|
213
|
+
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
|
214
|
+
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
|
215
|
+
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
|
216
|
+
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
|
217
|
+
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
|
218
|
+
esac
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
# Escape application args
|
|
222
|
+
save () {
|
|
223
|
+
for i do printf %s\\\\n "$i" | sed "s/'/'\\\\\\\\''/g;1s/^/'/;\$s/\$/' \\\\\\\\/" ; done
|
|
224
|
+
echo " "
|
|
225
|
+
}
|
|
226
|
+
APP_ARGS=$(save "$@")
|
|
227
|
+
|
|
228
|
+
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
|
229
|
+
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\\"-Dorg.gradle.appname=$APP_BASE_NAME\\"" -classpath "\\"$CLASSPATH\\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
|
230
|
+
|
|
231
|
+
exec "$JAVACMD" "$@"
|
|
232
|
+
`;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import { logger } from './utils/logger.js';
|
|
3
|
+
import { checkEnv } from './env/checkEnv.js';
|
|
4
|
+
import { installSdk } from './sdk/installSdk.js';
|
|
5
|
+
import { generateProject } from './template/generateProject.js';
|
|
6
|
+
import { setupGradle } from './gradle/setupGradle.js';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
export async function run(args) {
|
|
9
|
+
logger.banner();
|
|
10
|
+
// 1. Collect Input
|
|
11
|
+
let targetDir = args[0];
|
|
12
|
+
const defaultProjectName = targetDir || 'android-app';
|
|
13
|
+
const response = await prompts([
|
|
14
|
+
{
|
|
15
|
+
type: targetDir ? null : 'text',
|
|
16
|
+
name: 'projectName',
|
|
17
|
+
message: 'Project name:',
|
|
18
|
+
initial: defaultProjectName
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
type: 'select',
|
|
22
|
+
name: 'uiType',
|
|
23
|
+
message: 'Select UI Framework:',
|
|
24
|
+
choices: [
|
|
25
|
+
{ title: 'Jetpack Compose (Recommended)', value: 'compose' },
|
|
26
|
+
{ title: 'XML Views (Legacy)', value: 'views' }
|
|
27
|
+
],
|
|
28
|
+
initial: 0
|
|
29
|
+
}
|
|
30
|
+
], {
|
|
31
|
+
onCancel: () => {
|
|
32
|
+
logger.error('Operation cancelled');
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const projectName = response.projectName || targetDir;
|
|
37
|
+
const projectPath = path.resolve(process.cwd(), projectName);
|
|
38
|
+
const uiType = response.uiType;
|
|
39
|
+
// 2. Validate Environment
|
|
40
|
+
logger.step('Checking Environment...');
|
|
41
|
+
await checkEnv();
|
|
42
|
+
// 3. Setup Android SDK
|
|
43
|
+
logger.step('Setting up Android SDK...');
|
|
44
|
+
const sdkPath = await installSdk();
|
|
45
|
+
// 4. Generate Project Files
|
|
46
|
+
logger.step(`Scaffolding project in ${projectName}...`);
|
|
47
|
+
await generateProject({
|
|
48
|
+
projectPath,
|
|
49
|
+
projectName,
|
|
50
|
+
uiType,
|
|
51
|
+
sdkPath
|
|
52
|
+
});
|
|
53
|
+
// 5. Setup Gradle Wrapper
|
|
54
|
+
logger.step('Configuring Gradle...');
|
|
55
|
+
await setupGradle(projectPath);
|
|
56
|
+
logger.success(`Project created at ${projectPath}`);
|
|
57
|
+
logger.info('To get started:');
|
|
58
|
+
console.log(` cd ${projectName}`);
|
|
59
|
+
console.log(` ./gradlew installDebug`);
|
|
60
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { CONSTANTS } from '../utils/constants.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { execa } from 'execa';
|
|
7
|
+
import { pipeline } from 'stream/promises';
|
|
8
|
+
import { createWriteStream } from 'fs';
|
|
9
|
+
import AdmZip from 'adm-zip';
|
|
10
|
+
export async function installSdk() {
|
|
11
|
+
const isMac = process.platform === 'darwin';
|
|
12
|
+
// 1. Determine SDK Path
|
|
13
|
+
const sdkPath = process.env.ANDROID_HOME || path.join(os.homedir(), '.local', 'share', 'create-android-app', 'sdk');
|
|
14
|
+
if (process.env.ANDROID_HOME) {
|
|
15
|
+
logger.info(`Using ANDROID_HOME: ${sdkPath}`);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
logger.info(`Using local SDK path: ${sdkPath}`);
|
|
19
|
+
}
|
|
20
|
+
await fs.ensureDir(sdkPath);
|
|
21
|
+
// 2. Check for cmdline-tools
|
|
22
|
+
const cmdlineToolsRoot = path.join(sdkPath, 'cmdline-tools');
|
|
23
|
+
const cmdlineToolsLatest = path.join(cmdlineToolsRoot, 'latest');
|
|
24
|
+
const sdkManagerPath = path.join(cmdlineToolsLatest, 'bin', 'sdkmanager');
|
|
25
|
+
if (!fs.existsSync(sdkManagerPath)) {
|
|
26
|
+
logger.step('Android Command Line Tools not found. Downloading...');
|
|
27
|
+
await downloadCmdlineTools(sdkPath, cmdlineToolsLatest, isMac);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
logger.success('Command Line Tools found.');
|
|
31
|
+
}
|
|
32
|
+
// 3. Accept Licenses (Robust Approach: File Injection)
|
|
33
|
+
// Instead of piping 'yes', we write known hashes.
|
|
34
|
+
// These hashes correspond to standard Android SDK licenses.
|
|
35
|
+
// Source: common knowledge in Android community + verification.
|
|
36
|
+
const licensesDir = path.join(sdkPath, 'licenses');
|
|
37
|
+
await fs.ensureDir(licensesDir);
|
|
38
|
+
const androidSdkLicense = [
|
|
39
|
+
'8933bad161af4178b1185d1a37fbf41ea5269c55',
|
|
40
|
+
'd56f5187479451eabf01fb78af6dfcb131a6481e',
|
|
41
|
+
'24333f8a63b6825ea9c5514f83c2829b004d1fee',
|
|
42
|
+
].join('\n');
|
|
43
|
+
const androidSdkPreviewLicense = '84831b9409646a918e30573bab4c9c91346d8abd';
|
|
44
|
+
logger.step('Writing license files...');
|
|
45
|
+
await fs.writeFile(path.join(licensesDir, 'android-sdk-license'), androidSdkLicense);
|
|
46
|
+
await fs.writeFile(path.join(licensesDir, 'android-sdk-preview-license'), androidSdkPreviewLicense);
|
|
47
|
+
// 4. Install Packages
|
|
48
|
+
logger.step('Installing SDK packages (this may take a while)...');
|
|
49
|
+
const packages = CONSTANTS.SDK_PACKAGES;
|
|
50
|
+
try {
|
|
51
|
+
// With licenses pre-accepted, we just run sdkmanager directly.
|
|
52
|
+
// Use --verbose to verify output if needed, but keep it clean for user unless debug.
|
|
53
|
+
// Check if packages are already installed to skip slow process?
|
|
54
|
+
// sdkmanager is slow. If platforms/android-34 exists, maybe skip?
|
|
55
|
+
const platformCheck = path.join(sdkPath, 'platforms', 'android-34');
|
|
56
|
+
const buildToolsCheck = path.join(sdkPath, 'build-tools', '34.0.0');
|
|
57
|
+
if (fs.existsSync(platformCheck) && fs.existsSync(buildToolsCheck)) {
|
|
58
|
+
logger.success('SDK packages appear to be installed. Skipping redundant install.');
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
await execa(sdkManagerPath, [`--sdk_root=${sdkPath}`, ...packages], {
|
|
62
|
+
stdio: 'inherit', // Let user see progress bars from sdkmanager
|
|
63
|
+
env: {
|
|
64
|
+
ANDROID_HOME: sdkPath,
|
|
65
|
+
// Ensure JAVA_HOME is picked up if set, usually execa inherits env
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
logger.success('SDK packages installed.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
logger.error('Failed to install SDK packages.');
|
|
73
|
+
logger.error(e.message);
|
|
74
|
+
throw e; // Fail hard if SDK setup fails
|
|
75
|
+
}
|
|
76
|
+
return sdkPath;
|
|
77
|
+
}
|
|
78
|
+
async function downloadCmdlineTools(sdkPath, targetDir, isMac) {
|
|
79
|
+
const url = isMac ? CONSTANTS.CMDLINE_TOOLS_URL_MAC
|
|
80
|
+
: CONSTANTS.CMDLINE_TOOLS_URL_LINUX;
|
|
81
|
+
const zipPath = path.join(sdkPath, 'cmdline-tools.zip');
|
|
82
|
+
logger.info(`Downloading ${url}...`);
|
|
83
|
+
const response = await fetch(url);
|
|
84
|
+
if (!response.ok)
|
|
85
|
+
throw new Error(`Failed to download SDK: ${response.statusText}`);
|
|
86
|
+
if (!response.body)
|
|
87
|
+
throw new Error('No body in response');
|
|
88
|
+
await pipeline(response.body, createWriteStream(zipPath));
|
|
89
|
+
logger.info('Extracting (using adm-zip)...');
|
|
90
|
+
// Extract to temp folder
|
|
91
|
+
const tempDir = path.join(sdkPath, 'temp_extract');
|
|
92
|
+
await fs.ensureDir(tempDir);
|
|
93
|
+
const zip = new AdmZip(zipPath);
|
|
94
|
+
zip.extractAllTo(tempDir, true); // overwrite: true
|
|
95
|
+
// Structure: tempDir/cmdline-tools/...
|
|
96
|
+
// We need to find the root folder inside tempDir
|
|
97
|
+
const extractedContents = await fs.readdir(tempDir);
|
|
98
|
+
const rootFolder = extractedContents.find(f => fs.statSync(path.join(tempDir, f)).isDirectory());
|
|
99
|
+
if (!rootFolder)
|
|
100
|
+
throw new Error('Unknown zip structure');
|
|
101
|
+
const source = path.join(tempDir, rootFolder); // usually 'cmdline-tools'
|
|
102
|
+
// Move to targetDir (which is sdk/cmdline-tools/latest)
|
|
103
|
+
// Ensure parent exists
|
|
104
|
+
await fs.ensureDir(path.dirname(targetDir));
|
|
105
|
+
if (fs.existsSync(targetDir)) {
|
|
106
|
+
await fs.remove(targetDir);
|
|
107
|
+
}
|
|
108
|
+
await fs.move(source, targetDir);
|
|
109
|
+
// Cleanup
|
|
110
|
+
await fs.remove(tempDir);
|
|
111
|
+
await fs.remove(zipPath);
|
|
112
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { CONSTANTS } from '../utils/constants.js';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
export async function generateProject(options) {
|
|
9
|
+
const { projectPath, projectName, uiType, sdkPath } = options;
|
|
10
|
+
// 1. Ensure target directory
|
|
11
|
+
await fs.ensureDir(projectPath);
|
|
12
|
+
// 2. Resolve template paths
|
|
13
|
+
const templateRoot = path.resolve(__dirname, '../../templates');
|
|
14
|
+
const baseTemplate = path.join(templateRoot, 'base');
|
|
15
|
+
const uiTemplateName = uiType === 'compose' ? 'ui-compose' : 'ui-views';
|
|
16
|
+
const uiTemplate = path.join(templateRoot, uiTemplateName);
|
|
17
|
+
if (!fs.existsSync(baseTemplate)) {
|
|
18
|
+
throw new Error(`Template not found at ${baseTemplate}`);
|
|
19
|
+
}
|
|
20
|
+
// 3. Copy Base
|
|
21
|
+
logger.info(`Copying base template from ${baseTemplate}...`);
|
|
22
|
+
await fs.copy(baseTemplate, projectPath);
|
|
23
|
+
// 4. Copy UI specific files
|
|
24
|
+
if (fs.existsSync(uiTemplate)) {
|
|
25
|
+
logger.info(`Applying ${uiType} template...`);
|
|
26
|
+
await fs.copy(uiTemplate, projectPath, { overwrite: true });
|
|
27
|
+
}
|
|
28
|
+
// 5. Patch Configuration
|
|
29
|
+
logger.info(`Patching configuration...`);
|
|
30
|
+
const packageName = `com.example.${projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`;
|
|
31
|
+
// Replace in settings.gradle.kts
|
|
32
|
+
await patchFile(path.join(projectPath, 'settings.gradle.kts'), {
|
|
33
|
+
'{{PROJECT_NAME}}': projectName,
|
|
34
|
+
});
|
|
35
|
+
// Replace in app/build.gradle.kts
|
|
36
|
+
await patchFile(path.join(projectPath, 'app/build.gradle.kts'), {
|
|
37
|
+
'{{APPLICATION_ID}}': packageName,
|
|
38
|
+
'{{COMPILE_SDK}}': CONSTANTS.COMPILE_SDK.toString(),
|
|
39
|
+
'{{MIN_SDK}}': CONSTANTS.MIN_SDK.toString(),
|
|
40
|
+
'{{TARGET_SDK}}': CONSTANTS.TARGET_SDK.toString(),
|
|
41
|
+
});
|
|
42
|
+
// Replace in app/src/main/res/values/strings.xml
|
|
43
|
+
await patchFile(path.join(projectPath, 'app/src/main/res/values/strings.xml'), {
|
|
44
|
+
'{{PROJECT_NAME}}': projectName,
|
|
45
|
+
});
|
|
46
|
+
// Handle Source Code Relocation
|
|
47
|
+
const srcBase = path.join(projectPath, 'app/src/main/java');
|
|
48
|
+
const oldPackagePath = path.join(srcBase, 'com/example/template');
|
|
49
|
+
const newPackagePath = path.join(srcBase, ...packageName.split('.'));
|
|
50
|
+
if (fs.existsSync(oldPackagePath)) {
|
|
51
|
+
await fs.move(oldPackagePath, newPackagePath, { overwrite: true });
|
|
52
|
+
// Recursive file patching for package statement
|
|
53
|
+
await patchSourceFiles(newPackagePath, packageName);
|
|
54
|
+
// Clean up empty dirs (com/example/template -> com/example -> com)
|
|
55
|
+
// Only if empty.
|
|
56
|
+
await cleanEmptyDirs(srcBase);
|
|
57
|
+
}
|
|
58
|
+
// Create local.properties with SDK location
|
|
59
|
+
const localProperties = `sdk.dir=${sdkPath}`;
|
|
60
|
+
await fs.writeFile(path.join(projectPath, 'local.properties'), localProperties);
|
|
61
|
+
}
|
|
62
|
+
async function patchFile(filePath, replacements) {
|
|
63
|
+
if (!fs.existsSync(filePath))
|
|
64
|
+
return;
|
|
65
|
+
let content = await fs.readFile(filePath, 'utf-8');
|
|
66
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
67
|
+
content = content.replaceAll(key, value);
|
|
68
|
+
}
|
|
69
|
+
await fs.writeFile(filePath, content);
|
|
70
|
+
}
|
|
71
|
+
async function patchSourceFiles(dir, packageName) {
|
|
72
|
+
const files = await fs.readdir(dir);
|
|
73
|
+
for (const file of files) {
|
|
74
|
+
const fullPath = path.join(dir, file);
|
|
75
|
+
const stat = await fs.stat(fullPath);
|
|
76
|
+
if (stat.isDirectory()) {
|
|
77
|
+
await patchSourceFiles(fullPath, packageName);
|
|
78
|
+
}
|
|
79
|
+
else if (file.endsWith('.kt') || file.endsWith('.java')) {
|
|
80
|
+
await patchFile(fullPath, {
|
|
81
|
+
'{{PACKAGE_NAME}}': packageName
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function cleanEmptyDirs(dir) {
|
|
87
|
+
// Basic cleanup: remove com/example/template if empty
|
|
88
|
+
// We moved content from com/example/template to new path.
|
|
89
|
+
// So we check if com/example/template is empty, then com/example, then com.
|
|
90
|
+
// This is tricky because we don't know the exact depth of the old structure easily without hardcoding.
|
|
91
|
+
// Hardcoded for "com/example/template":
|
|
92
|
+
const oldPath = path.join(dir, 'com/example/template');
|
|
93
|
+
try {
|
|
94
|
+
if (fs.existsSync(oldPath) && (await fs.readdir(oldPath)).length === 0) {
|
|
95
|
+
await fs.rmdir(oldPath);
|
|
96
|
+
const parent = path.dirname(oldPath);
|
|
97
|
+
if ((await fs.readdir(parent)).length === 0) {
|
|
98
|
+
await fs.rmdir(parent);
|
|
99
|
+
const grandParent = path.dirname(parent);
|
|
100
|
+
if ((await fs.readdir(grandParent)).length === 0) {
|
|
101
|
+
await fs.rmdir(grandParent);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
// Ignore errors during cleanup
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// The "Truth" - Pinned versions for stability
|
|
2
|
+
export const CONSTANTS = {
|
|
3
|
+
// Tooling
|
|
4
|
+
JAVA_VERSION_REQ: 17,
|
|
5
|
+
NODE_VERSION_REQ: 18,
|
|
6
|
+
// Android SDK
|
|
7
|
+
CMDLINE_TOOLS_URL_LINUX: "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip",
|
|
8
|
+
CMDLINE_TOOLS_URL_MAC: "https://dl.google.com/android/repository/commandlinetools-mac-11076708_latest.zip",
|
|
9
|
+
SDK_PACKAGES: [
|
|
10
|
+
"platform-tools",
|
|
11
|
+
"platforms;android-34",
|
|
12
|
+
"build-tools;34.0.0",
|
|
13
|
+
"cmdline-tools;latest"
|
|
14
|
+
],
|
|
15
|
+
// Template Defaults
|
|
16
|
+
COMPILE_SDK: 34,
|
|
17
|
+
TARGET_SDK: 34,
|
|
18
|
+
MIN_SDK: 24,
|
|
19
|
+
KOTLIN_VERSION: "1.9.23",
|
|
20
|
+
COMPOSE_COMPILER_EXTENSION_VERSION: "1.5.11",
|
|
21
|
+
AGP_VERSION: "8.3.2",
|
|
22
|
+
// Gradle
|
|
23
|
+
GRADLE_VERSION: "8.7"
|
|
24
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import kleur from 'kleur';
|
|
2
|
+
export const logger = {
|
|
3
|
+
info: (msg) => console.log(kleur.blue('ℹ') + ' ' + msg),
|
|
4
|
+
success: (msg) => console.log(kleur.green('✔') + ' ' + msg),
|
|
5
|
+
warn: (msg) => console.log(kleur.yellow('⚠') + ' ' + msg),
|
|
6
|
+
error: (msg) => console.error(kleur.red('✖') + ' ' + msg),
|
|
7
|
+
step: (msg) => console.log(kleur.cyan('→') + ' ' + msg),
|
|
8
|
+
// Minimal "Vite-like" output
|
|
9
|
+
banner: () => {
|
|
10
|
+
console.log();
|
|
11
|
+
console.log(kleur.bgGreen().black(' CREATE-DROID '));
|
|
12
|
+
console.log();
|
|
13
|
+
}
|
|
14
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-droid",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "The fastest way to start an Android project. No Studio required.",
|
|
5
|
+
"author": "YELrhilassi",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/YELrhilassi/create-android-app.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"android",
|
|
13
|
+
"kotlin",
|
|
14
|
+
"compose",
|
|
15
|
+
"cli",
|
|
16
|
+
"create-droid",
|
|
17
|
+
"scaffold",
|
|
18
|
+
"mobile"
|
|
19
|
+
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"create-droid": "bin/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"bin",
|
|
25
|
+
"dist",
|
|
26
|
+
"templates"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"main": "dist/index.js",
|
|
33
|
+
"type": "module",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"adm-zip": "^0.5.16",
|
|
36
|
+
"execa": "^9.6.1",
|
|
37
|
+
"fs-extra": "^11.3.3",
|
|
38
|
+
"kleur": "^4.1.5",
|
|
39
|
+
"prompts": "^2.4.2",
|
|
40
|
+
"tar": "^7.5.9"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/adm-zip": "^0.5.7",
|
|
44
|
+
"@types/fs-extra": "^11.0.4",
|
|
45
|
+
"@types/node": "^25.2.3",
|
|
46
|
+
"@types/prompts": "^2.4.9",
|
|
47
|
+
"@types/tar": "^6.1.13",
|
|
48
|
+
"typescript": "^5.9.3"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[versions]
|
|
2
|
+
agp = "8.3.2"
|
|
3
|
+
kotlin = "1.9.23"
|
|
4
|
+
coreKtx = "1.12.0"
|
|
5
|
+
junit = "4.13.2"
|
|
6
|
+
junitVersion = "1.1.5"
|
|
7
|
+
espressoCore = "3.5.1"
|
|
8
|
+
lifecycleRuntimeKtx = "2.7.0"
|
|
9
|
+
activityCompose = "1.8.2"
|
|
10
|
+
composeBom = "2024.02.01"
|
|
11
|
+
appcompat = "1.6.1"
|
|
12
|
+
material = "1.11.0"
|
|
13
|
+
|
|
14
|
+
[libraries]
|
|
15
|
+
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
|
16
|
+
junit = { group = "junit", name = "junit", version.ref = "junit" }
|
|
17
|
+
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
|
18
|
+
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
|
19
|
+
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
|
20
|
+
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
|
21
|
+
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
|
22
|
+
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
|
23
|
+
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
|
24
|
+
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
|
25
|
+
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
|
26
|
+
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
|
27
|
+
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
|
28
|
+
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
|
29
|
+
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
|
30
|
+
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
|
31
|
+
|
|
32
|
+
[plugins]
|
|
33
|
+
android-application = { id = "com.android.application", version.ref = "agp" }
|
|
34
|
+
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
pluginManagement {
|
|
2
|
+
repositories {
|
|
3
|
+
google {
|
|
4
|
+
content {
|
|
5
|
+
includeGroupByRegex("com\\.android.*")
|
|
6
|
+
includeGroupByRegex("com\\.google.*")
|
|
7
|
+
includeGroupByRegex("androidx.*")
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
mavenCentral()
|
|
11
|
+
gradlePluginPortal()
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
dependencyResolutionManagement {
|
|
15
|
+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
16
|
+
repositories {
|
|
17
|
+
google()
|
|
18
|
+
mavenCentral()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
rootProject.name = "{{PROJECT_NAME}}"
|
|
23
|
+
include(":app")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
alias(libs.plugins.android.application)
|
|
3
|
+
alias(libs.plugins.jetbrains.kotlin.android)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
android {
|
|
7
|
+
namespace = "{{APPLICATION_ID}}"
|
|
8
|
+
compileSdk = {{COMPILE_SDK}}
|
|
9
|
+
|
|
10
|
+
defaultConfig {
|
|
11
|
+
applicationId = "{{APPLICATION_ID}}"
|
|
12
|
+
minSdk = {{MIN_SDK}}
|
|
13
|
+
targetSdk = {{TARGET_SDK}}
|
|
14
|
+
versionCode = 1
|
|
15
|
+
versionName = "1.0"
|
|
16
|
+
|
|
17
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
18
|
+
vectorDrawables {
|
|
19
|
+
useSupportLibrary = true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
buildTypes {
|
|
24
|
+
release {
|
|
25
|
+
isMinifyEnabled = false
|
|
26
|
+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
compileOptions {
|
|
30
|
+
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
31
|
+
targetCompatibility = JavaVersion.VERSION_1_8
|
|
32
|
+
}
|
|
33
|
+
kotlinOptions {
|
|
34
|
+
jvmTarget = "1.8"
|
|
35
|
+
}
|
|
36
|
+
buildFeatures {
|
|
37
|
+
compose = true
|
|
38
|
+
}
|
|
39
|
+
composeOptions {
|
|
40
|
+
kotlinCompilerExtensionVersion = "1.5.11"
|
|
41
|
+
}
|
|
42
|
+
packaging {
|
|
43
|
+
resources {
|
|
44
|
+
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
dependencies {
|
|
50
|
+
implementation(libs.androidx.core.ktx)
|
|
51
|
+
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
52
|
+
implementation(libs.androidx.activity.compose)
|
|
53
|
+
implementation(platform(libs.androidx.compose.bom))
|
|
54
|
+
implementation(libs.androidx.ui)
|
|
55
|
+
implementation(libs.androidx.ui.graphics)
|
|
56
|
+
implementation(libs.androidx.ui.tooling.preview)
|
|
57
|
+
implementation(libs.androidx.material3)
|
|
58
|
+
testImplementation(libs.junit)
|
|
59
|
+
androidTestImplementation(libs.androidx.junit)
|
|
60
|
+
androidTestImplementation(libs.androidx.espresso.core)
|
|
61
|
+
androidTestImplementation(platform(libs.androidx.compose.bom))
|
|
62
|
+
androidTestImplementation(libs.androidx.ui.test.junit4)
|
|
63
|
+
debugImplementation(libs.androidx.ui.tooling)
|
|
64
|
+
debugImplementation(libs.androidx.ui.test.manifest)
|
|
65
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
4
|
+
|
|
5
|
+
<application
|
|
6
|
+
android:allowBackup="true"
|
|
7
|
+
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
8
|
+
android:fullBackupContent="@xml/backup_rules"
|
|
9
|
+
android:icon="@mipmap/ic_launcher"
|
|
10
|
+
android:label="@string/app_name"
|
|
11
|
+
android:roundIcon="@mipmap/ic_launcher_round"
|
|
12
|
+
android:supportsRtl="true"
|
|
13
|
+
android:theme="@style/Theme.MyApplication"
|
|
14
|
+
tools:targetApi="31">
|
|
15
|
+
<activity
|
|
16
|
+
android:name=".MainActivity"
|
|
17
|
+
android:exported="true"
|
|
18
|
+
android:label="@string/app_name"
|
|
19
|
+
android:theme="@style/Theme.MyApplication">
|
|
20
|
+
<intent-filter>
|
|
21
|
+
<action android:name="android.intent.action.MAIN" />
|
|
22
|
+
|
|
23
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
24
|
+
</intent-filter>
|
|
25
|
+
</activity>
|
|
26
|
+
</application>
|
|
27
|
+
|
|
28
|
+
</manifest>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.activity.ComponentActivity
|
|
5
|
+
import androidx.activity.compose.setContent
|
|
6
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
7
|
+
import androidx.compose.foundation.layout.padding
|
|
8
|
+
import androidx.compose.material3.MaterialTheme
|
|
9
|
+
import androidx.compose.material3.Scaffold
|
|
10
|
+
import androidx.compose.material3.Surface
|
|
11
|
+
import androidx.compose.material3.Text
|
|
12
|
+
import androidx.compose.runtime.Composable
|
|
13
|
+
import androidx.compose.ui.Modifier
|
|
14
|
+
import androidx.compose.ui.tooling.preview.Preview
|
|
15
|
+
|
|
16
|
+
class MainActivity : ComponentActivity() {
|
|
17
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
18
|
+
super.onCreate(savedInstanceState)
|
|
19
|
+
setContent {
|
|
20
|
+
MaterialTheme {
|
|
21
|
+
Surface(
|
|
22
|
+
modifier = Modifier.fillMaxSize(),
|
|
23
|
+
color = MaterialTheme.colorScheme.background
|
|
24
|
+
) {
|
|
25
|
+
Greeting("Android")
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Composable
|
|
33
|
+
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
|
34
|
+
Text(
|
|
35
|
+
text = "Hello $name!",
|
|
36
|
+
modifier = modifier
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Preview(showBackground = true)
|
|
41
|
+
@Composable
|
|
42
|
+
fun GreetingPreview() {
|
|
43
|
+
MaterialTheme {
|
|
44
|
+
Greeting("Android")
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
alias(libs.plugins.android.application)
|
|
3
|
+
alias(libs.plugins.jetbrains.kotlin.android)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
android {
|
|
7
|
+
namespace = "{{APPLICATION_ID}}"
|
|
8
|
+
compileSdk = {{COMPILE_SDK}}
|
|
9
|
+
|
|
10
|
+
defaultConfig {
|
|
11
|
+
applicationId = "{{APPLICATION_ID}}"
|
|
12
|
+
minSdk = {{MIN_SDK}}
|
|
13
|
+
targetSdk = {{TARGET_SDK}}
|
|
14
|
+
versionCode = 1
|
|
15
|
+
versionName = "1.0"
|
|
16
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
buildTypes {
|
|
20
|
+
release {
|
|
21
|
+
isMinifyEnabled = false
|
|
22
|
+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
compileOptions {
|
|
26
|
+
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
27
|
+
targetCompatibility = JavaVersion.VERSION_1_8
|
|
28
|
+
}
|
|
29
|
+
kotlinOptions {
|
|
30
|
+
jvmTarget = "1.8"
|
|
31
|
+
}
|
|
32
|
+
buildFeatures {
|
|
33
|
+
viewBinding = true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
dependencies {
|
|
38
|
+
implementation(libs.androidx.core.ktx)
|
|
39
|
+
implementation(libs.androidx.appcompat)
|
|
40
|
+
implementation(libs.material)
|
|
41
|
+
testImplementation(libs.junit)
|
|
42
|
+
androidTestImplementation(libs.androidx.junit)
|
|
43
|
+
androidTestImplementation(libs.androidx.espresso.core)
|
|
44
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
4
|
+
|
|
5
|
+
<application
|
|
6
|
+
android:allowBackup="true"
|
|
7
|
+
android:dataExtractionRules="@xml/data_extraction_rules"
|
|
8
|
+
android:fullBackupContent="@xml/backup_rules"
|
|
9
|
+
android:icon="@mipmap/ic_launcher"
|
|
10
|
+
android:label="@string/app_name"
|
|
11
|
+
android:roundIcon="@mipmap/ic_launcher_round"
|
|
12
|
+
android:supportsRtl="true"
|
|
13
|
+
android:theme="@style/Theme.MyApplication"
|
|
14
|
+
tools:targetApi="31">
|
|
15
|
+
<activity
|
|
16
|
+
android:name=".MainActivity"
|
|
17
|
+
android:exported="true"
|
|
18
|
+
android:label="@string/app_name"
|
|
19
|
+
android:theme="@style/Theme.MyApplication">
|
|
20
|
+
<intent-filter>
|
|
21
|
+
<action android:name="android.intent.action.MAIN" />
|
|
22
|
+
|
|
23
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
24
|
+
</intent-filter>
|
|
25
|
+
</activity>
|
|
26
|
+
</application>
|
|
27
|
+
|
|
28
|
+
</manifest>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
5
|
+
import {{PACKAGE_NAME}}.databinding.ActivityMainBinding
|
|
6
|
+
|
|
7
|
+
class MainActivity : AppCompatActivity() {
|
|
8
|
+
|
|
9
|
+
private lateinit var binding: ActivityMainBinding
|
|
10
|
+
|
|
11
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
12
|
+
super.onCreate(savedInstanceState)
|
|
13
|
+
|
|
14
|
+
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
15
|
+
setContentView(binding.root)
|
|
16
|
+
|
|
17
|
+
binding.textView.text = "Hello Android!"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
4
|
+
xmlns:tools="http://schemas.android.com/tools"
|
|
5
|
+
android:layout_width="match_parent"
|
|
6
|
+
android:layout_height="match_parent"
|
|
7
|
+
tools:context=".MainActivity">
|
|
8
|
+
|
|
9
|
+
<TextView
|
|
10
|
+
android:id="@+id/textView"
|
|
11
|
+
android:layout_width="wrap_content"
|
|
12
|
+
android:layout_height="wrap_content"
|
|
13
|
+
android:text="Hello World!"
|
|
14
|
+
app:layout_constraintBottom_toBottomOf="parent"
|
|
15
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
16
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
17
|
+
app:layout_constraintTop_toTopOf="parent" />
|
|
18
|
+
|
|
19
|
+
</androidx.constraintlayout.widget.ConstraintLayout>
|