Cannot retrieve text from textfield with Riverpod - flutter

My code
class _GenericTextFieldState extends State<GenericTextField> {
#override
Widget build(BuildContext context) {
return CupertinoTextField(
controller: textFieldController,
padding: EdgeInsets.all(8),
prefix: Icon(Icons.email_outlined),
placeholder: widget.hint,
);
}
}
final textFieldController = TextEditingController();
final textFieldProvider = Provider<String> ( (_) => textFieldController.text);
the textFieldController is supplying the string to the textFieldProvider.
I am trying to get the string in another file using the consumer widget like so
class LoadingButton extends ConsumerWidget {
LoadingButton(this.buttonName);
final String buttonName;
#override
Widget build(BuildContext context,ScopedReader watch) {
String textInput = watch(textFieldProvider);
return RoundedLoadingButton(
successColor: mainColor,
errorColor: Colors.orange,
height: 40,
color: mainColor,
child: Text(buttonName, style: TextStyle(color: Colors.white)),
controller: _btnController,
onPressed: (){
mLog("Input from provider username: $textInput");
},
);
}
}
However the textInput variable is always empty.
What am I missing.

you can use onChanged with StateProvider
something like this
Full Example
final textFieldProvider = StateProvider<String>((ref) => "");
class Main extends StatelessWidget {
const Main({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return ProviderScope(
child: Scaffold(
appBar: AppBar(
title: Text('Title'),
),
body: Column(
children: [
Container(),
_GenericTextFieldState(),
LoadingButton("Test")
],
),
),
);
}
}
class _GenericTextFieldState extends StatelessWidget {
const _GenericTextFieldState({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return CupertinoTextField(
padding: EdgeInsets.all(8),
prefix: Icon(Icons.email_outlined),
placeholder: "Input text",
onChanged: (text) {
context.read(textFieldProvider).state = text;
},
);
}
}
class LoadingButton extends ConsumerWidget {
LoadingButton(this.buttonName);
final String buttonName;
#override
Widget build(BuildContext context, ScopedReader watch) {
String textInput = watch(textFieldProvider).state;
return RaisedButton(
child: Text(buttonName, style: TextStyle(color: Colors.white)),
onPressed: () {
print("Input from provider username: $textInput");
},
);
}
}

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"))
],
);
}
}

What we should do when the file gets so long

when I'm done with a page I can extract widget as I can for more readable, But this makes the file longer, So I find a solution I can use "part and part of " I can take extract widgets inside part of (E.g login_view_part.dart), this solves the problem, But I have read opinions about the non-use/bad-use of part and part of.
1- this is a good solution for problems like this? If not what should we
do?
2- we should use part and part of? anywhere. (except code generation)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:second_hand/core/constants/navigation/navigation_constants.dart';
import 'package:second_hand/core/extensions/context_extension.dart';
import 'package:second_hand/core/init/navigation/navigation_service.dart';
import 'package:second_hand/core/init/notifier/product_notifer.dart';
import 'package:second_hand/view/_product/_widgets/textformfield/custom_text_form_field.dart';
import 'package:second_hand/view/app/addproduct/include_some_details/viewmodel/include_some_details_view_model.dart';
enum ProductState {
verybad(name: 'Very Bad'),
bad(name: 'Bad'),
normal(name: 'Normal'),
good(name: 'Good'),
verygood(name: 'Very Good');
const ProductState({required this.name});
final String name;
}
class IncludeSomeDetailsView extends StatefulWidget {
const IncludeSomeDetailsView({super.key});
#override
State<IncludeSomeDetailsView> createState() => IncludeSomeDetailsViewState();
}
class IncludeSomeDetailsViewState extends IncludeSomeDetailsViewModel {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Include some details'),
),
body: Form(
key: formKey,
child: Padding(
padding: context.paddingAllMedium,
child: Column(
children: [
TitleTextFormField(titleController: titleController),
DescriptionTextFormField(describeController: describeController),
DropdownButton<ProductState>(
value: valueProductState,
items: ProductState.values
.map<DropdownMenuItem<ProductState>>(
(value) => DropdownMenuItem<ProductState>(
value: value,
child: Text(value.name),
),
)
.toList(),
onChanged: (productState) {
setState(
() {
stateController.text = productState!.name;
valueProductState = productState;
},
);
},
),
const Spacer(),
NextButton(
formKey: formKey,
titleController: titleController,
stateController: stateController,
describeController: describeController),
],
),
),
),
);
}
}
class TitleTextFormField extends StatelessWidget {
const TitleTextFormField({
Key? key,
required this.titleController,
}) : super(key: key);
final TextEditingController titleController;
#override
Widget build(BuildContext context) {
return Padding(
padding: context.paddingOnlyTopSmall,
child: CustomTextFormField(
controller: titleController,
labelText: 'title',
prefix: const Icon(Icons.title_outlined),
),
);
}
}
class DescriptionTextFormField extends StatelessWidget {
const DescriptionTextFormField({
Key? key,
required this.describeController,
}) : super(key: key);
final TextEditingController describeController;
#override
Widget build(BuildContext context) {
return Padding(
padding: context.paddingOnlyTopSmall,
child: CustomTextFormField(
controller: describeController,
labelText: 'description',
prefix: const Icon(Icons.description),
),
);
}
}
class NextButton extends StatelessWidget {
const NextButton({
Key? key,
required this.formKey,
required this.titleController,
required this.stateController,
required this.describeController,
}) : super(key: key);
final GlobalKey<FormState> formKey;
final TextEditingController titleController;
final TextEditingController stateController;
final TextEditingController describeController;
#override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
context.read<ProductNotifier>().setProduct(
title: titleController.text,
state: stateController.text,
description: describeController.text,
);
NavigationService.instance.navigateToPage(path: NavigationConstants.UPLOAD_PHOTOS);
}
},
child: const Text(
'Next',
),
);
}
}

How to access attributes of the widgets children

I am opening a new activity with the startTimer method How do I set the cyclesVal variable to an attribute of my custom NumberSelector widget so I can pass it to the next activity. Any way to access an internal variable of the NumberSelector widget would work too since I have set the attribute to the variable.
class ExcersizeInput extends StatelessWidget {
const ExcersizeInput({super.key});
void startTimer(BuildContext context) {
int cyclesVal = 1;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TimerPage(
cycleCount: cyclesVal,
),
));
}
#override
Widget build(BuildContext context) {
return Column(children: [
Column(
children: [
NumberSelector(
title: "Cycles",
),
],
),
ElevatedButton.icon(
onPressed: (() => {startTimer(context)}),
label: const Text("Start"),
icon: const Icon(Icons.start_outlined),
)
]);
}
}
Here's the NumberSelector code:
class NumberSelector extends StatefulWidget {
final String title;
const NumberSelector(
{super.key, required this.title});
#override
State<NumberSelector> createState() => _NumberSelectorState();
}
class _NumberSelectorState extends State<NumberSelector> {
int selectorValue = 1;
void updateValue(ifAdd) {
setState(() {
if (ifAdd) {
if (selectorValue < 9999) {
selectorValue++;
}
} else {
if (selectorValue > 1) {
selectorValue--;
}
}
});
}
#override
Widget build(BuildContext context) {
final ColorScheme colors = Theme.of(context).colorScheme;
return Column(
children: [
Text(widget.title,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
Row(
children: [
GestureDetector(
child: IconButton(
onPressed: () => updateValue(false),
icon: const Icon(Icons.remove),
style: styleContainedButton),
},
),
Text(selectorValue.toString()),
style: const TextStyle(fontSize: 16)),
GestureDetector(
child: IconButton(
onPressed: () => updateValue(true),
icon: const Icon(Icons.add),
style: styleContainedButton),
},
),
],
),
],
);
}
}
To get a variable from a stateful widget in flutter it needs a key that is linked to the state of the widget. Via the key, the variables can be accessed.
Define a widget you want to access from its parent and that contains any value:
class TestWidget extends StatefulWidget {
TestWidget({Key? key}) : super(key: key);
#override
State<TestWidget> createState() => TestWidgetState();
}
class TestWidgetState extends State<TestWidget> {
int? anyvalue;
#override
Widget build(BuildContext context) {
return Container();
}
}
Declare the key in the parent widget. Make sure the name of the state does not start with an underscore:
GlobalKey<TestWidgetState> _widget_key = GlobalKey();
Give the key to the widget in the build method of the parent widget:
TestWidget(
key: _widget_key,
)
Now the value of the child widget can be accessed in the parent widget via the key:
void afunction() {
print(_widget_key.currentState!.anyvalue);
}

Changing the state Widget of one through another Widget

MyHomePageState:
Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: bgColor,
body: ListView(
children: <Widget>[
Stack(
alignment: Alignment.topCenter,
children: <Widget>[
mainWidget(),
],
),
connectedStatusText(),
],
));
}
I'm trying to change the status of connectedStatusText() from mainWidget()!
My connectedStatus:
class connectedStatusText extends StatefulWidget
{
State<connectedStatusText> createState() {
return connectedStatus();
}
}
class connectedStatus extends State<connectedStatusText> {
String status = "IDLE";
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: RichText(
textAlign: TextAlign.center,
text: TextSpan(text: 'Status:', style: connectedStyle, children: [
TextSpan(text: status, style: disconnectedRed)
]),
),
);
}
}
I want to change the $status text to "connected" through ontap of mainWidget().
mainWidget:
....
class mainWidget extends StatefulWidget
{
MyED createState() => new MyED();
}
class MyED extends State<mainWidget> {
child: new GestureDetector(
onTap: () => setState(() {
//change here
}
tried to set a global variable to connectedStatus:
GlobalKey<connectedStatus> key = GlobalKey<connectedStatus>();
and change by ontap...
child: new GestureDetector(
onTap: () => setState(() {
//change here
key.currentState.status = "CONNECTED";
}
)
}
but it does not work!
Any help for me to change this text through another place?
Please refer to below example code to update state using ValueNotifier and ValueListenableBuilder.
ValueNotifer & ValueListenableBuilder can be used to hold value and update widget by notifying its listeners and reducing number of times widget tree getting rebuilt.
import 'package:flutter/material.dart';
const Color darkBlue = Color.fromARGB(255, 18, 32, 47);
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.dark().copyWith(
scaffoldBackgroundColor: darkBlue,
),
debugShowCheckedModeBanner: false,
home: Screen2(),
);
}
}
class Screen2 extends StatefulWidget {
final String userId; // receives the value
const Screen2({Key key, this.userId}) : super(key: key);
#override
_Screen2State createState() => _Screen2State();
}
class _Screen2State extends State<Screen2> {
final ValueNotifier<bool> updateStatus = ValueNotifier(false);
#override
Widget build(BuildContext context) {
double screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
backgroundColor: Colors.blue,
body: ListView(
children: <Widget>[
Stack(
alignment: Alignment.topCenter,
children: <Widget>[
mainWidget(
updateStatus: updateStatus,
),
],
),
connectedStatusText(
updateStatus: updateStatus,
),
],
),
); // uses the value
}
}
class connectedStatusText extends StatefulWidget {
final ValueNotifier<bool> updateStatus;
connectedStatusText({
Key key,
this.updateStatus,
}) : super(key: key);
State<connectedStatusText> createState() {
return connectedStatus();
}
}
class connectedStatus extends State<connectedStatusText> {
String status = "IDLE";
#override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.center,
child: /*
In order update widget we can use ValueListenableBuilder which updates the particular widget when the value changes (ValueNotifier value)
*/
ValueListenableBuilder(
valueListenable: widget.updateStatus,
builder: (context, snapshot, child) {
return RichText(
textAlign: TextAlign.center,
text: TextSpan(text: 'Status:', children: [
TextSpan(
text: (widget.updateStatus.value == true)
? "Active"
: status,
)
]),
);
}),
);
}
}
class mainWidget extends StatefulWidget {
final String userId; // receives the value
final ValueNotifier<bool> updateStatus;
mainWidget({
Key key,
this.userId,
this.updateStatus,
}) : super(key: key);
#override
_mainWidgetState createState() => _mainWidgetState();
}
class _mainWidgetState extends State<mainWidget> {
#override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
widget.updateStatus.value = !widget.updateStatus.value;
},
child: ValueListenableBuilder(
valueListenable: widget.updateStatus,
builder: (context, snapshot, child) {
return Text(snapshot.toString());
}));
// uses the value
}
}

Unable to reflect updated parent state in showModalBottomSheet

I am relatively new to Flutter and while I really like it I'm struggling to find a way to have state values in the parent be updated in showModalBottomSheet. I think I understand the issue to be that the values aren't reflecting in showModalBottomSheet when they change in the parent because showModalBottomSheet doesn't get rebuilt when the state updates.
I am storing title and content in the parent because I was also hoping to use it for editing as well as creating todos. I figured the showModalBottomSheet could be shared for both. I am attaching a picture on the simulator. What I am expecting is that when title changes (i.e. is no longer an empty string) then the Add To Do button should become enabled but it currently stays disabled unless I close the modal and re-open it.
Any help or insight would be greatly appreciated. Below is the code in my main.dart file which has showModalBottomSheet and has the state values that need to be passed down. NewToDo contains the text fields in the modal that capture the values and updates the state in main accordingly.
** EDIT **
I have seen this link but it doesn't really explain how to pass state from a parent widget down to a showBottomModalSheet widget, it just shows how to manage state within a showBottomModalSheet widget. My goal is to have the state change from within main to be able to be picked within showBottomModalSheet.
main.dart
import 'package:flutter/material.dart';
import './todoitem.dart';
import './todolist.dart';
import 'classes/todo.dart';
import './newtodo.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'To Do Homie',
theme: ThemeData(
primarySwatch: Colors.deepPurple,
),
home: const MyHomePage(title: "It's To Do's My Guy"),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
Key? key,
required this.title,
}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String content = '';
String title = '';
int maxId = 0;
ToDo? _todo;
final titleController = TextEditingController();
final contentController = TextEditingController();
List<ToDo> _todos = [];
void _addTodo(){
final todo = ToDo (
title: title,
id: maxId,
isDone: false,
content: content
);
if (_todo != null){
setState(() {
_todos[_todos.indexOf(_todo!)] = todo;
});
} else {
setState(() {
_todos.add(todo);
});
}
setState(() {
content = '';
maxId = maxId++;
title = '';
_todo = null;
});
contentController.text = '';
titleController.text = '';
}
#override
void initState() {
super.initState();
titleController.addListener(_handleTitleChange);
contentController.addListener(_handleContentChange);
futureAlbum = fetchAlbum();
}
void _handleTitleChange() {
setState(() {
title = titleController.text;
});
}
void _handleContentChange() {
setState(() {
content = contentController.text;
});
}
void _editTodo(ToDo todoitem){
setState(() {
_todo = todoitem;
content = todoitem.content;
title = todoitem.title;
});
contentController.text = todoitem.content;
titleController.text = todoitem.title;
}
void _deleteToDo(ToDo todoitem){
setState(() {
_todos = List.from(_todos)..removeAt(_todos.indexOf(todoitem));
});
}
void _clear(){
contentController.text = '';
titleController.text = '';
setState(() {
content = '';
title = '';
_todo = null;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: SingleChildScrollView(
child: Center(
child: Container(
alignment: Alignment.topCenter,
child: ToDoList(_todos, _editTodo, _deleteToDo)
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
print(context);
return Container(child:NewToDo(titleController, contentController, _addTodo, _clear, _todo),);
});
},
child: const Icon(Icons.add),
backgroundColor: Colors.deepPurple,
),
);
}
}
NewToDo.dart
import 'package:flutter/material.dart';
import './classes/todo.dart';
class NewToDo extends StatelessWidget {
final Function _addTodo;
final Function _clear;
final ToDo? _todo;
final TextEditingController titleController;
final TextEditingController contentController;
const NewToDo(this.titleController, this.contentController, this._addTodo, this._clear, this._todo, {Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return
Column(children: [
TextField(
decoration: const InputDecoration(
labelText: 'Title',
),
controller: titleController,
autofocus: true,
),
TextField(
decoration: const InputDecoration(
labelText: 'Details',
),
controller: contentController,
autofocus: true,
),
ButtonBar(
alignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: titleController.text.isNotEmpty ? () => _addTodo() : null,
child: Text(_todo != null ? 'Edit To Do' : 'Add To Do'),
style: ButtonStyle(
backgroundColor: titleController.text.isNotEmpty ? MaterialStateProperty.all<Color>(Colors.deepPurple) : null,
overlayColor: MaterialStateProperty.all<Color>(Colors.purple),
),
),
Visibility (
visible: titleController.text.isNotEmpty || contentController.text.isNotEmpty,
child: ElevatedButton(
onPressed: () => _clear(),
child: const Text('Clear'),
)),
])
],
);
}
}
TextControllers are listenable. You can just wrap your Column in two ValueListenables (one for each controller) and that will tell that widget to update whenever their values are updated.
#override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: contentController,
builder: (context, _content, child) {
return ValueListenableBuilder(
valueListenable: titleController,
builder: (context, _title, child) {
return Column(
children: [
TextField(
decoration: const InputDecoration(
labelText: 'Title',
),
controller: titleController,
autofocus: true,
),
TextField(
decoration: const InputDecoration(
labelText: 'Details',
),
controller: contentController,
autofocus: true,
),
ButtonBar(
alignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed:
titleController.text.isNotEmpty ? () => _addTodo() : null,
child: Text(_todo != null ? 'Edit To Do' : 'Add To Do'),
style: ButtonStyle(
backgroundColor: titleController.text.isNotEmpty
? MaterialStateProperty.all<Color>(Colors.deepPurple)
: null,
overlayColor: MaterialStateProperty.all<Color>(Colors.purple),
),
),
Visibility(
visible: titleController.text.isNotEmpty ||
contentController.text.isNotEmpty,
child: ElevatedButton(
onPressed: () => _clear(),
child: const Text('Clear'),
),
),
],
)
],
);
},
);
},
);
Another more general alternative I can think of is to use Provider (or, if you're familiar enough, regular InheritedWidgets) and the pattern suggested in its readme:
class Example extends StatefulWidget {
const Example({Key key, this.child}) : super(key: key);
final Widget child;
#override
ExampleState createState() => ExampleState();
}
class ExampleState extends State<Example> {
int _count;
void increment() {
setState(() {
_count++;
});
}
#override
Widget build(BuildContext context) {
return Provider.value(
value: _count,
child: Provider.value(
value: this,
child: widget.child,
),
);
}
}
where it suggests reading the count like this:
return Text(context.watch<int>().toString());
Except I'm guessing you can just provide the whole state of the widget to descenents by replacing _count with this to refer to the whole stateful widget. Don't know if this is recommended though.
ValueListenables would be my first choice and then maybe hooks to simplify their use though.