changing a nested child fields from its parent in flutter? - flutter

I have a widget A that has a nested widget B. I need to update a field on B (maybe by calling a function of B) when user click on a button in A. How can I do it?
EDIT: more details
Widget B is a widget that I use across my app so I want to leave him as a separate widget. Widget A is composed of widget B plus another button and I want that click on the button will change the the text that inside Widget B
EDIT2 - what I want to achieve:
Widget B is actually a Container that contains TextFormField with some other widgets. I use it for 5 form fields. now I want to add a field for location. the field for location consists of B plus a list that open when user type and there are results from google geocode. When the user clicks on one of the results, I want to set the TextFormField controller text to the value the user type.
So, I don't want to copy all logic and widgets for the text field from B to A.
In my code widget B is FormTextField and A is LocationField
class LocationField extends StatefulWidget {
Location meetingPointData;
LocationField(this.meetingPointData): super();
#override
_LocationFieldFieldState createState() => new _LocationFieldFieldState();
}
class _LocationFieldFieldState extends State<LocationField> {
OverlayEntry _overlayEntry;
var addresses;
final LayerLink _layerLink = LayerLink();
#override
void initState() {
super.initState();
}
onLocationInputChanged(text) {
this.findLocations(text);
}
#override
Widget build(BuildContext context) {
return CompositedTransformTarget(
link: this._layerLink,
child: FormTextField('meetingPoint', 'Meeting Point', "", "enter the name of a place or an address", TextInputType.text, onLocationInputChanged, true , {"empty": "Please enter the meeting point"})
);
}
OverlayEntry _createOverlayEntry() {
RenderBox renderBox = context.findRenderObject();
var size = renderBox.size;
var addressesWidget = <Widget>[];
for(var i=0; i< addresses.length; i++) {
addressesWidget.add( ListTile(
title: Text(addresses[i].addressLine),
onTap: () {
widget.meetingPointData = Location(addresses[i].addressLine);
setState(() {});
this._overlayEntry.remove();
this._overlayEntry = null;
},
),);
}
return OverlayEntry(
builder: (context) => Positioned(
width: size.width,
child: CompositedTransformFollower(
link: this._layerLink,
showWhenUnlinked: false,
offset: Offset(10.0, size.height - 45.0),
child: Material(
elevation: 4.0,
child: ListView(
padding: EdgeInsets.zero,
shrinkWrap: true,
children: addressesWidget
),
),
),
)
);
}
Future findLocations(text) async {
if(text == "") {
addresses = [];
return;
}
// From a query
final query = text;
try {
addresses = await Geocoder.local.findAddressesFromQuery(query);
var first = addresses.first;
print("${first.featureName} : ${first.coordinates}");
} catch (err) {
}
if(addresses.length > 0) {
if(this._overlayEntry != null) {
this._overlayEntry.remove();
this._overlayEntry = null;
}
this._overlayEntry = this._createOverlayEntry();
Overlay.of(context).insert(this._overlayEntry);
} else {
}
}
}

Assuming that the widget tree you're thinking of is:
A(
child: B(),
);
Then A should not be able to modify the state of B. Even if it is technically possible through global variables/GlobalKey, it is anti-pattern to do so.
Instead, you should move the state up in the tree and store your field inside A instead of B.

Related

Flutter Expansion Pannel not Expanding without immutable bool

I have an expansion panel in _buildCategoryListings() that does not expand when the header or the dropdown button is clicked. isExpanded is set to the boolean categoryView.isExpanded. Through printing via the console I can see that the setState is actually updating the bool value but it looks like the actual widget isn't being redrawn perhaps? If I manually set isExpanded to true I see the results I want from the GUI. I also had set isExtended to theExpanded (which is in MovieListingView) which raises the issue of a mutable variable being in a class that extends StatefulWidget, this did give me the desired results though.
The question: How do I get the expansion panel to update the categoryView.isExpanded (via theListings[panelIndex].isExpanded) bool and show it via the GUI?
Thank you in advance.
Side note I thought about using a provider to keep track of this bool but that seems like overkill.
class MovieListingView extends StatefulWidget {
#override
_MovieListingView createState() => _MovieListingView();
MovieListingView(this.movieList);
final MovieCatalog movieList;
//bool theExpanded = false;
List<MovieCategoryView> generateCategoryList() {
List<MovieCategoryView> tempList = [];
List<String> movieCategories = movieList.Categories;
movieCategories.forEach((category) {
MovieCategoryView categoryView = new MovieCategoryView(
movieCategoryName: category.toString(),
movieList: movieList.getMovieCardListByCategory(category));
tempList.add(categoryView);
});
return tempList;
}
}
class _MovieListingView extends State<MovieListingView> {
Widget build(BuildContext context) {
// TODO: implement build
return SingleChildScrollView(
physics: ScrollPhysics(),
padding: EdgeInsets.all(5.0),
child: _buildCategoryListings(),
);
}
List<MovieCategoryView> generateCategoryList() {
List<MovieCategoryView> tempList = [];
List<String> movieCategories = widget.movieList.Categories;
int counter = 0;
movieCategories.forEach((category) {
MovieCategoryView categoryView = new MovieCategoryView(
movieCategoryName: category.toString(),
movieList:
widget.movieList.getMenuItemCardListByCategory(category),
isExpanded: false);
tempList.add(categoryView);
});
return tempList;
}
Widget _buildCategoryListings() {
final List<MovieCategoryView> theListings = generateCategoryList();
return ExpansionPanelList(
expansionCallback: (panelIndex, isExpanded) {
setState(() {
theListings[panelIndex].isExpanded = !isExpanded;
//widget.theExpanded = !isExpanded;
});
},
children: theListings.map((MovieCategoryView movieCategoryView) {
return ExpansionPanel(
canTapOnHeader: true,
headerBuilder: (BuildContext context, bool isExpanded) {
return ListTile(
title: Text(movieCategoryView.movieCategoryName),
);
},
body: Column(
children: movieCategoryView.movieList,
),
isExpanded: movieCategoryView.isExpanded);
}).toList(),
);
}
}
class MovieCategoryView {
MovieCategoryView(
{#required this.movieCategoryName,
#required this.movieList,
this.isExpanded});
String movieCategoryName;
List<MovieCard> movieList;
bool isExpanded = false;
}
This is happening because whenever the setstate() is called whole widget tree is rebuild and thus when you try changing the isexpandable value ,is gets changed but the
function generateCategoryList(); again gets called and generates the previous list again and again.
Widget _buildCategoryListings() {
final List<MovieCategoryView> theListings = generateCategoryList();
To fix this call the generateCategoryList(); once in initState() and remove the line above line.

Set state for a button in a ListView

I have an input page to create a new reminder. On this page you will select several different variables (reminder type, start date, etc.) - at this stage I am just trying to get it work for two variables.
I have a button object that I create, which takes some text, a "isSelected" value (which changes the color to show it is selected) and an onPress callback. The plan is to use a loop to create one of these buttons for each of the necessary selection options and then feed that into a ListView, so you have a scroll-able list of selection option. As you select the item the properties of the new reminder object will update and the color will change to selected.
When I click the button, the value is selected (the print statement shows this) but the button does not change to the new isSelected value, despite a SetState being used. What is it I am missing here? Is it possible to feed buttons into a ListView like this and still have their state update? Or do you need to find another work around?
class AddReminder extends StatefulWidget {
#override
_AddReminderState createState() => _AddReminderState();
}
class _AddReminderState extends State<AddReminder> {
String addReminder = "";
Reminder newReminder = Reminder();
#override
List<Widget> getReminderTypesButton(
String selectionName, List selectionOptions, var reminderVariable) {
// create new list to add widgets to
List<Widget> selectionOptionsWidgets = [];
// loop through selection options and create buttons
for (String selection in selectionOptions) {
bool isSelectedValue = false;
selectionOptionsWidgets.add(
FullWidthButton(
text: selection,
isSelected: isSelectedValue,
onPress: () {
setState(() {
reminderVariable = selection;
isSelectedValue = true;
});
print(reminderVariable);
},
),
);
}
;
// return list of widgets
return selectionOptionsWidgets;
}
Widget build(BuildContext context) {
List<List<Widget>> newList = [
getReminderTypesButton("Type", reminderTypesList, newReminder.type),
getReminderTypesButton(
"Frequency", repeatFrequencyTypesList, newReminder.repeatFrequency)
];
List<Widget> widgetListUnwrap(List<List<Widget>> inputList) {
//takes list of list of widgets and converts to widget list (to feed into list view)
List<Widget> widgetsUnwrapped = [];
for (var mainList in inputList) {
for (var widgets in mainList) {
widgetsUnwrapped.add(widgets);
}
}
return widgetsUnwrapped;
}
return SafeArea(
child: Container(
color: Colors.white,
child: Column(
children: [
Hero(
tag: addReminder,
child: TopBarWithBack(
mainText: "New reminder",
onPress: () {
Navigator.pop(context);
},
)),
Expanded(
child: Container(
child: ListView(
children: widgetListUnwrap(newList),
shrinkWrap: true,
),
),
),
],
),
),
);
}
}
Here are the lists that I reference
List<String> reminderTypesList = [
"Appointment",
"Check-up",
"Other",
];
List<String> repeatFrequencyTypesList = [
"Never",
"Daily",
"Weekly",
"Monthly",
"Every 3 months",
"Every 6 months",
"Yearly",
];
List<List<String>> selectionOptions = [
reminderTypesList,
repeatFrequencyTypesList
];
The reason your state not changing is that every time you call setState(), the whole build function will run again. If you initiate a state (in this case isSelectedValue) within the build method (since the getReminderTypesButton() got called within the build), the code will run through the below line again and again, resetting the state to the initial value.
bool isSelectedValue = false;
This line will always set the isSelectedValue to false, no matter how many time you call setState.
In order to avoid this, you need to place the state outside of the build method, ideally in the FullWidthButton like this:
class FullWidthButton extends StatefulWidget {
const FullWidthButton({Key? key}) : super(key: key);
#override
_FullWidthButtonState createState() => _FullWidthButtonState();
}
class _FullWidthButtonState extends State<FullWidthButton> {
bool isSelectedValue = false;
#override
Widget build(BuildContext context) {
return InkWell(
onTap: () => setState(() => isSelectedValue = !isSelectedValue),
// ...other lines
);
}
}

flutter TabBarView sometimes not triggering on gesture swipe

I am using Flutter -Android studio. I have main screen with TabBarView control (2 pages). each page get data from sqfite database. both have same statefull widget class, but I pass parameter to look into database and display.
Issue : when I click tabbar header , data displayed is Ok. But when I swipe tabs, it sometimes work and sometimes does not work. as per below video. I have check and seems that Build event is not trigger every if Swipe was done.
Main widget with tabbarview
screen record
Note that 1st tab has only 1 record, while second has 6 records. tabClick works fine, but swipe sometime not work properly.
body: TabBarView(
controller: _tabController,
children: [
DisplayTransactions(
tmptransType: TransactionType.enIncome,
transacBloc: _transacBloc,
SelectedItemsCount: bRowSelectionCount,
onSelectionChanged: (count) {
setState(() {
bRowSelectionCount = count;
});
},
),
// Center(child: Text("Page 1")),
DisplayTransactions(
tmptransType: TransactionType.enExpense,
transacBloc: _transacBloc,
SelectedItemsCount: bRowSelectionCount,
onSelectionChanged: (count) {
setState(() {
bRowSelectionCount = count;
});
},
//Center(child: Text("Page 2")),
)
],
),
------ > Widget to display data into listview for each tabbar page
class DisplayTransactions extends StatefulWidget {
final TransactionType tmptransType;
final FinanceTransBlock transacBloc;
final Function onSelectionChanged;
int SelectedItemsCount;
/// -1 expense 1 income
DisplayTransactions(
{Key key,
#required this.tmptransType,
#required this.transacBloc,
this.SelectedItemsCount,
this.onSelectionChanged})
: super(key: key);
#override
_DisplayTransactionsState createState() => _DisplayTransactionsState();
}
class _DisplayTransactionsState extends State<DisplayTransactions> {
var isSelected = false;
var mycolor = Colors.white;
// int iselectionCount = 0;
#override
Widget build(BuildContext context) {
widget.transacBloc.transacType = widget.tmptransType;
debugPrint("----------------------------- ${widget.tmptransType}");
if (widget.SelectedItemsCount == 0) {// if no rows selected, then reload database based on trans type, eg expense, or income.. etc
widget.transacBloc.refresh();
}
return StreamBuilder<List<FinanceTransaction>>(
stream: widget.transacBloc.transacations,
builder: (context, snapshot) {
if (snapshot.hasData) {
List<FinanceTransaction> list = snapshot.data;
// return buildTaskListWal(snapshot.data);
return Padding(
It looks like the app is taking some time to retrieve the new data. Try checking for null on the snapshot.data and display a circularProgressIndicator.
if (snapshot.data == null) {
return CirgularProgressIndicator();
} else {
//display data
}

DragTarget widget is not responding

I am coding a chess game in flutter.
and this is the relevant bits of my code :
class Rank extends StatelessWidget {
final _number;
Rank(this._number);
#override
Widget build(BuildContext context) {
var widgets = <Widget>[];
for (var j = 'a'.codeUnitAt(0); j <= 'h'.codeUnitAt(0); j++) {
widgets
.add(
DroppableBoardSquare(String.fromCharCode(j) + this._number.toString())
);
//
}
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widgets);
}
}
class DroppableBoardSquare extends StatelessWidget {
final String _coordinate;
const DroppableBoardSquare(this._coordinate) ;
#override
Widget build(BuildContext context) {
return DragTarget(
builder:(BuildContext context, List candidate, List rejectedData){
return BoardSquare(_coordinate);
},
onAccept: (data ) {
print('Accepted');
},
onWillAccept: (data){
return true;
},
onLeave: (data) => print("leave"),);
}
}
class BoardSquare extends StatelessWidget {
final String _coordinate;
BoardSquare(this._coordinate);
#override
Widget build(BuildContext context) {
ChessBloc bloc = ChessBlocProvider.of(context);
return
StreamBuilder<chess.Chess>(
stream: bloc.chessState,
builder: (context, AsyncSnapshot<chess.Chess> chess) {
return DraggablePieceWidget(chess.data.get(_coordinate), _coordinate);
});
}
}
class DraggablePieceWidget extends StatelessWidget {
final chess.Piece _piece;
final String _coordinate;
DraggablePieceWidget(this._piece, String this._coordinate);
#override
Widget build(BuildContext context) {
return Draggable(
child: PieceWidget(_piece),
feedback: PieceWidget(_piece),
childWhenDragging: PieceWidget(null),
data: {"piece": _piece, "origin": _coordinate} ,
);
}
}
Now the problem is that I can drag the piece fine, but cannot drop them. None of the methods on DragTarget is getting called.
what I am doing wrong?
I developed a drag-n-drop photos grid, where you can drag photos to reorder them based on numeric indexes.
Essentially, I assume, it is the same thing as the chessboard concept you have.
The problem possibly occurs due to Draggable (DraggablePieceWidget) element being inside of DragTarget (DroppableBoardSquare).
In my app I made it the other way around - I placed DragTarget into Draggable.
Providing some pseudo-code as an example:
int _dragSelectedIndex;
int _draggingIndex;
// Basically this is what you'd use to build every chess item
Draggable(
maxSimultaneousDrags: 1,
data: index,
onDragStarted: () { _draggingIndex = index; print("Debug: drag started"); }, // Use setState for _draggingIndex, _dragSelectedIndex.
onDragEnd: (details) { onDragEnded(); _draggingIndex = null; print("Debug: drag ended; $details"); },
onDraggableCanceled: (_, __) { onDragEnded(); _draggingIndex = null; print("Debug: drag cancelled."); },
feedback: Material(type: MaterialType.transparency, child: Opacity(opacity: 0.85, child: Transform.scale(scale: 1.1, child: createDraggableBlock(index, includeTarget: false)))),
child: createDraggableBlock(index, includeTarget: true),
);
// This func is used in 2 places - Draggable's `child` & `feedback` props.
// Creating dynamic widgets through functions is a bad practice, switch to StatefulWidget if you'd like.
Widget createDraggableBlock(int index, { bool includeTarget = true }) {
if (includeTarget) {
return DragTarget(builder: (context, candidateData, rejectedData) {
if (_draggingIndex == index || candidateData.length > 0) {
return Container(); // Display empty widget in the originally selected cell, and in any cell that we drag the chess over.
}
// Display a chess, but wrapped in DragTarget widget. All chessboard cells will be displayed this way, except for the one you start dragging.
return ChessPiece(..., index: index);
}, onWillAccept: (int elemIndex) {
if (index == _draggingIndex) {
return false; // Do not accept the chess being dragged into it's own widget
}
setState(() { _dragSelectedIndex = index; });
return true;
}, onLeave: (int elemIndex) {
setState(() { _dragSelectedIndex = null; });
});
}
// Display a chess without DragTarget wrapper, e.g. for the draggable(feedback) widget
return ChessPiece(..., index: index);
}
onDragEnded() {
// Check whether _draggingIndex & _dragSelectedIndex are not null and act accordingly.
}
I assume if you change index system to custom objects that you have - this would work for you too.
Please let me know if this helped.

Flutter infinite/long list - memory issue and stack overflow error

my use case is to create a list view of articles (each item have the same look, there could be huge amount of articles, e.g. > 10000). I tried with
- ListView with ListView.builder: it supposes only to render the item when the item is displayed
- ScrollController: to determine when to load the next items (pagination)
- then I use List to store the data fetched from restful API using http, by adding the data from http to the List instance
this approach is OK, but in case the user keeps on scrolling pages, the List instance will have more and more items, it can crash with stack Overflow error.
If I don't call List.addAll(), instead I assign the data fetched from api, like: list = data;
I have problem that when the user scroll up, he/she won't be able to see the previous items.
Is there a good approach to solve this? Thanks!
import 'package:flutter/material.dart';
import 'package:app/model.dart';
import 'package:app/components/item.dart';
abstract class PostListPage extends StatefulWidget {
final String head;
DealListPage(this.head);
}
abstract class PostListPageState<T extends PostListPage> extends State<PostListPage> {
final int MAX_PAGE = 2;
DealListPageState(String head) {
this.head = head;
}
final ScrollController scrollController = new ScrollController();
void doInitialize() {
page = 0;
try {
list.clear();
fetchNextPage();
}
catch(e) {
print("Error: " + e.toString());
}
}
#override
void initState() {
super.initState();
this.fetchNextPage();
scrollController.addListener(() {
double maxScroll = scrollController.position.maxScrollExtent;
double currentScroll = scrollController.position.pixels;
double delta = 200.0; // or something else..
if ( maxScroll - currentScroll <= delta) {
fetchNextPage();
}
});
}
#override
void dispose() {
scrollController.dispose();
super.dispose();
}
void mergeNewResult(List<PostListItem> result) {
list.addAll(result);
}
Future fetchNextPage() async {
if (!isLoading && mounted) {
page++;
setState(() {
isLoading = true;
});
final List<PostListItem> result = await doFetchData(page);
setState(() {
if (result != null && result.length > 0) {
mergeNewResult(result);
} else {
//TODO show notification
}
isLoading = false;
});
}
}
Future doFetchData(final int page);
String head;
List<PostListItem> list = new List();
var isLoading = false;
int page = 0;
int pageSize = 20;
final int scrollThreshold = 10;
Widget buildProgressIndicator() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Center(
child: new Opacity(
opacity: isLoading ? 1.0 : 0.0,
child: new CircularProgressIndicator(),
),
),
);
}
#override
Widget build(BuildContext context) {
ListView listView = ListView.builder(
padding: const EdgeInsets.all(16.0),
itemBuilder: (BuildContext context, int index) {
if (index == list.length) {
return buildProgressIndicator();
}
if (index > 0) {
return Column(
children: [Divider(), PostListItem(list[index])]
);
}
return PostListItem(list[index]);
},
controller: scrollController,
itemCount: list.length
);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(head),
actions: <Widget>[
IconButton(
icon: Icon(Icons.search),
onPressed: () {
},
),
// action button
IconButton(
icon: Icon(Icons.more_horiz),
onPressed: () {
},
),
]
),
body: new RefreshIndicator(
onRefresh: handleRefresh,
child: listView
),
);
}
Future<Null> handleRefresh() async {
doInitialize();
return null;
}
}
in my case, when the list length is 600, I start to get stack overflow error like:
I/flutter ( 8842): Another exception was thrown: Stack Overflow
I/flutter ( 8842): Another exception was thrown: Stack Overflow
screen:
enter image description here
somehow flutter doesn't show any more details of the error.
I wrote some sample code for a related question about paginated scrolling, which you could check out.
I didn't implement cache invalidation there, but it would easily be extendable using something like the following in the getPodcast method to remove all items that are more than 100 indexes away from the current location:
for (key in _cache.keys) {
if (abs(key - index) > 100) {
_cache.remove(key);
}
}
An even more sophisticated implementation could take into consideration the scroll velocity and past user behavior to lay out a probability curve (or a simpler Gaussian curve) to fetch content more intelligently.