build: Android's built-in Kotlin, the three plugins applying KGP themselves patched in third_party/

AGP 9 compiles Kotlin itself (android.builtInKotlin=true in every app),
and warns that applying the Kotlin Gradle Plugin will soon fail.
flutter_web_auth_2 5.1.0, keyboard_height_plugin 0.3.0 and flutter_tts
4.2.5 still apply it unconditionally, so their pub releases are copied
into third_party/ with only their android/build.gradle changed, and
the root overrides them by path (third_party/README.md says when to
drop each). The servers' images copy third_party/, as pub refuses an
override whose path is missing. third_party/ isn't our own code: left
out of the analysis and the format check.

All four apps' APKs build without the warning. A Kotlin daemon's
session file that slipped into Train's android/ goes, and every app's
android/.gitignore keeps .kotlin/ out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-04 00:15:55 +01:00
commit 438ce20dbf
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
15 changed files with 8440 additions and 23 deletions

View file

@ -0,0 +1,67 @@
## 0.3.0
### iOS Updates
* Add Swift Package Manager support (fixes #17)
* Flutter 3.44+ will default to SwiftPM; CocoaPods support is retained for backward compatibility
* Added `Package.swift` manifest at `ios/keyboard_height_plugin/`
* Source files are symlinked so both CocoaPods and SwiftPM resolve the same code
## 0.2.0
**BREAKING CHANGES:**
This version requires modern Android build tooling. For projects using older versions, please continue using version 0.1.5.
### Requirements
- Android Gradle Plugin 8.6.0+
- Gradle 8.7+
- Kotlin 2.1.0+
- Java 17+
### Android Updates
* **BREAKING:** Update compileSdkVersion from 31 to 34 (fixes #14)
* **BREAKING:** Update Android Gradle Plugin from 7.2.0 to 8.6.0
* **BREAKING:** Update Kotlin from 1.7.10 to 2.1.0
* **BREAKING:** Migrate to declarative Gradle plugin system (Gradle 8.x requirement)
* Add namespace declaration for AGP 8.6+ compatibility
* minSdkVersion remains 16 (Android 4.1+) - no change to device compatibility
### iOS Updates
* Add Privacy Manifest (PrivacyInfo.xcprivacy) required for App Store submissions (fixes #6)
* Fix example app Podfile configuration to eliminate RunnerTests errors
### Infrastructure
* Add comprehensive CI/CD pipeline with GitHub Actions
* Add automated testing for code analysis, formatting, and builds
* Add PR template with testing checklist
* Add TESTING.md documentation
* Update example app .gitignore for Flutter-generated files
### Migration Guide
If you need to upgrade from 0.1.5:
1. Update your project to use AGP 8.6+, Gradle 8.7+, Kotlin 2.1+, and Java 17+
2. Follow the Flutter AGP migration guide: https://docs.flutter.dev/release/breaking-changes/flutter-gradle-plugin-apply
If your project cannot upgrade these dependencies yet, remain on version 0.1.5.
## 0.0.1
* Initial release.
## 0.0.2
* Documentation.
## 0.0.3
* More documentation.
## 0.0.4
* N/A
## 0.0.5
* Support for AGP 8.0.0
## 0.1.5
* Merge in fix for incorrect height when navigation bar is not visible. https://github.com/nschairer/keyboard_height_plugin/pull/8/files

View file

@ -0,0 +1,29 @@
BSD 3-Clause License
Copyright (c) 2023, Noah Schairer
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,85 @@
# Keyboard Height Plugin for Flutter
`keyboard_height_plugin` is a Flutter plugin for iOS and Android that provides the keyboard size before the keyboard animation occurs for showing or hiding it. This helps eliminate lag when positioning widgets around the keyboard, such as placing a TextField above the keyboard.
## Requirements
### Version 0.3.0+ (Current)
- **Android:**
- Android Gradle Plugin (AGP) 8.6.0+
- Gradle 8.7+
- Kotlin 2.1.0+
- Java 17+
- minSdkVersion 16 (Android 4.1+)
- compileSdkVersion 34+
- **iOS:**
- iOS 12.0+
- Supports both CocoaPods and Swift Package Manager
- Includes Privacy Manifest for App Store compliance
### Version 0.1.5 (Legacy Support)
For projects that cannot upgrade to the modern build tools, use version 0.1.5:
- Android Gradle Plugin 7.x
- Kotlin 1.7.x
- Java 11+
## Installation
To install `keyboard_height_plugin`, add it to your `pubspec.yaml` file under the `dependencies` section:
```yaml
dependencies:
keyboard_height_plugin: ^0.3.0
```
**For legacy projects:**
```yaml
dependencies:
keyboard_height_plugin: 0.1.5
```
## Usage
To use the `keyboard_height_plugin`, first import it in your Dart file:
```dart
import 'package:keyboard_height_plugin/keyboard_height_plugin.dart';
```
Next, create a stateful widget and declare a variable to store the keyboard height and create an instance of the `KeyboardHeightPlugin`:
```dart
class _HomePageState extends State<HomePage>; {
double _keyboardHeight = 0;
final KeyboardHeightPlugin _keyboardHeightPlugin = KeyboardHeightPlugin();
// ... rest of code ...
}
```
Then, initialize the `KeyboardHeightPlugin` in your `initState` method and listen for changes in the keyboard height:
```dart
@override
void initState() {
super.initState();
_keyboardHeightPlugin.onKeyboardHeightChanged((double height) {
setState(() {
_keyboardHeight = height;
});
});
}
```
Use the `_keyboardHeight` variable to position your widgets around the keyboard.
## Example
For a complete example on how to use the `keyboard_height_plugin`, please refer to the [`example`](example) directory in the repository.
## Contributing
If you encounter any issues or have suggestions for improvements, feel free to open an issue or submit a pull request on the project's GitHub repository.
## License
This plugin is licensed under the [BSD 3-Clause License](LICENSE).

View file

@ -0,0 +1,53 @@
group 'com.nschairer.keyboard_height_plugin'
version '1.0-SNAPSHOT'
buildscript {
ext.kotlin_version = '2.1.0'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.6.0'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 34
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
minSdkVersion 16
}
if (project.android.hasProperty("namespace")) {
namespace 'com.nschairer.keyboard_height_plugin'
}
}
// Halkyon's patch (4 Nov 2026): Kotlin built into the Android Gradle plugin
// (AGP 9, android.builtInKotlin=true) rather than the Kotlin Gradle plugin
// applied here, as Flutter's guide for plugin authors has it.
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
}
}

View file

@ -0,0 +1 @@
rootProject.name = 'keyboard_height_plugin'

View file

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.nschairer.keyboard_height_plugin">
</manifest>

View file

@ -0,0 +1,93 @@
package com.nschairer.keyboard_height_plugin
import android.graphics.Rect
import android.os.Build
import androidx.annotation.NonNull
import android.view.ViewTreeObserver
import androidx.annotation.RequiresApi
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.EventChannel
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
class KeyboardHeightPlugin : FlutterPlugin, EventChannel.StreamHandler, ActivityAware {
private val keyboardHeightEventChannelName = "keyboardHeightEventChannel"
private var eventSink: EventChannel.EventSink? = null
private var eventChannel: EventChannel? = null
private var activityPluginBinding: ActivityPluginBinding? = null
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, keyboardHeightEventChannelName)
eventChannel?.setStreamHandler(this)
}
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
eventChannel?.setStreamHandler(null)
}
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
val rootView = activityPluginBinding?.activity?.window?.decorView?.rootView
rootView?.viewTreeObserver?.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
val r = Rect()
rootView.getWindowVisibleDisplayFrame(r)
val screenHeight = rootView.height
val navigationBarHeight = getNavigationBarHeight();
var keypadHeight = screenHeight - r.bottom
if (isNavigationBarVisible()) {
keypadHeight -= navigationBarHeight
}
val displayMetrics = activityPluginBinding?.activity?.resources?.displayMetrics
val logicalKeypadHeight = keypadHeight / (displayMetrics?.density ?: 1f)
if (keypadHeight > screenHeight * 0.15) {
events?.success(logicalKeypadHeight.toDouble())
} else {
events?.success(0.0)
}
}
})
}
override fun onCancel(arguments: Any?) {
eventSink = null
}
private fun getNavigationBarHeight(): Int {
val resourceId = activityPluginBinding?.activity?.resources?.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resourceId != null && resourceId > 0) {
activityPluginBinding?.activity?.resources?.getDimensionPixelSize(resourceId) ?: 0
} else {
0
}
}
private fun isNavigationBarVisible(): Boolean {
val decorView = activityPluginBinding?.activity?.window?.decorView
val rootWindowInsets = decorView?.rootWindowInsets ?: return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
rootWindowInsets.isVisible(android.view.WindowInsets.Type.navigationBars())
} else {
val systemWindowInsetBottom = rootWindowInsets.systemWindowInsetBottom
systemWindowInsetBottom > 0
}
}
// Implement ActivityAware methods
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityPluginBinding = binding
}
override fun onDetachedFromActivityForConfigChanges() {
activityPluginBinding = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activityPluginBinding = binding
}
override fun onDetachedFromActivity() {
activityPluginBinding = null
}
}

View file

@ -0,0 +1,47 @@
import Flutter
import UIKit
public class KeyboardHeightPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private var eventChannel: FlutterEventChannel?
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = KeyboardHeightPlugin()
instance.eventChannel = FlutterEventChannel(name: "keyboardHeightEventChannel", binaryMessenger: registrar.messenger())
instance.eventChannel?.setStreamHandler(instance)
}
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
eventSink = events
registerKeyboardObservers()
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
unregisterKeyboardObservers()
return nil
}
private func registerKeyboardObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
private func unregisterKeyboardObservers() {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
@objc private func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo,
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
eventSink?(NSNumber(value: keyboardFrame.height))
}
}
@objc private func keyboardWillHide(notification: NSNotification) {
eventSink?(NSNumber(value: 0.0))
}
}

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -0,0 +1,24 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint keyboard_height_plugin.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'keyboard_height_plugin'
s.version = '0.0.1'
s.summary = 'A new Flutter plugin project.'
s.description = <<-DESC
A new Flutter plugin project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.resource_bundles = {'keyboard_height_plugin_privacy' => ['PrivacyInfo.xcprivacy']}
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

View file

@ -0,0 +1,23 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "keyboard_height_plugin",
platforms: [
.iOS(.v12)
],
products: [
.library(name: "keyboard-height-plugin", targets: ["keyboard_height_plugin"])
],
dependencies: [],
targets: [
.target(
name: "keyboard_height_plugin",
resources: [
.process("PrivacyInfo.xcprivacy")
]
)
]
)

View file

@ -0,0 +1,47 @@
import Flutter
import UIKit
public class KeyboardHeightPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private var eventChannel: FlutterEventChannel?
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = KeyboardHeightPlugin()
instance.eventChannel = FlutterEventChannel(name: "keyboardHeightEventChannel", binaryMessenger: registrar.messenger())
instance.eventChannel?.setStreamHandler(instance)
}
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
eventSink = events
registerKeyboardObservers()
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
unregisterKeyboardObservers()
return nil
}
private func registerKeyboardObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
private func unregisterKeyboardObservers() {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
@objc private func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo,
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
eventSink?(NSNumber(value: keyboardFrame.height))
}
}
@objc private func keyboardWillHide(notification: NSNotification) {
eventSink?(NSNumber(value: 0.0))
}
}

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -0,0 +1,29 @@
import 'dart:async';
import 'package:flutter/services.dart';
typedef KeyboardHeightCallback = void Function(double height);
class KeyboardHeightPlugin {
static const EventChannel _keyboardHeightEventChannel =
EventChannel('keyboardHeightEventChannel');
StreamSubscription? _keyboardHeightSubscription;
void onKeyboardHeightChanged(KeyboardHeightCallback callback) {
if (_keyboardHeightSubscription != null) {
_keyboardHeightSubscription!.cancel();
}
_keyboardHeightSubscription =
_keyboardHeightEventChannel.receiveBroadcastStream().listen(
(dynamic height) {
callback(height as double);
},
);
}
void dispose() {
if (_keyboardHeightSubscription != null) {
_keyboardHeightSubscription!.cancel();
}
}
}

View file

@ -0,0 +1,72 @@
name: keyboard_height_plugin
description: Flutter plugin that emits keyboard height before it shows (ios/android)
version: 0.3.0
repository: https://github.com/nschairer/keyboard_height_plugin
environment:
sdk: '>=2.19.6 <4.0.0'
flutter: ">=2.5.0"
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# This section identifies this Flutter project as a plugin project.
# The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
# which should be registered in the plugin registry. This is required for
# using method channels.
# The Android 'package' specifies package in which the registered class is.
# This is required for using method channels on Android.
# The 'ffiPlugin' specifies that native code should be built and bundled.
# This is required for using `dart:ffi`.
# All these are used by the tooling to maintain consistency when
# adding or updating assets for this project.
plugin:
platforms:
android:
package: com.nschairer.keyboard_height_plugin
pluginClass: KeyboardHeightPlugin
ios:
pluginClass: KeyboardHeightPlugin
# To add assets to your plugin package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/assets-and-images/#from-packages
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# To add custom fonts to your plugin package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/custom-fonts/#from-packages