active_keyboard_language 0.0.1
active_keyboard_language: ^0.0.1 copied to clipboard
Detects and streams the active keyboard (IME) language on iOS and Android, with an RTL-aware Directionality helper widget.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:active_keyboard_language/active_keyboard_language.dart';
void main() {
runApp(const MyApp());
}
const _seedColor = Color(0xFF3F51B5);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'keyboard_language',
themeMode: ThemeMode.system,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: _seedColor),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: _seedColor,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const KeyboardLanguageDemo(),
);
}
}
class KeyboardLanguageDemo extends StatefulWidget {
const KeyboardLanguageDemo({super.key});
@override
State<KeyboardLanguageDemo> createState() => _KeyboardLanguageDemoState();
}
class _LogEntry {
_LogEntry(this.label, this.language, this.time);
final String label;
final String? language;
final DateTime time;
}
class _KeyboardLanguageDemoState extends State<KeyboardLanguageDemo> {
String? _currentLanguage;
final List<_LogEntry> _log = [];
final _textController = TextEditingController();
StreamSubscription<String?>? _subscription;
@override
void initState() {
super.initState();
KeyboardLanguage.getCurrentLanguage().then((language) {
if (!mounted) return;
setState(() {
_currentLanguage = language;
_log.insert(0, _LogEntry('initial', language, DateTime.now()));
});
});
_subscription = KeyboardLanguage.onLanguageChanged.listen((language) {
if (!mounted) return;
setState(() {
_currentLanguage = language;
_log.insert(0, _LogEntry('changed', language, DateTime.now()));
});
});
}
@override
void dispose() {
_subscription?.cancel();
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isRtl = KeyboardLanguage.isRtl(_currentLanguage);
return Scaffold(
appBar: AppBar(
title: const Text('keyboard_language'),
centerTitle: false,
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
children: [
_LanguageStatusCard(language: _currentLanguage, isRtl: isRtl),
const SizedBox(height: 24),
Text(
'Type below, then tap the globe key to switch keyboard language.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
'The field clears itself on every switch — mixing scripts in '
'one field can otherwise land new characters in the wrong '
'place, since Android resets the caret when the IME subtype '
'changes.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
KeyboardAwareDirectionality(
controller: _textController,
child: TextField(
controller: _textController,
style: theme.textTheme.titleMedium,
decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(14),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
hintText: 'Type here…',
),
),
),
const SizedBox(height: 28),
Text('Event log', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
_EventLog(entries: _log),
],
),
),
);
}
}
class _LanguageStatusCard extends StatelessWidget {
const _LanguageStatusCard({required this.language, required this.isRtl});
final String? language;
final bool isRtl;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(20),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'CURRENT KEYBOARD LANGUAGE',
style: theme.textTheme.labelSmall?.copyWith(
color: theme.colorScheme.onPrimaryContainer.withValues(
alpha: 0.7,
),
letterSpacing: 0.8,
),
),
const SizedBox(height: 6),
Text(
language ?? 'unknown',
style: theme.textTheme.headlineMedium?.copyWith(
color: theme.colorScheme.onPrimaryContainer,
fontFamily: 'monospace',
fontWeight: FontWeight.w700,
),
),
],
),
),
_DirectionBadge(isRtl: isRtl),
],
),
);
}
}
class _DirectionBadge extends StatelessWidget {
const _DirectionBadge({required this.isRtl});
final bool isRtl;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final color = isRtl
? theme.colorScheme.tertiary
: theme.colorScheme.outline;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(999),
border: Border.all(color: color.withValues(alpha: 0.4)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isRtl
? Icons.format_textdirection_r_to_l
: Icons.format_textdirection_l_to_r,
size: 16,
color: color,
),
const SizedBox(width: 6),
Text(
isRtl ? 'RTL' : 'LTR',
style: theme.textTheme.labelMedium?.copyWith(
color: color,
fontWeight: FontWeight.w700,
),
),
],
),
);
}
}
class _EventLog extends StatelessWidget {
const _EventLog({required this.entries});
final List<_LogEntry> entries;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (entries.isEmpty) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
),
child: Text(
'No events yet.',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
);
}
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(14),
),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
for (final (index, entry) in entries.indexed) ...[
if (index > 0)
Divider(height: 1, color: theme.colorScheme.outlineVariant),
_EventLogRow(entry: entry, isLatest: index == 0),
],
],
),
);
}
}
class _EventLogRow extends StatelessWidget {
const _EventLogRow({required this.entry, required this.isLatest});
final _LogEntry entry;
final bool isLatest;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final changed = entry.label == 'changed';
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
child: Row(
children: [
Icon(
changed ? Icons.swap_horiz : Icons.play_circle_outline,
size: 18,
color: isLatest
? theme.colorScheme.primary
: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 10),
Expanded(
child: Text(
entry.language ?? 'unknown',
style: theme.textTheme.bodyMedium?.copyWith(
fontFamily: 'monospace',
fontWeight: isLatest ? FontWeight.w700 : FontWeight.w500,
),
),
),
Text(
_formatTime(entry.time),
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
],
),
);
}
static String _formatTime(DateTime time) {
String two(int n) => n.toString().padLeft(2, '0');
return '${two(time.hour)}:${two(time.minute)}:${two(time.second)}';
}
}