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();
});
}
Related
I hope you could help me!
Error saying 'tables' has not been initiliazed. But when I set tables = [] instead of
widget.data.then((result) {tables = result.tables;})
it works. I think the problem comes from my app state data which is a Future.
My simplified code:
class NavBar extends StatefulWidget {
final Future<Metadata> data;
const NavBar({Key? key, required this.data}) : super(key: key);
#override
State<NavBar> createState() => _NavBarState();
}
class _NavBarState extends State<NavBar> {
late List<MyTable> tables;
#override
void initState() {
widget.data.then((result) {
tables = result.tables;
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: buildPages(page.p)
)
);
}
Widget buildPages(index){
switch (index) {
case 0:
return ShowTablesNew(tables: tables);
case 1:
return const Details();
case 2:
return const ShowTables();
default:
return const ShowTables();
}
}
}
Future doesn't contain any data. It's an asynchronous computation that will provide data "later". The initialization error happens because the variable 'tables' is marked as late init but is accessed before the future is completed, when in fact it's not initialized yet.
Check this codelab for async programming with dart.
For your code you can use async/await in the initState method doing something like this
String user = '';
#override
void initState() {
asyncInitState();
super.initState();
}
void asyncInitState() async {
final result = await fetchUser();
setState(() {
user = result;
});
}
but since you're using a list of custom objects the most straightforward way is probably to use a FutureBuilder widget
I have a "WidgetBackGround" statefullwidget that return an animated background for my app,
I use it like this :
Scaffold( resizeToAvoidBottomInset: false, body: WidgetBackGround( child: Container(),),)
The problem is when I use navigator to change screen and reuse WidgetBackGround an other instance is created and the animation is not a the same state that previous screen.
I want to have the same animated background on all my app, is it possible to instance it one time and then just reuse it ?
WidgetBackGround.dart look like this:
final Widget child;
WidgetBackGround({this.child = const SizedBox.expand()});
#override
_WidgetBackGroundState createState() => _WidgetBackGroundState();
}
class _WidgetBackGroundState extends State<WidgetBackGround> {
double iter = 0.0;
#override
void initState() {
Future.delayed(Duration(seconds: 1)).then((value) async {
for (int i = 0; i < 2000000; i++) {
setState(() {
iter = iter + 0.000001;
});
await Future.delayed(Duration(milliseconds: 50));
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return CustomPaint(painter: SpaceBackGround(iter), child: widget.child);
}
}
this is not a solution, but maybe a valid workaround:
try making the iter a static variable,
this of course won't preserve the state of WidgetBackGround but will let the animation continue from its last value in the previous screen
A valid solution (not sure if it's the best out there):
is to use some dependency injection tool (for example get_it) and provide your WidgetBackGround object as a singleton for every scaffold in your app
Please check out this 36 seconds video for more clarity, cause it was getting too verbose explaning things : https://www.youtube.com/watch?v=W6WdQuLjrCs
My best guess
It's due to the provider.
App structure ->
Outer Page -> NoteList Page
The Outer Page code :
class OuterPage extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return OuterPageState();
}
}
class OuterPageState extends State<OuterPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
int _selectedTab = 0;
var noteList;
final _pageOptions = [
NoteList(),
AnotherPageScreen(),
];
#override
Widget build(BuildContext context) {
var noteProvider = Provider.of<NotesProvider>(context, listen: false);
var customFabButton;
if (_selectedTab == 0) {
customFabButton = FloatingActionButton(
// Password section
onPressed: () {
navigateToDetail(context, Note('', '', 2), 'Add Note');
},
child: Icon(Icons.add),
);
~~~ SNIP ~~~
The Notes Tab aka NoteList page code :
class NoteList extends StatefulWidget {
NoteList();
#override
NoteListState createState() => NoteListState();
}
class NoteListState extends State<NoteList> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
List<Note> noteList;
int count = 0;
#override
Widget build(BuildContext context) {
Provider.of<NotesProvider>(context).getNotes();
return Scaffold(
key: _scaffoldKey,
body: Provider.of<NotesProvider>(context).count > 0
? NoteListScreen(_scaffoldKey)
: CircularProgressIndicator());
}
}
For full code : check here : https://github.com/LuD1161/notes_app/tree/reusable_components
Update 1 - Possible solution is FutureBuilder
I know that there's a possible solution with FutureBuilder but I think even Provider is apt for this use case.
Moreover is it an anti-pattern here ?
Also, please don't suggest another package for the same thing, if possible try limiting the solution to Provider or base libraries.
Update 2 - Not possible with FutureBuilder
FutureBuilder can't be used here because there's a delete button in the list tile and hence when the note gets deleted the note list won't get updated.
The issue is coming because of the getNotes function you are calling from build method. You are calling notifyListeners from that function. It again re-builds the widget and calls the build method again and this cycle continues.
You either need to set false to the listen property of provider, but it will break your functionality. To fix this, you have to move the getNotes call from build function to initState like following:
#override
void initState() {
super.initState();
postInit(() {
Provider.of<NotesProvider>(context).getNotes();
});
}
Implement postInit (Reference: Flutter Issue 29515):
extension StateExtension<T extends StatefulWidget> on State<T> {
Stream waitForStateLoading() async* {
while (!mounted) {
yield false;
}
yield true;
}
Future<void> postInit(VoidCallback action) async {
await for (var isLoaded in waitForStateLoading()) {}
action();
}
}
Note: Instead of writing the postInit code, you can also use after_init package for same purpose.
Several other posts discussing similar kind of issues:
How to correctly fetch APIs using Provider in Flutter
Using provider in fetching data onLoad
Sorry if this is a novice question. I have the following repo file:
class TwitterRepo {
final TwitterRepoCallback _callback;
TwitterRepo(this._callback){
// do stuff
}
}
abstract class TwitterRepoCallback{
void onEvent();
}
In my UI file I have the following:
class TweetList extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _TweetListState();
}
}
class _TweetListState extends State<TweetList> implements TwitterRepoCallback {
final TwitterRepo _twitterRepo = TwitterRepo(this);
// other stuff like initState, build and onEvent
}
There is an error on
final TwitterRepo _twitterRepo = TwitterRepo(this);
where I use "this", Invalid reference to 'this' expression.
I'm at a loss on how to pass in my callback to receive events.
Try this.
class ParentPageState extends State<ParentPage> implement Callback{
...
#override
void callback(){
...
}
#override
void callback1(String str){
....
}
#override
Widget build(BuildContext context){
return Scaffold(
body : Container(
child : ChildPage(callback : this.callback, callback1 : this.callback1)
)
);
}
}
And ChildPage
import .....
//Main Point
typedef Callback = void Function();
typedef Callback1 = void Function(String str);
class ChildPage extends StatelessWidget{
final Callback _callback;
final Callback1 _callback1;
ChildPage({Callback callback, Callback1 callback1}): _callback : callback, _callback1 : callback1;
.....
#override
Widget build(BuildContext context){
return Container(
child : InkWell(
onPressed : (){
this._callback();
this._callback1("test");
},
child : ....
)
);
}
This is may have issue. The main point is "typedef"
I probably wouldn't use callbacks for this type of need. Instead I'd use some kind of InheritedWidget like system to grab data and propagate it down the widget tree. I know you just started, but a great tool is the Provider package. To do what you're trying to do here it'd look something like this:
class TwitterRepo extends ChangeNotifier{
//construct Repo
TwitterRepo(){
_setupNetworkListener();
}
List<Data> data = [];
//set up the way to listen to and get data here then add it to your list,
//finally notify your listeners of the data changes
_setupNetworkListener()async{
var _data = await gettingInfo();
data.addAll(_data);
notifyListeners();
}
}
class TwitterRepoUI extends StatefulWidget {
#override
_TwitterRepoUIState createState() => _TwitterRepoUIState();
}
class _TwitterRepoUIState extends State<TwitterRepoUI> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<TwitterRepo>(
builder: (context)=> TwitterRepo(),
child: Consumer<TwitterRepo>(
builder: (context, model, child){
return ListView.builder(
itemCount: model.data.length,
itemBuilder: (context, index){
return Center(
child: Text(index.toString()),
);
});
},
),
);
}
}
If you want to use the callback to notify the UI to render some new data, you may want to use Future or Stream. Anyway, the question is how to implement a callback/listener so here I give you some examples:
You can't declare a variable using this, you could initialize the variable on the constructor
_TweetListState() {
_twitterRepo = TwitterRepo(this);
}
or inside initState()
#override
void initState() {
super.initState();
_twitterRepo = TwitterRepo(this);
}
A better way to do this would be:
final TwitterRepo _twitterRepo = TwitterRepo();
#override
void initState() {
super.initState();
_twitterRepo.listen(this);
}
And the listen method implemented on TwitterRepo would set the callback
You could also use VoidCallback instead of TwitterRepoCallback:
#override
void initState() {
super.initState();
_twitterRepo.listen(() {
// Do stuff
});
}
Or a callback function using Function(String a, int b) instead of TwitterRepoCallback
#override
void initState() {
super.initState();
_twitterRepo.listen((a, b) {
// Do stuff
});
}
I'm using an inherited Widget to access a Bloc with some long running task (e.g. search).
I want to trigger the search on page 1 and continue to the next page when this is finished. Therefore I'm listening on a stream and wait for the result to happen and then navigate to the result page.
Now, due to using an inherited widget to access the Bloc I can't access the bloc with context.inheritFromWidgetOfExactType() during initState() and the exception as I read it, recommends doing this in didChangeDependencies().
Doing so this results in some weird behavior as the more often I go back and forth, the more often the stream I access fires which would lead to the second page beeing pushed multiple times. And this increases with each back and forth interaction. I don't understand why the stream why this is happening. Any insights here are welcome. As a workaround I keep a local variable _onSecondPage holding the state to avoid pushing several times to the second Page.
I found now How to call a method from InheritedWidget only once? which helps in my case and I could access the inherited widget through context.ancestorInheritedElementForWidgetOfExactType() and just listen to the stream and navigate to the second page directly from initState().
Then the stream behaves as I would expect, but the question is, does this have any other side effects, so I should rather get it working through listening on the stream in didChangeDependencides() ?
Code examples
My FirstPage widget listening in the didChangeDependencies() on the stream. Working, but I think I miss something. The more often i navigate from first to 2nd page, the second page would be pushed multiple times on the navigation stack if not keeping a local _onSecondPage variable.
#override
void didChangeDependencies() {
super.didChangeDependencies();
debugPrint("counter: $_counter -Did change dependencies called");
// This works the first time, after that going back and forth to the second screen is opened several times
BlocProvider.of(context).bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
void _handleRouting(bool isFinished) async {
if (isFinished && !_onSecondPage) {
_onSecondPage = true;
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => NAVIGATE TO OTHER PAGE");
await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
_onSecondPage = false;
} else {
debugPrint("counter: $_counter - finished: $isFinished : ${DateTime.now().toIso8601String()} => not finished, nothing to do now");
}
}
#override
void dispose() {
debugPrint("counter: $_counter - disposing my homepage State");
subscription?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
StreamBuilder(
stream: BlocProvider.of(context).bloc.counter.stream,
initialData: 0,
builder: (context, snapshot) {
_counter = snapshot.data;
return Text(
"${snapshot.data}",
style: Theme.of(context).textTheme.display1,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
A simple Bloc faking some long running work
///Long Work Bloc
class LongWorkBloc {
final BehaviorSubject<bool> startLongWork = BehaviorSubject<bool>();
final BehaviorSubject<bool> finished = BehaviorSubject<bool>();
int _counter = 0;
final BehaviorSubject<int> counter = BehaviorSubject<int>();
LongWorkBloc() {
startLongWork.stream.listen((bool start) {
if (start) {
debugPrint("Start long running work");
Future.delayed(Duration(seconds: 1), () => {}).then((Map<dynamic, dynamic> reslut) {
_counter++;
counter.sink.add(_counter);
finished.sink.add(true);
finished.sink.add(false);
});
}
});
}
dispose() {
startLongWork?.close();
finished?.close();
counter?.close();
}
}
Better working code
If I however remove the code to access the inherited widget from didChangeDependencies() and listen to the stream in the initState() it seems to be working properly.
Here I get hold of the inherited widget holding the stream through context.ancestorInheritedElementForWidgetOfExactType()
Is this ok to do so? Or what would be a flutter best practice in this case?
#override
void initState() {
super.initState();
//this works, but I don't know if this is good practice or has any side effects?
BlocProvider p = context.ancestorInheritedElementForWidgetOfExactType(BlocProvider)?.widget;
if (p != null) {
p.bloc.finished.stream.listen((bool isFinished) {
_handleRouting(isFinished);
});
}
}
Personally, I have not found any reason not to listen to BLoC state streams in initState. As long as you remember to cancel your subscription on dispose
If your BlocProvider is making proper use of InheritedWidget you should not have a problem getting your value inside of initState.
like So
void initState() {
super.initState();
_counterBloc = BlocProvider.of(context);
_subscription = _counterBloc.stateStream.listen((state) {
if (state.total > 20) {
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context) {
return TestPush();
}));
}
});
}
Here is an example of a nice BlocProvider that should work in any case
import 'package:flutter/widgets.dart';
import 'bloc_base.dart';
class BlocProvider<T extends BlocBase> extends StatefulWidget {
final T bloc;
final Widget child;
BlocProvider({
Key key,
#required this.child,
#required this.bloc,
}) : super(key: key);
#override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context) {
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider =
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<BlocBase>> {
#override
Widget build(BuildContext context) {
return _BlocProviderInherited<T>(
bloc: widget.bloc,
child: widget.child,
);
}
#override
void dispose() {
widget.bloc?.dispose();
super.dispose();
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
final T bloc;
_BlocProviderInherited({
Key key,
#required Widget child,
#required this.bloc,
}) : super(key: key, child: child);
#override
bool updateShouldNotify(InheritedWidget oldWidget) => false;
}
... and finally the BLoC
import 'dart:async';
import 'bloc_base.dart';
abstract class CounterEventBase {
final int amount;
CounterEventBase({this.amount = 1});
}
class CounterIncrementEvent extends CounterEventBase {
CounterIncrementEvent({amount = 1}) : super(amount: amount);
}
class CounterDecrementEvent extends CounterEventBase {
CounterDecrementEvent({amount = 1}) : super(amount: amount);
}
class CounterState {
final int total;
CounterState(this.total);
}
class CounterBloc extends BlocBase {
CounterState _state = CounterState(0);
// Input Streams/Sinks
final _eventInController = StreamController<CounterEventBase>();
Sink<CounterEventBase> get events => _eventInController;
Stream<CounterEventBase> get _eventStream => _eventInController.stream;
// Output Streams/Sinks
final _stateOutController = StreamController<CounterState>.broadcast();
Sink<CounterState> get _states => _stateOutController;
Stream<CounterState> get stateStream => _stateOutController.stream;
// Subscriptions
final List<StreamSubscription> _subscriptions = [];
CounterBloc() {
_subscriptions.add(_eventStream.listen(_handleEvent));
}
_handleEvent(CounterEventBase event) async {
if (event is CounterIncrementEvent) {
_state = (CounterState(_state.total + event.amount));
} else if (event is CounterDecrementEvent) {
_state = (CounterState(_state.total - event.amount));
}
_states.add(_state);
}
#override
void dispose() {
_eventInController.close();
_stateOutController.close();
_subscriptions.forEach((StreamSubscription sub) => sub.cancel());
}
}