BlocBuilder: setState() or markNeedsBuild() called during build - flutter

I tried out the flutter_bloc package but I run into this error:
The following assertion was thrown building BlocBuilder<MyCubit, MyState>(dirty, dependencies: [_InheritedProviderScope<MyCubit?>], state: _BlocBuilderBaseState<MyCubit, MyState>#8c21a):
setState() or markNeedsBuild() called during build.
This Form widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: Form-[LabeledGlobalKey<FormState>#24535]
state: FormState#05273
The widget which was currently being built when the offending call was made was: BlocBuilder<MyCubit, MyState>
dirty
dependencies: [_InheritedProviderScope<MyCubit?>]
state: _BlocBuilderBaseState<MyCubit, MyState>#8c21a
The relevant error-causing widget was:
BlocBuilder<MyCubit, MyState> BlocBuilder:file:///E:/Projekte/untitled9/lib/main.dart:47:18
Here is my code:
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'my_cubit.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return BlocProvider<MyCubit>(
create: (context) => MyCubit(),
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final formKey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo Home Page'),
),
body: Form(
key: formKey,
child: Center(
child: BlocBuilder<MyCubit, MyState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
initialValue: state.text,
onChanged: (value) {
if (!formKey.currentState!.validate()) {
return;
}
final newData = state.copyWith(text: value);
BlocProvider.of<MyCubit>(context).changeData(newData);
},
validator: (v) => v!.trim().isNotEmpty ? null : 'error',
),
ElevatedButton(
onPressed: formKey.currentState == null || !formKey.currentState!.validate() ? null : () { // this causes the error
print('hi');
},
child: const Text('Press'),
),
],
);
},
),
),
),
);
}
}
I know that the error is caused the by the conditional onPressed property of my ElevatedButton. If I replace the line onPressed: formKey.currentState == null || !formKey.currentState!.validate() ? null : () { with the simple onPressed: () { everything works fine. But I want to disable or enable the button based on the current user input and if it is valid.
What is the default why to fix this? Should the BLoC handle the validation?

The issue is happening from formKey.currentState!.validate(), this is trying to validate on build time.
onPressed: formKey.currentState == null ||
!formKey.currentState!.validate()
Try to
class _MyHomePageState extends State<MyHomePage> {
final formKey = GlobalKey<FormState>();
bool isValid = false;
late final TextEditingController controller = TextEditingController()
..addListener(() {
isValid =
formKey.currentState != null && formKey.currentState!.validate();
setState(() {});
});
and use
ElevatedButton(
onPressed: isValid
? () {
// this causes the error
print('hi');
}
: null,
child: const Text('Press'),
),

When you have the ...called during build error it usually means that your widget is not yet ready to consume its state. In this case, and if you really want to mingle with the state in the view without migrating it to the BLoC, you need to wrap your code in addPostFrameCallback
WidgetsBinding.instance.addPostFrameCallback(
(_) => {
// your onPressed logic
},
);
doing so will insure that your widget has at least once before consuming its state.
Below is an example:
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final formKey = GlobalKey<FormState>();
void Function()? myOnPress;
#override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
myOnPress =
formKey.currentState == null || !formKey.currentState!.validate()
? null
: () {
print('hi');
};
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo Home Page'),
),
body: Form(
key: formKey,
child: Center(
child: BlocBuilder<MyCubit, MyState>(
builder: (context, state) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
initialValue: state.text,
onChanged: (value) {
if (!formKey.currentState!.validate()) {
return;
}
final newData = state.copyWith(text: value);
BlocProvider.of<MyCubit>(context).changeData(newData);
},
validator: (v) => v!.trim().isNotEmpty ? null : 'error',
),
ElevatedButton(
onPressed: myOnPress,
child: const Text('Press'),
),
],
);
},
),
),
),
);
}
}

Related

How to continuously get whether the TextField's text is empty in Flutter?

I have a TextField. I want its text not to be empty. (so I want to know if the text is empty)
I have tried using the following code, but it doesn't work:
controller.text.trim().isEmpty()
My code:
TextFormField(
controller: controller,
),
controller.text.trim().isEmpty()
How to continuously get whether the TextField's text is empty in Flutter? I would appreciate any help. Thank you in advance!
full example:
code:
import 'package:flutter/material.dart';
void main() {
runApp(const 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> {
TextEditingController _controller = TextEditingController();
String _text = '';
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo Home Page'),
),
body: Container(
padding: const EdgeInsets.all(16),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(_text),
const SizedBox(height: 20),
TextField(
controller: _controller,
onChanged: (value) {
setState(() {
_text = value;
});
},
decoration: const InputDecoration(
hintText: 'Enter text',
),
),
// submit
ElevatedButton(
onPressed: _text.isEmpty
? null
: () {
setState(() {
_text = _controller.text;
});
},
child: const Text('Submit'),
),
],
),
),
),
);
}
}
It can be done without any temporary variable using ValueListenableBuilder
After some research figured out
controller.text by itself is not listenable
TextEditingController extends ValueNotifier<TextEditingValue> i.e you can use ValueListenableBuilder from material package to listen to text changes.
Code:
class _MyWidgetState extends State<MyWidget> {
late TextEditingController textEditingController;
#override
void initState() {
textEditingController = TextEditingController();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
children: [
TextField(
controller: textEditingController,
),
ValueListenableBuilder<TextEditingValue>(
valueListenable: textEditingController,
builder: (context, value, child) {
return ElevatedButton(
onPressed: value.text.isNotEmpty ? () {} : null,
child: const Text('I am disabled when text is empty'),
);
},
),
],
),
),
);
}
}
Without text:
With text:
You can add listener to your TextEditingController and call setState to update the UI.
late TextEditingController controller = TextEditingController()..addListener(() {
setState((){}); // to update the ui
});
The place you will use controller.text.trim().isEmpty() will show the updated state.
Example
class Test extends StatefulWidget {
const Test({super.key});
#override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
late TextEditingController controller = TextEditingController()
..addListener(() {
setState(() {}); // to update the ui
});
#override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: controller,
),
ElevatedButton(
onPressed: controller.text.trim().isEmpty ? null : () {},
child: Text("Button"))
],
);
}
}

Flutter Bloc does not change TextFormField initialValue

I'm using Bloc library and noticed after yielding a new state my TextFormField initialValue does not change.
My app is more complicated than this but I did a minimal example. Also tracking the state it is changing after pushing the events.
Bloc is supposed to rebuild the entire widget right. Am I missing something?
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as developer;
void main() {
runApp(MyApp());
}
enum Event { first }
class ExampleBloc extends Bloc<Event, int> {
ExampleBloc() : super(0);
#override
Stream<int> mapEventToState(Event event) async* {
yield state + 1;
}
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => ExampleBloc(),
child: Builder(
builder: (contex) => SafeArea(
child: BlocConsumer<ExampleBloc, int>(
listener: (context, state) {},
builder: (context, int state) {
developer.log(state.toString());
return Scaffold(
body: Form(
child: Column(
children: [
TextFormField(
autocorrect: false,
initialValue: state.toString(),
),
RaisedButton(
child: Text('Press'),
onPressed: () {
context.bloc<ExampleBloc>().add(Event.first);
},
)
],
),
),
);
}),
),
),
),
);
}
}
pubspec.yaml
name: form
description: A new Flutter project.
version: 1.0.0+1
environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
bloc: ^6.0.0
flutter_bloc: ^6.0.0
Edit
As #chunhunghan noted adding a UniqueKey solves this. I should have also mentioned that my case. the app emits events from the onChanged method of two TextFormField. This causes the Form to reset and remove the keyboard. autofocus does not work because there are two TextFormField wgich emit events.
You can copy paste run full code 1 and 2 below
You can provide UniqueKey() to Scaffold or TextFormField to force recreate
You can referecne https://medium.com/flutter/keys-what-are-they-good-for-13cb51742e7d for detail
if the key of the Element doesn’t match the key of the corresponding Widget. This causes Flutter to deactivate those elements and remove the references to the Elements in the Element Tree
Solution 1:
return Scaffold(
key: UniqueKey(),
body: Form(
Solution 2:
TextFormField(
key: UniqueKey(),
working demo
full code 1 Scaffold with UniqueKey
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as developer;
void main() {
runApp(MyApp());
}
enum Event { first }
class ExampleBloc extends Bloc<Event, int> {
ExampleBloc() : super(0);
#override
Stream<int> mapEventToState(Event event) async* {
yield state + 1;
}
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
print("build");
return MaterialApp(
home: BlocProvider(
create: (_) => ExampleBloc(),
child: Builder(
builder: (contex) => SafeArea(
child: BlocConsumer<ExampleBloc, int>(
listener: (context, state) {},
builder: (context, int state) {
print("state ${state.toString()}");
developer.log(state.toString());
return Scaffold(
key: UniqueKey(),
body: Form(
child: Column(
children: [
TextFormField(
autocorrect: false,
initialValue: state.toString(),
),
RaisedButton(
child: Text('Press'),
onPressed: () {
context.bloc<ExampleBloc>().add(Event.first);
},
)
],
),
),
);
}),
),
),
),
);
}
}
full code 2 TextFormField with UniqueKey
import 'package:flutter/material.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'dart:developer' as developer;
void main() {
runApp(MyApp());
}
enum Event { first }
class ExampleBloc extends Bloc<Event, int> {
ExampleBloc() : super(0);
#override
Stream<int> mapEventToState(Event event) async* {
yield state + 1;
}
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
print("build");
return MaterialApp(
home: BlocProvider(
create: (_) => ExampleBloc(),
child: Builder(
builder: (contex) => SafeArea(
child: BlocConsumer<ExampleBloc, int>(
listener: (context, state) {},
builder: (context, int state) {
print("state ${state.toString()}");
developer.log(state.toString());
return Scaffold(
body: Form(
child: Column(
children: [
TextFormField(
key: UniqueKey(),
autocorrect: false,
initialValue: state.toString(),
),
RaisedButton(
child: Text('Press'),
onPressed: () {
context.bloc<ExampleBloc>().add(Event.first);
},
)
],
),
),
);
}),
),
),
),
);
}
}
You should not be rebuilding the entire Form just because you want to update the value of the TextFormField, try using a TextEditingController and update the value on the listener.
TextEditingController _controller = TextEditingController();
BlocProvider(
create: (_) => ExampleBloc(),
child: Builder(
builder: (contex) => SafeArea(
child: BlocListener<ExampleBloc, int>(
listener: (context, state) {
_controller.text = state.toString();
},
child: Scaffold(
body: Form(
child: Column(
children: [
TextFormField(
controller: _controller,
autocorrect: false,
),
RaisedButton(
child: Text('Press'),
onPressed: () {
context.bloc<ExampleBloc>().add(Event.first);
},
)
],
),
),
);
}),
I also had the exact same problem. While adding the Unique Key the flutter keeps building the widget and my keyboard unfocus each time. The way I solved it is to add a debounce in onChanged Event of the TextField.
class InputTextWidget extends StatelessWidget {
final Function(String) onChanged;
Timer _debounce;
void _onSearchChanged(String value) {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 2000), () {
onChanged(value);
});
}
#override
Widget build(BuildContext context) {
return TextFormField(
controller: TextEditingController(text: value)
..selection = TextSelection.fromPosition(
TextPosition(offset: value.length),
),
onChanged: _onSearchChanged,
onEditingComplete: onEditingCompleted,
);
}
}
Hope if this help for someone, working with form, bloc and and has too update the form.
Edit: Although adding a debounce help show what. I have changed the code to be more robust. Here is the change.
InputTextWidget (Changed)
class InputTextWidget extends StatelessWidget {
final Function(String) onChanged;
final TextEditingController controller;
void _onSearchChanged(String value) {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 2000), () {
onChanged(value);
});
}
#override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
onChanged: _onSearchChanged,
onEditingComplete: onEditingCompleted,
);
}
}
And on my presentation end
class _NameField extends StatelessWidget {
const _NameField({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final TextEditingController _controller = TextEditingController();
return BlocConsumer<SomeBloc,
SomeState>(
listenWhen: (previous, current) =>
previous.name != current.name,
listener: (context, state) {
final TextSelection previousSelection = _controller.selection;
_controller.text = state.name;
_controller.selection = previousSelection;
},
buildWhen: (previous, current) =>
previous.name != current.name,
builder: (context, state) => FormFieldDecoration(
title: "Name",
child: InputTextWidget(
hintText: "AWS Certification",
textInputType: TextInputType.name,
controller: _controller,
onChanged: (value) => context
.read< SomeBloc >()
.add(SomeEvent(
value)),
),
),
);
}
}
This edit is working perfectly.
Final Edit:
I added a key? key on my bloc state and pass this key to the widget. If I needed to redraw the form again, I changed the key to UniqueKey from the event. This is the by far easiest way I have implemented bloc and form together. If you needed explanation, please comment here, I will add it later.

Constructor of screen is called every time a change occurs in that screen

When tapping a textformfield in a pushed screen, the constructor of the screen is called again and the textformfield loses its value. Also, I think that every change happens in that screen causes its constructor to be called again, and I don't know the reason at all.
Here is a sample code that generates the error:
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
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> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Hello',
style: TextStyle(color: Colors.black, fontSize: 30.0),
)
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => NextScreen(Bloc())));
},
child: Icon(Icons.add),
),
);
}
}
And here is the screen to be pushed
class NextScreen extends StatefulWidget {
final _bloc;
NextScreen(this._bloc);
#override
_NextScreenState createState() => _NextScreenState();
}
class _NextScreenState extends State<NextScreen> {
#override
void dispose() {
widget._bloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.all(100.0),
child: IconButton(
onPressed: () {
Navigator.of(context).pop();
},
icon: Icon(Icons.arrow_back),
iconSize: 20.0,
),
),
StreamBuilder<String>(
stream: widget._bloc.stream,
builder: (context, snapshot) {
return TextFormField(
controller: widget._bloc.controller,
onFieldSubmitted: widget._bloc.submitData(),
decoration: InputDecoration(
hintText: 'Enter your name..',
errorText: snapshot.data,
),
);
})
],
),
);
}
}
A bloc that validates the user input
class Bloc {
TextEditingController _controller;
TextEditingController get controller => _controller;
BehaviorSubject<String> _subject;
BehaviorSubject<String> _validatorSubject;
Stream<String> get stream => _validatorSubject.stream;
void submitData() {
_subject.sink.add(controller.text);
}
void _validate(String text) {
if (!RegExp(r'[0-9]').hasMatch(text)) {
_validatorSubject.sink.add('numbers only');
} else {
_validatorSubject.sink.add(null);
}
}
Bloc() {
_controller = TextEditingController();
_subject = BehaviorSubject<String>();
_validatorSubject = BehaviorSubject<String>();
_subject.stream.listen(_validate);
}
void dispose() {
_subject.close();
_validatorSubject.close();
}
}
Opening and closing a keyboard will rebuild the whole screen.
The real culprit here is the textController :
controller: widget._bloc.controller,
The solution which worked for me is to remove this line.
Also to get and validate the changed text you can use onChanged in the text field, which return a String.
Like this :
.
.
.
return TextFormField(
onChanged: widget._bloc.submitData,
decoration: InputDecoration
.
.
.
And you submitData() method would go like this :
void submitData(String data) {
_subject.sink.add(data);
}

Accessing a method of state class using its stateful widget?

I have a method in state class, but I need to access that method in outside using its widget class reference,
class TestFormState extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _testState();
}
}
class _testFormState extends State<TestFormState> {
int count = 1;
#override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.green,
child: Text("Count : $count"),
),
);
}
clickIncrease(){
setState(() { count += 1; });
}
}
and I need to access the above widget`s clickIncrease in another widget, like below code,
class TutorialHome extends StatelessWidget {
TestFormState test;
#override
Widget build(BuildContext context) {
// Scaffold is a layout for the major Material Components.
return Scaffold(
body: Column(
children: <Widget>[
test = TestFormState(),
FlatButton(
child: Text("Increase"),
onPressed: (){
test.state.clickIncrease(); // This kind of thing I need to do
},
),
]
),
);
}
I wrote above code just for demostrate the issue.
I have a trick, but I don't know if it is a bad practice or not.
class TestFormState extends StatefulWidget {
_TestFormState _testFormState;
#override
State<StatefulWidget> createState() {
_testFormState = _TestFormState();
return _testFormState;
}
}
class _TestFormState extends State<TestFormState> {
int count = 1;
#override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.green,
child: Text("Count : $count"),
),
);
}
clickIncrease(){
setState(() { count += 1; });
}
}
Now, you can access it here :
class TutorialHome extends StatelessWidget {
TestFormState test;
#override
Widget build(BuildContext context) {
// Scaffold is a layout for the major Material Components.
return Scaffold(
body: Column(
children: <Widget>[
TextButton(
child: Text("Increase"),
onPressed: () {
test._testFormState
.clickIncrease(); // This is accessable
},
),
]
),
);
}
}
I suggest taking a look at ValueNotifier
I think there is a better way to manage your app state in an easy way and I agree that using provider could be effective.
Provide the model to all widgets within the app. We're using
ChangeNotifierProvider because that's a simple way to rebuild
widgets when a model changes. We could also just use Provider, but
then we would have to listen to Counter ourselves.
Read Provider's docs to learn about all the available providers.
Initialize the model in the builder. That way, Provider can own
Counter's lifecycle, making sure to call dispose when not needed
anymore.
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
Simplest possible model, with just one field. ChangeNotifier is a
class in flutter:foundation. Counter does not depend on Provider.
class Counter with ChangeNotifier {
int count = 1;
void clickIncrease() {
count += 1;
notifyListeners();
}
}
Consumer looks for an ancestor Provider widget and retrieves its
model (Counter, in this case). Then it uses that model to build
widgets, and will trigger rebuilds if the model is updated.
You can access your providers anywhere you have access to the context.
One way is to use Provider<Counter>.of(context).
The provider package also defines extension methods on context itself.
You can call context.watch<Counter>() in a build method of any
widget to access the current state of Counter, and to ask Flutter to
rebuild your widget anytime Counter changes.
You can't use context.watch() outside build methods, because that
often leads to subtle bugs. Instead, you should use
context.read<Counter>(), which gets the current state but doesn't
ask Flutter for future rebuilds.
Since we're in a callback that will be called whenever the user taps
the FloatingActionButton, we are not in the build method here. We
should use context.read().
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Scaffold is a layout for the major Material Components.
return Scaffold(
appBar: AppBar(
title: Text('Flutter Demo Home Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Count:'),
Consumer<Counter>(
builder: (context, counter, child) => Text(
'${counter.value}',
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
// I've change the button to `FloatingActionButton` for better ui experience.
floatingActionButton: FloatingActionButton(
// Here is the implementation that you are looking for.
onPressed: () {
var counter = context.read<Counter>();
counter.increment();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Complete code:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => Counter(),
child: MyApp(),
),
);
}
class Counter with ChangeNotifier {
int count = 1;
void clickIncrease() {
count += 1;
notifyListeners();
}
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Demo Home Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Count:'),
Consumer<Counter>(
builder: (context, counter, child) => Text(
'${counter.count}',
style: Theme.of(context).textTheme.headline4,
),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
var counter = context.read<Counter>();
counter.clickIncrease();
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
Actual app:
For more information on the provider package (where Provider comes from), please see the package documentation.
For more information on state management in Flutter, and a list of other approaches, head over to the State management page at flutter.dev.
There is a built in method findAncestorStateOfType to find Ancestor _MyAppState class of the Parent MyApp class.
Here is the Code
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
static void setLocale(BuildContext context, Locale locale) {
_MyAppState? state = context.findAncestorStateOfType<_MyAppState>();
state!.setLocale(locale);
}
#override
_MyAppState createState() => _MyAppState();
}
// ignore: use_key_in_widget_constructors
class _MyAppState extends State<MyApp> {
// const MyApp({Key? key}) : super(key: key)
late Locale _locale;
void setLocale(Locale value) {
setState(() {
_locale = value;
});
}
}
class TestForm extends StatelessWidget {
final int _count;
TestForm(int count) : _count = count;
#override
Widget build(BuildContext context) {
return Center(
child: Container(
color: Colors.green,
child: Text('Count : $_count'),
),
);
}
}
class TutorialHome extends StatefulWidget {
#override
State<TutorialHome> createState() => _TutorialHomeState();
}
class _TutorialHomeState extends State<TutorialHome> {
int _count = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
TestForm(_count), // <---
TextButton(
child: Text("Increase"),
onPressed: () => setState(() => _count++),
),
],
),
);
}
}

How to call a void function everywhere in my Flutter app using InheritedWidget

I have a main.dart and has a button in center. When user tabs the button it navigate into home.dart page. My home.dart page also has a button on center and when user tabs the button it navigate to details page. The app tree and code is shown below.
I try to implement the "InheritedWidget" in my home.dart so after the home.dart as deep as I go I can call the "void _handleUserInteraction" function using "InheritedWidget". Unluckly I am keep getting error that says:
I/flutter (20715): The getter 'handleOnTap' was called on null.
I/flutter (20715): Receiver: null
I/flutter (20715): Tried calling: handleOnTap
home.dart code:
import 'package:flutter/material.dart';
import 'dart:async';
import 'main.dart';
import 'details.dart';
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Timer timer;
// TODO: 1 - INIT STATE
#override
void initState() {
super.initState();
setState(() {
_initializeTimer();
});
}
// TODO: 3 - INITIALIZE TIMER
void _initializeTimer() {
timer = Timer.periodic(const Duration(minutes: 5), (__) {
_logOutUser();
});
}
// TODO: 4 - LOG OUT USER
void _logOutUser() {
timer.cancel();
Navigator.push(
context, new MaterialPageRoute(builder: (context) => new MyApp()));
}
// TODO: 5 - HANDLE USER INTERACTION
// void _handleUserInteraction([_]) {
void _handleUserInteraction() {
print("+++++++ _handleUserInteraction Header ++++++++");
if (!timer.isActive) {
return;
}
timer.cancel();
_initializeTimer();
print("+++++++ _handleUserInteraction Footer ++++++++");
}
#override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData(
primarySwatch: Colors.red,
),
home: LoginState(
callback: _handleUserInteraction,
child: Builder(builder: homeScreenBuilder)),
);
}
#override
Widget homeScreenBuilder(BuildContext context) {
Function() _callback = LoginState.of(context).callback;
return GestureDetector(
onTap: _callback,
onDoubleTap: _callback,
onLongPress: _callback,
onTapCancel: _callback,
child: new Scaffold(
appBar: AppBar(
title: Text("HOME PAGE"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'GOTO DETAILS PAGE',
),
new RaisedButton(
child: new Text("Details"),
onPressed: () {
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => new Details()));
})
],
),
),
));
}
class LoginState extends InheritedWidget {
final Widget child;
final Function() callback;
final Key key;
LoginState({#required this.callback, #required this.child, this.key})
: super(key: key);
#override
bool updateShouldNotify(LoginState oldWidget) {
return true;
}
static LoginState of(BuildContext context) =>
context.inheritFromWidgetOfExactType(LoginState);
}
details.dart code:
import 'package:flutter/material.dart';
import 'home.dart';
class Details extends StatefulWidget {
#override
_DetailsState createState() => _DetailsState();
}
class _DetailsState extends State<Details> {
#override
Widget build(BuildContext context) {
Function() _callback = LoginState.of(context).callback;
return GestureDetector(
onTap: _callback,
onDoubleTap: _callback,
onLongPress: _callback,
onTapCancel: _callback,
child: new Scaffold(
appBar: AppBar(
title: Text("Details PAGE"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Every time Tabed it reset the home timer',
),
],
),
),
));
}
}
UPDATE:
I change my home.dart code. The onTap: _callback is working but in details.dart I get same error saying that:
error: - The getter 'callback' was called on null.
The reason why you're getting the error The getter 'callback' was called on null. is because LoginState.of(context) is null.
class _DetailsState extends State<Details> {
#override
Widget build(BuildContext context) {
Function() _callback = LoginState.of(context).callback;
...
}
}
Since you're using InheritedWidget, I assume that you may be attempting to do state management. If so, you can check this guide on implementing app state management. One way of doing this is with the use of provider.
You can try running the sample below. I've based it from the given sample.
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'dart:async';
import 'details.dart';
void main() {
// https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple#changenotifierprovider
runApp(ChangeNotifierProvider(
create: (context) => LoginState(),
child: MyApp(),
));
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
void initState() {
super.initState();
// This allows access to LoginState data if no UI changes needed
// https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple#providerof
Provider.of<LoginState>(context, listen: false).initializeTimer();
}
#override
Widget build(BuildContext context) {
// Consumer grants access to LoginState
// https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple#consumer
return Consumer<LoginState>(
builder: (context, loginState, child) {
return GestureDetector(
onTap: () => loginState.handleUserInteraction(),
// onDoubleTap: _callback,
// onLongPress: _callback,
// onTapCancel: _callback,
child: new Scaffold(
appBar: AppBar(
title: Text("HOME PAGE"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'GOTO DETAILS PAGE',
),
new RaisedButton(
child: new Text("Details"),
onPressed: () {
Navigator.push(
context,
new MaterialPageRoute(
builder: (context) => new Details()));
})
],
),
),
));
},
);
}
}
// https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple#changenotifier
class LoginState extends ChangeNotifier {
Timer _timer;
void initializeTimer() {
_timer = Timer.periodic(const Duration(minutes: 5), (__) {
logOutUser();
});
}
void logOutUser() {
_timer.cancel();
}
void handleUserInteraction() {
print("+++++++ _handleUserInteraction Header ++++++++");
if (!_timer.isActive) {
return;
}
_timer.cancel();
initializeTimer();
print("+++++++ _handleUserInteraction Footer ++++++++");
}
}
details.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'main.dart';
class Details extends StatefulWidget {
#override
_DetailsState createState() => _DetailsState();
}
class _DetailsState extends State<Details> {
#override
Widget build(BuildContext context) {
return Consumer<LoginState>(
builder: (context, loginState, child) {
return GestureDetector(
onTap: () => loginState.handleUserInteraction(),
// onDoubleTap: _callback,
// onLongPress: _callback,
// onTapCancel: _callback,
child: new Scaffold(
appBar: AppBar(
title: Text("Details PAGE"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Every time Tabed it reset the home timer',
),
],
),
),
));
},
);
}
}