How to know the end of Hero animation in flutter? - flutter

I want to achieve this:
But when Hero animation starts keyboard is forced to dismiss:
I tried to use widgets callback which is triggered after layout. But this callback is fired whenever hero animation starts. I also tried to use Future.delayed(Duration(seconds: 2), but it does not help. Everything is working as expected if I only remove Hero widget from the widget tree.
Here is my first Screen:
import 'package:flutter/material.dart';
import 'package:move_me_delivery/components/rounded_app_bar.dart';
import 'package:move_me_delivery/components/search_field.dart';
import '../screens.dart';
class HomeTab extends StatelessWidget {
const HomeTab({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: RoundedAppBar(title: ""),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
children: [
SearchTextField(
onFocusChange: (val) async {
if(val){
await Navigator.push(context, PageRouteBuilder(
transitionDuration: Duration(milliseconds: 400),
pageBuilder: (_, __, ___) => SearchScreen()));
}
},
)
],
),
)
);
}
}
Here is my second screen:
import 'package:flutter/material.dart';
import 'package:line_awesome_flutter/line_awesome_flutter.dart';
import 'package:move_me_delivery/components/search_field.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({Key? key}) : super(key: key);
#override
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final _focusNode = FocusNode();
#override
void initState() {
super.initState();
_focusNode.requestFocus();
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: SafeArea(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
children: [
SearchTextField(
focus: _focusNode,
onCancel: (){
FocusScope.of(context).unfocus();
Navigator.pop(context);
},
inputDecoration: InputDecoration(
prefixIcon: Icon(LineAwesomeIcons.search, color: Colors.black,),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.blue, width: 1))
),
),
],
),
),
),
),
);
}
}
And finally here is my SearchField screen with Hero animation:
import 'package:flutter/material.dart';
import 'package:line_awesome_flutter/line_awesome_flutter.dart';
import 'package:move_me_delivery/data/styles.dart';
class SearchTextField extends StatefulWidget {
const SearchTextField({Key? key,
this.onFocusChange,
this.focus,
this.onCancel,
this.inputDecoration
}) : super(key: key);
final void Function(bool hasFocus)? onFocusChange;
final FocusNode? focus;
final VoidCallback? onCancel;
final InputDecoration? inputDecoration;
#override
_SearchTextFieldState createState() => _SearchTextFieldState();
}
class _SearchTextFieldState extends State<SearchTextField>{
late FocusNode _focus;
#override
void initState() {
super.initState();
_focus = widget.focus ?? new FocusNode();
_focus.addListener(
(){
if(widget.onFocusChange != null){
widget.onFocusChange!(_focus.hasFocus);
}
}
);
}
#override
Widget build(BuildContext context) {
return Hero(
tag: "search",
child: Material(
type: MaterialType.card,
child: Row(
children: [
Expanded(
child: TextField(style: AppTextStyles.body2,
focusNode: _focus,
decoration: InputDecoration(
prefixIcon: Icon(LineAwesomeIcons.search, color: Colors.black,),
// suffixIcon: Text("Cancel"),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.blue, width: 1))
))),
if(widget.onCancel != null)
GestureDetector(
onTap: widget.onCancel,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Cancel"),
),
)
],
),
),
);
}
}

The reason of keyboard dimiss is when hero animation start flight TextField unmounted so its focus loss and then keyboard dismiss.
And why TextFiled become to be unmounted, you need to understand how Hero Animaiton work, refer this https://docs.flutter.dev/development/ui/animations/hero-animations.
do something at the end of hero, you can do like below:
child: Hero(
tag: "hero_tag",
flightShuttleBuilder: ((flightContext, animation, flightDirection, fromHeroContext, toHeroContext) {
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// the end of hero animation end
_focusNode.requestFocus();
}
});

The interface you want to achieve doesn't necessarily use Hero widgets. It can be done with other animations. But, if you wan't to use Hero, you can try a rather hacky solution:
On your Screen 1, set these two properties in your Hero's TextField:
Hero(
tag: 'search',
child: Material(
type: MaterialType.transparency,
child: TextField(
readOnly: true,
showCursor: true,
onTap: () {
Navigator.push() //to SearchScreen()
}
),
),
),
Then, on Screen 2:
Hero(
tag: 'search',
child: Material(
type: MaterialType.transparency,
child: TextField(
autofocus: true,
),
),
),
You'll have to avoid using the same SearchTextField on both screens; they each need their own as I showed. Also, you can probably remove all of that FocusNode code if you use this method.
Disclaimer: I haven't tested this code. It's just something to try

I solved this exact same issue by creating a FocusNode that'll requestFocus at the end of the hero animation. However, it is also imperative that as a return for the flightShuttleBuilder function you return a widget similar to the one on the destination, except for the fact that it won't include this FocusNode.
Here's how it looks (on the destination page):
child: Hero(
tag: 'your_tag',
flightShuttleBuilder: (_, animation, __, ___, ____) {
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_focusNode.requestFocus();
}
});
return TextField();
},
child: TextField(focusNode: _focusNode),
),

Related

How do I make a text field appear when I click a widget?

Hi, I'm new to the flutter and looking for a way to expand a widget so that when I click on it, I get a text field that allows me to input data from the user.
So far, I've tried dynamic test fields or gesture detectors, but I couldn't find the answer I wanted, so I'm asking questions.
Is there any class that I can refer to?
You need to do few things...
create a variable
bool textFieldDisplayed = false;
Wrap your widget with GestureDetector and use onTap of GestureDetector.
onTap: () {
textFieldDisplayed = true;
setState(() {});
},
check the condition before your textField
if(textFieldDisplayed)
TextFormField()
The whole code is below and you can make some changes as per yours....
class _MyHomePageState extends State<MyHomePage> {
TextEditingController controller = TextEditingController();
bool textFieldDisplayed = false;
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
textFieldDisplayed = false;
setState(() {});
},
child: Scaffold(
body: GestureDetector(
onTap: () {
textFieldDisplayed = true;
setState(() {});
},
child: Center(
child: Container(
color: Colors.blue,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if(textFieldDisplayed)
Padding(
padding: const EdgeInsets.all(10.0),
child: SizedBox(
width: 100,
child: TextFormField(
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 1, color: Colors.white),
borderRadius: BorderRadius.circular(5.0),
),
border: OutlineInputBorder(
borderSide: const BorderSide(width: 1, color: Colors.white),
borderRadius: BorderRadius.circular(5.0),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 1,color: Colors.white),
borderRadius: BorderRadius.circular(5.0),
),
),
controller: controller,
),
),
),
Container(height: 20,width: 100,)
],
),
),
),
),
),
);
}
}
You can achieve this using the Visibility widget. Rohan's answer is correct but I wouldn't recommend using if statements in building widgets in a list since it makes the code look messy. I'll put and example bellow:
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool showWidget = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: Column(
children: [
Visibility(
visible: showWidget,
child: MyWidget()
),
MyButton(
onTap: (){
setState((){
showWidget = !showWidget;
});
}
)
],
),
),
),
);
}
}
class MyWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Text(
'Hello, World!',
style: Theme.of(context).textTheme.headline4,
);
}
}
class MyButton extends StatelessWidget {
final Function() onTap;
const MyButton({required this.onTap});
#override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: const Text('Press me!')
);
}
}
When visibility's value is true, it will display the content of its child property. Otherwise it will return a const SizedBox.shrink() by default. Or, you can change whatever widget you want to return adding the 'replacement' property.
Copy the code above and try on a new DartPad. Good Luck!

How to pass parameters using pushNamedAndRemoveUntil?

I need to pass parameters to the initial page using pushNamedAndRemoveUntil .
I need to re-pass an arg that takes color and textController.text to valueText and color on the initial page. You need to do this with pushNamedAndRemoveUntil.
Please help implement this functionality.
Screen with arg:
class TextValue extends StatefulWidget {
static const routeName = '/text_value';
const TextValue({Key? key}) : super(key: key);
#override
State<TextValue> createState() => _TextValueState();
}
class _TextValueState extends State<TextValue> {
// controller for textField
TextEditingController textController = TextEditingController();
#override
void dispose() {
textController.dispose();
super.dispose();
}
//arg variable
ColorArguments? arg;
#override
Widget build(BuildContext context) {
//get arg from ColorPickerScreen
arg ??= ModalRoute.of(context)!.settings.arguments as ColorArguments;
return Scaffold(
backgroundColor: arg?.color,
appBar: AppBar(
title: const Text('Enter a value'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: textController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'(^\d*\.?\d*)'))
],
style: const TextStyle(color: Colors.black),
decoration: const InputDecoration(
hintText: 'Enter a value',
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2)),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2))),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (textController.text.isEmpty) {
} else {
Navigator.pushNamedAndRemoveUntil(
context, HomeScreen.routeName, (route) => false,
);
}
},
child: const Text('Done'),
)
],
),
),
);
}
}
Initial Screen:
import 'package:flutter/material.dart';
import 'package:flutter_app/screens/color_picker_screen.dart';
class HomeScreen extends StatefulWidget {
final String valueText;
final ColorArguments? color;
static const routeName = '/home';
const HomeScreen({Key? key, required this.valueText, required this.color})
: super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// navigation to the next screen
void _colorScreen() {
Navigator.push(context,
MaterialPageRoute(builder: (context) => const ColorPicker()));
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Home'),
centerTitle: true,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: _colorScreen, child: const Text('Choose a color')),
const SizedBox(height: 30.0),
TextFormField(
readOnly: true,
initialValue: widget.valueText,
),
const SizedBox(height: 100),
Container(
width: 50,
height: 50,
color: widget.color?.color,
),
],
),
),
);
}
}
You can use the named arguments parameter of the pushNamedAndRemoveUntil method. All you need to do is create a custom object for passing all the values you need to the push call. The arguments can be accessed in your initial screen by using
final args = ModalRoute.of(context)!.settings.arguments
You can refer to this Flutter cookbook on navigation using named routes with arguments.
Why dont you consider using Class contructors and passing data like this
Navigator.push(context,
MaterialPageRoute(builder: (context) => const ColorPicker(<Your_data>)));

flutter : How to Update OverlayEntry value instead of create new one

I am trying to use OverlayEntry on onChanged callback from TextField.
body: Center(
child: Container(
child: TextField(
focusNode: _focusNode,
key: _textFieldKey,
style: _textFieldStyle,
onChanged: (String nextText) {
showOverlaidTag(context, nextText);
},
),
width: 400.0,
),
)
The problem is when everytime onChanged is triggered, new Overlay is created and lay on each other? I want to just new text replaced with new text and the old overlay remains?
showOverlaidTag(BuildContext context, String newText) async {
OverlayEntry suggestionTagoverlayEntry = OverlayEntry(
builder: (BuildContext context) => DropDownBody(
focusNode: _focusNode,
newText : newText
),
);
overlayState.insert(suggestionTagoverlayEntry);
//await Future.delayed(Duration(milliseconds: 10000));
suggestionTagoverlayEntry.remove();
}
This is DropDownBody
import 'package:flutter/material.dart';
class DropDownBody extends StatefulWidget {
const DropDownBody({Key? key, required this.focusNode, required this.newText})
: super(key: key);
final FocusNode focusNode;
final String newText;
#override
_DropDownBodyState createState() => _DropDownBodyState();
}
class _DropDownBodyState extends State<DropDownBody> {
#override
Widget build(BuildContext context) {
print("=====> build");
return Positioned(
top: widget.focusNode.offset.dy + 50,
left: 0,
child: Material(
elevation: 4.0,
color: Colors.transparent,
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
color: Colors.lightBlueAccent,
child: Row(
children: [
Expanded(
child: Text(
'Show tag here',
style: TextStyle(
fontSize: 20.0,
),
),
),
],
),
)),
);
}
}
I had to fix a few syntax errors in your code. I implemented the dropdownbody code in part. works great.
class HomePageWidget extends StatefulWidget {
HomePageWidget({Key? key}) : super(key: key);
final formKey=GlobalKey<FormState>();
TextEditingController text_controller= TextEditingController();
#override
_HomePageWidgetState createState() => _HomePageWidgetState();
}
class _HomePageWidgetState extends State<HomePageWidget> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text("test changenotify"),
),
body: Form( key:widget.formKey, child: Column(
children: [
TextFormField(
decoration: InputDecoration(icon: const Icon(Icons.person),
hintText: "Input some text",
labelText: "Input",
),
onChanged: (String nextText){
//debugPrint(nextText);
showOverlaidTag(context,nextText);
//Future<bool> val=showOverlaidTag(context,nextText);
//val.then((result)=>result);
},
controller: widget.text_controller),
],
)));
}
showOverlaidTag(BuildContext context, String newText) async {
OverlayEntry suggestionTagoverlayEntry = OverlayEntry(
builder: (BuildContext context) =>
Positioned(
top:100,
child:
Material(elevation:4.0,
color:Colors.transparent,
child:Container(
width:MediaQuery.of(context).size.width*0.9,
color:Colors.lightBlueAccent,
child:
Row(children: [Expanded(child: Text(
//focusNode: _focusNode,
newText
))],)
),
)));
//overlayState.insert(suggestionTagoverlayEntry);
Overlay.of(context)?.insert(suggestionTagoverlayEntry);
await Future.delayed(Duration(milliseconds: 2000));
suggestionTagoverlayEntry.remove();
return suggestionTagoverlayEntry;
}
}
use boolean and try to call the insert only the first time
if(isOverlayAlreadyRunning == false){
entry = OverlayEntry(builder: (context) {
return Positioned(
);
});
overlay?.insert(entry!);
isOverlayAlreadyRunning = true;
}
and use setState((){}) to update the variable
try this, did a little refactoring for the text, it doesn't change much. Call hideOverlay() when you want to remove it
import 'package:flutter/material.dart';
class DropDownBody extends StatefulWidget {
const DropDownBody({Key? key, required this.focusNode, required this.newText})
: super(key: key);
final FocusNode focusNode;
final String newText;
#override
_DropDownBodyState createState() => _DropDownBodyState();
}
class _DropDownBodyState extends State<DropDownBody> {
OverlayEntry? overlayEntry;
String? text;
void insertOverlay(BuildContext context) {
if (overlayEntry == null || !overlayEntry!.mounted) {
overlayEntry = OverlayEntry(
builder: (context) => DropDownBody(focusNode: _focusNode, newText: text??'sus')
);
final overlay = Overlay.of(context)!;
overlay.insert(overlayEntry!);
}
}
void hideOverlay() {
overlayEntry!.remove();
overlayEntry == null;
}
#override
Widget build(BuildContext context) {
print("=====> build");
return Positioned(
top: widget.focusNode.offset.dy + 50,
left: 0,
child: Material(
elevation: 4.0,
color: Colors.transparent,
child: Container(
width: MediaQuery.of(context).size.width * 0.9,
color: Colors.lightBlueAccent,
child: Row(
children: [
Expanded(
child: Text(
'Show tag here',
style: TextStyle(
fontSize: 20.0,
),
),
),
],
),
)),
);
}
}

No Material widget found. Hero Animation is not working with TextField

I want to achieve this animation with TextField:
but getting this instead:
Here is my TextField widget:
import 'package:flutter/material.dart'; import 'package:line_awesome_flutter/line_awesome_flutter.dart'; import 'package:move_me_delivery/data/styles.dart';
class SearchTextField extends StatefulWidget { const SearchTextField({Key? key,
this.onFocusChange,
this.focus,
this.onCancel,
this.inputDecoration }) : super(key: key);
final void Function(bool hasFocus)? onFocusChange; final FocusNode? focus; final VoidCallback? onCancel; final InputDecoration? inputDecoration;
#override _SearchTextFieldState createState() =>
_SearchTextFieldState(); }
class _SearchTextFieldState extends State<SearchTextField> { FocusNode _focus = new FocusNode();
#override void initState() {
super.initState();
_focus = widget.focus ?? new FocusNode();
_focus.addListener(
(){
if(widget.onFocusChange != null){
widget.onFocusChange!(_focus.hasFocus);
}
}
); }
#override Widget build(BuildContext context) {
return Hero(
tag: "search",
child: Row(
children: [
Expanded(
child: TextField(style: AppTextStyles.body2,
focusNode: _focus,
decoration: InputDecoration(
prefixIcon: Icon(LineAwesomeIcons.search, color: Colors.black,),
// suffixIcon: Text("Cancel"),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.blue, width: 1))
))),
if(widget.onCancel != null)
GestureDetector(
onTap: widget.onCancel,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Cancel"),
),
)
],
),
); } }
And here is my first screen:
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:move_me_delivery/components/rounded_app_bar.dart';
import 'package:move_me_delivery/components/search_field.dart';
import '../screens.dart';
class HomeTab extends StatelessWidget {
const HomeTab({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: RoundedAppBar(title: ""),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
children: [
SearchTextField(
onFocusChange: (val) async {
if(val){
Navigator.push(
context,
PageRouteBuilder(
transitionDuration: Duration(seconds: 3),
pageBuilder: (_, __, ___) => SearchScreen()));
// await Get.to(() => SearchScreen());
}
},
)
],
),
)
);
}
}
and here is my second screen:
import 'package:flutter/material.dart';
import 'package:line_awesome_flutter/line_awesome_flutter.dart';
import 'package:move_me_delivery/components/search_field.dart';
import 'package:move_me_delivery/data/styles.dart';
class SearchScreen extends StatefulWidget {
const SearchScreen({Key? key}) : super(key: key);
#override
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
final _focusNode = FocusNode();
#override
void initState() {
super.initState();
_focusNode.requestFocus();
}
#override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: SafeArea(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
children: [
SearchTextField(
focus: _focusNode,
onCancel: (){
FocusScope.of(context).unfocus();
Navigator.pop(context);
},
inputDecoration: InputDecoration(
prefixIcon: Icon(LineAwesomeIcons.search, color: Colors.black,),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Colors.blue, width: 1))
),
),
],
),
),
),
),
);
}
}
My console:
======== Exception caught by widgets library =======================================================
The following assertion was thrown building TextField(focusNode: FocusNode#0fba2, decoration: InputDecoration(prefixIcon: Icon(IconData(U+0F002), color: Color(0xff000000)), filled: true, fillColor: Color(0xffffffff), border: OutlineInputBorder()), style: TextStyle(inherit: true, color: Color(0xff000000), size: 15.0, weight: 400, style: normal), dirty, dependencies: [MediaQuery, UnmanagedRestorationScope], state: _TextFieldState#43a28):
No Material widget found.
TextField widgets require a Material widget ancestor.
In material design, most widgets are conceptually "printed" on a sheet of material. In Flutter's material library, that material is represented by the Material widget. It is the Material widget that renders ink splashes, for instance. Because of this, many material library widgets require that there be a Material widget in the tree above them.
To introduce a Material widget, you can either directly include one, or use a widget that contains Material itself, such as a Card, Dialog, Drawer, or Scaffold.
The specific widget that could not find a Material ancestor was: TextField
focusNode: FocusNode#0fba2
decoration: InputDecoration(prefixIcon: Icon(IconData(U+0F002), color: Color(0xff000000)), filled: true, fillColor: Color(0xffffffff), border: OutlineInputBorder())
style: TextStyle(inherit: true, color: Color(0xff000000), size: 15.0, weight: 400, style: normal)
dirty
dependencies: [MediaQuery, UnmanagedRestorationScope]
state: _TextFieldState#43a28
The ancestors of this widget were:
: Expanded
flex: 1
: Row
direction: horizontal
mainAxisAlignment: start
crossAxisAlignment: center
dependencies: [Directionality]
renderObject: RenderFlex#afc02
: GetMaterialApp
: MyApp
...
The relevant error-causing widget was:
TextField file:///Users/akbarpulatov/Desktop/tests/move_me_delivery/lib/components/search_field.dart:49:20
When the exception was thrown, this was the stack:
#0 debugCheckHasMaterial.<anonymous closure> (package:flutter/src/material/debug.dart:27:7)
#1 debugCheckHasMaterial (package:flutter/src/material/debug.dart:48:4)
#2 _TextFieldState.build (package:flutter/src/material/text_field.dart:1116:12)
#3 StatefulElement.build (package:flutter/src/widgets/framework.dart:4691:27)
#4 ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:4574:15)
...
====================================================================================================
The error message you got already explains the problem very well, so I'll just give you a solution. Wrap the child of your Hero widget in a Material:
Hero(
tag: "search",
child: Material(
type: MaterialType.transparency,
child: Row(
children: [
//TextField(),
//GestureDetector(),
],
),
),
);
An alternative fix is to wrap the Inkwell in Material when the transition occurs:
Source: https://github.com/flutter/flutter/issues/34119
Hero(
tag: "image",
flightShuttleBuilder: (BuildContext flightContext, Animation<double> animation, HeroFlightDirection flightDirection, BuildContext fromHeroContext, BuildContext toHeroContext) => Material(child: toHeroContext.widget),
...

Method is called twice in StreamBuilder which contains custom dialog in Flutter

I create a loading dialog and put it in StreamBuilder. At the same time, there is a method named _loadingText as the dialog parameter. When I click the 'Go Run' button, the _loadingText method is called twice.
As the same way, I used the flutter build-in dialog showAboutDialog, everything is OK.
If I remove the StreamBuilder, the _loadingText is called once too.
It takes me one day!!!
Any help is appreciated. Thanks in advance...
main.dart:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:view_animation/loading_dialog.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
StreamController<String> _streamController;
TextEditingController _inputController;
#override
void initState() {
super.initState();
_streamController = StreamController<String>.broadcast();
_inputController = TextEditingController();
_inputController.addListener(() {
_streamController.add(_inputController.text);
});
}
#override
void dispose() {
super.dispose();
_streamController.close();
}
String _loadingText() {
print('===== 2. Method run OVER =====');
return 'Loading...';
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_inputContainer(),
SizedBox(
height: 20,
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(26),
),
child: StreamBuilder(
stream: _streamController.stream.map((text) => text.length > 4),
builder: (context, snap) {
return FlatButton(
color: Color(0xFFFFAC0B),
disabledColor: Colors.black12,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
padding: EdgeInsets.symmetric(vertical: 15, horizontal: 12.5),
onPressed: snap.data != null && snap.data
? () {
print('===== 1. show dialog =====');
showDialog(
context: context,
builder: (BuildContext context) {
return LoadingDialog(
loadingText: _loadingText(),
);
});
// showAboutDialog(context: context, applicationName: _loadingText());
}
: null,
child: Text(
'GO RUN',
style: TextStyle(fontSize: 12, color: Colors.white),
),
);
},
),
),
],
)),
);
}
Widget _inputContainer() {
return Container(
width: 200,
padding: EdgeInsets.only(left: 20, right: 20),
decoration: BoxDecoration(
color: Color(0xFFFFAC0B),
borderRadius: BorderRadius.circular(36.0),
),
child: TextField(
controller: _inputController,
keyboardType: TextInputType.number,
maxLines: 1,
cursorColor: Colors.orange,
style: TextStyle(
color: Colors.white,
fontSize: 24,
),
decoration: InputDecoration(
border: InputBorder.none,
hintText: "Let's GO",
hintStyle: TextStyle(color: Colors.white54, fontSize: 20),
),
),
);
}
}
loading_dialog.dart
import 'package:flutter/material.dart';
class LoadingDialog extends StatefulWidget {
final String loadingText;
final bool outsideDismiss;
final Function dismissCallback;
final Future<dynamic> requestCallback;
LoadingDialog(
{Key key,
this.loadingText = "Loading...",
this.outsideDismiss = true,
this.dismissCallback,
this.requestCallback,
})
: super(key: key);
#override
_LoadingDialogState createState() => _LoadingDialogState();
}
class _LoadingDialogState extends State<LoadingDialog> {
void _dismissDialog(){
if(widget.dismissCallback != null) {
widget.dismissCallback();
}
Navigator.of(context).pop();
}
#override
void initState() {
print('===== 3. loading init =====');
if (widget.requestCallback != null) {
widget.requestCallback.then((_) => Navigator.of(context).pop());
}
super.initState();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.outsideDismiss ? _dismissDialog : null,
child: Material(
type: MaterialType.transparency,
child: Center(
child: SizedBox(
width: 120.0,
height: 120.0,
child: Container(
decoration: ShapeDecoration(
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0)
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
new CircularProgressIndicator(),
new Padding(
padding: const EdgeInsets.only(
top: 20.0,
),
child: new Text(
widget.loadingText,
style: new TextStyle(fontSize: 12.0),
),
),
],
),
),
),
),
),
);
}
}
log gif here
That's because when you tap on button first time your TextField is still active that means new state comes and flutter rebuilds itself. When you tap on button second your Textfield is inactive.
The points are when you pass the function to the onTap widget it's going to execute when it building state and calling a function without tapping on it:
So instead of a passing method to the OnTap, try something like this:
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () widget.outsideDismiss ? ()
{
this._dismissDialog();
} : null,
...