I am using GetX plugin to navigate between the pages. And when I navigate back to the page which has Text input field I get 'Duplicate GlobalKey detected in widget tree'
Main Page
import 'package:flutter/material.dart';
import 'package:flutter_application_1/binding.dart';
import 'package:flutter_application_1/controller.dart';
import 'package:flutter_application_1/next.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:get/get.dart';
void main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return GetMaterialApp(
title: 'Kids Game',
debugShowCheckedModeBanner: false,
home: const HomePage(),
initialBinding: HomeBinding(),
builder: EasyLoading.init(),
);
}
}
class HomePage extends GetView<HomeController> {
const HomePage({Key? key}) : super(key: key);
static final formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
Form(
key: formKey,
child: TextFormField(
textAlign: TextAlign.center,
autocorrect: false,
controller: controller.editCtrl,
)),
InkWell(
onTap: () {
Get.to(() => NextPage())
},
child: Text("Next"),
)
],
),
);
}
}
NextPage Widget
import 'package:flutter/material.dart';
import 'package:flutter_application_1/controller.dart';
import 'package:flutter_application_1/main.dart';
import 'package:get/get.dart';
class NextPage extends StatelessWidget {
final homeCtrl = Get.find<HomeController>();
NextPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Text("this is next page"),
InkWell(
onTap: () {
Get.offAll(() => HomePage(), transition: Transition.downToUp);
},
child: const Text("Go Back"),
),
],
),
),
);
}
}
Controller.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class HomeController extends GetxController {
//GlobalKey<FormState> formKey = GlobalKey<FormState>();
final editCtrl = TextEditingController();
}
Binding
import 'package:flutter_application_1/controller.dart';
import 'package:get/get.dart';
class HomeBinding implements Bindings {
#override
void dependencies() {
Get.lazyPut(
() => HomeController(),
);
}
}
When I navigate to NextPage, and then back to HomePage I get the error
Duplicate GlobalKey detected in widget tree.
I read a few different posts, and people had recommended using static with formkey as that has resolved the issue for them so I tried doing the same but it didn't work for me.
When leaving the page with Getx use one of these options, depending on your routing solution:
Get.offAndToNamed('next_screen')
or
Get.off(NextScreen());
This removes the current page from the navigation stack and thus the error Duplicate GlobalKey detected in widget tree should be gone for good when reentering the page.
Source:
https://github.com/jonataslaw/getx/blob/master/documentation/en_US/route_management.md
The issue got resolved. I did the following, in case someone else is facing the issue. I declared the key as private and change the variable from final.
GlobalKey<FormState> _formKey = GlobalKey<FormState>();
And based on a lot of answers I have read, the key is for UI layer so it shouldn't be declared in the controller.
Just change the form key for each widget. Forexample: Just use globalFormKey1 for the first widget and GlobalFormKey2 for the second one. This will not give the error of duplications.
Related
I want to develop a more advanced version of the ListView widget as a package. This is my first package.
I wrote a code like this:
modern_card_listview.dart:
library modern_card_listview;
import 'package:flutter/material.dart';
class CardListView extends StatefulWidget {
List items;
CardListView({Key? key, required this.items}) : super(key: key);
#override
State<CardListView> createState() => _CardListViewState();
}
class _CardListViewState extends State<CardListView> {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: ListView.builder(
itemCount: widget.items.length,
itemBuilder: ((context, index) {
return Card(
child: ListTile(
title: widget.items[index],
),
);
}),
),
),
);
}
}
I guess I need to use the dart file in the test folder to test the code.
modern_card_listview_test.dart:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:modern_card_listview/modern_card_listview.dart';
CardListView _listView = CardListView(
items: const ["Bu bir", "Bu iki", "Bu uc", "Bu dort", "Bu bes"]);
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: _listView,
),
);
}
}
How can I try the package I wrote? When I ran the code above, No tests were found. I get an error.This is my first pack and I'm still new. I would be grateful if you help.
I couldn't find a solution
I think it's very normal to use StatefulWidget to implement the following in an app that controls ListView scrolling.
//sample(a)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
MaterialApp(
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
ElevatedButton(
child: Text('Go to ListViewPage'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ChangeNotifierProvider<ListViewPageController>(
create: (_) => ListViewPageController(),
child: ListViewPage(),
),
),
);
},
),
],
),
),
);
}
}
class ListViewPage extends StatefulWidget {
const ListViewPage({Key? key}) : super(key: key);
#override
State<ListViewPage> createState() => _ListViewPageState();
}
class _ListViewPageState extends State<ListViewPage> {
final ScrollController sc = ScrollController();
#override
void dispose() {
sc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
controller: sc,
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text('$index'));
}),
);
}
}
On the other hand, I think that the following implementation that manages ListViewPage scrolling using ChangeNotifier/Provider (Not Riverpod) without using StatefulWidget can also be considered. The actual execution itself is done without problems.
////sample(b)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ListViewPageController extends ChangeNotifier {
ScrollController sc = ScrollController();
}
void main() {
runApp(
MaterialApp(
home: HomePage(),
),
);
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
ElevatedButton(
child: Text('Go to ListViewPage'),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
ChangeNotifierProvider<ListViewPageController>(
create: (_) => ListViewPageController(),
child: ListViewPage(),
),
),
);
},
),
],
),
),
);
}
}
class ListViewPage extends StatelessWidget {
const ListViewPage({Key? key}) : super(key: key);
//↓Is this part mandatory?
/*
late final ScrollController sc = context.read<ListViewPageController>().sc;
#override
void dispose() {
sc.dispose();
super.dispose();
}
*/
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
controller: context.read<ListViewPageController>().sc,
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(title: Text('$index'));
}),
);
}
}
Question1
Sample(b) has the same behavior as sample(a), but since ListViewPage is StatelessWidget, sc.dispose is not called.
My current understanding is that the ListViewPageController will be destroyed when we back from ListViewPage to HomePage, but will the ScrollController be also automatically destroyed at that point?
Shouldn't I write the code about sc.dispose myself?
Or do I have to make the ListViewPage a StatefulWidget and write the code myself to call sc.dispose in state.dispose?
Question2
Recently, I heard the following claim (explanation).
"Subclasses of ChangeNotifier (or StateNotfier) should not depend on any UI code.
Flutter's ScrollController/PageController/TextEditingController/Forms, etc. are classes in the Flutter SDK (that is, UI code).
So in this example the ListViewPageController should not have(depend on) a ScrollController. ”
Is this correct?
I also saw another sample code that uses Scrollable.ensureVisible to control scrolling, but since this Scrollable class is also UI code, is it a bad practice to use it in a ChangeNotifier subclass as shown below?
(In this case each ListTile should have a GlobalKey.)
Personally, I think it would be easier to understand if the ChangeNotifier subclass held the ScrollController,PageController, etc.
How important is this theory when developing in Flutter?
class ListViewPageController extends ChangeNotifier {
ScrollController sc = ScrollController();
scrollRequest(int scrollIndex) {
Scrollable.ensureVisible(keyList[scrollIndex - 100].currentContext!,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
alignment: 0.5,
alignmentPolicy: ScrollPositionAlignmentPolicy.explicit);
}
}
Q1. In sample b, sc is an obj< ScrollController > in another obj< ListViewPageController >, let's say it's lc.
When lc is (disposed)->deleted, surely will sc be (disposed)->deleted.
Q2. Holding ScrollController in ListViewPageController may cause error when you accidentally dispose ListViewPageController while ListViewPage was still alive.
If you want to access ScrollController, try inject it into ListViewPageController would be a better practice.
I have an issue with updating text inside TextFormField when using Provider as state management.
I reduced my problem to an abstract one (I removed all the clutter code) and here how it works:
there is a someValue in AppState
the someValue can be edited via Form->TextFormField
the someValue is to be reflected as a title of the AppBar when typing (onChange)
the someValue can be updated from external source (in the example it is a button that updates it)
when someValue is updated from external source, it MUST be updated in text Form->TextFormField as well
The last one is causing me the problem. Consider the following code:
AppState.dart
import 'package:flutter/foundation.dart';
class AppState extends ChangeNotifier{
String someValue = '';
updateSomeValue(String newValue){
someValue = newValue;
notifyListeners();
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:text_ctrl_issue/app_state.dart';
void main() {
runApp(ChangeNotifierProvider(create: (_) => AppState(), child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: 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({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late TextEditingController _controller;
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_controller = TextEditingController();
}
#override
Widget build(BuildContext context) {
final provider = Provider.of<AppState>(context);
// following line of code makes it possible for text to be changed by button
// and reflected in TextFormField
// but it causes nasty side effect, that when typing, cursor always goes to beginning of the line
_controller.text = provider.someValue;
return Scaffold(
appBar: AppBar(
title: Text(provider.someValue),
),
body: Center(
child: Form(
key: _formKey,
child: Column(children: [
TextFormField(
controller: _controller,
onChanged: (value) {
provider.updateSomeValue(value);
},
),
ElevatedButton(
onPressed: () {
provider.updateSomeValue('foo_bar');
},
child: Text('change text external source'))
])),
),
);
}
}
The problem:
When I added the line _controller.text = provider.someValue; it fixed the issue of updating TextFormField when button is clicked, but it create new issue, that when typing in TextFormField, it is also triggered, cause carret of text field to move to the beginning of the text field.
How to make it work so the text (value) of a TextFormField can be updated externally, without causing carret issue when typing?
EDIT
The answer of Yeasin Sheikh using addListener doesn't quite work (it is hacky) because:
it listens to every event (e.g. onFocus or cursor changed)
it does not take into account situation that EleveatedButton is in different scope than _controller (e.g. is in different widget).
An easy way of doing this by listening TextEditingController, while the TextFormField is the ruler here.
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late TextEditingController _controller;
#override
void dispose() {
_controller.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
_controller = TextEditingController()
..addListener(() {
Provider.of<AppState>(context, listen: false)
.updateSomeValue(_controller.text);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.watch<AppState>().someValue),
),
body: Center(
child: Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _controller,
),
ElevatedButton(
onPressed: () {
_controller.text = 'foo_bar';
},
child: Text('change text external source'))
],
),
),
),
);
}
}
Also, you can check riverpod
import 'package:flutter/foundation.dart';
class AppState extends ChangeNotifier
{
TextEditingController _controller=TextEditingController();
TextEditingController get controller=>_controller();
String someValue = '';
updateSomeValue(String newValue)
{
someValue = newValue;
notifyListeners();
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:text_ctrl_issue/app_state.dart';
void main() {
runApp(ChangeNotifierProvider(create: (_) => AppState(), child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: 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({Key? key}) : super(key: key);
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
#override
void dispose() {
super.dispose();
}
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
final provider = Provider.of<AppState>(context);
return Scaffold(
appBar: AppBar(
title: Text(provider.someValue),
),
body: Center(
child: Form(
key: _formKey,
child: Column(children: [
TextFormField(
controller: Provider.controller,
onChanged: (v) {
provider.updateSomeValue(v);
},
),
ElevatedButton(
onPressed: () {
provider.updateSomeValue('foo_bar');
},
child: Text('change text external source'))
])),
),
);
}
}
Question:
How to call methodA() from onPressed() of IconButton.
I've tryed to do this by using GlobalKey:
GlobalKey<_MyButtonState> globalKey = GlobalKey();
But it's returns an error.
I have read many forums on this and I have tried all the solutions posed but none of them are working for me.
CODE:
main.dart
import 'package:flutter/material.dart';
import 'button.dart';
void main() {
runApp(MaterialApp( title: 'My app', home: MyApp(),));
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.help),
onPressed: () {
// how can I call methodA from here?
},
),
),
body: HomePage(),
),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
#override
Widget build(BuildContext context) {
return Center(
child: MyButton(),
);
}
}
button.dart
import 'package:flutter/material.dart';
class MyButton extends StatefulWidget {
#override
_MyButtonState createState() => _MyButtonState();
}
class _MyButtonState extends State<MyButton> {
#override
Widget build(BuildContext context) {
return Container( );
}
void methodA(){
print('methodA');
}
}
I have read many forums on this and I have tried all the solutions posed but none of them are working for me.
first, you will have to import the file as a package in main.dart:
Main.dart: (just writing the way to import the file)
import 'package:prioject_name/file_name.dart';
Note: this is for files under lib directory.
if your file is under a different directory inside lib
then add the path accordingly,
eg: Button.dart is inside the widgets folder inside the lib folder:
lib
|____widgets
|____Button.dart
then the import statement will be as follows:
import 'package:prioject_name/widgets/Button.dart';
Then try your global key method to call the function:
If it is still not working then you can use my method,
how I call methods from different class in onPressed or onTapped:
your Button.dart file.
import 'package:flutter/material.dart';
// changed the method definition class
class MyButton extends StatefulWidget {
void methodA(){
print('methodA');
}
#override
_MyButtonState createState() => _MyButtonState();
}
class _MyButtonState extends State<MyButton> {
#override
Widget build(BuildContext context) {
...
widget.methodA(); // this would call the method A, anywhere inside the Widget build() function.
return Container( );
}
}
Now in Main.dart:
import 'package:prioject_name/Button.dart';
//call the function here using className().functioName();
....
onPressed(){
MyButton().methodA();
}
Take a look at the InheritedWidget class (and watch the videos).
Base class for widgets that efficiently propagate information down the
tree.
You can look at creating an InheritedWidget that contains a ValueNotifier.
class MyInheritedWidget extends InheritedWidget {
final ValueNotifier<int> buttonTapCountNotifier;
const MyInheritedWidget({
Key key,
#required this.buttonTapCountNotifier,
#required Widget child,
}) : assert(child != null),
super(key: key, child: child);
static MyInheritedWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
}
}
Your MyButton class can call MyInheritedWidget.of(context).buttonTapCountNotifier to get hold of the ValueNotifier and add a listener to it.
Each time the ValueNotifier notifies your MyButton class that the value has been incremented, you can execute methodA.
You could use the Provider package which is quite the preferred method to manage state in Flutter apps. This will help you as well in organizing and growing the app in a clever way.
Take a look at the working code below.
define a ChangeNotifier (PressedProvider) which will save
current state of the app in a unique location and the behavior of your onPress function
you wrap your app
with a ChangeNotifierProvider widget
you wrap the receiving
Widget with a Consumer
you get the Provider.of() when you
need to do something and call a method on it
it will notify the Consumer of a change
Code:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
final Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(ChangeNotifierProvider<PressedProvider>( // 2
create: (_) => PressedProvider(),
child: MyApp(),
));
}
class PressedProvider extends ChangeNotifier { // 1
void pressButton() {
print("pressButton");
notifyListeners();
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(scaffoldBackgroundColor: darkBlue),
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
leading: Consumer<PressedProvider>( // 3
builder: (_, provider, widget) => IconButton(
icon: Icon(Icons.help),
onPressed: () {
provider.pressButton();
},
),
),
),
body: Center(
child: MyButton(),
),
),
);
}
}
class MyButton extends StatelessWidget {
#override
Widget build(BuildContext context) {
PressedProvider provider = Provider.of<PressedProvider>(context); // 4
return Center(
child: RawMaterialButton(
child: Text("Press me"),
onPressed: () => provider.pressButton()),
);
}
}
could you please show me how can I notify my statefull child widget that somewhere in parent user clicks on button?
I have two separate .dart files
in the first file I described main screen widget with FAB
and in the second one I have ListWidget (like RecyclerView)
If user tap on FAB I want notify my ListWidget about it so it can e.g. add one more item.
I have java/android background but it's quite hard for me to change my mind flow.
The first option would be to build the child widget each time you add an item to the list, passing the list as a parameter to the child.
But using streams is a nice way to avoid rebuilding the child widget each time. I think the following code is a good starting point (You could also use a StreamBuilder to build the list leveraging the stream).
In main.dart
import 'dart:async';
import 'package:base_test_project/expanding_list.dart';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
StreamController<int> _controller = StreamController<int>();
int _number = 0;
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new ExpandingList(stream: _controller.stream),
),
floatingActionButton: new FloatingActionButton(
onPressed: () {_controller.add(_number++);},
child: new Icon(Icons.add),
),
);
}
}
In expanding_list.dart
import 'dart:async';
import 'package:flutter/material.dart';
class ExpandingList extends StatefulWidget {
Stream<int> stream;
ExpandingList({this.stream});
#override
_ExpandingListState createState() => _ExpandingListState();
}
class _ExpandingListState extends State<ExpandingList> {
List<int> _myList = [];
#override
void initState() {
super.initState();
widget.stream.listen((number) {
setState(() { _myList.add(number); });
});
}
#override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _myList.length,
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.all(15.0), child: Text("Item ${_myList[index]}"));
});
}
}