I'm trying to write a fully reactive hello-world example in Flutter, specifically, a simple counter. Here, I have a null-stream from a "+1" button which gets triggered on button press:
class ReactiveButton extends StatelessWidget {
final _controller = StreamController();
Stream<void> pressedEvent;
final text;
ReactiveButton({this.text = ""}) {
pressedEvent = _controller.stream;
}
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () => _controller.sink.add(null),
child: Text(text),
);
}
}
I want to convert that stream into a sequence of natural numbers to feed into an RxText. So I use the zipWith method from rxdart with a tight stream of natural numbers, discarding the null value. Now the problem is that using the stream of natural numbers created by a tight while (true) loop results in an infinite cycle because (presumably) the stream never returns control between yields:
Stream<int> naturals() async* {
int count = 0;
while (true) {
yield count++;
}
}
Alternatively, I can use Stream.periodic constructor with a close to zero period. However, this results into my application gradually becoming sluggish (presumably, due to the streams indefinitely counting in vain).
So what I need is a lazy stream that only produces values when asked (immediately). That actually sounds like an Iterable but for Iterable I don't have a method to zip it with a stream. I thought about implementing a function which takes a list of streams/iterables and returns a stream of lists, working more or less like the regular zipper. It seems rather complicated though, and I'm not sure I can write a production-ready implementation.
The question is: what are my options? Is there a ready-to-go solution for this? Should I at all try to go full reactive with Flutter?
Related
The following code produces two values as output. It's clear why it prints in func. But where does the 2nd value come from (null)?
func(){
print("in func");
}
void main() {
var x = func;
print(x());
}
Output:
in func
null
You treated Dart like a scripting language and as a result, you got puzzled. Treat Dart as a the real programming language it is and it will be crystal clear:
dynamic func() {
// ^-- "dynamic" was inserted by your compiler,
// to make up for the fact you failed to mention a return type
print("in func");
// this is inserted by your compiler, to make up for the
// fact that a non-void function failed to provide a return value:
return null;
}
void main() {
var x = func;
// so this first calls the function, which prints it's line
// and then it prints the functions return value, which is null.
print(x());
}
Be explicit when programming. Use the power this language gives you.
When in doubt, turn on analysis. It will tell you what I told you: you missed to explicitely state things and now the compiler has to cover for you. That makes it hard to see what actually happens. Don't be hard on future readers. One of them is you, 5 seconds from now. Make it easy for yourself and be explicit about what code is doing.
Is there way to late initialize for final variables. The problem is many values initialized with entry point to the class, which is not constructor. Hence they cannot be final right now. But in scope of particular class they will not be changed. For ex.
Controller controller;
double width;
void setup(final itemWidth) {
controller = MyController();
width = itemWidth;
}
Could it be possible? Right now I see only solution as a annotation. You might think it's for visual effect. But in fact it helps to avoid unpredictable flow during testing.
It is now possible to late initialize variables. For more information see Dart's documentation. The text below is copied from Dart's documentation:
Late final variables
You can also combine late with final:
// Using null safety:
class Coffee {
late final String _temperature;
void heat() { _temperature = 'hot'; }
void chill() { _temperature = 'iced'; }
String serve() => _temperature + ' coffee';
}
Unlike normal final fields, you do not have to initialize the field in its declaration or in the constructor initialization list. You can assign to it later at runtime. But you can only assign to it once, and that fact is checked at runtime. If you try to assign to it more than once — like calling both heat() and chill() here — the second assignment throws an exception. This is a great way to model state that gets initialized eventually and is immutable afterwards.
Suppose there is a widget with a method controlling visibility animation, toggleVisibility(). In a BLoC pattern, I want to use a stream to invoke that function. I find this pretty tricky.
Since it is an animation rather than a complete redraw, a StreamBuilder doesn't fit.
Manually add a listener to the BLoC streams isn't convenient either.
In initeState() function of the target widget, we don't have a context, so it's hard to get a reference to the BLoC.
Edit: This is not the case after I read Rémi Rousselet's answer. We can access the context even outside of build() function, because State<T> class has a property named 'context' and is documented in Flutter's docs.... I wasn't aware of that.
In build(context) function of the target widget, we have the context. But the widget can be frequently re-built, so you have to manually clean the outdated subscriptions. Otherwise it will create tons of garbages.
A hack with StreamBuilder can do, since the StreamBuilder has implemented all the subscription and unsubscription functionalities. Insert a StreamBuilder somewhere in the layout of the target widget.
StreamBuilder(
stream: Bloc.of(context).stream,
builder: (context, snapshot){
toggleVisiblity();
return Container():
}
);
But this is really a hack. It mixed layout with logic and introduced a useless widget which could cause layout bugs.
So I wonder if there is a good way of doing this in flutter.
You cannot use StreamBuilder to do that. You have to manually listen to the stream
class Example extends StatefulWidget {
#override
ExampleState createState() => ExampleState();
}
class ExampleState extends State<Example> {
StreamSubscription subscription;
#override
void didChangeDependencies() {
super.didChangeDependencies();
Stream stream = Bloc.of(context).someStream;
subscription?.cancel();
subscription = stream.listen((value) {
// do something
});
}
#override
void dispose() {
subscription?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}
I have a periodic timer in one of my StatelessWidget's. Without going too much into detail, here is a snippet of code producing a timer:
class AnniversaryHomePage extends StatelessWidget {
. . .
void _updateDisplayTime(StoreInheritedWidget inheritedWidget) {
String anniversaryString = inheritedWidget.prefs.getString('anniversaryDate');
inheritedWidget.store.dispatch(DateTime.parse(anniversaryString));
}
/// Widget Methods
#override
Widget build(BuildContext context) {
final inheritedWidget = StoreInheritedWidget.of(context);
new Timer.periodic(this.refreshRate, (Timer timer) => _updateDisplayTime(inheritedWidget));
. . .
}
When I try pumping my the application's starting point into flutter test, I get the following error message:
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following assertion was thrown running a test:
A periodic Timer is still running even after the widget tree was disposed.
'package:flutter_test/src/binding.dart': Failed assertion: line 668 pos 7:
'_fakeAsync.periodicTimerCount == 0'
The question is, am I using my Timer.periodic incorrectly? If not, how do I mitigate this error?
The issue is that creating a Timer creates a resource which must be disposed, and therefore your widget is actually Stateful and not stateless. Specifically, the build method may be called 60 times a second (or more if the platform is 120fps). Any less is just an optimization.
You have a very critical bug in your code - the build method is creating a new Timer every time it is called. And since your timers are never cancelled, you could have hundreds or potentially thousands of events dispatched to your store.
To avoid situations like this, the framework has a State class, with an initState and dispose lifecycle. The framework promises that if it rebuilds your widget, it won't call initState more than once and it will always call dispose. This allows you to create a Timer once and reuse it on subsequent calls to build.
For example, you can move most of your logic to the State like below. Leave the refreshRate on the widget, and you can even cancel and update your timer using the didUpdateWidget lifecycle too.
class AnniversaryHomePage extends StatefulWidget {
#override
State createState() => new AnniversaryHomePageState();
}
class AnniversaryHomePageState extends State<AnniversaryHomePage> {
Timer _timer;
#override
void initState() {
_timer = new Timer.periodic(widget.refreshRate,
(Timer timer) => _updateDisplayTime(inheritedWidget));
super.initState();
}
#override
void dispose() {
_timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
...
}
}
I think I'm lacking in a fundamental understanding of dart, but basically what I want to do is something like this:
void main() {
new MyClass();
}
class MyClass {
MyClass() {
CanvasElement canvas = querySelector("#myCanvas");
CanvasRenderingContext2D context = canvas.context2D;
}
}
However, canvas is a null object by the time I try to get the context. How can I do this from within the class. Also, I don't want to do this:
void main() {
CanvasElement canvas = querySelector("#myCanvas");
new MyClass(canvas);
}
class MyClass {
CanvasElement canvas
MyClass(this.canvas) {
canvas = this.canvas;
CanvasRenderingContext2D context = canvas.context2D;
}
}
Because I need to be able to do this completely from within the class. Is this just not how dart works, or am I missing something?
Did you try your second example? It doesn't make a difference if you call querySelector from main() or from within a class.
Do you use Angular or Polymer?
Angular or Polymer components introduce shadowDOM. querySelector() doesn't cross shadowDOM boundaries and it therefore doesn't find elements inside an elements shadowDOM.
To query for elements inside a shadowDOM you query for the component and then you can continue the search.
querySelector("somecomponent").shadowRoot.querySelector("someothercomponent").shadowRoot.querySelector("#myCanvas");
You have to ensure that the DOM including all shadowDOMs is fully built before you can query them.
If you run your code from within a component pub your code into the onShadowRoot method (see NgComponent ready event for more details)