How to update flutter widget using RxDart BehaviorSubject? - dart

Im new to flutter and dart so i have created simple counter flutter app , explained in article.
But when stream does not update widget when subject adds values.
can someone help me to find the issue.
my main widget class
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
CounterBloc _counterBloc = new CounterBloc(initialCount: 0);
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Counter(),
floatingActionButton: FloatingActionButton(
onPressed: _counterBloc.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
and Counter widget
class Counter extends StatefulWidget {
#override
_CounterState createState() => _CounterState();
}
class _CounterState extends State<Counter> {
CounterBloc _counterBloc = new CounterBloc(initialCount: 1);
#override
void dispose() {
_counterBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _counterBloc.counterObservable,
builder: (context, snapshot) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${snapshot.hasData}',
style: Theme.of(context).textTheme.display1,
),
],
),
),
);
}
}
and here is my bloc class
class CounterBloc {
int initialCount =
1; //if the data is not passed by paramether it initializes with 0
BehaviorSubject<int> _subjectCounter;
CounterBloc({this.initialCount}) {
_subjectCounter = new BehaviorSubject<int>.seeded(
this.initialCount); //initializes the subject with element already
}
Observable<int> get counterObservable => _subjectCounter.stream;
void increment() {
initialCount++;
print(initialCount);
_subjectCounter.add(initialCount);
}
void decrement() {
initialCount--;
_subjectCounter.add(initialCount);
}
void dispose() {
_subjectCounter.close();
}
}
Can some one help me to find the issue.
Thanks.

You are using two separate instances of CounterBloc in MyHomePage and Counter classes. A simple solution would be to pass the CounterBloc of MyHomePage to Counter:
MyHomePage
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
MyHomePageState
class _MyHomePageState extends State<MyHomePage> {
CounterBloc _counterBloc = CounterBloc(initialCount: 0);
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Counter(
bloc: _counterBloc,
),
floatingActionButton: FloatingActionButton(
onPressed: _counterBloc.increment,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Counter
class Counter extends StatefulWidget {
Counter({Key key, this.bloc}) : super(key: key);
final CounterBloc bloc;
#override
_CounterState createState() => _CounterState();
}
CounterState
class _CounterState extends State<Counter> {
CounterBloc _counterBloc;
#override
void dispose() {
_counterBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
CounterBloc _counterBloc = widget.bloc;
return StreamBuilder(
stream: _counterBloc.counterObservable,
builder: (context, snapshot) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.display1,
),
],
),
),
);
}
}
But in the long run, rather than passing your Bloc explicitly as a parameter, you should use a BlocProvider, which will implicitly assigns the instance of the parent classes' Bloc to your children.

Related

Flutter - RepaintBoundary causes state reset of StatefulWidget

I have a preview widget that loads data after a user tap. This state (already tapped or not) should not be lost while scrolling (the preview is located in a list) or navigating through other screen.
The scrolling is solved by adding AutomaticKeepAliveClientMixin which saves the state when scrolling away.
Now i also need to wrap the preview widget (actually a more complex widget that contains the preview) with a RepaintBoundary, to be able to make a "screenshot" of this widget alone.
Before i wrap the widget with a RepaintBoundary, the state is saved both while scrolling and navigating to another screen.
After i add the RepaintBoundary the scrolling still works but for navigation the state is reset.
How can i wrap a Stateful widget that should hold its state with a RepaintBoundary?
Code is a simplified example of my implementation with the same problem.
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
final title = 'Test';
return MaterialApp(
title: title,
home: Scaffold(
appBar: AppBar(
title: Text(title),
),
body: TestList(40),
),
);
}
}
class TestList extends StatefulWidget {
final int numberOfItems;
TestList(this.numberOfItems);
#override
_TestListState createState() => _TestListState();
}
class _TestListState extends State<TestList> {
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView.builder(
itemCount: widget.numberOfItems,
itemBuilder: (context, index) {
return RepaintBoundary(
key: GlobalKey(),
child: Preview()
);
},
);
}
}
class Preview extends StatefulWidget {
#override
_PreviewState createState() => _PreviewState();
}
class _PreviewState extends State<Preview> with AutomaticKeepAliveClientMixin {
bool loaded;
#override
void initState() {
super.initState();
print('_PreviewState initState.');
loaded = false;
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
print('_PreviewState build.');
if(loaded) {
return GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => NewScreen()),
);
},
child: ListTile(
title: Text('Loaded. Tap to navigate.'),
leading: Icon(Icons.visibility),
),
);
} else {
return GestureDetector(
onTap: () {
setState(() {
loaded = true;
});
},
child: ListTile(
title: Text('Tap to load.'),
),
);
}
}
}
class NewScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('New Screen')),
body: Center(
child: Text(
'Navigate back and see if loaded state is gone.',
style: TextStyle(fontSize: 14.0),
),
),
);
}
}
Take a look at RepaintBoundary.wrap, it assigns the RepaintBoundary widget a key based on its child or childIndex so state is maintained:
class _TestListState extends State<TestList> {
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView.builder(
itemCount: widget.numberOfItems,
itemBuilder: (context, index) {
return RepaintBoundary.wrap(
Preview(),
index,
);
},
);
}
}
https://api.flutter.dev/flutter/widgets/RepaintBoundary/RepaintBoundary.wrap.html
EDIT: As per the below comments, it looks like this solution would break the screenshot ability so you'd have to store the list of children widgets in your state like so:
class _TestListState extends State<TestList> {
List<Widget> _children;
#override
void initState() {
super.initState();
_children = List.generate(
widget.numberOfItems,
(_) => RepaintBoundary(
key: GlobalKey(),
child: Preview(),
));
}
#override
Widget build(BuildContext context) {
print('_TestListState build.');
return ListView(children: _children);
}
}

Add time to countdown timer during while counting down in Flutter

I am trying to create a countdown timer application where I can press a button to add more time while the timer is actively counting down just like every microwave oven where there is a button you can press to add an extra minute to it's run time while it's running without stopping anything.
import 'package:flutter/material.dart';
void main() {
runApp(new MaterialApp(
home: new MyApp(),
));
}
class Countdown extends AnimatedWidget {
Countdown({ Key key, this.animation }) : super(key: key, listenable: animation);
Animation<int> animation;
#override
build(BuildContext context){
return new Text(
animation.value.toString(),
style: new TextStyle(fontSize: 150.0),
);
}
}
class MyApp extends StatefulWidget {
State createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
AnimationController _controller;
static const int kStartValue = 4;
#override
void initState() {
super.initState();
_controller = new AnimationController(
vsync: this,
duration: new Duration(seconds: kStartValue),
);
}
#override
Widget build(BuildContext context) {
return new Scaffold(
floatingActionButton: new FloatingActionButton(
child: new Icon(Icons.play_arrow),
onPressed: () => _controller.forward(from: 0.0),
),
body: new Container(
child: new Center(
child: new Countdown(
animation: new StepTween(
begin: kStartValue,
end: 0,
).animate(_controller),
),
),
),
);
}
}
This example from a similar question about timers makes sense to me and has been my jumping off point. I know I need to change the duration and replace the animation with a new one with the appropriate duration, but I never get anything close to the correct behavior I am looking for.
I think the below is what you're trying to accomplish. Tapping the FloatingActionButton adds 5 seconds.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Countdown Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Countdown Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _timeRemaining = 10;
Timer _timer;
#override
void initState() {
_timer = Timer.periodic(Duration(seconds: 1), (Timer t) => _getTime());
super.initState();
}
#override
void dispose() {
_timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Text('$_timeRemaining'),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
_timeRemaining += 5;
}),
);
}
void _getTime() {
setState(() {
_timeRemaining == 0 ? _timeRemaining = 0 : _timeRemaining--;
});
}
}

How can I pass the callback to another StatefulWidget?

For example, I have two StatefulWidget to monitor the same callback method. How should I do this? In case I have more than three StatefulWidget to monitor its events?
class WidgetOne extends StatefulWidget {
#override
_WidgetOneState createState() => new _WidgetOneState();
}
class _WidgetOneState extends State<WidgetOne> {
// this is the callback, the widget two want listen the callback too
bool _onNotification(ScrollNotification notification){
}
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new NotificationListener(child: new ListView(shrinkWrap: true,),
onNotification: _onNotification),
new WidgetTwo()
],
);
}
}
class WidgetTwo extends StatefulWidget {
#override
_WidgetTwoState createState() => new _WidgetTwoState();
}
class _WidgetTwoState extends State<WidgetTwo> {
// in this,How Can I get the callback in WidgetOne?
#override
Widget build(BuildContext context) {
return new Container();
}
}
You can't and shouldn't. Widgets should never depends on the architecture of other widgets.
You have two possibilities :
Merge WidgetTwo and WidgetOne. As separating them doesn't makes sense (at least with what you provided).
Modify WidgetTwo to take a child. And add that ListView as child of WidgetTwo. So that it can wrap the list into it's own NotificationListener.
Your solution could be possible by using setState() and pass your state function in constructor of WidgetTwo. I did an example below, main idea of this example is I have MyHomePage as my main Widget and MyFloatButton (which I want to customise as another StatefulWidget), so when pressing the FAB i need to call increment counter function in MyHomePage. Lets take a look below how I do that.
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
//Consider this function as your's _onNotification and important to note I am using setState() within :)
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(
widget.title,
style: TextStyle(color: Colors.white),
),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button $_counter times:',
style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
),
],
),
),
floatingActionButton: new MyFloatButton(_incrementCounter),//here I am calling MyFloatButton Constructor passing _incrementCounter as a function
);
}
}
class MyFloatButton extends StatefulWidget {
final Function onPressedFunction;
// Here I am receiving the function in constructor as params
MyFloatButton(this.onPressedFunction);
#override
_MyFloatButtonState createState() => new _MyFloatButtonState();
}
class _MyFloatButtonState extends State<MyFloatButton> {
#override
Widget build(BuildContext context) {
return new Container(
padding: EdgeInsets.all(5.0),
decoration: new BoxDecoration(color: Colors.orangeAccent, borderRadius: new BorderRadius.circular(50.0)),
child: new IconButton(
icon: new Icon(Icons.add),
color: Colors.white,
onPressed: widget.onPressedFunction,// here i set the onPressed property with widget.onPressedFunction. Remember that you should use "widget." in order to access onPressedFunction here!
),
);
}
}
Now Consider MyHomePage as your WidgetOne, MyFloatButton as your WidgetTwo and _incrementCounter function as your _onNotification. Hope you will achieve what you want :)
(I did example generically so anyone can understand according to scenario they are facing)
You can use the built-in widget property for stateful widgets.
https://api.flutter.dev/flutter/widgets/State/widget.html
So in WidgetOne it will be
new WidgetTwo(callback: callback)
in WidgetTwo
class WidgetTwo extends StatefulWidget {
final Function callback;
WidgetTwo({this.callback});
#override
_WidgetTwoState createState() => new _WidgetTwoState();
}
and in _WidgetTwoState you can access it as
widget.callback

Flutter How to prevent Text from blocking Gesture detection?

I have a 'Container' wrapped in a 'GestureDetector'. I've added 'Text' over the 'Container' at runtime and the 'Text' prevents parts of the 'Container' from detecting Gestures. How to prevent 'Text' from blocking Gesture detection?
class MyButton extends StatefulWidget {
#override
_MyButtonState createState() => new _MyButtonState();
}
class _MyButtonState extends State<MyButton> {
#override
Widget build(BuildContext context) {
return new GestureDetector(
onTapDown: myTapDown,
child: new Container(),
);
}
void myTapDown(TapDownDetails details) {
showDialog(
context: context,
child: new Text(
'SOME TEXT',
textScaleFactor: 2.0,
),
);
}
}
class MyPage extends StatefulWidget {
#override
_MyPageState createState() => new _MyPageState();
}
class _MyPageState extends State<MyPage> {
#override
Widget build(BuildContext context) {
return new Stack(
children: <Widget>[
new MyButton(),
new Column(
children: <Widget>[new Text('NAME'),new Text('NUMBER')],
)
],
);
}
}

Flutter Listview in stateless widget with initial offset

I have my own StatelessWidget with a ListView. I want it's state to be managed by parent StatefulWidget.
The behaviour I desire is that if I change a value, listView scrolls (or even jumps - it doesn't matter) to that value.
I thought that if I create stateless widget every time parent's setState() method is being invoked, the scrollController with initialOffset would make the list "move" but it doesn't. What is worth mentioning is that on first build initialOffset works as it should.
Here is example code of my problem:
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 5;
void _incrementCounter() {
setState(() {
_counter++;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new MyClass(_counter),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
child: new Icon(Icons.add),
),
);
}
}
class MyClass extends StatelessWidget {
final int extraValue;
final ScrollController scrollController;
MyClass(this.extraValue):
scrollController = new ScrollController(initialScrollOffset: extraValue*50.0);
#override
Widget build(BuildContext context) {
return new ListView.builder(
itemExtent: 50.0,
itemCount: 100,
controller: scrollController,
itemBuilder: (BuildContext context, int index) {
if (index != extraValue)
return new Text(index.toString());
else
return new Text("EXTRA" + index.toString());
});
}
}
I'm not sure if it's a bug or my mistake.
Any ideas might be helpful :)
EDIT:
Inspired by Ian Hickson's answer I have solution to my problem:
void _incrementCounter() {
setState(() {
_counter++;
myClass.scrollController.animateTo(_counter*50.0, duration: new Duration(seconds: 1), curve: new ElasticOutCurve());
});
}
The initial offset is... the initial offset. Not the current offset. :-)
You can cause the offset to change by calling methods on the ScrollController, like animateTo.

Resources