I have several TextFormField on a screen. If I tap one of the fields the keyboard opens as expected however if I then select a new screen from the Drawer menu the keyboard closes and as soon as the new screen finishes loading the keyboard automatically opens again. More than that if I type something the text field is updated in the background if I return to the screen with the TextFormField it shows the correct input.
I would expect the screen/widget to be disposed of when navigating to another screen(widget) from the navigation menu, and I definitely should not be able to update the content of a widget's text field while in another widget.
// Form Field
Form(key: _constructionFormKey,
child: Column(children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const SizedBox(
width: 100,
child: Text(
'Homes',
style: regularBoldText,
),
),
SizedBox(
width: 75,
child: Text(
'${widget.tribe.homes} (${calculatePercent(widget.tribe.land, widget.tribe.homes)}%)',
style: regularText,
),
),
SizedBox(
height: 18,
width: MediaQuery.of(context).size.width / 3,
child: TextFormField(
autovalidateMode:
AutovalidateMode.onUserInteraction,
onChanged: (String? newValue) {
if (newValue != null && isNumber(newValue)) {
setState(() {
buildHomes = int.parse(newValue);
});
// Requiered or variable will not clear properly
// when the user deletes input content
} else if (newValue == null || newValue.isEmpty) {
setState(() {
buildHomes = 0;
});
}
},
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
],
style: const TextStyle(fontSize: 10),
decoration: const InputDecoration(
border: OutlineInputBorder()),
keyboardType: TextInputType.number,
),
),
],
),));
// Home Screen where I have the navigation logic.
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
//! Default to tribe overview screen - 1 -, 0 is mail screen
int _drawerNavIndex = 3;
/// [setSelectedTab] will update the current screen based on the tapped option
/// from [DrawerContentWidget]
void setSelectedTab(index) {
// if the [_drawerNavIndex] is not the same as [index] update it to [index]
// value
if (_drawerNavIndex != index) {
setState(() {
_drawerNavIndex = index;
});
}
}
/// [selectedTabContent] will return the screen selected from the
/// [DrawerContentWidget] based on [_drawerNavIndex]
Widget selectedTabContent() {
List<Widget> pages = [
// Tribe Screens
const TribeMailScreen(),
const TribeHomeScreen(),
const TribeAdvisorScreen(),
const ConstructionScreen()
// Alliance
];
return IndexedStack(
index: _drawerNavIndex,
children: pages,
);
}
#override
Widget build(BuildContext context) {
TribeSummary tribe = Provider.of<TribeSummary>(context, listen: true);
// If the tribe uid value is `placeHolderTribe` assume that there is no
// existing or active tribe for this account
if (tribe.uid == 'placeHolderTribe') {
return Scaffold(
/// TODO: create a proper drawer or appBar for the [StartTribeWidget]
appBar: AppBar(
title: const Text('Orkfia'),
),
body: const StartTribeWidget(),
);
// If the tribe `uid` value is `placeHolderTribe` assume that an error
// occurred while trying to get the tribe stream or while the tribe stream
// is parsed to [TribeSummary], log should give more information
} else if (tribe.uid == 'placeHolderErrorTribe') {
// TODO: create a bettter error screen for this situation
return const Center(
child: Text('Unable to retrieve tribe data'),
);
}
// This Scaffold wraps the entire app, anything here will be avilable
// globally
return Scaffold(
// App Bar
appBar: const AppBarContent(),
// [DrawerContentWidget] holds all the drawer content, it requires
// [selectedTab] function to handle the navigation between screens
drawer: DrawerContentWidget(
setSelectedTab: setSelectedTab,
selectedTabIndex: _drawerNavIndex,
),
// Display the contents of the selected screen
body: selectedTabContent(),
// Reserved
bottomNavigationBar: SizedBox(
height: 50,
child: Container(
color: Colors.red[100],
child: const Center(child: Text('Reserved space')),
)),
);
}
}
Use TextEditingController for every TextFormField to solve this problem.
A controller for an editable text field.
First Whenever the user modifies a text field with an associated TextEditingController, the text field updates value and the controller notifies its listeners. Listeners can then read the text and selection properties to learn what the user has typed or how the selection has been updated.
Second, remember to dispose of the TextEditingController inside dispose() when it is no longer needed. This will ensure we discard any resources used by the object.
To close keyboard from screen
you can use GesterDetector widget.
FocusManager.instance.primaryFocus?.unfocus();
or use can below for hot fix
FocusScope.of(context).unfocus();
Example is given below
class MyStatefulWidget extends StatefulWidget {
const MyStatefulWidget({super.key});
#override
State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
final TextEditingController _controller = TextEditingController();
#override
void initState() {
super.initState();
_controller.addListener(() {
final String text = _controller.text.toLowerCase();
_controller.value = _controller.value.copyWith(
text: text,
selection:
TextSelection(baseOffset: text.length, extentOffset: text.length),
composing: TextRange.empty,
);
});
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
alignment: Alignment.center,
padding: const EdgeInsets.all(6),
child: TextFormField(
controller: _controller,
decoration: const InputDecoration(border: OutlineInputBorder()),
),
),
);
}
}
When you navigate to a new page, you are not really disposing of the previous page. The new page is simply added on top of the previous page. You could try wrapping the entire scaffold in a GestureDetector with the following onTap function:
FocusScope.of(context).unfocus();
This will make sure the keyboard is dismissed when you push a new page with user taps.
Related
I am working on a real estate app where I would like to display a list of properties, that is retrieved from the Algolia database, and search them from the search input field by typing the I.D of the properties. Like this
I have successfully linked/setup Firebase & Algolia. And I am able to display the properties on the screen, using infinite_scroll_pagination and algolia_helper_flutter packages.
The problem I am facing is I cannot search the houses by typing the I.D of the properties.
Please check out my code, tell me where I went wrong. Thank you.
Best,
class HousesListView extends StatefulWidget {
const HousesListView({Key? key}) : super(key: key);
#override
State<HousesListView> createState() => _HousesListViewState();
}
class _HousesListViewState extends State<HousesListView> {
// textController for search box input
final _searchTextController = TextEditingController();
// pageController from infinite_scroll_pagination package
final PagingController<int, MdlAlgoliaProperties> pagingController =
PagingController(firstPageKey: 0);
/// Component holding search filters from algolia_helper_flutter package
final _filterState = FilterState();
// search houses in Algolia Database
final _houseDatabase = HitsSearcher.create(
applicationID: AlgoliaCredentials.applicationID,
apiKey: AlgoliaCredentials.apiKey,
state: const SearchState(
indexName: AlgoliaCredentials.hitsIndex,
facetFilters: ['a2-propertyType: House']));
// stream and display list of properties on the screen
Stream<PropertiesPage> get displayPropertiesOnThePage =>
_houseDatabase.responses.map(PropertiesPage.fromResponse);
/// Get stream of search result, like the number of the result from the search box
Stream<SearchMetadata> get searchMetadata =>
_houseDatabase.responses.map(SearchMetadata.fromResponse);
#override
void initState() {
super.initState();
// listen to keystroke & query the results by the letters that user types in
_searchTextController
.addListener(() => _houseDatabase.query(_searchTextController.text));
// load properties on the page
displayPropertiesOnThePage.listen((properties) {
if (properties.pageKey == 0) pagingController.refresh();
pagingController.appendPage(
properties.alogliaPPT, properties.nextPageKey);
}).onError((error) => pagingController.error = error);
// error here!
// this loads the list of house successfully and properly when its enabled, but search does not work anymore
// but, when this disable, the search works, but it does not load the list of houses anymore
pagingController.addPageRequestListener((pageKey) =>
_houseDatabase.applyState((state) => state.copyWith(page: pageKey))); //<= error occur in this line
// connect database and filter state
_houseDatabase.connectFilterState(_filterState);
// pageController listens to filterState
_filterState.filters.listen((_) => pagingController.refresh());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBarTitle(context, 'List of Houses'),
backgroundColor: ZayyanColorTheme.zayyanGrey,
endDrawer: const Drawer(
width: 350,
child: HouseFilter(),
),
body: Center(
child: Column(
children: [
SizedBox(
height: 44,
child: TextField(
controller: _searchTextController,
decoration: const InputDecoration(
border: InputBorder.none,
hintText: 'Enter a search term',
prefixIcon: Icon(Icons.search),
),
),
),
StreamBuilder<SearchMetadata>(
stream: searchMetadata,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Text('${snapshot.data!.nbHits} hits'),
);
},
),
Expanded(
child: _hits(context),
),
],
),
),
);
}
Widget _hits(BuildContext context) {
return PropertyHitsListView(
pagingController: pagingController,
noItemsFound: (context) => const NoResultsView(),
onHitClick: (objectID) {
print(objectID);
},
);
}
#override
void dispose() {
_searchTextController.dispose();
_houseDatabase.dispose();
_filterState.dispose();
pagingController.dispose();
super.dispose();
}
}
I am trying to implement a ListView for Desktop applications, which is able to multiselect the items. On Desktop we do this by either clicking an item, or clicking and holding the control key. To select an item, you simply can add an Inkwell or GestureRecognizer, but how do I detect on click that there is also the control key pressed? I couldn't find any suggestions
You can play with this widget. Make sure to run as desktop mode.
we need to listen keyboard event. For that I am using RawKeyboardListener.
keep track ctrl event
single selection happen on normal tap by clearing previous selected item, but while _isCTRLPressed don't clear the selected items
onTap: () {
if (!_isCTRLPressed) _selectedIndex.clear();
_onTap(index);
}
Demo widget
class ItemSelection extends StatefulWidget {
const ItemSelection({Key? key}) : super(key: key);
#override
State<ItemSelection> createState() => _ItemSelectionState();
}
class _ItemSelectionState extends State<ItemSelection> {
List<int> _selectedIndex = [];
void _onTap(index) {
if (_selectedIndex.contains(index)) {
_selectedIndex.remove(index);
} else {
_selectedIndex.add(index);
}
setState(() {});
}
final fc = FocusNode();
// you can use list for multi-purpose
bool _isCTRLPressed = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: RawKeyboardListener(
focusNode: fc,
autofocus: true,
onKey: (event) {
if (event.isKeyPressed(LogicalKeyboardKey.controlLeft)) {
if (event is RawKeyDownEvent) {
_isCTRLPressed = true;
}
} else {
_isCTRLPressed = false;
}
},
child: GridView.count(
crossAxisCount: 6,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
children: List.generate(
55,
(index) => GestureDetector(
onTap: () {
if (!_isCTRLPressed) _selectedIndex.clear();
_onTap(index);
debugPrint("ctrlPressed $_isCTRLPressed");
},
child: Container(
color: _selectedIndex.contains(index)
? Colors.cyanAccent
: Colors.grey,
alignment: Alignment.center,
child: Text(index.toString()),
),
),
),
),
),
);
}
}
On press key 1, ListView adds 1 tile, on press key 2 ListView removes one tile, though after clicking with mouse outside of ListView or Text() widget, keyboard keys stop responding without any error being shown in terminal.
I thought, that maybe FocusNode was disposed after clicking outside of ListView, though, after testing, this seems not to be the case
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class OnTapWidgetIssue extends StatefulWidget {
OnTapWidgetIssue({Key? key}) : super(key: key);
String testOnTap = '';
int nOfList = 1;
#override
_OnTapWidgetIssueState createState() => _OnTapWidgetIssueState();
}
class _OnTapWidgetIssueState extends State<OnTapWidgetIssue> {
final FocusNode _focusNode = FocusNode();
#override
void dispose() {
_focusNode.dispose();
print('_focusNode.dispose()');
super.dispose();
}
void _handleKeyEvent(RawKeyEvent event) {
if (event is RawKeyDownEvent &&
event.data.logicalKey == LogicalKeyboardKey.digit1) {
widget.nOfList += 1;
setState(() {});
}
if (event is RawKeyDownEvent &&
event.data.logicalKey == LogicalKeyboardKey.digit2) {
if (widget.nOfList > 1) {
widget.nOfList--;
setState(() {});
} else {}
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
drawer: MenuDrawer(),
appBar: AppBar(title: Text('OnTap-widget.Issue')),
body: RawKeyboardListener(
autofocus: true,
focusNode: _focusNode, // <-- more magic
onKey: _handleKeyEvent,
child: Column(children: [
Text(widget.testOnTap, style: TextStyle(fontSize: 52.0)),
Text('''
press 1 to add ListTile
press 2 to remove ListTile
'''),
Expanded(
child: Row(
children: [
Expanded(
flex: 2,
child: SizedBox(),
),
Expanded(
flex: 1,
// child: SizedBox(),
// // ),
child: ListView.builder(
itemCount: widget.nOfList,
// itemCount: widget.testOnTap.length,
itemBuilder: (_, i) {
return ListTile(
title: Text('$i'),
onTap: () {
widget.testOnTap = widget.testOnTap + i.toString();
setState(() {});
},
// Handle your onTap here.
);
},
),
),
Expanded(
flex: 2,
child: SizedBox(),
),
],
),
),
]),
),
);
}
}
Also Im getting error when clicking to go to new page in the app
Error: A FocusNode was used after being disposed.
Once you have called dispose() on a FocusNode, it can no longer be used.
at Object.throw_ [as throw] (http://localhost:49535/dart_sdk.js:5061:11)
at http://localhost:49535/packages/flutter/src/foundation/change_notifier.dart.lib.js:66:21
at focus_manager.FocusNode.new.[_debugAssertNotDisposed] (http://localhost:49535/packages/flutter/src/foundation/change_notifier.dart.lib.js:69:25)
at focus_manager.FocusNode.new.notifyListeners (http://localhost:49535/packages/flutter/src/foundation/change_notifier.dart.lib.js:131:41)
at focus_manager.FocusNode.new.[_notify] (http://localhost:49535/packages/flutter/src/widgets/widget_inspector.dart.lib.js:42893:12)
at focus_manager.FocusManager.new.[_applyFocusChange] (http://localhost:49535/packages/flutter/src/widgets/widget_inspector.dart.lib.js:43665:26)
at Object._microtaskLoop (http://localhost:49535/dart_sdk.js:38778:13)
at _startMicrotaskLoop (http://localhost:49535/dart_sdk.js:38784:13)
at http://localhost:49535/dart_sdk.js:34519:9
How ever, I don't get this error when selecting exercise page in drawer menu, only when going to this new page from home page. Exercise and Home pages are kinda similar, but still different in some aspects.
Thank
Technically, you are not adding the onTap to the ListView.builder, you're adding it to every single ListTile added by the builder. :)
Declare your two state variables:
String testOnTap = '';
int nOfList = 1;
inside the _OnTapWidgetIssueState class, not the OnTapWidgetIssue class. The convention is to name them _testOnTap and _nOfList respectively since they are private to the class.
And update the two variables INSIDE the setState call, not outside it.
I have been working with flutter just a while now but I have never experienced such a weird problem. basically I'm checking if there is a logged on username show them the main page and if not show them the signup page. after the user signs up (and logs in at the same time) I want to take him to my main page. even though I return a new Scaffold the mobile screen doesn't change at all. not with a hot load or anything. but after stopping the program and running it again (because the user is logged in) it automatically goes to my main page (which I want to do without having to stop the program and running it again. any ideas why this is happening couldn't find anything related to this.
import 'package:sociagrowapp/models/user.dart';
import 'package:sociagrowapp/Authenticate/SignIn.dart';
import 'package:sociagrowapp/HomePages/Home.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Wrapper extends StatefulWidget{
#override
createState() => _Wrapper();
}
class _Wrapper extends State<Wrapper> {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
print(user);
// return either the Home or Authenticate widget
if (user == null){
print('Should Changed 3');
return Scaffold(
body: SignIn()
);
}
else {
print('Should Changed');
print('Should Changed2');
return PageData()
}
}
}
Just in case I will add the PagesData Code but I don't think it is related to that.
import 'package:flutter/material.dart';
import 'package:sociagrowapp/services/auth.dart';
int selectedbotnavi = 0;
class DailyTasks extends StatefulWidget
{
#override
createState() => _DailyTasks();
}
class _DailyTasks extends State<DailyTasks>
{
Widget build(BuildContext context)
{
return Center(
child: Text("15")
);
}
}
class Settings extends StatefulWidget
{
#override
createState() => _Settings();
}
class _Settings extends State<Settings>
{
String _httpdataretrieved;
Widget build(BuildContext context)
{
return Column(
children: <Widget>[
Container(width:MediaQuery.of(context).size.width,
child: Text('Your Account Username',style: TextStyle(fontWeight: FontWeight.w400),),
alignment: Alignment.center,
padding: EdgeInsetsDirectional.fromSTEB(0, 20, 0, 0),
),
Container(width:MediaQuery.of(context).size.width,
child: Text(' Important: Your Account must be public for SociaGrow. \n There are limited Features available to private Accounts',style: TextStyle(fontWeight: FontWeight.w900,fontSize:14),
),
alignment: Alignment.center,
padding: EdgeInsetsDirectional.fromSTEB(0, 5, 0, 20),
),
Container(child: TextField(
obscureText: true,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Username',
),
),
width: MediaQuery.of(context).size.width * 0.8,
alignment: Alignment.center,
padding: EdgeInsetsDirectional.fromSTEB(0, 0, 0, 15),
),
Container(
child: RaisedButton(
child: Text('Change Username'),
),
)
],
);
}
}
List <Widget> Pages = [new DailyTasks(),new DailyTasks(),new DailyTasks()];
class PageData extends StatefulWidget
{
#override
createState() => _PageData();
}
class _PageData extends State<PageData>
{
void _changeselectbotnaviindex(int index)
{
selectedbotnavi = index;
setState(() {
});
}
final AuthService _auth = AuthService();
#override
Widget build(BuildContext context)
{
return Scaffold(
appBar: AppBar(title: Container(
child: Image.asset('assets/Logo.png',width: 100,height: 200,),
padding: EdgeInsetsDirectional.fromSTEB(0, 10, 0 , 0),
),
actions: <Widget>[
FlatButton(
child: Text('Sign out'),
onPressed: () async {
await this._auth.signOut();
},
),
],
),
body: Pages[selectedbotnavi],
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
items :[
BottomNavigationBarItem(icon: Icon(Icons.timelapse),title:Text('Daily Tasks')),
BottomNavigationBarItem(icon: Icon(Icons.call_made),title:Text('Growth')),
BottomNavigationBarItem(icon: Icon(Icons.settings),title:Text('Settings')),],
currentIndex: selectedbotnavi,
onTap: _changeselectbotnaviindex,
selectedItemColor: Colors.amber[800],
unselectedItemColor: Colors.black,
showUnselectedLabels: true,
)
);
}
}
That is not the way you navigate to a new page in Flutter.
In Flutter, the way to navigate between pages is with Navigator, which is a widget that manages a set of child widgets with a stack discipline. That is, Navigator has everything ready for you to navigate between pages easily. When you create an app with MaterialApp, Flutter attaches a Navigator to the top of the widget tree for you under the hood. This way, you can access the Navigator in your widget tree via context, by calling Navigator.of(context).
In your case, when the user taps the sign up button in your sign up page, you should do something like:
Navigator.of(context).pushReplacement(MaterialPageRoute(builder: (_) => PageData()));
This way, your app will navigate to PageData when the user signs in.
Check out this Flutter.dev article on navigating pages for more details on the topic.
You have to call SetState() so your build method is called again.
You could add a VoidListener to your SignIn(onLogin:(){setState(() {});})
usually I can disable/grey-out a button until a TextFormField meets certain parameters in flutter by something like this:
TextFormField(
controller: _controller
value: (value)
)
SubmitButton(
onPressed: _controller.text.isNotEmpty ? _submit : null;
)
But when compiled as a website the Button seems no longer aware of the controller value...
I have tried targeting in several different ways, e.g. _controller.value.text.isEmpty and _controller.text.isEmpty...
I'm guessing I'm missing something or this method just isn't possible for web ... Is there any other way to get the same result?
To be honest, your code shouldn't work in flutter mobile either, but may be works because of screen keyboard causes widget rebuild when showing or hiding.
To fix this issue we have to use stateful widget with state variable like canSubmit and update it in textField's listener onChange with setState method. Then every time the text changes, our stateful widget will update the submit button..
class Page extends StatefulWidget {
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
bool canSubmit;
#override
void initState() {
canSubmit = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
onChanged: (value) {
setState(() {
canSubmit = value.isNotEmpty;
});
},
),
RaisedButton(
onPressed: canSubmit ? _submit : null,
child: Text('Submit'),
)
],
),
),
);
}
void _submit() {
print('Submitted');
}
}