create-droid 1.0.6 → 1.1.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 +10 -4
- package/dist/index.js +6 -3
- package/dist/template/generateProject.js +31 -34
- package/package.json +1 -1
- package/templates/base/gradle.properties +8 -1
- package/templates/compose-library/library/build.gradle.kts +46 -0
- package/templates/compose-library/library/src/main/java/com/example/template/ExampleComponent.kt +16 -0
- package/templates/mobile-compose-navigation/app/build.gradle.kts +56 -0
- package/templates/mobile-compose-navigation/app/src/main/AndroidManifest.xml +21 -0
- package/templates/mobile-compose-navigation/app/src/main/java/com/example/template/MainActivity.kt +84 -0
- package/templates/tv-compose/app/build.gradle.kts +53 -0
- package/templates/tv-compose/app/src/main/AndroidManifest.xml +26 -0
- package/templates/tv-compose/app/src/main/java/com/example/template/MainActivity.kt +64 -0
package/README.md
CHANGED
|
@@ -35,7 +35,12 @@ npx create-droid my-app
|
|
|
35
35
|
|
|
36
36
|
Follow the interactive prompts:
|
|
37
37
|
1. **Project Name**: Defaults to directory name.
|
|
38
|
-
2. **
|
|
38
|
+
2. **Template Selection**:
|
|
39
|
+
* **Jetpack Compose (Mobile)**: Modern phone/tablet starter.
|
|
40
|
+
* **Compose with Navigation**: Includes Navigation, BottomBar, and multi-screen setup.
|
|
41
|
+
* **Compose for TV**: Optimized for Android TV with `tv-material`.
|
|
42
|
+
* **Compose Library**: Foundation for publishing reusable UI components.
|
|
43
|
+
* **XML Views (Legacy)**: For maintenance or classic development.
|
|
39
44
|
|
|
40
45
|
### After Scaffolding
|
|
41
46
|
|
|
@@ -54,9 +59,10 @@ npm run build
|
|
|
54
59
|
|
|
55
60
|
The generated project is **clean** and follows modern best practices. It includes a `package.json` with convenience scripts:
|
|
56
61
|
|
|
57
|
-
* `npm run dev`: Watches
|
|
58
|
-
* `npm run build`: Generates a release APK
|
|
59
|
-
* `npm
|
|
62
|
+
* `npm run dev`: High-speed development loop. Watches code and auto-deploys via `--continuous` and `--configuration-cache`.
|
|
63
|
+
* `npm run build`: Generates a production release APK.
|
|
64
|
+
* `npm run clean:deep`: Purges all build artifacts and Gradle cache to reclaim disk space.
|
|
65
|
+
* `npm test`: Runs unit tests.
|
|
60
66
|
|
|
61
67
|
### 📱 ADB Scripts (Wireless Debugging)
|
|
62
68
|
|
package/dist/index.js
CHANGED
|
@@ -20,10 +20,13 @@ export async function run(args) {
|
|
|
20
20
|
{
|
|
21
21
|
type: 'select',
|
|
22
22
|
name: 'uiType',
|
|
23
|
-
message: 'Select
|
|
23
|
+
message: 'Select Template:',
|
|
24
24
|
choices: [
|
|
25
|
-
{ title: 'Jetpack Compose (
|
|
26
|
-
{ title: '
|
|
25
|
+
{ title: 'Jetpack Compose (Mobile)', value: 'compose', description: 'Recommended for phone/tablet apps' },
|
|
26
|
+
{ title: 'Compose with Navigation', value: 'mobile-compose-navigation', description: 'Includes Navigation, BottomBar, Screens' },
|
|
27
|
+
{ title: 'Compose for TV', value: 'tv-compose', description: 'Optimized for Android TV (Leanback)' },
|
|
28
|
+
{ title: 'Compose Library', value: 'compose-library', description: 'Scaffold for publishing UI libraries' },
|
|
29
|
+
{ title: 'XML Views (Legacy)', value: 'views', description: 'Classic View-based Android development' }
|
|
27
30
|
],
|
|
28
31
|
initial: 0
|
|
29
32
|
}
|
|
@@ -8,19 +8,24 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
8
8
|
const __dirname = path.dirname(__filename);
|
|
9
9
|
export async function generateProject(options) {
|
|
10
10
|
const { projectPath, projectName, uiType, sdkPath } = options;
|
|
11
|
+
const isLibrary = uiType === 'compose-library';
|
|
12
|
+
const moduleName = isLibrary ? 'library' : 'app';
|
|
11
13
|
// 1. Ensure target directory
|
|
12
14
|
await fs.ensureDir(projectPath);
|
|
13
15
|
// 2. Resolve template paths
|
|
14
16
|
const templateRoot = path.resolve(__dirname, '../../templates');
|
|
15
17
|
const baseTemplate = path.join(templateRoot, 'base');
|
|
16
|
-
const
|
|
17
|
-
const uiTemplate = path.join(templateRoot, uiTemplateName);
|
|
18
|
+
const uiTemplate = path.join(templateRoot, uiType);
|
|
18
19
|
if (!fs.existsSync(baseTemplate)) {
|
|
19
20
|
throw new Error(`Template not found at ${baseTemplate}`);
|
|
20
21
|
}
|
|
21
22
|
// 3. Copy Base
|
|
22
23
|
logger.info(`Copying base template from ${baseTemplate}...`);
|
|
23
24
|
await fs.copy(baseTemplate, projectPath);
|
|
25
|
+
if (isLibrary) {
|
|
26
|
+
// Remove default app module if it's a library
|
|
27
|
+
await fs.remove(path.join(projectPath, 'app'));
|
|
28
|
+
}
|
|
24
29
|
// Rename _gitignore to .gitignore
|
|
25
30
|
const gitignorePath = path.join(projectPath, '_gitignore');
|
|
26
31
|
if (fs.existsSync(gitignorePath)) {
|
|
@@ -37,48 +42,53 @@ export async function generateProject(options) {
|
|
|
37
42
|
// Replace in settings.gradle.kts
|
|
38
43
|
await patchFile(path.join(projectPath, 'settings.gradle.kts'), {
|
|
39
44
|
'{{PROJECT_NAME}}': projectName,
|
|
45
|
+
'include(":app")': `include(":${moduleName}")`
|
|
40
46
|
});
|
|
41
|
-
// Replace in
|
|
42
|
-
|
|
47
|
+
// Replace in module build.gradle.kts
|
|
48
|
+
const moduleBuildFile = path.join(projectPath, moduleName, 'build.gradle.kts');
|
|
49
|
+
await patchFile(moduleBuildFile, {
|
|
43
50
|
'{{APPLICATION_ID}}': packageName,
|
|
44
51
|
'{{COMPILE_SDK}}': CONSTANTS.COMPILE_SDK.toString(),
|
|
45
52
|
'{{MIN_SDK}}': CONSTANTS.MIN_SDK.toString(),
|
|
46
53
|
'{{TARGET_SDK}}': CONSTANTS.TARGET_SDK.toString(),
|
|
47
54
|
});
|
|
48
|
-
// Replace in
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
// Replace in strings.xml
|
|
56
|
+
const stringsPath = path.join(projectPath, moduleName, 'src/main/res/values/strings.xml');
|
|
57
|
+
if (fs.existsSync(stringsPath)) {
|
|
58
|
+
await patchFile(stringsPath, {
|
|
59
|
+
'{{PROJECT_NAME}}': projectName,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
52
62
|
// Handle Source Code Relocation
|
|
53
|
-
const srcBase = path.join(projectPath, '
|
|
63
|
+
const srcBase = path.join(projectPath, moduleName, 'src/main/java');
|
|
54
64
|
const oldPackagePath = path.join(srcBase, 'com/example/template');
|
|
55
65
|
const newPackagePath = path.join(srcBase, ...packageName.split('.'));
|
|
56
66
|
if (fs.existsSync(oldPackagePath)) {
|
|
57
67
|
await fs.move(oldPackagePath, newPackagePath, { overwrite: true });
|
|
58
68
|
// Recursive file patching for package statement
|
|
59
69
|
await patchSourceFiles(newPackagePath, packageName);
|
|
60
|
-
// Clean up empty dirs
|
|
61
|
-
// Only if empty.
|
|
70
|
+
// Clean up empty dirs
|
|
62
71
|
await cleanEmptyDirs(srcBase);
|
|
63
72
|
}
|
|
64
73
|
// Create local.properties with SDK location
|
|
65
74
|
const localProperties = `sdk.dir=${sdkPath}`;
|
|
66
75
|
await fs.writeFile(path.join(projectPath, 'local.properties'), localProperties);
|
|
67
|
-
// 6. Setup NPM Scripts
|
|
76
|
+
// 6. Setup NPM Scripts
|
|
68
77
|
logger.info('Adding npm convenience scripts...');
|
|
69
78
|
const packageJson = {
|
|
70
79
|
name: projectName.toLowerCase().replace(/[^a-z0-9-]/g, '-'),
|
|
71
80
|
version: "0.1.0",
|
|
72
81
|
private: true,
|
|
73
82
|
scripts: {
|
|
74
|
-
"dev":
|
|
83
|
+
"dev": `./gradlew installDebug --continuous --configuration-cache --parallel --offline`,
|
|
75
84
|
"start": "npm run dev",
|
|
76
|
-
"build":
|
|
77
|
-
"build:debug":
|
|
78
|
-
"test":
|
|
79
|
-
"lint":
|
|
80
|
-
"clean":
|
|
81
|
-
"
|
|
85
|
+
"build": `./gradlew assembleRelease`,
|
|
86
|
+
"build:debug": `./gradlew assembleDebug`,
|
|
87
|
+
"test": `./gradlew test`,
|
|
88
|
+
"lint": `./gradlew lint`,
|
|
89
|
+
"clean": `./gradlew clean`,
|
|
90
|
+
"clean:deep": "rm -rf .gradle app/build build library/build",
|
|
91
|
+
"help": `./gradlew --help`,
|
|
82
92
|
"adb": "node scripts/adb.js",
|
|
83
93
|
"adb:devices": "npm run adb devices",
|
|
84
94
|
"adb:connect": "npm run adb connect",
|
|
@@ -88,12 +98,6 @@ export async function generateProject(options) {
|
|
|
88
98
|
}
|
|
89
99
|
};
|
|
90
100
|
await fs.writeJSON(path.join(projectPath, 'package.json'), packageJson, { spaces: 2 });
|
|
91
|
-
// Ensure scripts dir exists (since we copy base first, but adb.js is new)
|
|
92
|
-
// Wait, templates/base/scripts/adb.js is ALREADY in the base template.
|
|
93
|
-
// We just need to ensure the `scripts` folder is copied correctly.
|
|
94
|
-
// Since we copy `templates/base`, it should be fine.
|
|
95
|
-
// Make scripts executable if needed (node doesn't need +x but good practice)
|
|
96
|
-
// await fs.chmod(path.join(projectPath, 'scripts/adb.js'), 0o755);
|
|
97
101
|
// 7. Initialize Git
|
|
98
102
|
try {
|
|
99
103
|
logger.info('Initializing git repository...');
|
|
@@ -103,7 +107,7 @@ export async function generateProject(options) {
|
|
|
103
107
|
logger.success('Git repository initialized.');
|
|
104
108
|
}
|
|
105
109
|
catch (e) {
|
|
106
|
-
logger.warn('Failed to initialize git repository.
|
|
110
|
+
logger.warn('Failed to initialize git repository.');
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
async function patchFile(filePath, replacements) {
|
|
@@ -131,11 +135,6 @@ async function patchSourceFiles(dir, packageName) {
|
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
137
|
async function cleanEmptyDirs(dir) {
|
|
134
|
-
// Basic cleanup: remove com/example/template if empty
|
|
135
|
-
// We moved content from com/example/template to new path.
|
|
136
|
-
// So we check if com/example/template is empty, then com/example, then com.
|
|
137
|
-
// This is tricky because we don't know the exact depth of the old structure easily without hardcoding.
|
|
138
|
-
// Hardcoded for "com/example/template":
|
|
139
138
|
const oldPath = path.join(dir, 'com/example/template');
|
|
140
139
|
try {
|
|
141
140
|
if (fs.existsSync(oldPath) && (await fs.readdir(oldPath)).length === 0) {
|
|
@@ -150,7 +149,5 @@ async function cleanEmptyDirs(dir) {
|
|
|
150
149
|
}
|
|
151
150
|
}
|
|
152
151
|
}
|
|
153
|
-
catch (e) {
|
|
154
|
-
// Ignore errors during cleanup
|
|
155
|
-
}
|
|
152
|
+
catch (e) { }
|
|
156
153
|
}
|
package/package.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
# Performance Optimizations
|
|
2
|
+
org.gradle.jvmargs=-Xmx1536m -Dfile.encoding=UTF-8 -XX:+UseParallelGC
|
|
3
|
+
org.gradle.parallel=true
|
|
4
|
+
org.gradle.caching=true
|
|
5
|
+
org.gradle.configuration-cache=true
|
|
6
|
+
|
|
7
|
+
# Android Optimizations
|
|
2
8
|
android.useAndroidX=true
|
|
3
9
|
kotlin.code.style=official
|
|
4
10
|
android.nonTransitiveRClass=true
|
|
11
|
+
android.enableJetifier=false
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
plugins {
|
|
2
|
+
alias(libs.plugins.android.library)
|
|
3
|
+
alias(libs.plugins.jetbrains.kotlin.android)
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
android {
|
|
7
|
+
namespace = "{{APPLICATION_ID}}"
|
|
8
|
+
compileSdk = {{COMPILE_SDK}}
|
|
9
|
+
|
|
10
|
+
defaultConfig {
|
|
11
|
+
minSdk = {{MIN_SDK}}
|
|
12
|
+
|
|
13
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
14
|
+
consumerProguardFiles("consumer-rules.pro")
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
buildTypes {
|
|
18
|
+
release {
|
|
19
|
+
isMinifyEnabled = false
|
|
20
|
+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
compileOptions {
|
|
24
|
+
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
25
|
+
targetCompatibility = JavaVersion.VERSION_1_8
|
|
26
|
+
}
|
|
27
|
+
kotlinOptions {
|
|
28
|
+
jvmTarget = "1.8"
|
|
29
|
+
}
|
|
30
|
+
buildFeatures {
|
|
31
|
+
compose = true
|
|
32
|
+
}
|
|
33
|
+
composeOptions {
|
|
34
|
+
kotlinCompilerExtensionVersion = "1.5.11"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dependencies {
|
|
39
|
+
implementation(libs.androidx.core.ktx)
|
|
40
|
+
implementation(platform(libs.androidx.compose.bom))
|
|
41
|
+
implementation(libs.androidx.ui)
|
|
42
|
+
implementation(libs.androidx.material3)
|
|
43
|
+
testImplementation(libs.junit)
|
|
44
|
+
androidTestImplementation(libs.androidx.junit)
|
|
45
|
+
androidTestImplementation(libs.androidx.espresso.core)
|
|
46
|
+
}
|
package/templates/compose-library/library/src/main/java/com/example/template/ExampleComponent.kt
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package {{PACKAGE_NAME}}
|
|
2
|
+
|
|
3
|
+
import androidx.compose.material3.Text
|
|
4
|
+
import androidx.compose.runtime.Composable
|
|
5
|
+
import androidx.compose.ui.Modifier
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* An example component from your library.
|
|
9
|
+
*/
|
|
10
|
+
@Composable
|
|
11
|
+
fun LibraryGreeting(name: String, modifier: Modifier = Modifier) {
|
|
12
|
+
Text(
|
|
13
|
+
text = "Hello $name from the Library!",
|
|
14
|
+
modifier = modifier
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
compose = true
|
|
34
|
+
}
|
|
35
|
+
composeOptions {
|
|
36
|
+
kotlinCompilerExtensionVersion = "1.5.11"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
dependencies {
|
|
41
|
+
implementation(libs.androidx.core.ktx)
|
|
42
|
+
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
43
|
+
implementation(libs.androidx.activity.compose)
|
|
44
|
+
implementation(platform(libs.androidx.compose.bom))
|
|
45
|
+
implementation(libs.androidx.ui)
|
|
46
|
+
implementation(libs.androidx.ui.graphics)
|
|
47
|
+
implementation(libs.androidx.ui.tooling.preview)
|
|
48
|
+
implementation(libs.androidx.material3)
|
|
49
|
+
|
|
50
|
+
// Navigation
|
|
51
|
+
implementation("androidx.navigation:navigation-compose:2.7.7")
|
|
52
|
+
|
|
53
|
+
testImplementation(libs.junit)
|
|
54
|
+
androidTestImplementation(libs.androidx.junit)
|
|
55
|
+
androidTestImplementation(libs.androidx.espresso.core)
|
|
56
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
<application
|
|
4
|
+
android:allowBackup="true"
|
|
5
|
+
android:icon="@mipmap/ic_launcher"
|
|
6
|
+
android:label="@string/app_name"
|
|
7
|
+
android:roundIcon="@mipmap/ic_launcher_round"
|
|
8
|
+
android:supportsRtl="true"
|
|
9
|
+
android:theme="@style/Theme.MyApplication">
|
|
10
|
+
<activity
|
|
11
|
+
android:name=".MainActivity"
|
|
12
|
+
android:exported="true"
|
|
13
|
+
android:label="@string/app_name"
|
|
14
|
+
android:theme="@style/Theme.MyApplication">
|
|
15
|
+
<intent-filter>
|
|
16
|
+
<action android:name="android.intent.action.MAIN" />
|
|
17
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
18
|
+
</intent-filter>
|
|
19
|
+
</activity>
|
|
20
|
+
</application>
|
|
21
|
+
</manifest>
|
package/templates/mobile-compose-navigation/app/src/main/java/com/example/template/MainActivity.kt
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
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.padding
|
|
7
|
+
import androidx.compose.material.icons.Icons
|
|
8
|
+
import androidx.compose.material.icons.filled.Home
|
|
9
|
+
import androidx.compose.material.icons.filled.Settings
|
|
10
|
+
import androidx.compose.material3.*
|
|
11
|
+
import androidx.compose.runtime.*
|
|
12
|
+
import androidx.compose.ui.Modifier
|
|
13
|
+
import androidx.navigation.NavDestination.Companion.hierarchy
|
|
14
|
+
import androidx.navigation.NavGraph.Companion.findStartDestination
|
|
15
|
+
import androidx.navigation.compose.NavHost
|
|
16
|
+
import androidx.navigation.compose.composable
|
|
17
|
+
import androidx.navigation.compose.currentBackStackEntryAsState
|
|
18
|
+
import androidx.navigation.compose.rememberNavController
|
|
19
|
+
|
|
20
|
+
class MainActivity : ComponentActivity() {
|
|
21
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
22
|
+
super.onCreate(savedInstanceState)
|
|
23
|
+
setContent {
|
|
24
|
+
MainScreen()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
sealed class Screen(val route: String, val label: String, val icon: @Composable () -> Unit) {
|
|
30
|
+
object Home : Screen("home", "Home", { Icon(Icons.Filled.Home, contentDescription = null) })
|
|
31
|
+
object Settings : Screen("settings", "Settings", { Icon(Icons.Filled.Settings, contentDescription = null) })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@Composable
|
|
35
|
+
fun MainScreen() {
|
|
36
|
+
val navController = rememberNavController()
|
|
37
|
+
val items = listOf(Screen.Home, Screen.Settings)
|
|
38
|
+
|
|
39
|
+
MaterialTheme {
|
|
40
|
+
Scaffold(
|
|
41
|
+
bottomBar = {
|
|
42
|
+
NavigationBar {
|
|
43
|
+
val navBackStackEntry by navController.currentBackStackEntryAsState()
|
|
44
|
+
val currentDestination = navBackStackEntry?.destination
|
|
45
|
+
items.forEach { screen ->
|
|
46
|
+
NavigationBarItem(
|
|
47
|
+
icon = screen.icon,
|
|
48
|
+
label = { Text(screen.label) },
|
|
49
|
+
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
|
|
50
|
+
onClick = {
|
|
51
|
+
navController.navigate(screen.route) {
|
|
52
|
+
popUpTo(navController.graph.findStartDestination().id) {
|
|
53
|
+
saveState = true
|
|
54
|
+
}
|
|
55
|
+
launchSingleTop = true
|
|
56
|
+
restoreState = true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
) { innerPadding ->
|
|
64
|
+
NavHost(navController, startDestination = Screen.Home.route, Modifier.padding(innerPadding)) {
|
|
65
|
+
composable(Screen.Home.route) { HomeScreen() }
|
|
66
|
+
composable(Screen.Settings.route) { SettingsScreen() }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Composable
|
|
73
|
+
fun HomeScreen() {
|
|
74
|
+
Surface {
|
|
75
|
+
Text("Welcome Home!")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Composable
|
|
80
|
+
fun SettingsScreen() {
|
|
81
|
+
Surface {
|
|
82
|
+
Text("Settings Screen")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
compose = true
|
|
34
|
+
}
|
|
35
|
+
composeOptions {
|
|
36
|
+
kotlinCompilerExtensionVersion = "1.5.11"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
dependencies {
|
|
41
|
+
implementation(libs.androidx.core.ktx)
|
|
42
|
+
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
43
|
+
implementation(libs.androidx.activity.compose)
|
|
44
|
+
|
|
45
|
+
// TV Compose
|
|
46
|
+
implementation("androidx.tv:tv-foundation:1.0.0-alpha10")
|
|
47
|
+
implementation("androidx.tv:tv-material:1.0.0-alpha10")
|
|
48
|
+
|
|
49
|
+
implementation(platform(libs.androidx.compose.bom))
|
|
50
|
+
implementation(libs.androidx.ui)
|
|
51
|
+
implementation(libs.androidx.ui.graphics)
|
|
52
|
+
implementation(libs.androidx.ui.tooling.preview)
|
|
53
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
|
|
4
|
+
<uses-feature android:name="android.software.leanback" android:required="false" />
|
|
5
|
+
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
|
6
|
+
|
|
7
|
+
<application
|
|
8
|
+
android:allowBackup="true"
|
|
9
|
+
android:icon="@mipmap/ic_launcher"
|
|
10
|
+
android:label="@string/app_name"
|
|
11
|
+
android:banner="@mipmap/ic_launcher"
|
|
12
|
+
android:roundIcon="@mipmap/ic_launcher_round"
|
|
13
|
+
android:supportsRtl="true"
|
|
14
|
+
android:theme="@style/Theme.MyApplication">
|
|
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
|
+
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
23
|
+
</intent-filter>
|
|
24
|
+
</activity>
|
|
25
|
+
</application>
|
|
26
|
+
</manifest>
|
|
@@ -0,0 +1,64 @@
|
|
|
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.*
|
|
7
|
+
import androidx.compose.runtime.*
|
|
8
|
+
import androidx.compose.ui.Modifier
|
|
9
|
+
import androidx.compose.ui.graphics.Color
|
|
10
|
+
import androidx.compose.ui.unit.dp
|
|
11
|
+
import androidx.tv.material3.*
|
|
12
|
+
|
|
13
|
+
class MainActivity : ComponentActivity() {
|
|
14
|
+
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
15
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
16
|
+
super.onCreate(savedInstanceState)
|
|
17
|
+
setContent {
|
|
18
|
+
MaterialTheme {
|
|
19
|
+
Surface(
|
|
20
|
+
modifier = Modifier.fillMaxSize(),
|
|
21
|
+
shape = ClickableSurfaceDefaults.shape(),
|
|
22
|
+
color = ClickableSurfaceDefaults.color()
|
|
23
|
+
) {
|
|
24
|
+
TvContent()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
32
|
+
@Composable
|
|
33
|
+
fun TvContent() {
|
|
34
|
+
Column(
|
|
35
|
+
modifier = Modifier.padding(48.dp),
|
|
36
|
+
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
37
|
+
) {
|
|
38
|
+
Text(
|
|
39
|
+
text = "Welcome to Android TV",
|
|
40
|
+
style = MaterialTheme.typography.displayMedium
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
|
44
|
+
repeat(5) { index ->
|
|
45
|
+
StandardCard(index)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@OptIn(ExperimentalTvMaterial3Api::class)
|
|
52
|
+
@Composable
|
|
53
|
+
fun StandardCard(index: Int) {
|
|
54
|
+
Surface(
|
|
55
|
+
onClick = { /* Handle click */ },
|
|
56
|
+
modifier = Modifier.size(150.dp, 100.dp),
|
|
57
|
+
shape = ClickableSurfaceDefaults.shape(shape = MaterialTheme.shapes.medium),
|
|
58
|
+
color = ClickableSurfaceDefaults.color(focusedColor = Color.White)
|
|
59
|
+
) {
|
|
60
|
+
Box(contentAlignment = androidx.compose.ui.Alignment.Center) {
|
|
61
|
+
Text("Item $index")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|