Related
I'm working on an audiobook listening app. There I have my main page, which consists of 3 page widgets linked through a Page View that I can swipe to navigate through these pages. On each page I have laid exactly one banner ad from google_mobile_ads package. The problem is that if I add those 3 banners, my app starts to lag a lot, I did check the release version of my app, it is still laggy. In order to understand better the situation I will explain how my ad loading works.
I have a file, where I store the classes regarding those banner ads:
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/material.dart';
late AnchoredAdaptiveBannerAdSize adaptiveBannerSize;
class AdWidgets {
static Container? libraryPag;
static Container? homePag;
static Container? settingsPag;
}
void _setLoaded() {
AdState.loaded = true;
}
class AdState {
static bool loaded = false;
late Future<InitializationStatus> initialization;
AdState(this.initialization);
String get bannerAdUnitId => 'ca-app-pub-3940256099942544/6300978111';
BannerAdListener get adListener => _adListener;
BannerAdListener _adListener = BannerAdListener(
onAdLoaded: (ad) {
_setLoaded();
}
);
}
There is the AdState class which I use to load my banner ads by providing them a BannerAdListener and an AdUnitId, the bool loaded is used in the widgets that display those banners in order for them to know if the ads where loaded and can be displayed properly.
Next I have a Stateful Widget Content, that I use to navigate through my app routes:
class _ContentState extends State<Content> with SingleTickerProviderStateMixin {
#override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
final adState = Provider.of<AdState>(context);
adState.initialization.then((status) {
AdWidgets.libraryPag = Container(
width: MediaQuery.of(context).size.width,
height: adaptiveBannerSize.height.toDouble(),
child: AdWidget(
ad: BannerAd(
adUnitId: adState.bannerAdUnitId,
size: adaptiveBannerSize,
request: AdRequest(),
listener: adState.adListener
)
..load(),
)
);
AdWidgets.homePag = Container(
width: 320,
height: 100,
child: AdWidget(ad: BannerAd(
adUnitId: adState.bannerAdUnitId,
size: AdSize.largeBanner,
request: AdRequest(),
listener: adState.adListener
)
..load()),
);
AdWidgets.settingsPag = Container(
width: 320,
height: 250,
child: AdWidget(ad: BannerAd(
adUnitId: adState.bannerAdUnitId,
size: AdSize.mediumRectangle,
request: AdRequest(),
listener: adState.adListener
)
..load()
)
);
});
}
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: Settings.theme,
builder: (context, value, _) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Settings.theme.value == 'Dark' ? Brightness.light : Brightness.dark,
));
return WillPopScope(
onWillPop: () async {
if (bottomBarIndex.value != 1) {
moveHome();
}
return false;
},
child: Scaffold(
resizeToAvoidBottomInset: false,
body: Navigator(
key: Content.contentNavigatorKey,
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
Widget builder;
switch (settings.name) {
case '/':
builder = MainPage();
break;
case '/bookPage':
Book book = settings.arguments as Book;
builder = BookPage(book: book);
bottomBarIndex.value = -1;
break;
case '/addBook':
builder = AddBookPage();
bottomBarIndex.value = -1;
break;
case '/changeCover':
Book book = settings.arguments as Book;
builder = ChangeCoverPage(book: book);
bottomBarIndex.value = -1;
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
return PageRouteBuilder(
transitionDuration: Duration(milliseconds: 300),
transitionsBuilder:
(context, animation, secAnimation, child) {
animation = CurvedAnimation(
parent: animation, curve: Curves.easeIn);
return FadeTransition(
opacity: animation,
child: child,
);
},
pageBuilder: (context, animation, secAnimation) {
return builder;
});
},
onPopPage: (route, result) {
return route.didPop(result);
},
),
));
},
);
}
}
In the didChangeDependencies method I initialize the adState (the way it is done in the Monetizing apps with Flutter official tutorial) and then assign three banner widgets to the each of the static Container from the AdWidgets custom class. Then I just display those Containers as I please, for example:
class LibraryPage extends StatefulWidget{
const LibraryPage({Key? key}) : super(key: key);
#override
_LibraryPageState createState() => _LibraryPageState();
}
class _LibraryPageState extends State<LibraryPage> with TickerProviderStateMixin {
late TabController _tabController;
late AppBar appBar;
#override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
appBar = AppBar(
brightness: Settings.theme.value == 'Dark' ? Brightness.dark : Brightness.light,
shadowColor: Settings.theme.value == 'Dark' ? Color.fromRGBO(0, 0, 0, 0.1) : Color.fromRGBO(0, 0, 0, 0.5),
backgroundColor: Settings.colors[2],
title: Text('Library'),
bottom: TabBar(
controller: _tabController,
labelPadding: EdgeInsets.zero,
labelStyle: TextStyle(
fontFamily: 'Montserrat',
fontWeight: FontWeight.w500
),
labelColor: Settings.colors[3],
indicator: UnderlineTabIndicator(
borderSide: BorderSide(color: Settings.colors[3], width: 2)
),
tabs: [
Tab(
text: 'Reading',
),
Tab(
text: 'New',
),
Tab(
text: 'Read',
)
],
),
);
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Settings.colors[1],
resizeToAvoidBottomInset: false,
appBar: appBar,
body: Column(
children: [
if (AdWidgets.libraryPag != null && AdState.loaded)
AdWidgets.libraryPag!
else
Container(
width: MediaQuery.of(context).size.width,
height: adaptiveBannerSize.height.toDouble(),
color: Settings.colors[0],
child: Center(
child: Text(
'Ad not loaded',
style: TextStyle(
color: Settings.colors[4],
fontFamily: 'Open Sans'),
)),
),
ScrollConfiguration(
behavior: MyBehavior(),
child: SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height - appBar.preferredSize.height - appBar.bottom!.preferredSize.height - adaptiveBannerSize.height,
child: TabBarView(
controller: _tabController,
children: [
Category(category: 'reading'),
Category(category: 'new'),
Category(category: 'read')
],
),
),
),
],
),
);
}
}
As you can see there, I check if the AdWidgets.libraryPag is not null and if the ad was loaded and then if true - I display my ad.
The major issue is that - when I navigate from one page to another or scroll down it is very laggy even in the release build, also my bottomNavBar starts flickering a lot. Also when I do any kind of action in my app I get a stack of messages in of the console log saying D/AudioManager(30711): getStreamVolume isRestricted mode = 0. My guess is that the ads are reloading too often causing some performance issues.
Here is a small portion of my console log:
W/ContentCatcher(30711): Failed to notify a WebView
W/Choreographer(30711): Already have a pending vsync event. There should only be one at a time.
W/Choreographer(30711): OPTS_INPUT: First frame was drawed before optimized, so skip!
W/ContentCatcher(30711): Failed to notify a WebView
W/ContentCatcher(30711): Failed to notify a WebView
W/Choreographer(30711): Already have a pending vsync event. There should only be one at a time.
W/Choreographer(30711): Already have a pending vsync event. There should only be one at a time.
W/ContentCatcher(30711): Failed to notify a WebView
W/ContentCatcher(30711): Failed to notify a WebView
W/Choreographer(30711): OPTS_INPUT: First frame was drawed before optimized, so skip!
W/Choreographer(30711): OPTS_INPUT: First frame was drawed before optimized, so skip!
W/ContentCatcher(30711): Failed to notify a WebView
I/Ads (30711): Use RequestConfiguration.Builder().setTestDeviceIds(Arrays.asList("B0B49AF69923DB48675A21F6F88D2525")) to get test ads on this device.
D/AudioManager(30711): getStreamVolume isRestricted mode = 0
D/AudioManager(30711): getStreamVolume isRestricted mode = 0
How can I fix this performance issue?
Refer to this answer on issue,
Prior to Android 10 AndroidView Should have better performance. if this is the case, maybe we can check the device OS and use AndroidView Instead of PlatformViewLink.
atrope make fork from repo google_mobile_ads and do this trick for better performance for device os less than Android 10
Use this repo instead of official google_ads in your pubspec.yaml
google_mobile_ads:
git:
url: https://github.com/SuaMusica/googleads-mobile-flutter.git
path: packages/google_mobile_ads
ref: feature/suamusica
Special thanks for atrope
The problem is not in your implementation or code. It is because the package is using androidView which is very expensive when handled by devices with android below 10. You can force flutter to switch to platformviewlink like this and it will fix the issue for the moment.
if (Platform.isAndroid) {
androidInfo = await DeviceInfoPlugin().androidInfo;
final isAndroidOld = (androidInfo.version.sdkInt ?? 0) < 29; //Android 10
useHybridComposition = remoteConfig.getBool(
isAndroidOld
? RemoteConfigKey.useHybridCompositionOlderOS
: RemoteConfigKey.useHybridCompositionNewerOS,
);
if (isAndroidOld && useHybridComposition) {
await PlatformViewsService.synchronizeToNativeViewHierarchy(false);
}
}
I'm trying to load an image from firebase storage to my app however I have this weird issue where the profile page(where this image is loading) keeps flickering. The image is loading fine however the whole widget keeps flickering. I have narrowed the issue down to the setState() called within the function getProfilePic() after some debugging, however I do not know if it's the function itself or my call to said function.
P.S there is no issue with the fileURL or picRef.getDownloadURL() as I've tested this with a random internet image as well and got the same flickering.
class profileTab extends StatefulWidget {
#override
_profileTabState createState() => _profileTabState();
}
class _profileTabState extends State<profileTab> {
User user = FirebaseAuth.instance.currentUser;
String _image = "https://picsum.photos/250?image=9";
Reference picRef = FirebaseStorage.instance.ref().child(FirebaseAuth.instance.currentUser.uid);
Future<Widget> getProfilePic() async {
await picRef.getDownloadURL().then((fileURL){
setState(() {
_image = fileURL;
});
});
}
#override
Widget build(BuildContext context) {
getProfilePic();
return StreamBuilder(
stream: FirebaseFirestore.instance.collection('users').doc(user.uid).snapshots(),
builder: (context, snapshot){
if (snapshot.connectionState == ConnectionState.active){
return ListView(
children: <Widget>[
SizedBox(height: 100.0,),
CircleAvatar(
radius: 100.0,
backgroundColor: Colors.lightBlueAccent,
child: ClipOval(
child: SizedBox(
width: 180.0,
height: 180.0,
child: Image.network(_image,fit: BoxFit.fill,),
),
),
),
SizedBox(height: 30.0,),
Center(child: Text("Name: " + snapshot.data.data()['name'],textScaleFactor: 3.0,)),
]
);
}
else {
return CircularProgressIndicator();
}
},
);
}
}
getProfilePic is redrawing widget by calling setState.
setState calls build method which calls getProfilePic.
Therefore, when first time build method is called we call getProfilePic which again updates widget tree.
Fix: Inside getProfilePic add check to call setState if _image is null which will redraw widget only once.
It would be better if you use Image.network. You can refer this
https://www.woolha.com/tutorials/flutter-display-image-from-network-url-show-loading
When I want to do "Extract to widget", it raises an error : "reference to an enclosing class method cannot be extracted"
I know there is some variables that must get their data from class constructor but I want Android studio to extract the widget then, I will correct the mistaken codes, like Visual Studio that without any error extract the code to a new widget then it needs to copy the new extracted widget to a new dart file and correct the mistakes.
I want to extract the Card widget part.
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' as prefix0;
import 'package:intl/intl.dart';
import '../model/transaction.dart';
class TransactionList extends StatelessWidget {
final List<Transaction> transactions;
final Function deleteTx;
TransactionList(this.transactions, this.deleteTx);
#override
Widget build(BuildContext context) {
return transactions.isEmpty
? LayoutBuilder(
builder: (ctx, constraint) {
return Column(
children: <Widget>[
Text(
'There is no transaction',
style: Theme.of(context).textTheme.title,
textDirection: prefix0.TextDirection.rtl,
),
SizedBox(
height: 10,
),
Container(
height: constraint.maxHeight * 0.6,
child: Image.asset(
'assets/images/yalda.png',
fit: BoxFit.cover,
))
],
);
},
)
: ListView.builder(
itemCount: transactions.length,
itemBuilder: (ctx, index) {
return **Card**(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 5),
elevation: 5,
child: ListTile(
leading: CircleAvatar(
radius: 30,
child: Padding(
padding: const EdgeInsets.all(8),
child: FittedBox(
child: Text('\$${transactions[index].amount}')),
),
),
title: Text(
transactions[index].title,
style: Theme.of(context).textTheme.title,
),
subtitle: Text(DateFormat.yMMMd()
.format(transactions[index].date)
.toString()),
trailing: MediaQuery.of(context).size.width > 360
? FlatButton.icon(
onPressed: () => deleteTx(transactions[index].id),
icon: const Icon(Icons.delete),
label: const Text('Delete'),
textColor: Theme.of(context).errorColor,
)
: IconButton(
icon: const Icon(Icons.delete),
color: Theme.of(context).errorColor,
onPressed: () => deleteTx(transactions[index].id),
),
),
);
});
}
}
Simply use "Extract Method" instead of "Extract Widget". VSCode will add all the returns and references.
Edit: If you want to use "Extract Widget" only then simply wrap that widget in a Container and then use "Extract Widget" on that widget.
If that doesn't work, comment out setState() function inside the widget and try again.
Your deleteTx might contain a setState(() {}) method, try to comment that part of your code where you're calling deleteTx it and just put it back after your extraction.
Just remove or comment the setState() {} from your widget and it gonna works.
transform onpressed etc. property to comments and then try again 'Extract Widget' and go on
I had the same issue and in my case it was because of ListView.builder as yours.
So it is easy to fix, Simply make a class and return Card in Widget build and return it in ListView.builder inside the TransactionList class with the desired arguments.
You have to care about a few things:
Whenever you are extracting a widget, that widget should not contain any variable which is used in the current working page.
All the functions or methods should be parametric.
It is because you are referencing a variable(for example, transactions) in your Card widget from the enclosing class i.e. TransactionList. The best way to extract in this case could be to just make stateless/stateful widget outside your class and cut the Card widget and paste it as the return type of the build method of that Widget you created. And you can reference those variables using the constructor of that widget you created.
If you can comment out deleteTx(transactions[index].id) parts of your code and then use onPressed: (){}, you will be able to extract to widget.
After the extraction you can use:
onPressed: (){
setState(() {
deleteTx(transactions[index].id);
});
}
there might be some local referance to the variable in the current class,so if there some referance we cant extract a widget from there.
You can use the "Extract Method". It's a simple way. VSCode will add all the returns and references.
If we extract the widget it will StatelessWidget. StatelessWidget doesn't support changing state so if you use any Onchange property SetState it never extract
so please remove setState(() {});
Just remove or comment the setState() {} from your widget
Or
Just remove or comment the MediaQuery.of(context).size.width from your widget
Whenever I scroll in my listview, I get this error spammed in console:
ScrollController not attached to any scroll views.
'package:flutter/src/widgets/scroll_controller.dart':
Failed assertion: line 110 pos 12: '_positions.isNotEmpty'
I've been trying to fix this all day and I'd like to have someone else take a look at it.
There are more problems with this code, but right now I'm mainly interested in fixing this error.
I've tried to use Listview.builder, checking for hController.hasClients and many more small things. They didn't seem to change anything
class MyHome extends StatefulWidget {
#override
MyHomeState createState() => new MyHomeState();
}
class MyHomeState extends State<MyHome> with SingleTickerProviderStateMixin {
ScrollController hController;
ScrollController tController;
ScrollController fController;
ScrollController bController;
#override
void initState() {
super.initState();
hController = new ScrollController()..addListener(_scrollListener);
tController = new ScrollController()..addListener(_scrollListener);
fController = new ScrollController()..addListener(_scrollListener);
bController = new ScrollController()..addListener(_scrollListener);
}
#override
void dispose() {
super.dispose();
hController.removeListener(_scrollListener);
tController.removeListener(_scrollListener);
fController.removeListener(_scrollListener);
bController.removeListener(_scrollListener);
}
#override
Widget build(BuildContext context) {
return new DefaultTabController(
length: 4,
child: new Scaffold(
//Removed AppBar for readability
body: new TabBarView(
children: [
new Container(// hot
child: ListView(
controller: hController,
children: <Widget>[
Utils.show("hot")
],
),
),
new Container( //Trending
child: ListView(
controller: tController,
children: <Widget>[
Utils.show("trending")
],
),
),
new Container( //Fresh
child: ListView(
controller: fController,
children: <Widget>[
Utils.show("fresh")
],
),
),
new Container( //Best
child: ListView(
controller: bController,
children: <Widget>[
Utils.show("best")
],
),
),
],
),
));
}
void _scrollListener() {
if (hController.position.extentAfter == 0.0) {
setState(() {
Utils.show("hot");
});
}else if (tController.position.extentAfter == 0.0) {
setState(() {
Utils.show("trending");
});
} else if (fController.position.extentAfter == 0.0) {
setState(() {
Utils.show("fresh");
});
} else if (bController.position.extentAfter == 0.0) {
setState(() {
Utils.show("best");
});
}
}
}
Edit: For some clarity, the first time I posted this code, I used tController twice. This was ofcourse a mistake, but did not solve the error. The error happens when scrolling in every one of the four listviews.
To avoid such type of errors use the getter
ScrollController.hasClient
If this is false, then members that interact with the
[ScrollPosition],such as [position], [offset], [animateTo], and [jumpTo],
must not be called.
for example:
if (_controller.hasClients) {
_controller.animateTo(
...
}
The problem is inside _scrollListener.
When you are checking controllers in if-else there is only one view at the scene. Means only one listview is rendered & only one scrollcontroller is completely setup. But in your code they are checking all scrollcontroller's positions in single function. Thats why you are getting this error. First check if controller have the positions, which it will only have if the controller is attached & view is rendered correctly. After that check for extentAfter value.
Exa -
if (hController.positions.length > 0 && tController.position.extentAfter == 0.0) {
}
else if (tController.positions.length > 0 && tController.position.extentAfter == 0.0) {
}
& so on
check controller does not have client ant then delay jump:
if (!_scrollController.hasClients) {
_scrollController.animateTo(_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut);
}
If you paste your code correctly - it seems there can be mistake:
new Container(// hot
child: ListView(
controller: tController,
children: <Widget>[
Utils.show("hot")
],
),
),
new Container( //Trending
child: ListView(
controller: tController,
children: <Widget>[
Utils.show("trending")
],
),
),
You have used tController two times and haven't used hController
Update your flutter sdk it will solve this problem
That is work for me
run this on your cmd - flutter update
I am collecting user input with a TextFormField and when the user presses a FloatingActionButton indicating they are done, I want to dismiss the on screen keyboard.
How do I make the keyboard go away automatically?
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
MyHomePageState createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
TextEditingController _controller = new TextEditingController();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.send),
onPressed: () {
setState(() {
// send message
// dismiss on screen keyboard here
_controller.clear();
});
},
),
body: new Container(
alignment: FractionalOffset.center,
padding: new EdgeInsets.all(20.0),
child: new TextFormField(
controller: _controller,
decoration: new InputDecoration(labelText: 'Example Text'),
),
),
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
);
}
}
void main() {
runApp(new MyApp());
}
For Flutter version 2 or latest :
Since Flutter 2 with null safety this is the best way:
FocusManager.instance.primaryFocus?.unfocus();
Note: using old ways leads to some problems like keep rebuild states;
For Flutter version < 2 :
As of Flutter v1.7.8+hotfix.2, the way to go is:
FocusScope.of(context).unfocus();
Comment on PR about that:
Now that #31909 (be75fb3) has landed, you should use
FocusScope.of(context).unfocus() instead of
FocusScope.of(context).requestFocus(FocusNode()), since FocusNodes are
ChangeNotifiers, and should be disposed properly.
-> DO NOT use ̶r̶e̶q̶u̶e̶s̶t̶F̶o̶c̶u̶s̶(̶F̶o̶c̶u̶s̶N̶o̶d̶e̶(̶)̶ anymore.
F̶o̶c̶u̶s̶S̶c̶o̶p̶e̶.̶o̶f̶(̶c̶o̶n̶t̶e̶x̶t̶)̶.̶r̶e̶q̶u̶e̶s̶t̶F̶o̶c̶u̶s̶(̶F̶o̶c̶u̶s̶N̶o̶d̶e̶(̶)̶)̶;̶
Read more about the FocusScope class in the flutter docs.
Note: This answer is outdated. See the answer for newer versions of Flutter.
You can dismiss the keyboard by taking away the focus of the TextFormField and giving it to an unused FocusNode:
FocusScope.of(context).requestFocus(FocusNode());
Solution with FocusScope doesn't work for me.
I found another:
import 'package:flutter/services.dart';
SystemChannels.textInput.invokeMethod('TextInput.hide');
It solved my problem.
For Flutter 1.17.3 (stable channel as of June 2020), use
FocusManager.instance.primaryFocus.unfocus();
Following code helped me to hide keyboard
void initState() {
SystemChannels.textInput.invokeMethod('TextInput.hide');
super.initState();
}
To dismiss the keyboard (1.7.8+hotfix.2 and above) just call the method below:
FocusScope.of(context).unfocus();
Once the FocusScope.of(context).unfocus() method already check if there is focus before dismiss the keyboard it's not needed to check it. But in case you need it just call another context method: FocusScope.of(context).hasPrimaryFocus
Looks like different approaches for different version. I am using Flutter v1.17.1 and the below works for me.
onTap: () {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
currentFocus.focusedChild.unfocus();
}
}
GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child:Container(
alignment: FractionalOffset.center,
padding: new EdgeInsets.all(20.0),
child: new TextFormField(
controller: _controller,
decoration: new InputDecoration(labelText: 'Example Text'),
),
), })
try this on tap gesture
None of the above solutions don't work for me.
Flutter suggests this -
Put your widget inside new GestureDetector() on which tap will hide keyboard and onTap use FocusScope.of(context).requestFocus(new FocusNode())
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
var widget = new MaterialApp(
home: new Scaffold(
body: new Container(
height:500.0,
child: new GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(new FocusNode());
},
child: new Container(
color: Colors.white,
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
new TextField( ),
new Text("Test"),
],
)
)
)
)
),
);
return widget;
}}
For me, the Listener above App widget is the best approach I've found:
Listener(
onPointerUp: (_) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
currentFocus.focusedChild.unfocus();
}
},
child: MaterialApp(
title: 'Flutter Test App',
theme: theme,
...
),
)
This may simplify the case. Below code will work only if keyboard is open
if(FocusScope.of(context).isFirstFocus) {
FocusScope.of(context).requestFocus(new FocusNode());
}
As in Flutter everything is a widget, I decided to wrap the FocusScope.of(context).unfocus(); approach in a short utility widget.
Just create the KeyboardHider widget:
import 'package:flutter/widgets.dart';
/// A widget that upon tap attempts to hide the keyboard.
class KeyboardHider extends StatelessWidget {
/// Creates a widget that on tap, hides the keyboard.
const KeyboardHider({
required this.child,
Key? key,
}) : super(key: key);
/// The widget below this widget in the tree.
final Widget child;
#override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => FocusScope.of(context).unfocus(),
child: child,
);
}
}
Now, you can wrap any widget (very convenient when using a good IDE) with the KeyboardHider widget, and then when you tap on something, the keyboard will close automatically. It works well with forms and other tappable areas.
class SimpleWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return KeyboardHider(
/* Here comes a widget tree that eventually opens the keyboard,
* but the widget that opened the keyboard doesn't necessarily
* takes care of hiding it, so we wrap everything in a
* KeyboardHider widget */
child: Container(),
);
}
}
You can use unfocus() method from FocusNode class.
import 'package:flutter/material.dart';
class MyHomePage extends StatefulWidget {
MyHomePageState createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
TextEditingController _controller = new TextEditingController();
FocusNode _focusNode = new FocusNode(); //1 - declare and initialize variable
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.send),
onPressed: () {
_focusNode.unfocus(); //3 - call this method here
},
),
body: new Container(
alignment: FractionalOffset.center,
padding: new EdgeInsets.all(20.0),
child: new TextFormField(
controller: _controller,
focusNode: _focusNode, //2 - assign it to your TextFormField
decoration: new InputDecoration(labelText: 'Example Text'),
),
),
);
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: new MyHomePage(),
);
}
}
void main() {
runApp(new MyApp());
}
To summarize, this is a working solution for Flutter 1.17:
Wrap your Widget like this:
GestureDetector(
onTap: FocusScope.of(context).unfocus,
child: YourWidget(),
);
if you use CustomScrollView, just put,
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
You can wrap your widget with "GestureDetector", then assign "FocusScope.of(context).unfocus()" to its onTap function
GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: child,
);
_dismissKeyboard(BuildContext context) {
FocusScope.of(context).requestFocus(new FocusNode());
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onTap: () {
this._dismissKeyboard(context);
},
child: new Container(
color: Colors.white,
child: new Column(
children: <Widget>[/*...*/],
),
),
);
}
Call this function when you needed
void hideKeyboard(BuildContext context) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
FocusManager.instance.primaryFocus?.unfocus();
}
}
You can also declare a focusNode for you textfield and when you are done you can just call the unfocus method on that focusNode
and also dispose it
class MyHomePage extends StatefulWidget {
MyHomePageState createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
TextEditingController _controller = new TextEditingController();
/// declare focus
final FocusNode _titleFocus = FocusNode();
#override
void dispose() {
_titleFocus.dispose();
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(),
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.send),
onPressed: () {
setState(() {
// send message
// dismiss on screen keyboard here
_titleFocus.unfocus();
_controller.clear();
});
},
),
body: new Container(
alignment: FractionalOffset.center,
padding: new EdgeInsets.all(20.0),
child: new TextFormField(
controller: _controller,
focusNode: _titleFocus,
decoration: new InputDecoration(labelText: 'Example Text'),
),
),
);
}
}
FocusScope.of(context).unfocus() has a downside when using with filtered listView.
Apart from so many details and concisely, use keyboard_dismisser package in https://pub.dev/packages/keyboard_dismisser will solve all the problems.
I have created this function to my base code, so far works well!!
void hideKeyword(BuildContext context) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus && currentFocus.focusedChild != null) {
currentFocus.focusedChild.unfocus();
}
}
FocusScope.of(context).unfocus(); doesn't work.
This code works for me at flutter ver 2.2.3 and null safety.
WidgetsBinding.instance?.focusManager.primaryFocus?.unfocus()
Source: https://github.com/flutter/flutter/issues/20227#issuecomment-512860882
For example, put this code in MyAppState to apply hide keyboard when touch outside for whole app.
return GestureDetector(
onTap: () =>
WidgetsBinding.instance?.focusManager.primaryFocus?.unfocus(),
child: MaterialApp(
title: 'Flutter Demo',
theme: getTheme(),
home: _body(),
),
);
Use SystemChannels.textInput.invokeMethod('TextInput.hide');. It will close/dismiss the keyboard when the screen loads.
void initState() {
super.initState();
SystemChannels.textInput.invokeMethod('TextInput.hide');
}
====== Dismiss the keyboard after clicking out of the TextField =======
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(), //this will dismiss keyboard
child: Scaffold(
body: SafeArea(
.........
====== Dismiss the keyboard when scrolling the screen =======
ListView(
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, //this will dismiss
children: [
..........
The SingleChildScrollView widget also have this property.
You can use this one.
FocusScope.of(context).requestFocus(FocusNode());
And you can use this onTap of GestureDetector or InkWell like this.
`GestureDetector(
onTap: () {`
// THIS FOCUS SCOPE WILL CLOSE THE KEYBOARD
FocusScope.of(context).requestFocus(FocusNode());
forgotPasswordAPI(emailController.text);
},``
add this code inside build widget
FocusScope.of(context).requestFocus(FocusNode());
If your keyboard still won't turn off , don't forget add focusNode to TextField. The above information was helpful, but forgetting to add focusNode bothered me a bit. Here an example.
TextField(
focusNode: FocusNode(),
textController: _controller,
autoFocus: false,
textStyle: TextStyle(fontSize: 14),
onFieldSubmitted: (text) {},
onChanged: (text) {},
hint: 'Enter the code',
hintColor: CustomColors.mediumGray,
suffixAsset: _voucherController.text.length == 7
? Assets.ic_approved_voucher
: null,
isIcon: false,
isObscure: false,
maxLength: 7,
)
closeKeyboard(BuildContext context) {
var currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.unfocus();
}
}
#override
Widget build(BuildContext context) {
_keyboardVisible = MediaQuery.of(context).viewInsets.bottom != 0;
size = MediaQuery.of(context).size;
return GestureDetector(
onTap: () {
closeKeyboard(context);
},
child: Scaffold(
backgroundColor: Colors.white,
body: Container(
width: double.maxFinite,
height: double.maxFinite,
child: _buildUI(vm)),
),
);
}
try using a TextEditingController.
at the begining,
final myController = TextEditingController();
#override
void dispose() {
// Clean up the controller when the widget is disposed.
myController.dispose();
super.dispose();
}
and in the on press event,
onPressed: () {
myController.clear();}
this will dismiss the keybord.
If you use TextField(maxLines: null) and just want to show Done button ON the screen keyboard to hide it, the code below works.
TextField(
keyboardType: TextInputType.text,
maxLines: null,
)
Side note: why in the first place doesn't the keyboard show Done button? The reason is found in the implementation of TextField:
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),