Flutter - Can't change child state from parent widget - android

Problem:
I have a parent widget SnippetTestEditor and a stateful child widget NoteTab. If a button in the parent widget is pressed, the child widget should get updated.
Both classes have a bool _editMode. I pass the bool from the parent widget to the child widget via the constructor. From my understandig, I need to call setState() in the parent widget and change the bool within setState(). This change should automatically be reflected in the child widget. But it's not....
So how can I get the child widget to change?
Code:
class _SnippetTestEditorState extends State<SnippetTestEditor> with SingleTickerProviderStateMixin {
bool _editMode = true;
...
#override
void initState() {
super.initState();
_tabs = List();
_tabNames = List();
List<CodeSnippet> codeSnippets = this.widget._note.codeSnippets;
for(CodeSnippet snippet in codeSnippets){
_tabs.add(CodeTab(_editMode);
...
}
#override
Widget build(BuildContext context) {
return Scaffold(
...
body: TabBarView(
controller: _tabController,
children: _tabs
),
...
actions: <Widget>[
IconButton(
icon: Icon(Icons.check),
onPressed: (){
setState(() {
_editMode = false;
});
},
)
...
class CodeTab extends StatefulWidget{
bool _editMode;
CodeTab(this._editMode);
#override
State<StatefulWidget> createState() => CodeTabState();
}
class CodeTabState extends State<CodeTab> {
#override
Widget build(BuildContext context) {
return this.widget._editMode ? ...

This happens because _editMode value is passed to CodeTab only once, inside initState(). So, even though the build method is called multiple times, the CodeTab instances in _tabs do not get updated.
you should move the code to create tabs in a method inside the state class:
getTabs(){
List<CodeSnippet> codeSnippets = widget._note.codeSnippets;
for(CodeSnippet snippet in codeSnippets){
_tabs.add(CodeTab(_editMode);
return _tabs;
}
and use getTabs() in build:
body: TabBarView(
controller: _tabController,
children: getTabs(),
),
If you have any doubts in this solution, Let me know in the comments.

Related

Changing an attribute in a parent widget from a child widget

I am trying to create an app. I want to change an attribute in a parent class (so it can display a Visibility item, SlidingUpPanel) after submitting a query in the child class (which is a search bar).
I have tried using a callback function and followed How can I pass the callback to another StatefulWidget? but it doesn't seem to do anything when I submit my query in the search bar. Previously, I have also tried to use Navigator.push to call SlidingUpPanel directly but it resulted in a black screen with only SlidingUpPanel.
Parent class (Map):
class MapPage extends StatefulWidget {
const MapPage({Key? key}) : super(key: key);
#override
_MapPageState createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
bool IsVisible = false;
void callbackFunction() {
setState(() {
IsVisible = true;
});
}
...
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
...
Visibility(
visible: IsVisible,
child: SlidingUpPanel()),
SearchPage(callbackFunction),
]),
);
}
}
Child class (Search Bar):
class SearchPage extends StatefulWidget {
final Function callback;
SearchPage(this.callback);
#override
_SearchPageState createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
...
#override
Widget build(BuildContext context) {
return FloatingSearchBar(
...
onSubmitted: (query) {
// callback here, but doesn't seem to do anything
widget.callback;
controller.close();
},
...
);
}
}
To actually call a function (execute it) you must use ().
Try calling the callback function like this:
onSubmitted: (query) {
// use () to actually call the function
widget.callback();
controller.close();
},

TabController calling initState again if tabs are clicked in sequence

I'm new to flutter. I needed my app to contain 4 different widgets. Each widget has it own data to read from the server in the initState method. First time the layout is loaded initState is called and gets the data from the server fine. So all is working except that I noticed the initState is called again if I click on non-adjacent tabs.
For example: If I Clicked on Tab 3 then Tab 2, after loading them the first time, the previous state is loaded fine and initState is not called again. However, If I clicked Tab 4 then Tab 1 or Tab 2, after loading them the first time, the initState of both tabs is called again and goes to the server to re-fetch the data.
I tried to use if (this.mounted) in the initState but it is evaluated as true and still fetches data from the server again if tabs aren't selected in the same order.
import 'package:flutter/material.dart';
import 'app_layouts.dart';
void main() {
runApp(MaterialApp(home: HomePage2()));
}
class HomePage2 extends StatefulWidget {
#override
_HomePage2State createState() => _HomePage2State();
}
class _HomePage2State extends State<HomePage2> with SingleTickerProviderStateMixin {
static final List<MyTab> myTabs = [
MyTab(tab: Tab(icon: Icon(Icons.home)), tabView: SimpleTabView()),
MyTab(tab: Tab(icon: Icon(Icons.calendar_today)), tabView: SimpleTab2View()),
MyTab(tab: Tab(icon: Icon(Icons.message)), tabView: SimpleTabView()),
MyTab(tab: Tab(icon: Icon(Icons.note)), tabView: SimpleTab2View()),
];
var _tabController;
#override
void initState() {
super.initState();
_tabController = TabController(length: myTabs.length, vsync: this);
_tabController.addListener(() {
//I added a custom tab controller, as I need to be notified with tab change events
});
}
#override
void dispose() {
_tabController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Test Tab Issue'),
bottom: TabBar(
tabs: myTabs.map((tab) => tab.tab).toList(),
controller: _tabController,
),
),
body: TabBarView(
children: myTabs.map((tab) => tab.tabView).toList(),
controller: _tabController,
),
);
}
}
class MyTab {
Tab tab;
Widget tabView;
MyTab({this.tab, this.tabView});
}
class SimpleTabView extends StatefulWidget {
#override
_SimpleTabViewState createState() => _SimpleTabViewState();
}
class _SimpleTabViewState extends State<SimpleTabView> with AutomaticKeepAliveClientMixin {
bool isDoingTask = true;
#override
void initState() {
super.initState();
print('initState called ...');
if (this.mounted) {
this.getTask();
}
}
#override
Widget build(BuildContext context) {
super.build(context);
return new Stack(
children: <Widget>[
Text('Tab view'),
Loader(showLoading: isDoingTask),
],
);
}
void getTask() async {
setState(() {
isDoingTask = true;
});
print("${new DateTime.now()} Pause for 3 seconds");
await new Future.delayed(const Duration(seconds: 3));
if (!this.mounted) return null;
setState(() {
isDoingTask = false;
});
}
#override
bool get wantKeepAlive => true;
}
//Exactly the same as SimpleTabView except the class name
class SimpleTab2View extends StatefulWidget {....
I expect the initState method to not be called again since I'm already using with AutomaticKeepAliveClientMixin, and it was called the first time already.
This issue has been fixed on master as mentioned on this GitHub issue thread.

How to detect scroll position of ListView in Flutter

I'm using ListView widget to show items as a list. In a window three, items viewing must the middle item place in the middle.
So how can I detect position of ListView when scrolling stop?
How to detect ListView Scrolling stopped?
I used NotificationListener that is a widget that listens for notifications bubbling up the tree. Then use ScrollEndNotification, which indicates that scrolling has stopped.
For scroll position I used _scrollController that type is ScrollController.
NotificationListener(
child: ListView(
controller: _scrollController,
children: ...
),
onNotification: (t) {
if (t is ScrollEndNotification) {
print(_scrollController.position.pixels);
}
//How many pixels scrolled from pervious frame
print(t.scrollDelta);
//List scroll position
print(t.metrics.pixels);
},
),
majidfathi69's answer is good, but you don't need to add a controller to the list:
(Change ScrollUpdateNotification to ScrollEndNotification when you only want to be notified when scroll ends.)
NotificationListener<ScrollUpdateNotification>(
child: ListView(
children: ...
),
onNotification: (notification) {
//How many pixels scrolled from pervious frame
print(notification.scrollDelta);
//List scroll position
print(notification.metrics.pixels);
},
),
You can also achieve this functionality with the following steps
import 'package:flutter/material.dart';
class YourPage extends StatefulWidget {
YourPage({Key key}) : super(key: key);
#override
_YourPageState createState() => _YourPageState();
}
class _YourPageState extends State<YourPage> {
ScrollController _scrollController;
double _scrollPosition;
_scrollListener() {
setState(() {
_scrollPosition = _scrollController.position.pixels;
});
}
#override
void initState() {
_scrollController = ScrollController();
_scrollController.addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text('Position $_scrollPosition pixels'),
),
body: Container(
child: ListView.builder(
controller: _scrollController,
itemCount: 200,
itemBuilder: (context, index) {
return ListTile(
leading: Icon(Icons.mood),
title: Text('Item: $index'),
);
},
),
),
);
}
}
The NotificationListener now accepts a type argument which makes the code shorter :)
NotificationListener<ScrollEndNotification>(
child: ListView(
controller: _scrollController,
children: ...
),
onNotification: (notification) {
print(_scrollController.position.pixels);
// Return true to cancel the notification bubbling. Return false (or null) to
// allow the notification to continue to be dispatched to further ancestors.
return true;
},
),
If you want to detect the scroll position of your ListView, you can simply use this;
Scrollable.of(context).position.pixels
In addition to #seddiq-sorush answer, you can compare the current position to _scrollController.position.maxScrollExtent and see if the list is at the bottom
https://coflutter.com/flutter-check-if-the-listview-reaches-the-top-or-the-bottom/ Source
If some want to Detect the bottom of a listview then use this way
NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
if (notification.metrics.atEdge) {
if (notification.metrics.pixels == 0) {
print('At top');
} else {
print('At bottom');
}
}
return true;
},
child: ListView.builder(itemBuilder: (BuildContext context) {
return YourItemWidget;
})
)
I would say You can easily detect Scroll Position by
#override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(() {
var _currectScrollPosition = _scrollController.position.pixels;//this is the line
});
}
but If you are going to call setState in addListener() ; It is okay, but this will cause to rebuild your entire build(context). This is not a good practice for Animations specially.
What I would recommand is to seprate your scrolling widget into a seprate StatefulWidget , Then get the sate of that widget by calling
_yourScrollableWidgetKey.currentState?.controller.addListener(() {
//code.......
setState(() {});
});
Note: Set a GlobalKey, and assign to your StatFulWidget.
final _yourScrollableWidgetKey = GlobalKey<_YourScrollAbleWidget>();
StackedPositionedAnimated(
key: _yourScrollableWidgetKey,
),

Flutter setState() method calling

As I am a new dev in Flutter it’s very confusing me to when should I call setState() ?, If I call this entire application is reloading (redrawing view) in build(). I want to update one TextView widget value in tree widgets structure
Here is example. On click on fab you recreate only _MyTextWidget
StreamController<int> _controller = StreamController<int>.broadcast();
int _seconds = 1;
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.cyan.withOpacity(0.3),
width: 300.0,
height: 200.0,
child: _MyTextWidget(_controller.stream)),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
_controller.add(_seconds++);
},
child: Icon(Icons.add),
),
);
}
...
class _MyTextWidget extends StatefulWidget {
_MyTextWidget(this.stream);
final Stream<int> stream;
#override
State<StatefulWidget> createState() => _MyTextWidgetState();
}
class _MyTextWidgetState extends State<_MyTextWidget> {
int secondsToDisplay = 0;
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: widget.stream,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
return snapshot.hasData ? Text(snapshot.data.toString()) : Text('nodata');
});
}
}
To make it simple, SetState() {} invalidates the widget in which it is called and forces the widget to rebuild itself by calling build(). That means that every child widgets are being rebuilt.
There are other methods you can use to pass data to a widget down a tree and make it rebuilt itself (and all its chidlren) than using SetState () {}. Those are really helpfull, especially if the widget you want to rebuilt is far away from yours in the widget tree.
One of them is the example provided by #andrey-turkovsky that uses a combination of StreamBuilder and a Stream. The StreamBuidler is a widget that rebuilt itself when there is an interaction in a Stream. Based on that, the idea is to wrap your TextView in a StreamBuilder, and use the stream to sent the data you want your TextView to display.

Flutter setState of child widget without rebuilding parent

I have a parent that contain a listView and a floatingActionButton i would like to hide the floatingActionButton when the user starts scrolling i have managed to do this within the parent widget but this requires the list to be rebuilt each time.
I have moved the floatingActionButton to a separate class so i can update the state and only rebuild that widget the problem i am having is passing the data from the ScrollController in the parent class to the child this is simple when doing it through navigation but seams a but more awkward without rebuilding the parent!
A nice way to rebuild only a child widget when a value in the parent changes is to use ValueNotifier and ValueListenableBuilder. Add an instance of ValueNotifier to the parent's state class, and wrap the widget you want to rebuild in a ValueListenableBuilder.
When you want to change the value, do so using the notifier without calling setState and the child widget rebuilds using the new value.
import 'package:flutter/material.dart';
class Parent extends StatefulWidget {
#override
_ParentState createState() => _ParentState();
}
class _ParentState extends State<Parent> {
ValueNotifier<bool> _notifier = ValueNotifier(false);
#override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(onPressed: () => _notifier.value = !_notifier.value, child: Text('toggle')),
ValueListenableBuilder(
valueListenable: _notifier,
builder: (BuildContext context, bool val, Widget? child) {
return Text(val.toString());
}),
],
);
}
#override
void dispose() {
_notifier.dispose();
super.dispose();
}
}
For optimal performance, you can create your own wrapper around Scaffold that gets the body as a parameter. The body widget will not be rebuilt when setState is called in HideFabOnScrollScaffoldState.
This is a common pattern that can also be found in core widgets such as AnimationBuilder.
import 'package:flutter/material.dart';
main() => runApp(MaterialApp(home: MyHomePage()));
class MyHomePage extends StatefulWidget {
#override
State<StatefulWidget> createState() => MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
ScrollController controller = ScrollController();
#override
Widget build(BuildContext context) {
return HideFabOnScrollScaffold(
body: ListView.builder(
controller: controller,
itemBuilder: (context, i) => ListTile(title: Text('item $i')),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: Icon(Icons.add),
),
controller: controller,
);
}
}
class HideFabOnScrollScaffold extends StatefulWidget {
const HideFabOnScrollScaffold({
Key key,
this.body,
this.floatingActionButton,
this.controller,
}) : super(key: key);
final Widget body;
final Widget floatingActionButton;
final ScrollController controller;
#override
State<StatefulWidget> createState() => HideFabOnScrollScaffoldState();
}
class HideFabOnScrollScaffoldState extends State<HideFabOnScrollScaffold> {
bool _fabVisible = true;
#override
void initState() {
super.initState();
widget.controller.addListener(_updateFabVisible);
}
#override
void dispose() {
widget.controller.removeListener(_updateFabVisible);
super.dispose();
}
void _updateFabVisible() {
final newFabVisible = (widget.controller.offset == 0.0);
if (_fabVisible != newFabVisible) {
setState(() {
_fabVisible = newFabVisible;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: widget.body,
floatingActionButton: _fabVisible ? widget.floatingActionButton : null,
);
}
}
Alternatively you could also create a wrapper for FloatingActionButton, but that will probably break the transition.
I think using a stream is more simpler and also pretty easy.
You just need to post to the stream when your event arrives and then use a stream builder to respond to those changes.
Here I am showing/hiding a component based on the focus of a widget in the widget hierarchy.
I've used the rxdart package here but I don't believe you need to. also you may want to anyway because most people will be using the BloC pattern anyway.
import 'dart:async';
import 'package:rxdart/rxdart.dart';
class _PageState extends State<Page> {
final _focusNode = FocusNode();
final _focusStreamSubject = PublishSubject<bool>();
Stream<bool> get _focusStream => _focusStreamSubject.stream;
#override
void initState() {
super.initState();
_focusNode.addListener(() {
_focusStreamSubject.add(_focusNode.hasFocus);
});
}
#override
void dispose() {
_focusNode.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: <Widget>[
_buildVeryLargeComponent(),
StreamBuilder(
stream: _focusStream,
builder: ((context, AsyncSnapshot<bool> snapshot) {
if (snapshot.hasData && snapshot.data) {
return Text("keyboard has focus")
}
return Container();
}),
)
],
),
);
}
}
You can use StatefulBuilder and use its setState function to build widgets under it.
Example:
import 'package:flutter/material.dart';
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
int count = 0;
#override
Widget build(BuildContext context) {
return Column(
children: [
// put widget here that you do not want to update using _setState of StatefulBuilder
Container(
child: Text("I am static"),
),
StatefulBuilder(builder: (_context, _setState) {
// put widges here that you want to update using _setState
return Column(
children: [
Container(
child: Text("I am updated for $count times"),
),
RaisedButton(
child: Text('Update'),
onPressed: () {
// Following only updates widgets under StatefulBuilder as we are using _setState
// that belong to StatefulBuilder
_setState(() {
count++;
});
})
],
);
}),
],
);
}
}

Categories

Resources