call a function of a parent widget each time when a child widget is rebuilt - flutter

I have two classes.
CustomParentWidget
CustomChildWidget(which shows CircularProgressIndicator if loading and CentralisedAssetLoader id loaded)
Now i need My CustomParentWidget height should be changed after rendering CustomChildWidget..
It is achieved by adding
WidgetsBinding.instance
.addPostFrameCallback((_) => calculateMaxOptionHeight());
in initState of CustomParentWidget..
The problem here is CustomParentWidget is getting height of CircularProgressIndicator only..
Since CustomChildWidget return CircularProgressIndicator at first,
addPostFrameCallBack of CustomParentWidget thinks that ChildWidget has been Rendered and setting the Height to parent..
But i need addPostFrameCallBack to be called again when the CentralisedAssetLoader is also Rendered.
Is there a way to call a function whenever a child widget is rendered?
class CustomParentWidget extends StatefulWidget {
CustomParentWidget();
#override
_CustomParentWidgetState createState() =>
_CustomParentWidgetState();
}
class _CustomParentWidgetState
extends State<CustomParentWidget> {
double widgetHeight;
#override
initState() {
//calling the calculate Function after the Layout is Rendered
WidgetsBinding.instance
.addPostFrameCallback((_) => calculateMaxOptionHeight());
super.initState();
}
void calculateWidgetHeight() {
setState(){
widgetHeight = getWidgetHeight();
}
}
#override
Widget build(BuildContext context) {
return Container(
child: CustomChildWidget(height: widgetHeight),
);
}
}
class CustomChildWidget extends StatefulWidget {
CustomChildWidget({this.height});
#override
_CustomChildWidgetState createState() => _CustomChildWidgetState();
}
class _CustomChildWidgetState extends State<CustomChildWidget> {
double height;
double width;
bool isLoaded;
#override
void initState() {
isLoaded = false;
prepareData();
super.initState();
}
void prepareData() async {
try {
height = getheight();
width = getwidth();
} catch (error) {}
setState(() {
height = height;
width = width;
isLoaded = true;
});
}
#override
Widget build(BuildContext context) {
final screenMaxWidth = MediaQuery.of(context).size.width;
if (!isLoaded) {
return CircularProgressIndicator();
}
return Container(
width: min(screenMaxWidth, width),
height: height,
child: CentralisedAssetLoader(
fit: BoxFit.contain,
assetIdentifier: assetIdentifier,
),
);
}
}

You're really working against the grain of fast performant widget layout. See the basics about layout in https://flutter.dev/docs/development/ui/layout/constraints.
However, if you really don't mind slowing down a bit, you can write very flexible custom layouts with the package:boxy (https://pub.dev/packages/boxy).

Related

flutter: how to make pull down to refresh flutter webview using the official webview_flutter package

I want to add refresher in flutter web view
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Stack(
children: <Widget>[
Container(
child: Center(
child: Text(_title),
),
),
],
)),
body: SafeArea(
child: WebView(
key: _key,
javascriptMode: JavascriptMode.unrestricted,
initialUrl: _url)),
);
}
}
#peter-koltai: Many thanks for this! I really appreciated your solution the height is working correctly, even the height is coming a bit late (page content is seen, but scrolling height not there), but there were other issues. (Sorry I can't vote you up)
Issues w/ SingleChildScrollView:
SingleChildScrollView has always the absolute height of the page e.g. if a text box was not expanded from the beginning (javascript), the scroll height exceeds the page height.
The WebView gets the whole scroll area height, but doesn't know the display size, so if a bottom or top modal sheet appears, they are not rendered correctly in the view area of the screen but in the absolute complete height of the scroll area, so then you have to scroll e.g. 6000px up and down.
The scroll position stays where you left somewhere in your previous absolute page height, if you browse further w/o a page refresh.
Complete code:
So the solution of #shalin-shah gave me this nice working solution:
I calculate the dragging down distance (>20% of screen height) if you start at the top=0 of the page which then shows the RefreshIndicator until onPageFinished.
webview.dart:
The RefreshIndicator gets a Completer if dragging down distance is reached and starts the reloading with the spinning, which is completed if page finishes loading.
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_web_refresh/pull_to_refresh.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MyWebViewWidget extends StatefulWidget {
final String initialUrl;
const MyWebViewWidget({
Key? key,
required this.initialUrl,
}) : super(key: key);
#override
State<MyWebViewWidget> createState() => _MyWebViewWidgetState();
}
class _MyWebViewWidgetState extends State<MyWebViewWidget> with WidgetsBindingObserver {
late WebViewController _controller;
// Drag to refresh helpers
final DragGesturePullToRefresh pullToRefresh = DragGesturePullToRefresh();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addObserver(this);
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
#override
void dispose() {
// remove listener
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
#override
void didChangeMetrics() {
// on portrait / landscape or other change, recalculate height
pullToRefresh.setRefreshDistance(MediaQuery.of(context).size.height);
}
#override
Widget build(context) {
return RefreshIndicator(
key: _refreshIndicatorKey,
onRefresh: () {
Completer<void> completer = pullToRefresh.refresh();
_controller.reload();
return completer.future;
},
child: WebView(
initialUrl: widget.initialUrl,
javascriptMode: JavascriptMode.unrestricted,
zoomEnabled: true,
gestureNavigationEnabled: true,
gestureRecognizers: <Factory<OneSequenceGestureRecognizer>>{
pullToRefresh.dragGestureRecognizer(_refreshIndicatorKey),
},
onWebViewCreated: (WebViewController webViewController) {
_controller = webViewController;
pullToRefresh.setController(_controller);
},
onPageStarted: (String url) { pullToRefresh.started(); },
onPageFinished: (finish) { pullToRefresh.finished(); },
onWebResourceError: (error) {
debugPrint(
'MyWebViewWidget:onWebResourceError(): ${error.description}');
pullToRefresh.finished();
},
),
);
}
}
pull_to_refresh.dart:
After drag start from top=0 of the page and is always downward, the moving distance is calculated, and when it exceeds 20% of the screen size the RefreshIndicator show() is called.
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:webview_flutter/webview_flutter.dart';
// Fixed issue: https://github.com/flutter/flutter/issues/39389
class AllowVerticalDragGestureRecognizer extends VerticalDragGestureRecognizer {
#override
//override rejectGesture here
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
}
class DragGesturePullToRefresh {
static const double EXCEEDS_LOADING_TIME = 3000;
static const double REFRESH_DISTANCE_MIN = .2;
late WebViewController _controller;
// loading
Completer<void> completer = Completer<void>();
int msLoading = 0;
bool isLoading = true;
// drag
bool dragStarted = false;
double dragDistance = 0;
double refreshDistance = 200;
Factory<OneSequenceGestureRecognizer> dragGestureRecognizer(final GlobalKey<RefreshIndicatorState> refreshIndicatorKey) {
return Factory<OneSequenceGestureRecognizer>(() => AllowVerticalDragGestureRecognizer()
// Got the original idea from https://stackoverflow.com/users/15862916/shalin-shah:
// https://stackoverflow.com/questions/57656045/pull-down-to-refresh-webview-page-in-flutter
..onDown = (DragDownDetails dragDownDetails) {
// if the page is still loading don't allow refreshing again
if (!isLoading ||
(msLoading > 0 && (DateTime.now().millisecondsSinceEpoch - msLoading) > EXCEEDS_LOADING_TIME)) {
_controller.getScrollY().then((scrollYPos) {
if (scrollYPos == 0) {
dragStarted = true;
dragDistance = 0;
}
});
}
}
..onUpdate = (DragUpdateDetails dragUpdateDetails) {
calculateDrag(refreshIndicatorKey, dragUpdateDetails.delta.dy);
}
..onEnd = (DragEndDetails dragEndDetails) { clearDrag(); }
..onCancel = () { clearDrag(); });
}
void setController(WebViewController controller){ _controller = controller; }
void setRefreshDistance(double height){ refreshDistance = height * REFRESH_DISTANCE_MIN; }
Completer<void> refresh() {
if (!completer.isCompleted) {
completer.complete();
}
completer = Completer<void>();
started();
return completer;
}
void started() {
msLoading = DateTime.now().millisecondsSinceEpoch;
isLoading = true;
}
void finished() {
msLoading = 0;
isLoading = false;
// hide the RefreshIndicator
if (!completer.isCompleted) {
completer.complete();
}
}
void clearDrag() {
dragStarted = false;
dragDistance = 0;
}
void calculateDrag(final GlobalKey<RefreshIndicatorState> refreshIndicatorKey, double dy) async {
if (dragStarted && dy >= 0) {
dragDistance += dy;
// Show the RefreshIndicator
if (dragDistance > refreshDistance) {
debugPrint(
'DragGesturePullToRefresh:refreshPage(): $dragDistance > $refreshDistance');
clearDrag();
unawaited(refreshIndicatorKey.currentState?.show());
}
/*
The web page scrolling is not blocked, when you start to drag down from the top position of
the page to start the refresh process, e.g. like in the chrome browser. So the refresh process
is stopped if you start to drag down from the page top position and then up before reaching
the distance to start the refresh process.
*/
} else {
clearDrag();
}
}
}
This fix was helpful for the gesture events flutter webview VerticalDragGestureRecognizer get no callback but only onDown and onCancel.
The complete code is on github too.
Gif, I am not allowed to post one...
Differences w/o SingleChildScrollView or to e.g. the chrome browser
=> Fixed: Go to update
The RefreshIndicator shows no initial animation by dragging it down until the distance is reached to start the refresh process. (Can be added differently)
The web page scrolling is not blocked, when you start to drag down from the top position of the page to start the refresh process, e.g. like in the chrome browser. So the refresh process is stopped if you start to drag down from the page's top position and then up before reaching the distance to start the refresh process. Check the method in refreshPage() in the pull_to_refresh.dart for my solution and the comment.
I find the differences irrelevant 🤷‍♀️ as the issues destroyed the browsing expierence.
Update
I changed using ScrollNotification which RefreshIndicator interprets right when FixedScrollMetrics are set. So we have the original animation like in SingleChildScrollView or e.g. chrome browser.
github
Complete code:
webview.dart:
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_web_refresh/pull_to_refresh.dart';
import 'package:webview_flutter/webview_flutter.dart';
class MyWebViewWidget extends StatefulWidget {
final String initialUrl;
const MyWebViewWidget({
Key? key,
required this.initialUrl,
}) : super(key: key);
#override
State<MyWebViewWidget> createState() => _MyWebViewWidgetState();
}
class _MyWebViewWidgetState extends State<MyWebViewWidget>
with WidgetsBindingObserver {
late WebViewController _controller;
late DragGesturePullToRefresh dragGesturePullToRefresh;
#override
void initState() {
super.initState();
dragGesturePullToRefresh = DragGesturePullToRefresh();
WidgetsBinding.instance!.addObserver(this);
if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
#override
void dispose() {
// remove listener
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
#override
void didChangeMetrics() {
// on portrait / landscape or other change, recalculate height
dragGesturePullToRefresh.setHeight(MediaQuery.of(context).size.height);
}
#override
Widget build(context) {
return
// NotificationListener(
// onNotification: (scrollNotification) {
// debugPrint('MyWebViewWidget:NotificationListener(): $scrollNotification');
// return true;
// }, child:
RefreshIndicator(
onRefresh: () => dragGesturePullToRefresh.refresh(),
child: Builder(
builder: (context) => WebView(
initialUrl: widget.initialUrl,
javascriptMode: JavascriptMode.unrestricted,
zoomEnabled: true,
gestureNavigationEnabled: true,
gestureRecognizers: {Factory(() => dragGesturePullToRefresh)},
onWebViewCreated: (WebViewController webViewController) {
_controller = webViewController;
dragGesturePullToRefresh
.setContext(context)
.setController(_controller);
},
onPageStarted: (String url) { dragGesturePullToRefresh.started(); },
onPageFinished: (finish) { dragGesturePullToRefresh.finished();},
onWebResourceError: (error) {
debugPrint(
'MyWebViewWidget:onWebResourceError(): ${error.description}');
dragGesturePullToRefresh.finished();
},
),
),
);
}
}
pull_to_refresh.dart:
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/gestures.dart';
import 'package:webview_flutter/webview_flutter.dart';
// Fixed issue: https://github.com/flutter/flutter/issues/39389
class DragGesturePullToRefresh extends VerticalDragGestureRecognizer {
static const double EXCEEDS_LOADING_TIME = 3000;
late BuildContext _context;
late WebViewController _controller;
// loading
Completer<void> completer = Completer<void>();
int msLoading = 0;
bool isLoading = true;
// drag
double height = 200;
bool dragStarted = false;
double dragDistance = 0;
#override
//override rejectGesture here
void rejectGesture(int pointer) {
acceptGesture(pointer);
}
void _clearDrag() {
dragStarted = false;
dragDistance = 0;
}
DragGesturePullToRefresh setContext(BuildContext context) { _context = context; return this; }
DragGesturePullToRefresh setController(WebViewController controller) { _controller = controller; return this; }
void setHeight(double height) { this.height = height; }
Future refresh() {
if (!completer.isCompleted) {
completer.complete();
}
completer = Completer<void>();
started();
_controller.reload();
return completer.future;
}
void started() {
msLoading = DateTime.now().millisecondsSinceEpoch;
isLoading = true;
}
void finished() {
msLoading = 0;
isLoading = false;
// hide the RefreshIndicator
if (!completer.isCompleted) {
completer.complete();
}
}
FixedScrollMetrics _getMetrics(double minScrollExtent, double maxScrollExtent,
double pixels, double viewportDimension, AxisDirection axisDirection) {
return FixedScrollMetrics(
minScrollExtent: minScrollExtent,
maxScrollExtent: maxScrollExtent,
pixels: pixels,
viewportDimension: viewportDimension,
axisDirection: axisDirection);
}
DragGesturePullToRefresh() {
onStart = (DragStartDetails dragDetails) {
// debugPrint('MyWebViewWidget:onStart(): $dragDetails');
if (!isLoading ||
(msLoading > 0 && (DateTime.now().millisecondsSinceEpoch - msLoading) > EXCEEDS_LOADING_TIME)) {
_controller.getScrollY().then((scrollYPos) {
if (scrollYPos == 0) {
dragStarted = true;
dragDistance = 0;
ScrollStartNotification(
metrics: _getMetrics(0, height, 0, height, AxisDirection.down),
dragDetails: dragDetails,
context: _context)
.dispatch(_context);
}
});
}
};
onUpdate = (DragUpdateDetails dragDetails) {
if (dragStarted) {
double dy = dragDetails.delta.dy;
dragDistance += dy;
ScrollUpdateNotification(
metrics: _getMetrics(
dy > 0 ? 0 : dragDistance, height,
dy > 0 ? (-1) * dy : dragDistance, height,
dragDistance < 0 ? AxisDirection.up : AxisDirection.down),
context: _context,
scrollDelta: (-1) * dy)
.dispatch(_context);
if (dragDistance < 0) {
_clearDrag();
}
}
};
onEnd = (DragEndDetails dragDetails) {
ScrollEndNotification(
metrics: _getMetrics(0, height, dragDistance, height, AxisDirection.down),
context: _context)
.dispatch(_context);
_clearDrag();
};
onCancel = () {
ScrollUpdateNotification(
metrics: _getMetrics(0, height, 1, height, AxisDirection.up),
context: _context,
scrollDelta: 0)
.dispatch(_context);
_clearDrag();
};
}
}
It can be done, basic problem is that RefreshIndicator only works with a scrollable item as child, and WebView is not scrollable from Flutter's point of view (the loaded contents are scrollable). So you have to wrap it into some kind of scrollable, but there comes the other problem: you have to know the height to do so, and you still need the contents of WebView to be scrollable, so that you can scroll up and down on the loaded web page.
The solution includes the following steps, partially using accepted answer here.
Create a RefreshIndicator and add a SingleChildScrollView as its child.
Inside the scroll view add a Container to hold the WebView as child.
Set some initial height for the Container, for example the height of the screen.
After the page is loaded or refreshed, use JavaScript code to get the loaded document's height in the browser.
Resize the Container with the acquired height.
Watch for orientation change, because the same page will have different height in portrait and landscape, and refresh Container height accordingly.
This solution is not perfect. First, as you will see from print outputs in debug console, height settings occur not only when strictly necessary. Second, if the content of a web page changes so that the height of the loaded documents changes as well, without actually reloading the page, height will be not synced. (For example if you add rows to a table on a dynamic webpage.)
Complete code:
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'WebView with RefreshIndicator',
home: Scaffold(
appBar: AppBar(title: Text('WebView with RefreshIndicator')),
body: SafeArea(
child: MyWebWiew(),
)),
);
}
}
class MyWebWiew extends StatefulWidget {
const MyWebWiew({Key? key}) : super(key: key);
#override
_MyWebWiewState createState() => _MyWebWiewState();
}
class _MyWebWiewState extends State<MyWebWiew> with WidgetsBindingObserver {
WebViewController? _webViewController;
// height of the WebView with the loaded content
double? _webViewHeight;
// is true while a page loading is in progress
bool _isPageLoading = true;
#override
void initState() {
super.initState();
// add listener to detect orientation change
WidgetsBinding.instance!.addObserver(this);
}
#override
void dispose() {
// remove listener
WidgetsBinding.instance!.removeObserver(this);
super.dispose();
}
#override
void didChangeMetrics() {
// on portrait / landscape or other change, recalculate height
_setWebViewHeight();
}
#override
Widget build(BuildContext context) {
// on initial loading, get height using MediaQuery,
// this will be used until page is loaded
if (_webViewHeight == null) {
final initalWebViewHeight = MediaQuery.of(context).size.height;
print('WebView inital height set to: $initalWebViewHeight');
_webViewHeight = initalWebViewHeight;
}
return RefreshIndicator(
// reload page
onRefresh: () => _webViewController!.reload(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Container(
height: _webViewHeight,
child: WebView(
javascriptMode: JavascriptMode.unrestricted,
initialUrl: 'https://flutter.dev',
onWebViewCreated: (WebViewController webViewController) {
_webViewController = webViewController;
},
onPageStarted: (String url) {
setState(() {
_isPageLoading = true;
});
},
onPageFinished: (String url) {
setState(() {
_isPageLoading = false;
});
// if page load is finished, set height
_setWebViewHeight();
})),
));
}
void _setWebViewHeight() {
// we don't update if WebView is not ready yet
// or page load is in progress
if (_webViewController == null || _isPageLoading) {
return;
}
// execute JavaScript code in the loaded page
// to get body height
_webViewController!
.evaluateJavascript('document.body.clientHeight')
.then((documentBodyHeight) {
// set height
setState(() {
print('WebView height set to: $documentBodyHeight');
_webViewHeight = double.parse(documentBodyHeight);
});
});
}
}

How to force stateful widget redraw using keys?

I'm making an app that pulls data from an API and displays it in a view (MVC style).
I need to figure out how to force my view widget to redraw itself. Right now I tried with ValueKeys and ObjectKeys but to no avail.
There's lots and lots of code so I am going to use snippets as much as possible to keep it clear. (If you need to see more code feel free to ask)
Here's my view widget:
class view extends StatefulWidget{
view({
Key key,
this.count = 0,
}) : super(key: key);
int count;
String _action='';
var _actionParams='';
var _data;
Function(String) callback;
void setAction(String newAction){
_action = newAction;
}
void setActionParams(String params){
_actionParams = jsonDecode(params);
}
void setData(String data){
_data = jsonDecode(data);
}
void incrementCounter(){
count++;
}
#override
_viewState createState() => _viewState();
}
class _viewState extends State<view>{
Object redrawObject = Object();
#override
Widget build(BuildContext context) {
/*
switch(widget._action){
case '':
break;
default:
return null;
}
*/
return Text("Counter: "+widget.count.toString());
}
#override
void initState(){
this.redrawObject = widget.key;
super.initState();
}
}
You can see in the commented code that I am planning to change the way the view builds itself in function of the data that gets passed to it.
What I have tried so far is to pass a ValueKey/ObjectKey to the view from main.dart in a constructor and then changing the object at runtime. Unfortunately that did not work.
At the top of my main.dart(accessible from anywhere within main) I have this:
Object redraw = Object();
final dataView = new view(key: ObjectKey(redraw));
Then in the body of the homepage I have the view and a floating button right under.
If I press the button it should increment the counter inside the view and force it to redraw. Here's the code I have tried so far:
body: Center(
child: dataView
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.badge),
onPressed: (){
dataView.incrementCounter();
redraw = new Object();
},
),
From what I understand, if the object that was used as a key gets changed, then flutter should rebuild the state for the widget. So I'm setting my object to a new object but it's not working.
I also tried something like this:
onPressed: (){
setState((){
dataView.incrementCounter();
redraw = new Object();
});
},
Eventually I'd like to use a navigator in conjunction with my view widget (so that we have a back button) but I don't know if this is possible.
It feels a bit like I'm fighting with the framework. Is there a different paradigm I should use (like pages?) or is it possible for me to do it this way?
How do I force my view widget to get redrawn?
Using Göktuğ Vatandaş' answer and GlobalKeys I was able to figure it out.
I made a reDraw() function inside the state and then I called it from my main using a GlobalKey.
Note: Wrapping in a container and using a key for the container is not necessary. Calling setState() is enough to force a redraw.
Here's the new view widget:
import 'dart:convert';
import 'package:flutter/cupertino.dart';
GlobalKey<_viewState> viewKey = GlobalKey();
class view extends StatefulWidget{
view({
Key key,
this.count = 0,
}) : super(key: key);
int count;
String _action='';
var _actionParams='';
var _data;
Function(String) callback;
void setAction(String newAction){
_action = newAction;
}
void setActionParams(String params){
_actionParams = jsonDecode(params);
}
void setData(String data){
_data = jsonDecode(data);
}
void incrementCounter(){
count++;
}
#override
_viewState createState() => _viewState();
}
class _viewState extends State<view>{
#override
Widget build(BuildContext context) {
/*
switch(widget._action){
case '':
break;
default:
return null;
}
*/
return Text("Counter: "+widget.count.toString());
}
#override
void initState(){
super.initState();
}
void reDraw(){
setState((){});
}
}
Here's where I declare the view widget in my main:
final dataView = new view(key: viewKey);
Here's where I call the reDraw() function:
floatingActionButton: FloatingActionButton(
child: Icon(Icons.badge),
onPressed: (){
dataView.incrementCounter();
viewKey.currentState.reDraw();
},
),
Thanks Göktuğ Vatandaş!
You can check flutter_phoenix's logic for redraw effect. I think its very useful or you can just use package itself. Basically it does what you trying to achive.
It creates a unique key in state.
Key _key = UniqueKey();
Injects it to a container.
#override
Widget build(BuildContext context) {
return Container(
key: _key,
child: widget.child,
);
}
And when you call rebirth it just refresh key and that causes view to rebuild.
void restartApp() {
setState(() {
_key = UniqueKey();
});
}

How can I Insert another Widget in place of another when one is pressed?

I am an entry level flutter developer and I've been stuck for a while on this problem, I've tried different things
I am try to make the 'Note Title' (that particular widget), pop out
The app_issue description
then this should show instead
desired result
The two different content under the "My Notes" tab are two Stateful Widgets and the "My Notes" tab is another Stateful Widget on its own
I've tried using a function but it doesn't work
enum MyNoteContent {
staticNote,
dynamicNote,
}
MyNoteContent selectedContent = MyNoteContent.staticNote;
Widget updateMyNotes() {
if (selectedContent == MyNoteContent.staticNote) {
return MyNoteStatic();
} else {
return MyNoteDynamic();
}
}
and then i call the function in the MyNotes Widget
class _MyNotesTabState extends State<MyNotesTab> {
#override
Widget build(BuildContext context) {
return updateMyNotes();
}
}
I trying to update the value in the first content that is shown (in its own Widget), so that when it is pressed, it should change
class _MyNoteStaticState extends State<MyNoteStatic> {
#override
Widget build(BuildContext context) {
return Container(
child: RawMaterialButton(
onPressed: () {
setState(() {
selectedContent = MyNoteContent.dynamicNote;
updateMyNotes();
});
},
but it does not work
Code to Reproduce the Problem
The issue here is that the setState you're calling is for the _MyNoteStaticState class. This means it will only rebuild the MyNoteStatic widget. But in order for the page to change you need to rebuild its parent _MyNotesTabState. So you need to call the setState method of _MyNotesTabState which can be done by passing a callback down from _MyNotesTabState to _MyNoteStaticState.
First, move updateMyNotes & selectedContent into the _MyNotesTabState class since that's the only place they're needed.
Make a new function that rebuilds _MyNotesTabState and changes selectedContent in _MyNotesTabState.
void changeNote() {
setState(() {
selectedContent = MyNoteContent.dynamicNote;
});
}
Pass it down to MyNoteStatic
Widget updateMyNotes() {
if (selectedContent == MyNoteContent.staticNote) {
return MyNoteStatic(changeNote);
} else {
return MyNoteDynamic();
}
}
and modify MyNoteStatic to accept this callback as a parameter
class MyNoteStatic extends StatefulWidget {
final VoidCallback callback;
MyNoteStatic(this.callback);
#override
_MyNoteStaticState createState() => _MyNoteStaticState();
}
Then pass this callback to your button instead of what you currently have:
child: RawMaterialButton(
onPressed: widget.callback,
)
Full relevant code incorporating the above changes:
enum MyNoteContent {
staticNote,
dynamicNote,
}
class MyNotesTab extends StatefulWidget {
#override
_MyNotesTabState createState() => _MyNotesTabState();
}
class _MyNotesTabState extends State<MyNotesTab> {
MyNoteContent selectedContent = MyNoteContent.staticNote;
#override
Widget build(BuildContext context) {
return updateMyNotes();
}
Widget updateMyNotes() {
if (selectedContent == MyNoteContent.staticNote) {
return MyNoteStatic(changeNote);
} else {
return MyNoteDynamic();
}
}
void changeNote() {
setState(() {
selectedContent = MyNoteContent.dynamicNote;
});
}
}
//Static Note
class MyNoteStatic extends StatefulWidget {
final VoidCallback callback;
MyNoteStatic(this.callback);
#override
_MyNoteStaticState createState() => _MyNoteStaticState();
}
class _MyNoteStaticState extends State<MyNoteStatic> {
#override
Widget build(BuildContext context) {
return Container(
child: RawMaterialButton(
onPressed: widget.callback,
...

Is there a better way to constantly rebuild a widget?

I have widget with data that changes regularly and I'm using a Timer.periodic to rebuild the widget. This starts out working smoothly but becomes choppy pretty quickly is there a better way to do this?
class _MainScreenState extends State<MainScreen> {
static const Duration duration = Duration(milliseconds: 16);
update(){
system.updatePos(duration.inMilliseconds/1000);
setState(() {});
}
#override
Widget build(BuildContext context) {
Timer.periodic(duration, (timer){
update();
});
return PositionField(
layoutSize: widget.square,
children: system.map
);
}
}
You are making a big mistake:
The build method must never have any side effects, because it is called again whenever setState is called (or when some higher up widget changes, or when the user rotates the screen...).
Instead, you want to create your Timer in initState, and cancel it on dispose:
class TimerTest extends StatefulWidget {
#override
_TimerTestState createState() => _TimerTestState();
}
class _TimerTestState extends State<TimerTest> {
Timer _timer;
int _foo = 0;
// this is only called once when the widget is attached
#override
void initState() {
super.initState();
_timer = Timer.periodic(Duration(seconds: 1), (timer) => _update());
}
// stop the timer when the widget is detached and destroyed
#override
void dispose() {
_timer.cancel();
super.dispose();
}
void _update() {
setState(() {
_foo++;
});
}
#override
Widget build(BuildContext context) {
return Text('Foo: ${_foo}');
}
}

How to periodically set state?

I am trying to build a countdown app for special days for example 11 d 2 h 30 m 23s to the new year but I can't reload my state every second so it just shows me the second that I loaded the page I don't know how to dynamically reload the page.
import 'package:flutter/material.dart';
class RopSayac extends StatefulWidget {
_RopSayacState createState() => _RopSayacState();
}
class _RopSayacState extends State<RopSayac> {
var now = DateTime.now().second.toString();
String asd(){
setState(() {
now = DateTime.now().second.toString();
});
return now;
}
#override
Widget build(BuildContext context) {
return Container(
child: new Text(asd()),
);
}
}
This is what I got and it doesn't reload time. I am pretty new on the flutter.
As pskink and Günter mentioned, use a Timer. You can even use the periodic constructor that would fit well your scenario.
Note you don't need the asd() function. When you call setState(), the build method will be called automatically passing the new now property value.
If you want, use initState to set an initial value and, as in this example, setup the Timer.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(title: 'Timer Periodic Demo', home: RopSayac());
}
}
class RopSayac extends StatefulWidget {
_RopSayacState createState() => _RopSayacState();
}
class _RopSayacState extends State<RopSayac> {
String _now;
Timer _everySecond;
#override
void initState() {
super.initState();
// sets first value
_now = DateTime.now().second.toString();
// defines a timer
_everySecond = Timer.periodic(Duration(seconds: 1), (Timer t) {
setState(() {
_now = DateTime.now().second.toString();
});
});
}
#override
Widget build(BuildContext context) {
return Container(
child: Center(
child: new Text(_now),
),
);
}
}
This recursive method should be enough for what you want. The seconds set as 1 will keep triggering setState each second, thus refreshing your widget tree.
void _timer() {
Future.delayed(Duration(seconds: 1)).then((_) {
setState(() {
print("1 second closer to NYE!");
// Anything else you want
});
_timer();
});
}
There's this excellent library called timer_builder. I think it'll help you out.
Example from the pub page:
import 'package:timer_builder/timer_builder.dart';
class ClockWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return TimerBuilder.periodic(Duration(seconds: 1), //updates every second
builder: (context) {
return Text("${DateTime.now()}");
}
);
}
}
Here's what I do. In my case I'm polling a webservice in _liveUpdate()
void startUpdates(AppState appState) async {
await new Future.delayed(const Duration(milliseconds: 100));
while (true) {
_liveUpdate();
appState.setState(() {});
await new Future.delayed(const Duration(seconds : 15));
}