Closed LostInDarkMath closed 1 year ago
Thx @LostInDarkMath You have an example to reproduce? Our examples seemed to work fine. Will try to take a look asap 🙇
Here is an minimal example that can reproduce this bug:
emoji_picker_flutter: 1.4.0
keyboard_utils: ^1.3.4
import 'dart:io';
import 'package:emoji_picker_flutter/emoji_picker_flutter.dart';
import 'package:flutter/material.dart' hide KeyboardListener;
import 'package:flutter/foundation.dart' as foundation;
import 'package:keyboard_utils/keyboard_utils.dart';
import 'package:keyboard_utils/keyboard_listener.dart';
void main() => runApp(const MyApp());
class EmojiPickerWidget extends StatelessWidget {
const EmojiPickerWidget({
required this.textEditingController,
super.key,
});
final TextEditingController textEditingController;
@override
Widget build(BuildContext context) {
final shortestSide = MediaQuery.of(context).size.shortestSide;
final theme = Theme.of(context);
return EmojiPicker(
textEditingController: textEditingController,
config: Config(
columns: shortestSide ~/ 50,
emojiSizeMax: 32 * (!foundation.kIsWeb && Platform.isIOS ? 1.30 : 1.0),
verticalSpacing: 0,
horizontalSpacing: 0,
initCategory: Category.RECENT,
bgColor: theme.backgroundColor,
indicatorColor: theme.indicatorColor,
iconColor: theme.iconTheme.color!,
iconColorSelected: theme.primaryColor,
loadingIndicator: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color> (Colors.blue),
),
),
showRecentsTab: true,
recentsLimit: 28,
noRecents: Padding(
padding: const EdgeInsets.all(15.0),
child: Text(
'no recent emojis',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
color: theme.brightness == Brightness.dark
? Colors.white60
: Colors.black38,
),
),
),
tabIndicatorAnimDuration: kTabScrollDuration,
categoryIcons: const CategoryIcons(),
buttonMode: ButtonMode.MATERIAL,
),
);
}
}
class Chat extends StatefulWidget {
const Chat({
super.key,
});
@override
State<Chat> createState() => _ChatState();
}
class _ChatState extends State<Chat> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final _textController = TextEditingController();
final _keyboardUtils = KeyboardUtils();
late int _idKeyboardListener;
final focusNode = FocusNode();
bool isEmojiKeyboardVisible = false;
bool isKeyboardVisible = false;
double _keyboardHeight = 0;
final Set<double> _possibleKeyboardHeights = {};
set keyboardHeight(double value) {
if(!_possibleKeyboardHeights.contains(value)){
return;
}
if(value == _keyboardHeight) {
return;
}
_keyboardHeight = value;
}
@override
void initState() {
super.initState();
_keyboardHeight = 300;
_idKeyboardListener = _keyboardUtils.add(
listener: KeyboardListener(
willHideKeyboard: () {
if(isKeyboardVisible) {
isKeyboardVisible = false;
isEmojiKeyboardVisible = false;
} else {
}
setState(() {});
},
willShowKeyboard: (maybeCorrectKeyboardHeight) async {
_possibleKeyboardHeights
..add(maybeCorrectKeyboardHeight)
..add(maybeCorrectKeyboardHeight + WidgetsBinding.instance.window.viewPadding.top / WidgetsBinding.instance.window.devicePixelRatio);
isKeyboardVisible = true;
isEmojiKeyboardVisible = true;
setState(() {});
},
),
);
}
@override
void didChangeDependencies(){
super.didChangeDependencies();
keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
}
@override
void dispose(){
_keyboardUtils.unsubscribeListener(subscribingId: _idKeyboardListener);
if (_keyboardUtils.canCallDispose()) {
_keyboardUtils.dispose();
}
focusNode.dispose();
_textController.dispose();
super.dispose();
}
Future<void> onEmojiButtonPressed() async {
if(isEmojiKeyboardVisible){
if(isKeyboardVisible){
FocusManager.instance.primaryFocus?.unfocus();
isKeyboardVisible = false;
} else {
focusNode.unfocus();
await Future<void>.delayed(const Duration(milliseconds: 1));
if(!mounted) return;
FocusScope.of(context).requestFocus(focusNode);
}
} else {
assert(!isKeyboardVisible, 'keyboard is visible');
setState(() {
isEmojiKeyboardVisible = true;
});
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
Expanded(
child: SizedBox(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Expanded(
child: Container(
color: Colors.green,
height: 200,
),
),
Row(
children: [
IconButton(
icon: Icon(isKeyboardVisible || !isEmojiKeyboardVisible ? Icons.emoji_emotions_outlined : Icons.keyboard_rounded),
onPressed: onEmojiButtonPressed,
),
Expanded(
child: TextField(
controller: _textController,
focusNode: focusNode,
),
),
],
),
],
),
),
),
Offstage(
offstage: !isEmojiKeyboardVisible,
child: SizedBox(
height: _keyboardHeight,
child: EmojiPickerWidget(
textEditingController: _textController,
),
),
),
],
),
);
}
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('Bug demo'),
),
body: const Chat(),
);
}
}
Toggle between the keyboard and the emoji picker by using the IconButton
. But the error only occurs sometimes. It's not very deterministic but the probability is high enough hopefully ;)
The following assertion was thrown during a scheduler callback:
Controller's length property (9) does not match the number of tabs (17) present in TabBar's tabs property.
When the exception was thrown, this was the stack:
#0 _TabBarState._debugScheduleCheckHasValidTabsCount.<anonymous closure>.<anonymous closure> (package:flutter/src/material/tabs.dart:1162:11)
#1 _TabBarState._debugScheduleCheckHasValidTabsCount.<anonymous closure> (package:flutter/src/material/tabs.dart:1168:8)
#2 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1175:15)
#3 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1113:9)
#4 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1015:5)
#5 _invoke (dart:ui/hooks.dart:148:13)
#6 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:318:5)
#7 _drawFrame (dart:ui/hooks.dart:115:31)
Hope that helps :)
@LostInDarkMath Thanks I could reproduce the issue and found that if noRecents
Widget is not a const it's causing this issue. I'm not exactly sure why yet, but I assume having a non-const Widget in Config forces Flutter to rebuild it every time and then didUpdateWidget
is calling _updateEmojis
which is async and multiple rebuilds will lead to race condition and more tabs than we want :)
I think it's the same issue described in #81.
I will think of an solution or happy to hear suggestions, but for now I recommend to make sure all Widgets used inside Config are being a const
Widget.
@Fintasys Thanks for your fast reply!
Unfortunately, it is impossible to make noRecents
a constant Widget
because the displayed text is localized so that every user sees the text in his language.
I'll hope you find another solution for this. Have a nice day!
Wrapping your code inside a Stateless/Stateful Widget (own context) with const constructor also works. But I will try keep looking for better solution, i'm sure there must be something :)
noRecents: const NoRecentEmoji(),
class NoRecentEmoji extends StatelessWidget {
const NoRecentEmoji({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.all(15.0),
child: Text(
'no recent emojis',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 20,
color: theme.brightness == Brightness.dark
? Colors.white60
: Colors.black38,
),
),
);
}
}
Will close this issue for now. I have added information about it in ReadMe. If I come across a better solution I will add it. Thank you again for the example to replicate 🙇
Thank you, that works fine! :+1:
Context:
The bug was not in version 1.3.1 of this library.