Support host android apps with kts gradle files for add to app (#156502)

Allows applying of `include_flutter.groovy` via the `apply from:` syntax, which allows using a host app that is using the Gradle Kotlin DSL (the default these days when creating an Android app in AS).

Explanation: The `include_flutter.groovy` script is currently not able to be called by Kotlin gradle files, because it is [intended to be invoked with the following lines](https://docs.flutter.dev/add-to-app/android/project-setup#depend-on-the-modules-source-code):
```
setBinding(new Binding([gradle: this]))                                // new
evaluate(new File(                                                     // new
    settingsDir.parentFile,                                            // new
    'flutter_module/.android/include_flutter.groovy'                   // new
))
```

`setBinding` isn't part of the Kotlin gradle DSL, and there isn't (that I can find) an easy Kotlin equivalent. If this binding isn't set, the reference to `gradle` in `include_flutter.groovy` is wrong, which breaks the script.

This PR modifies `include_flutter.groovy` to also support being invoked through the standard way of invoking a script via the Gradle Groovy/Kotlin DSLs, which is `apply from:` (or it's slightly different Kotlin syntax). The start of the script identifies which of the two approaches is being used by checking if the binding is set, and then initializes some variables differently depending on the case.

If we land this, I believe we should update the example Gradle files for both the `kts` and `groovy` cases to prefer the `apply from` syntax as I think this is the syntax most developers would be more familiar with already seeing in their Gradle files.
This commit is contained in:
Gray Mackall 2024-10-16 15:47:27 -07:00 committed by GitHub
parent 54bf532305
commit d1d9954c45
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 1018 additions and 15 deletions

View File

@ -1001,7 +1001,8 @@ targets:
- bin/**
- .ci.yaml
- name: Linux module_test
- name: Linux build_android_host_app_with_module_aar
bringup: true
recipe: devicelab/devicelab_drone
timeout: 60
properties:
@ -1013,7 +1014,27 @@ targets:
]
tags: >
["devicelab", "hostonly", "linux"]
task_name: module_test
task_name: build_android_host_app_with_module_aar
runIf:
- dev/**
- packages/flutter_tools/**
- bin/**
- .ci.yaml
- name: Linux build_android_host_app_with_module_source
bringup: true
recipe: devicelab/devicelab_drone
timeout: 60
properties:
dependencies: >-
[
{"dependency": "android_sdk", "version": "version:34v3"},
{"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"},
{"dependency": "open_jdk", "version": "version:17"}
]
tags: >
["devicelab", "hostonly", "linux"]
task_name: build_android_host_app_with_module_source
runIf:
- dev/**
- packages/flutter_tools/**
@ -4067,7 +4088,8 @@ targets:
- bin/**
- .ci.yaml
- name: Mac module_test
- name: Mac build_android_host_app_with_module_aar
bringup: true
recipe: devicelab/devicelab_drone
timeout: 60
properties:
@ -4078,7 +4100,26 @@ targets:
]
tags: >
["devicelab", "hostonly", "mac"]
task_name: module_test
task_name: build_android_host_app_with_module_aar
runIf:
- dev/**
- packages/flutter_tools/**
- bin/**
- .ci.yaml
- name: Mac build_android_host_app_with_module_source
bringup: true
recipe: devicelab/devicelab_drone
timeout: 60
properties:
dependencies: >-
[
{"dependency": "android_sdk", "version": "version:34v3"},
{"dependency": "open_jdk", "version": "version:17"}
]
tags: >
["devicelab", "hostonly", "mac"]
task_name: build_android_host_app_with_module_source
runIf:
- dev/**
- packages/flutter_tools/**
@ -5703,7 +5744,8 @@ targets:
- bin/**
- .ci.yaml
- name: Windows module_test
- name: Windows build_android_host_app_with_module_aar
bringup: true
recipe: devicelab/devicelab_drone
timeout: 60
properties:
@ -5715,7 +5757,27 @@ targets:
]
tags: >
["devicelab", "hostonly", "windows"]
task_name: module_test
task_name: build_android_host_app_with_module_aar
runIf:
- dev/**
- packages/flutter_tools/**
- bin/**
- .ci.yaml
- name: Windows build_android_host_app_with_module_source
bringup: true
recipe: devicelab/devicelab_drone
timeout: 60
properties:
dependencies: >-
[
{"dependency": "android_sdk", "version": "version:34v3"},
{"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"},
{"dependency": "open_jdk", "version": "version:17"}
]
tags: >
["devicelab", "hostonly", "windows"]
task_name: build_android_host_app_with_module_source
runIf:
- dev/**
- packages/flutter_tools/**

View File

@ -265,7 +265,8 @@
/dev/devicelab/bin/tasks/macos_chrome_dev_mode.dart @andrewkolos @flutter/tool
/dev/devicelab/bin/tasks/module_custom_host_app_name_test.dart @andrewkolos @flutter/tool
/dev/devicelab/bin/tasks/module_host_with_custom_build_test.dart @andrewkolos @flutter/tool
/dev/devicelab/bin/tasks/module_test.dart @andrewkolos @flutter/tool
/dev/devicelab/bin/tasks/build_android_host_app_with_module_aar.dart @andrewkolos @flutter/tool
/dev/devicelab/bin/tasks/build_android_host_app_with_module_source.dart @gmackall @flutter/android
/dev/devicelab/bin/tasks/module_test_ios.dart @jmagman @flutter/tool
/dev/devicelab/bin/tasks/native_assets_ios_simulator.dart @dcharkes @flutter/ios
/dev/devicelab/bin/tasks/native_ui_tests_macos.dart @cbracken @flutter/desktop

View File

@ -35,16 +35,15 @@ TaskFunction combine(List<TaskFunction> tasks) {
/// Tests that the Flutter module project template works and supports
/// adding Flutter to an existing Android app.
class ModuleTest {
ModuleTest(
this.buildTarget, {
ModuleTest({
this.gradleVersion = '7.6.3',
});
final String buildTarget;
static const String buildTarget = 'module-gradle';
final String gradleVersion;
Future<TaskResult> call() async {
section('Running: $buildTarget');
section('Running: $buildTarget-$gradleVersion');
section('Find Java');
final String? javaHome = await findJavaHome();
@ -228,6 +227,7 @@ class ModuleTest {
flutterDirectory.path,
'dev',
'integration_tests',
'pure_android_host_apps',
'android_host_app_v2_embedding',
),
),
@ -449,7 +449,7 @@ class ModuleTest {
Future<void> main() async {
await task(combine(<TaskFunction>[
// ignore: avoid_redundant_argument_values
ModuleTest('module-gradle-7.6', gradleVersion: '8.4').call,
ModuleTest('module-gradle-7.6', gradleVersion: '8.4-rc-3').call,
ModuleTest(gradleVersion: '8.4').call,
ModuleTest(gradleVersion: '8.4-rc-3').call,
]));
}

View File

@ -0,0 +1,430 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:flutter_devicelab/framework/apk_utils.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;
final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew';
final String gradlewExecutable =
Platform.isWindows ? '.\\$gradlew' : './$gradlew';
final String fileReadWriteMode = Platform.isWindows ? 'rw-rw-rw-' : 'rw-r--r--';
final String platformLineSep = Platform.isWindows ? '\r\n' : '\n';
/// Combines several TaskFunctions with trivial success value into one.
TaskFunction combine(List<TaskFunction> tasks) {
return () async {
for (final TaskFunction task in tasks) {
final TaskResult result = await task();
if (result.failed) {
return result;
}
}
return TaskResult.success(null);
};
}
/// Tests that the Flutter module project template works and supports
/// adding Flutter to an existing Android app.
class ModuleTest {
ModuleTest({
this.gradleVersion = '7.6.3',
});
static const String buildTarget = 'module-gradle';
final String gradleVersion;
Future<TaskResult> call() async {
section('Running: $buildTarget-$gradleVersion');
section('Find Java');
final String? javaHome = await findJavaHome();
if (javaHome == null) {
return TaskResult.failure('Could not find Java');
}
print('\nUsing JAVA_HOME=$javaHome');
section('Create Flutter module project');
final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.');
final Directory projectDir = Directory(path.join(tempDir.path, 'hello'));
try {
await inDirectory(tempDir, () async {
await flutter(
'create',
options: <String>['--org', 'io.flutter.devicelab', '--template=module', 'hello'],
);
});
section('Create package with native assets');
await flutter(
'config',
options: <String>['--enable-native-assets'],
);
const String ffiPackageName = 'ffi_package';
await createFfiPackage(ffiPackageName, tempDir);
section('Add FFI package');
final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml'));
String content = await pubspec.readAsString();
content = content.replaceFirst(
'dependencies:$platformLineSep',
'dependencies:$platformLineSep $ffiPackageName:$platformLineSep path: ..${Platform.pathSeparator}$ffiPackageName$platformLineSep',
);
await pubspec.writeAsString(content, flush: true);
await inDirectory(projectDir, () async {
await flutter(
'packages',
options: <String>['get'],
);
});
section('Add read-only asset');
final File readonlyTxtAssetFile = await File(path.join(
projectDir.path,
'assets',
'read-only.txt'
))
.create(recursive: true);
if (!exists(readonlyTxtAssetFile)) {
return TaskResult.failure('Failed to create read-only asset');
}
if (!Platform.isWindows) {
await exec('chmod', <String>[
'444',
readonlyTxtAssetFile.path,
]);
}
content = content.replaceFirst(
'$platformLineSep # assets:$platformLineSep',
'$platformLineSep assets:$platformLineSep - assets/read-only.txt$platformLineSep',
);
await pubspec.writeAsString(content, flush: true);
section('Add plugins');
content = content.replaceFirst(
'${platformLineSep}dependencies:$platformLineSep',
'${platformLineSep}dependencies:$platformLineSep',
);
await pubspec.writeAsString(content, flush: true);
await inDirectory(projectDir, () async {
await flutter(
'packages',
options: <String>['get'],
);
});
section('Build ephemeral host app');
await inDirectory(projectDir, () async {
await flutter(
'build',
options: <String>['apk'],
);
});
final bool ephemeralHostApkBuilt = exists(File(path.join(
projectDir.path,
'build',
'host',
'outputs',
'apk',
'release',
'app-release.apk',
)));
if (!ephemeralHostApkBuilt) {
return TaskResult.failure('Failed to build ephemeral host .apk');
}
section('Clean build');
await inDirectory(projectDir, () async {
await flutter('clean');
});
section('Make Android host app editable');
await inDirectory(projectDir, () async {
await flutter(
'make-host-app-editable',
options: <String>['android'],
);
});
section('Build editable host app');
await inDirectory(projectDir, () async {
await flutter(
'build',
options: <String>['apk'],
);
});
final bool editableHostApkBuilt = exists(File(path.join(
projectDir.path,
'build',
'host',
'outputs',
'apk',
'release',
'app-release.apk',
)));
if (!editableHostApkBuilt) {
return TaskResult.failure('Failed to build editable host .apk');
}
section('Add to existing Android app');
final Directory hostApp = Directory(path.join(tempDir.path, 'hello_host_app'));
mkdir(hostApp);
recursiveCopy(
Directory(
path.join(
flutterDirectory.path,
'dev',
'integration_tests',
'pure_android_host_apps',
'host_app_kotlin_gradle_dsl',
),
),
hostApp,
);
copy(
File(path.join(projectDir.path, '.android', gradlew)),
hostApp,
);
copy(
File(path.join(projectDir.path, '.android', 'gradle', 'wrapper',
'gradle-wrapper.jar')),
Directory(path.join(hostApp.path, 'gradle', 'wrapper')),
);
// Modify gradle version to passed in version.
// This is somehow the wrong file.
final File gradleWrapperProperties = File(path.join(
hostApp.path, 'gradle', 'wrapper', 'gradle-wrapper.properties'));
String propertyContent = await gradleWrapperProperties.readAsString();
propertyContent = propertyContent.replaceFirst(
'REPLACEME',
gradleVersion,
);
section(propertyContent);
await gradleWrapperProperties.writeAsString(propertyContent, flush: true);
final File analyticsOutputFile =
File(path.join(tempDir.path, 'analytics.log'));
section('Build debug host APK');
await inDirectory(hostApp, () async {
if (!Platform.isWindows) {
await exec('chmod', <String>['+x', 'gradlew']);
}
await exec(gradlewExecutable,
<String>['app:assembleDebug'],
environment: <String, String>{
'JAVA_HOME': javaHome,
'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path,
},
);
});
section('Check debug APK exists');
final String debugHostApk = path.join(
hostApp.path,
'app',
'build',
'outputs',
'apk',
'debug',
'app-debug.apk',
);
if (!exists(File(debugHostApk))) {
return TaskResult.failure('Failed to build debug host APK');
}
section('Check files in debug APK');
checkCollectionContains<String>(<String>[
...flutterAssets,
...debugAssets,
...baseApkFiles,
'lib/arm64-v8a/lib$ffiPackageName.so',
'lib/armeabi-v7a/lib$ffiPackageName.so',
], await getFilesInApk(debugHostApk));
section('Check debug AndroidManifest.xml');
final String androidManifestDebug = await getAndroidManifest(debugHostApk);
if (!androidManifestDebug.contains('''
<meta-data
android:name="flutterProjectType"
android:value="module" />''')
) {
return TaskResult.failure("Debug host APK doesn't contain metadata: flutterProjectType = module ");
}
final String analyticsOutput = analyticsOutputFile.readAsStringSync();
if (!analyticsOutput.contains('cd24: android')
|| !analyticsOutput.contains('cd25: true')
|| !analyticsOutput.contains('viewName: assemble')) {
return TaskResult.failure(
'Building outer app produced the following analytics: "$analyticsOutput" '
'but not the expected strings: "cd24: android", "cd25: true" and '
'"viewName: assemble"'
);
}
section('Check file access modes for read-only asset from Flutter module');
final String readonlyDebugAssetFilePath = path.joinAll(<String>[
hostApp.path,
'app',
'build',
'intermediates',
'assets',
'debug',
'mergeDebugAssets',
'flutter_assets',
'assets',
'read-only.txt',
]);
final File readonlyDebugAssetFile = File(readonlyDebugAssetFilePath);
if (!exists(readonlyDebugAssetFile)) {
return TaskResult.failure('Failed to copy read-only asset file');
}
String modes = readonlyDebugAssetFile.statSync().modeString();
print('\nread-only.txt file access modes = $modes');
if (modes.compareTo(fileReadWriteMode) != 0) {
return TaskResult.failure('Failed to make assets user-readable and writable');
}
section('Build release host APK');
await inDirectory(hostApp, () async {
await exec(gradlewExecutable,
<String>['app:assembleRelease'],
environment: <String, String>{
'JAVA_HOME': javaHome,
'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path,
},
);
});
final String releaseHostApk = path.join(
hostApp.path,
'app',
'build',
'outputs',
'apk',
'release',
'app-release-unsigned.apk',
);
if (!exists(File(releaseHostApk))) {
return TaskResult.failure('Failed to build release host APK');
}
section('Check files in release APK');
checkCollectionContains<String>(<String>[
...flutterAssets,
...baseApkFiles,
'lib/arm64-v8a/lib$ffiPackageName.so',
'lib/arm64-v8a/libapp.so',
'lib/arm64-v8a/libflutter.so',
'lib/armeabi-v7a/lib$ffiPackageName.so',
'lib/armeabi-v7a/libapp.so',
'lib/armeabi-v7a/libflutter.so',
], await getFilesInApk(releaseHostApk));
section('Check the NOTICE file is correct');
await inDirectory(hostApp, () async {
final File apkFile = File(releaseHostApk);
final Archive apk = ZipDecoder().decodeBytes(apkFile.readAsBytesSync());
// Shouldn't be missing since we already checked it exists above.
final ArchiveFile? noticesFile = apk.findFile('assets/flutter_assets/NOTICES.Z');
final Uint8List? licenseData = noticesFile?.content as Uint8List?;
if (licenseData == null) {
return TaskResult.failure('Invalid license file.');
}
final String licenseString = utf8.decode(gzip.decode(licenseData));
if (!licenseString.contains('skia') || !licenseString.contains('Flutter Authors')) {
return TaskResult.failure('License content missing.');
}
});
section('Check release AndroidManifest.xml');
final String androidManifestRelease = await getAndroidManifest(debugHostApk);
if (!androidManifestRelease.contains('''
<meta-data
android:name="flutterProjectType"
android:value="module" />''')
) {
return TaskResult.failure("Release host APK doesn't contain metadata: flutterProjectType = module ");
}
section('Check file access modes for read-only asset from Flutter module');
final String readonlyReleaseAssetFilePath = path.joinAll(<String>[
hostApp.path,
'app',
'build',
'intermediates',
'assets',
'release',
'mergeReleaseAssets',
'flutter_assets',
'assets',
'read-only.txt',
]);
final File readonlyReleaseAssetFile = File(readonlyReleaseAssetFilePath);
if (!exists(readonlyReleaseAssetFile)) {
return TaskResult.failure('Failed to copy read-only asset file');
}
modes = readonlyReleaseAssetFile.statSync().modeString();
print('\nread-only.txt file access modes = $modes');
if (modes.compareTo(fileReadWriteMode) != 0) {
return TaskResult.failure('Failed to make assets user-readable and writable');
}
return TaskResult.success(null);
} on TaskResult catch (taskResult) {
return taskResult;
} catch (e) {
return TaskResult.failure(e.toString());
} finally {
rmTree(tempDir);
}
}
}
Future<void> main() async {
await task(combine(<TaskFunction>[
// ignore: avoid_redundant_argument_values
ModuleTest(gradleVersion: '8.7').call,
]));
}

View File

@ -178,6 +178,7 @@ Future<void> main() async {
flutterDirectory.path,
'dev',
'integration_tests',
'pure_android_host_apps',
'android_custom_host_app',
),
),

View File

@ -0,0 +1,2 @@
This directory contains minimal Android apps used by integration tests for testing add-to-app
use cases.

View File

@ -0,0 +1,2 @@
[*.{kt,kts}]
ktlint = disabled

View File

@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties

View File

@ -0,0 +1,2 @@
This directory contains a minimal Android app that uses the Kotlin DSL for its Gradle files.
It is used in add-to-app integration testing.

View File

@ -0,0 +1,70 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
}
android {
namespace = "com.example.myapplication"
compileSdk = 34
defaultConfig {
applicationId = "com.example.myapplication"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
dependencies {
implementation(project(":flutter"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}

View File

@ -0,0 +1,21 @@
# 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

View File

@ -0,0 +1,24 @@
package com.example.myapplication
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.myapplication", appContext.packageName)
}
}

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MyApplication">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@style/Theme.MyApplication"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
/>
</application>
</manifest>

View File

@ -0,0 +1,55 @@
package com.example.myapplication
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.myapplication.ui.theme.MyApplicationTheme
import io.flutter.embedding.android.FlutterActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
startActivity(
FlutterActivity.createDefaultIntent(this)
)
}
}
@Composable
fun Greeting(
name: String,
modifier: Modifier = Modifier
) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
MyApplicationTheme {
Greeting("Android")
}
}

View File

@ -0,0 +1,11 @@
package com.example.myapplication.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@ -0,0 +1,58 @@
package com.example.myapplication.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun MyApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@ -0,0 +1,20 @@
package com.example.myapplication.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography =
Typography(
bodyLarge =
TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@ -0,0 +1,7 @@
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<resources>
<string name="app_name">My Application</string>
</resources>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<resources>
<style name="Theme.MyApplication" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2014 The Flutter Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

View File

@ -0,0 +1,17 @@
package com.example.myapplication
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@ -0,0 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.jetbrains.kotlin.android) apply false
}

View File

@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true

View File

@ -0,0 +1,30 @@
[versions]
agp = "8.5.2"
kotlin = "1.9.0"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

View File

@ -0,0 +1,6 @@
#Mon Oct 14 12:37:17 PDT 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-REPLACEME-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -0,0 +1,26 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
google()
mavenCentral()
val flutterStorageUrl = System.getenv("FLUTTER_STORAGE_BASE_URL") ?: "https://storage.googleapis.com"
maven("$flutterStorageUrl/download.flutter.io")
}
}
rootProject.name = "My Application"
include(":app")
apply(from = File(settingsDir.parentFile.toString() + "/hello/.android/include_flutter.groovy"))

View File

@ -1,5 +1,17 @@
def scriptFile = getClass().protectionDomain.codeSource.location.toURI()
def flutterProjectRoot = new File(scriptFile).parentFile.parentFile
def gradle = null
def flutterProjectRoot = null
// The second block handles the original syntax for including Flutter modules, which used a Groovy
// method that isn't a part of the Kotlin Gradle DSL (setBinding). The first block handles the
// preferred way of including Flutter modules, which is to use the apply from: Gradle syntax.
if (!getBinding().getVariables().containsKey("gradle")) {
gradle = this
flutterProjectRoot = gradle.buildscript.getSourceFile().getParentFile().getParentFile().absolutePath
} else {
gradle = getBinding().getVariables().get("gradle")
def scriptFile = getClass().protectionDomain.codeSource.location.toURI()
flutterProjectRoot = new File(scriptFile).parentFile.parentFile.absolutePath
}
gradle.include ":flutter"
gradle.project(":flutter").projectDir = new File(flutterProjectRoot, ".android/Flutter")