nested listview.builder with List<TextEditingController> - flutter

i have a nested listview.builder in my code.
Something like this:
listview.separated(
itemBuilder: (context, parentindex) {
return Padding(
...code
Listview.builder(
itemBuilder: (context, childindex){
return Padding(
...code)})}
)
how do I use a textEditingController wherein each textfield will be unique.
I tried using controller: _textcontroller[parentindex] but each parentindex will be changed upon input of textfield.
I was thinking like controller: _textcontroller[parentindex][childindex] so that each textfield edit will be unique but i know above code doesn't work or do i need to change something to make it work?
Thank you for your time.

Basicaly, create a stateful widget with each child to store your TextEditingController and make your code more clearly.
Flutter is widgets/components so don't put all things in to 1 widget.
Example: Copy this code below and paste to dartpad to see example app.
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: MyApp()));
class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
var todos = <Todo>[
Todo('Go shoping, buy a snack'),
Todo('Kick the ball'),
];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Todos')),
body: ListView.builder(
itemCount: todos.length + 1,
itemBuilder: (context, index) {
if (index == todos.length) {
newTodo() => setState(() => todos = [...todos, Todo('')]);
return ElevatedButton(onPressed: newTodo, child: const Text('new'));
}
updateTodo(Todo? newTodo) {
if (newTodo != null) setState(() => todos[index] = newTodo);
}
return RenderTodo(todo: todos[index], onChange: updateTodo);
},
),
);
}
}
class RenderTodo extends StatefulWidget {
const RenderTodo({
required this.todo,
required this.onChange,
this.onDelete,
Key? key,
}) : super(key: key);
final Todo todo;
final VoidCallback? onDelete;
final void Function(Todo newTodo) onChange;
#override
State<RenderTodo> createState() => _RenderTodoState();
}
class _RenderTodoState extends State<RenderTodo> {
late final TextEditingController controller;
#override
void initState() {
controller = TextEditingController(text: widget.todo.message);
super.initState();
}
#override
void dispose() {
controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
onChange(text) => widget.onChange(widget.todo.copyWith(message: text));
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
autofocus: true,
onChanged: onChange,
controller: controller,
decoration: const InputDecoration(labelText: 'Input the message'),
),
),
);
}
}
class Todo {
String message;
Todo(this.message);
Todo copyWith({String? message}) => Todo(message ?? this.message);
}

Related

How to setState widget by other widget Flutter ,simplecode below

right widget has gesterdetector that adds a String ("ZzZ") to List;
left widget shows all String there in String list by List view Buildder,
right widget adds "ZzZ" to list after pressing the button successfully but it dosent sets ui state...
in android studio after hot reload it shows all added "ZzZ"
import 'package:flutter/material.dart';
List<String> ListOfZzZ=[];
class homescreen extends StatefulWidget {
#override
_homescreenState createState() => _homescreenState();
}
class _homescreenState extends State<homescreen> {
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: Row(children: [
Expanded(child:RightSidewidget()),
Expanded(child:LeftSidewidget())
],
)),
);
}
}
class RightSidewidget extends StatefulWidget {
#override
_RightSidewidgetState createState() => _RightSidewidgetState();
}
class _RightSidewidgetState extends State<RightSidewidget> {
#override
Widget build(BuildContext context) {
return GestureDetector(
child: Container(child:Text("add new ZzZ"),),
**onTap: (){
setState(() {
ListOfZzZ.add("ZzZ");
});},);**
}
}
class LeftSidewidget extends StatefulWidget {
#override
_LeftSidewidgetState createState() => _LeftSidewidgetState();
}
class _LeftSidewidgetState extends State<LeftSidewidget> {
#override
Widget build(BuildContext context) {
return Container(child:
ListView.builder(
itemCount: ListOfZzZ.length,
itemBuilder: (context,index)=>Text(ListOfZzZ[index])),);
}
}
check the Provider package it can help you achieve what you want, ere is a really good tutorial by the flutter devs showing how to use manage the state of your app and notify widgets of the changes other widgets have.
setState rebuild in very specyfic way. you can read about this in here:
https://api.flutter.dev/flutter/widgets/State/setState.html
in simple world setState call the nearest build (I think this is not full true, but this intuitions works for me)
In your code when you tap right widget and call setState only rightwidget will be rebuild.
So this is the easy solutions:
Make left and right widget statless.
In homescreen in row add gestureDetector(or textButton like in my example) and here call setState. When you do that, all homeSreen will be rebuild so left and right widget too. and your list will be actual. Here is example:
List<String> ListOfZzZ = [];
class homescreen extends StatefulWidget {
#override
_homescreenState createState() => _homescreenState();
}
class _homescreenState extends State<homescreen> {
#override
Widget build(BuildContext context) {
return Material(
child: Scaffold(
body: Row(
children: [
Expanded(
child: TextButton(
onPressed: () => setState(() {
ListOfZzZ.add("ZzZ");
}),
child: RightSidewidget())),
Expanded(child: LeftSideWidget())
],
)),
);
}
}
class RightSidewidget extends StatelessWidget {
const RightSidewidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
color: Colors.amber[50],
child: Text("add new ZzZ"),
);
}
}
class LeftSideWidget extends StatelessWidget {
const LeftSideWidget({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: ListView.builder(
itemCount: ListOfZzZ.length,
itemBuilder: (context, index) => Text(ListOfZzZ[index])),
);
}
}
The hard way, but more elegant and better is to use some state manager like bloc. Here is official site: https://bloclibrary.dev/#/gettingstarted
there is a lot of tutorials and explanations. But this is not solutions for 5 minutes.
Edit: I make some solution with BLoC. I hope this help. I use flutter_bloc and equatable packages in version 7.0.1
void main() {
EquatableConfig.stringify = kDebugMode;
Bloc.observer = SimpleBlocObserver();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('myList'),
),
body: BlocProvider(
create: (context) => MylistBloc()..add(AddToList('Start')),
child: Row(
children: [
Expanded(flex: 1, child: buttonsPanel()),
Expanded(flex: 1, child: ListOfZzZ()),
],
),
),
),
);
}
}
class ListOfZzZ extends StatefulWidget {
const ListOfZzZ({Key? key}) : super(key: key);
#override
_ListOfZzZState createState() => _ListOfZzZState();
}
class _ListOfZzZState extends State<ListOfZzZ> {
late MylistBloc _mylistBloc;
#override
Widget build(BuildContext context) {
return BlocBuilder<MylistBloc, MylistState>(
//builder: (context, state) {return ListView.builder(itemBuilder: (BuildContext context,int index){return ListTile(title: state.positions[index];)},);},
builder: (context, state) {
if (state.positions.isEmpty) {
return const Center(child: Text('no posts'));
} else {
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(state.positions[index]));
},
itemCount: state.positions.length,
);
}
},
);
}
}
class buttonsPanel extends StatefulWidget {
const buttonsPanel({Key? key}) : super(key: key);
#override
_buttonsPanelState createState() => _buttonsPanelState();
}
class _buttonsPanelState extends State<buttonsPanel> {
late MylistBloc _mylistBloc;
#override
void initState() {
super.initState();
_mylistBloc = context.read<MylistBloc>();
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () => {_mylistBloc.add(AddToList('Spam'))},
child: Text('Spam')),
TextButton(
onPressed: () => {_mylistBloc.add(AddToList('Ham'))},
child: Text('Ham')),
],
);
}
class SimpleBlocObserver extends BlocObserver {
#override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
print(transition);
}
#override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
print(error);
super.onError(bloc, error, stackTrace);
}
}
class MylistState extends Equatable {
final List<String> positions;
final int lenght;
const MylistState({this.positions = const <String>[], this.lenght = 0});
#override
List<Object> get props => [positions];
#override
String toString() => 'Lenght: {$lenght} Positions: {$positions}';
#override
MylistState copyWith(List<String>? positions) {
return MylistState(positions: positions ?? this.positions);
}
}
abstract class MylistEvent extends Equatable {
const MylistEvent();
#override
List<Object> get props => [];
}
class AddToList extends MylistEvent {
final String posToAdd;
#override
AddToList(this.posToAdd);
}
class MylistBloc extends Bloc<MylistEvent, MylistState> {
MylistBloc() : super(MylistState(positions: const <String>[]));
#override
Stream<MylistState> mapEventToState(
MylistEvent event,
) async* {
if (event is AddToList) {
yield await _mapListToState(state, event.posToAdd);
}
}
Future<MylistState> _mapListToState(
MylistState state, String posToAdd) async {
List<String> positions = [];
positions.addAll(state.positions);
positions.add(posToAdd);
return MylistState(positions: positions, lenght: positions.length);
}
}
}

Separating UI and Logic in Flutter

Normally, I use a separate class with an object declared on the top of the widget. I wish to know what is the problem with that architecture.
I came across an entire package in Flutter, WidgetView, which needs to declare a dependency, then make a state object, and then do the same thing.
Why not just a simple class for achieving the same. like below
class NewAccountComponent extends StatelessWidget {
final NewAccountComponentLogic logic = NewAccountComponentLogic();
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Enter a Unique Account Number'),
titlePadding: EdgeInsets.all(20.0),
content: TextFormField(
controller: logic.controller,
onPressed: () => logic.clearTextFormField(),
),
),
}
class NewAccountComponentLogic {
static String accountNumber;
static bool existsAccountNumber;
TextEditingController controller = TextEditingController();
clearTextFormField() {
controller.text = '';
accountNumber = '';
}
You can separate widget logic and presentation in many ways. One that I've seen (and that you mention) is using the WidgetView pattern. You can do it without any dependency:
Create an abstract class thats contains the logic that all WidgetViews should be implement:
For Stateless widgets:
abstract class StatelessView<T1> extends StatelessWidget {
final T1 widget;
const StatelessView(this.widget, {Key key}) : super(key: key);
#override
Widget build(BuildContext context);
}
For Stateful widgets:
abstract class WidgetView<T1, T2> extends StatelessWidget {
final T2 state;
T1 get widget => (state as State).widget as T1;
const WidgetView(this.state, {Key key}) : super(key: key);
#override
Widget build(BuildContext context);
}
Create your widget normmally:
// Note it's a StatefulWidget because accountNumber mutates
class NewAccountComponent extends StatefulWidget {
#override
_NewAccountComponentState createState() => _NewAccountComponentState();
}
class _NewAccountComponentState extends State<NewAccountComponent> {
String accountNumber;
bool existsAccountNumber;
final TextEditingController controller = TextEditingController();
clearTextFormField() {
controller.text = '';
accountNumber = '';
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Enter a Unique Account Number'),
titlePadding: EdgeInsets.all(20.0),
content: TextFormField(
controller: controller,
onSaved: (value) => clearTextFormField(),
),
);
}
}
If the widget is a Stateful
class NewAccountComponent extends StatefulWidget {
#override
_NewAccountComponentController createState() => _NewAccountComponentController();
}
// State suffix renamed to Controller
// This class has all widget logic
class _NewAccountComponentController extends State<NewAccountComponent> {
String accountNumber;
bool existsAccountNumber;
final TextEditingController controller = TextEditingController();
clearTextFormField() {
controller.text = '';
accountNumber = '';
}
// In build, returns a new instance of your view, sending the current state
#override
Widget build(BuildContext context) => _NewAccountComponentView(this);
}
// View extends of WidgetView and has a current state to access widget logic
// with widget you can access to StatefulWidget parent
class _NewAccountComponentView
extends WidgetView<NewAccountComponent, _NewAccountComponentController> {
_NewAccountComponentView(_NewAccountComponentController state): super(state);
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Enter a Unique Account Number'),
titlePadding: EdgeInsets.all(20.0),
content: TextFormField(
controller: state.controller,
onSaved: (value) => state.clearTextFormField(),
),
);
}
}
If it's Stateless, change from:
class MyStatelessWidget extends StatelessWidget {
final String textContent = "Hello!";
const MyStatelessWidget({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text(textContent),
);
}
}
to:
// Widget and logic controller are unit
class MyStatelessWidget extends StatelessWidget {
final String textContent = "Hello!";
const MyStatelessWidget({Key key}) : super(key: key);
#override
Widget build(BuildContext context) => _MyStatelessView(this);
}
// The view is separately
class _MyStatelessView extends StatelessView<MyStatelessWidget> {
_MyStatelessView(MyStatelessWidget widget) : super(widget);
#override
Widget build(BuildContext context) {
return Container(
child: Text(widget.textContent),
);
}
}
References:
Flutter: WidgetView — A Simple Separation of Layout and Logic
I've modified your code a little bit. If you change your code as like the following code, hopefully, you will get the expected output.
class NewAccountComponent extends StatelessWidget {
final NewAccountComponentLogic logic = NewAccountComponentLogic(
'123456',
true,
TextEditingController(),
);
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Enter a Unique Account Number'),
titlePadding: EdgeInsets.all(20.0),
content: TextFormField(
controller: logic.controller,
),
actions: <Widget>[
TextButton(
child: Text('Done'),
onPressed: () {
print(logic.controller.text);
logic.clearTextFormField();
},
),
],
);
}
}
class NewAccountComponentLogic {
String accountNumber;
bool existsAccountNumber;
TextEditingController controller;
NewAccountComponentLogic(
this.accountNumber,
this.existsAccountNumber,
this.controller,
);
void clearTextFormField() {
controller.text = '';
accountNumber = '';
}
#Ignacior has also given a nice solution which you can follow.

How to read store values outside of the render tree in Flutter

I am learning flutter and trying to integrate with redux for store managment.
All the examples I see the store is accessed in the render part of the widget, for example like so:
Padding(
padding: EdgeInsets.all(2.0),
child: StoreConnector<AppState, List<Photo>>(
converter: (store) => store.state.photos,
builder: (_, photos) {
return GridView.builder(
itemCount: photos.length,
itemBuilder: (BuildContext context, int index) {
final photoUrl = photos[index].portrait;
return Padding(
padding: EdgeInsets.all(1.0),
child: new Image.network(
photoUrl,
fit: BoxFit.cover,
),
);
},
gridDelegate:
// .......
but how to get a value from the store in initState, and detect if that value changes, for example? And can I have a listener for certain value outside the render tree?
Thank you.
Edit for clarity, what I am looking for is some Flutter equivalent of react's useSelector being able to get the change in value in useEffect
Edit: I was thinking that an option (though not the answer to the question) could be to pass the values from the parent and then use didChangeDependencies() in the child
I Don't know much about the react's useSelector and redux but In Flutter you can Pass Function as a Paramter which help to change the value across different widgets.
Example
class MainClass extends StatefulWidget {
MainClass({Key key}) : super(key: key);
#override
_MainClassState createState() => _MainClassState();
}
class _MainClassState extends State<MainClass> {
int value = 0;
changeValue(int newValue) {
setState(() {
value = newValue;
});
}
#override
void initState() {
print('value is $value');
super.initState();
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
InnerClass2(
value: value,
),
InnerClass(
changeValue: changeValue,
),
],
),
);
}
}
class InnerClass extends StatefulWidget {
Function changeValue;
InnerClass({#required this.changeValue, Key key}) : super(key: key);
#override
_InnerClassState createState() => _InnerClassState();
}
class _InnerClassState extends State<InnerClass> {
#override
Widget build(BuildContext context) {
return Container(
child: RaisedButton(
onPressed: () {
widget.changeValue(2);
},
),
);
}
}
class InnerClass2 extends StatelessWidget {
final int value;
const InnerClass2({#required this.value, Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
child: Text(value.toString()),
);
}
}

How to set focus to TextField in ListView

I have a ListView that has a TextField widget in its children. Listview's items can be changed dynamically. When I press the "Add row" button, a new row should be added and the textfield belongs to newly added row should be focused (keyboard should be shown.) How can I achieve this?
Here is my sample code:
import 'package:flutter/material.dart';
class MainPage extends StatefulWidget {
MainPage({Key key}) : super(key: key);
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
List<String> list = ['one', 'two', 'three', 'four', 'five', 'six', 'seven'];
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('NoteList App'),
),
body: Column(children: <Widget>[
Expanded(child: _buildList(context)),
FlatButton(
onPressed: () {
setState(() {
list.add('new');
});
},
child: Text('Add row'))
]));
}
Widget _buildList(BuildContext context) {
return ListView.builder(
itemCount: list.length,
padding: const EdgeInsets.only(top: 1.0),
itemBuilder: (context, index) {
return ListItem(textContent: list[index]);
},
);
}
}
class ListItem extends StatelessWidget {
var _txt = TextEditingController();
final String textContent;
ListItem({Key key, this.textContent}) : super(key: key) {
_txt.text = textContent ?? '';
}
#override
Widget build(BuildContext context) {
return TextField(
controller: _txt,
textInputAction: TextInputAction.go,
);
}
}
You can copy paste run full code below
You can define an Item class and put FocusNode in it
then use FocusScope.of(context).requestFocus
code snippet
class Item {
String textContent;
FocusNode myFocusNode;
TextEditingController myController;
Item(this.textContent, this.myFocusNode, this.myController);
}
List<Item> list = [
Item('one', FocusNode(), TextEditingController()),
Item('two', FocusNode(), TextEditingController()),
Item('three', FocusNode(), TextEditingController()),
Item('four', FocusNode(), TextEditingController())
];
onPressed: () {
setState(() {
list.add(Item('new', FocusNode(), TextEditingController()));
});
WidgetsBinding.instance.addPostFrameCallback((_) {
FocusScope.of(context)
.requestFocus(list[list.length - 1].myFocusNode);
});
},
working demo
full code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class Item {
String textContent;
FocusNode myFocusNode;
TextEditingController myController;
Item(this.textContent, this.myFocusNode, this.myController);
}
class MainPage extends StatefulWidget {
MainPage({Key key}) : super(key: key);
_MainPageState createState() => _MainPageState();
}
List<Item> list = [
Item('one', FocusNode(), TextEditingController()),
Item('two', FocusNode(), TextEditingController()),
Item('three', FocusNode(), TextEditingController()),
Item('four', FocusNode(), TextEditingController())
];
class _MainPageState extends State<MainPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('NoteList App'),
),
body: Column(children: <Widget>[
Expanded(child: _buildList(context)),
FlatButton(
onPressed: () {
setState(() {
list.add(Item('new', FocusNode(), TextEditingController()));
});
WidgetsBinding.instance.addPostFrameCallback((_) {
FocusScope.of(context)
.requestFocus(list[list.length - 1].myFocusNode);
});
},
child: Text('Add row'))
]));
}
Widget _buildList(BuildContext context) {
return ListView.builder(
itemCount: list.length,
padding: const EdgeInsets.only(top: 1.0),
itemBuilder: (context, index) {
return ListItem(index: index);
},
);
}
#override
void dispose() {
list.forEach((element) {
element.myFocusNode.dispose();
element.myController.dispose();
});
super.dispose();
}
}
class ListItem extends StatefulWidget {
final int index;
ListItem({Key key, this.index}) : super(key: key);
#override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
#override
void initState() {
super.initState();
list[widget.index].myController.text = list[widget.index].textContent;
}
#override
Widget build(BuildContext context) {
return TextField(
focusNode: list[widget.index].myFocusNode,
controller: list[widget.index].myController,
textInputAction: TextInputAction.go,
);
}
}
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: MainPage(),
);
}
}

Animate resizing a ListView item on tap

I have a multi-line Text() inside a ListView item.
By default I only want to show 1 line. When the user taps this item i want it to show all lines. I achieve this by setting the maxLines property of the Text-Widget dynamically to 1 or null.
This works great, but the resizing occurs immediatly but I want to animate this transition.
Here is some example code:
class ListPage extends StatelessWidget {
const ListPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('List Example'),
),
body: ListView.separated(
itemBuilder: (context, index) {
return ListItem();
},
itemCount: 3,
separatorBuilder: (_, int index) => Divider(),
),
);
}
}
class ListItem extends StatefulWidget {
ListItem({Key key}) : super(key: key);
#override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
bool _expanded;
#override
void initState() {
_expanded = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
setState(() {
_expanded = !_expanded;
});
},
child: Text(
'Line1\nLine2\nLine3',
maxLines: _expanded ? null : 1,
softWrap: true,
style: const TextStyle(fontSize: 22),
),
);
}
}
I also already tried using an AnimatedSwitcher like this:
class ListPage extends StatelessWidget {
const ListPage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('List Example'),
),
body: ListView.separated(
itemBuilder: (context, index) {
return ListItem();
},
itemCount: 3,
separatorBuilder: (_, int index) => Divider(),
),
);
}
}
class ListItem extends StatefulWidget {
ListItem({Key key}) : super(key: key);
#override
_ListItemState createState() => _ListItemState();
}
class _ListItemState extends State<ListItem> {
bool _expanded;
Widget _myAnimatedWidget;
#override
void initState() {
_expanded = false;
_myAnimatedWidget = ExpandableText(key: UniqueKey(), expanded: _expanded);
super.initState();
}
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
setState(() {
_expanded = !_expanded;
_myAnimatedWidget =
ExpandableText(key: UniqueKey(), expanded: _expanded);
});
},
child: AnimatedSwitcher(
duration: Duration(milliseconds: 2000),
child: _myAnimatedWidget,
),
);
}
}
class ExpandableText extends StatelessWidget {
const ExpandableText({Key key, this.expanded}) : super(key: key);
final expanded;
#override
Widget build(BuildContext context) {
return Text(
'Line1\nLine2\nLine3',
maxLines: expanded ? null : 1,
softWrap: true,
style: const TextStyle(fontSize: 22),
);
}
}
This animates the Text-Widget but the ListView-Row still resizes immediatly.
What is my mistake? Is the approach of setting the maxLines property maybe wrong for my problem?
Thanks for your help !
Have a great day !
Thanks to Joao's comment I found the right answer:
I just had to wrap my Widget inside the AnimatedSize() widget. That's all :)