In my app the menu have items with "cut-off" times. Once the cut-off time is reached, the item turns red and becomes disabled with a note stating "Cut-off time missed".
This works fine except that for a user watching the menu when the cutoff time passes the display does not update dynamically. Which in not a major issue for my current app, if somebody wanted to sit and watch the menu and notice that it only updates when you close and re-open the menu, I don't care.
But as a learning exercise it would be good if I could make it so that the menu items gets rebuilt once the time is passed.
I however do not want to turn the large Menu tree into a stateful widget. Doing this with a stateful widget is easy enough and the minimal example below (Based off of the flutter default app) shows the desired effect. There are two fields which are updated periodically (on every second) using a timer which just calls setState every second.
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 Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
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> {
DateTime _markedTime;
// Current Time plus a few seconds....
void _markTheTime() {
setState(() {
_markedTime = DateTime.now().add(Duration(seconds: 15));
});
}
// For the purpose of this test we won't use the intl package.
String hhmmssFromDateTime(DateTime tm) {
if (tm == null) {
return null;
}
return '${zeroPadded(tm.hour)}:${zeroPadded(tm.minute)}:${zeroPadded(
tm.second)}';
}
String zeroPadded(int number) => number.toString().padLeft(2, '0');
String get markedTime => hhmmssFromDateTime(_markedTime);
String get currentTime => hhmmssFromDateTime(DateTime.now());
Timer rebuildTimer;
#override
void initState() {
rebuildTimer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_markedTime == null
? Text('Mark the time with the FAB...')
: Text(
'Marked Time: $markedTime',
style: Theme
.of(context)
.textTheme
.bodyText1
.copyWith(
color: DateTime.now().isAfter(_markedTime)
? Colors.red
: Colors.blue),
),
Text(
'Current Time: $currentTime',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _markTheTime,
tooltip: 'Set',
child: Icon(Icons.lock_clock),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
I believe that the solution lies in a StatefulBuilder wrapped around the Column, in stead of a stateful Widget. But I don't understand StatefulBuilders even though they look really handy.
Also I wonder whether it would be possible to wrap another one around the FloatingActionButton to say disable the button once it has been tapped until the marked time expires.
I think what I want to achieve ultimately is a way to cause various parts of the widget tree to rebuild independantly of one another. So for example using separate timers, the Fab should rebuild at the time when the timer expires, while another periodic timer should update the "Time display" every second.
Use the timer_builder package. An example:
#override
Widget build(BuildContext context) {
return TimerBuilder.scheduled(
[cutoffTime],
builder: (context) {
...
}
);
}
Related
I am having a hard time understanding how to deal with states in some specific situations with Flutter.
For example, say I need a page where the click of a button fetches data from an API. Such a request could take time or any kind of problems could happen. For this reason, I would probably use the BLoC pattern to properly inform the user while the request goes through various "states" such as loading, done, failed and so on.
Now, say I have a page that uses a Timer to periodically (every 1sec) update a Text Widget with the new elapsed time value of a Stopwatch. A timer needs to be properly stopped (timer.cancel()) once it is not used anymore. For this reason, I would use a Stateful Widget and stop the timer directly in the dispose state.
However, what should one do when they have a page which both :
Fetches data from API and/or other services which require correctly handling states.
Uses a Timer or anything (streams ?) that requires proper canceling/closing/disposing of...
Currently, I have a page which makes API calls and also holds such a Timer. The page is dealt with the BLoC pattern and the presence of the Timer already makes the whole thing a little tedious. Indeed, creating a BLoC "just" to update a Timer feels a little bit like overkill.
But now, I am facing a problem. If the user doesn't go through the page the "regular" way and decides to use the "back button" of his phone: I never get to cancel the Timer. This, in turn, will throw the following error: Unhandled Exception: Looking up a deactivated widget's ancestor is unsafe.
Indeed, even after having pop() to the previous page, the Timer is still running and trying to complete its every 1sec duty :
Timer.periodic(
const Duration(seconds: 1),
(Timer t) {
context.read<TimerBloc>().add(const Update());
},
);
}
Could someone please explain to me how such "specific" situations should be handled? There must be something, a tiny little concept that I am not totally understand, and I can feel it is slowing me down sometimes.
Thank you very much in advance for any help.
First off, this is opinionated.
Even though you've described a lot, it is a bit tricky to follow your cases and how you've (specifically) implemented it. But I'll give it a shot at describing things to consider.
There are several ways to handle this. I'll try to answer your questions.
There is nothing wrong with always or only having Stateless widgets and Blocs.
There is nothing wrong with combining Stateful widgets and Blocs.
Consider the case with a page with both a bloc and e.g. a Timer updating a particular text field on that page. Why should one widget handle both? It sounds like the page could be stateless (using the bloc), that has the text field in it, but that text field could/should perhaps be a separate StatefulWidget that only hold the timer, or equivalent. Meaning that sometimes people put to much responsibility in one huge widget, when it in fact should be split into several smaller ones.
I don't understand why you would face that error, it is no problem having both a bloc and a timer in a stateful widget, with poping and using backbutton with proper disposal and timer being reset. See the full code example below.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const FirstPage(),
);
}
}
class FirstPage extends StatelessWidget {
const FirstPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('First page'),
),
body: Center(
child: ElevatedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider<FetcherCubit>(
create: (context) => FetcherCubit(),
child: const SecondPage(),
),
)),
child: const Text('Second page')),
),
);
}
}
class SecondPage extends StatefulWidget {
const SecondPage({super.key});
#override
State<SecondPage> createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
late final Timer myTimer;
int value = 0;
#override
void initState() {
super.initState();
myTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
value++;
});
});
}
#override
void dispose() {
super.dispose();
myTimer.cancel();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Second page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Timer value: ${value.toString()}'),
ElevatedButton(
onPressed: () => context.read<FetcherCubit>().fetch(),
child: const Text('Fetch!'),
),
BlocBuilder<FetcherCubit, FetcherState>(
builder: (context, state) {
late final String text;
if (state is FetcherInitial) {
text = 'Initial';
} else if (state is FetcherLoading) {
text = 'Loading';
} else {
text = 'Completed';
}
return Text(text);
},
)
],
),
),
);
}
}
class FetcherCubit extends Cubit<FetcherState> {
FetcherCubit() : super(FetcherInitial());
Future<void> fetch() async {
emit(FetcherLoading());
await Future.delayed(const Duration(seconds: 3));
emit(FetcherCompleted());
}
}
#immutable
abstract class FetcherState {}
class FetcherInitial extends FetcherState {}
class FetcherLoading extends FetcherState {}
class FetcherCompleted extends FetcherState {}
The result if you build it:
I'm trying to update a child widget's orientation with Rotated Box, based on my device orientation.
However, I have a constraint: I don't want my main UI app to rotate. I want to keep it always locked (like "portrait up"), but I still want to receive device orientation updates.
This is the partial solution I've found, using native_device_orientation package and SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).
The problem is: it's super sensible to changes, so it is not suitable in a user experience perspective (as pointed by native_device_orientation author here). Also, there is a 45 degree position that the sensor orientation goes frenzy, since the sensor is in the sweet spot between two orientations.
Is there another way or perspective to solve this problem?
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:native_device_orientation/native_device_orientation.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
visualDensity: VisualDensity.adaptivePlatformDensity,
),
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> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
int get_turns_based_on_orientation(NativeDeviceOrientation orientation) {
switch(orientation) {
_counter++;
case NativeDeviceOrientation.portraitDown:
return 2;
case NativeDeviceOrientation.portraitUp:
return 0;
case NativeDeviceOrientation.landscapeLeft:
return 1;
case NativeDeviceOrientation.landscapeRight:
return 3;
case NativeDeviceOrientation.unknown:
return 0;
}
}
#override
void initState() {
super.initState();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: NativeDeviceOrientationReader(
useSensor: true,
builder: (context) {
final orientation = NativeDeviceOrientationReader.orientation(context);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
RotatedBox(
quarterTurns: get_turns_based_on_orientation(orientation),
child: Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
),
],
),
);
}
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
In order to have your orientation changed without any oscillation, you can try to reduce the sensibility with this as a workaround:
Every time you receive a device orientation change, store the orientation received and the moment in time this change happened.
Check after a specified amount of time if the orientation changed, if it has not changed in a while, re-render the screen based on this new stable orientation.
Always render the screen based on the last stable orientation.
Adapting to your code it would be as follows:
[...]
//State Class properties
DateTime timeOfLastChange = DateTime.now();
NativeDeviceOrientation stableOrientation = NativeDeviceOrientation.portraitUp;
NativeDeviceOrientation currentOrientation = NativeDeviceOrientation.portraitUp;
[...]
builder: (context) {
currentOrientation = NativeDeviceOrientationReader.orientation(context);
timeOfLastChange = DateTime.now();
Future.delayed(Duration(milliseconds: 500), () {
if (DateTime
.now()
.difference(timeOfLastChange)
.inMilliseconds > 500) {
setState((){stableOrientation = currentOrientation;});
}
});
[...]
quarterTurns: get_turns_based_on_orientation(stableOrientation),
[...]
}
Keep in mind that the 500 value is a constant that can be adjusted to increase or decrease the delay in orientation changes.
I think you are not calling the setPreferredOrientations method in the correct place. You need to call it in your initState() and dispose() methods depend on your requirements. Widget build() method is the reason for the super sensible.
I don't get any errors upon running, I just get a blank page. I guess it has to do with the layout arrangement, but that's just a guess.
I switched the class to statful in the scaffold body, so I can use setState(){} under the flatButton. I don't know why nothing is showing on the screen though, because I would assume at least the AppBar would be on the screen. Can the body extend over the AppBar?
import 'package:flutter/material.dart';
import 'dart:math';
int colorNumber = 1;
void main()=> runApp(StructureBuild());
class StructureBuild extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text('Random Color Generator',
style: TextStyle(color: Colors.white)),
backgroundColor: Colors.blue,
),
body: BodyBuild()
));
}
}
class BodyBuild extends StatefulWidget {
#override
_BodyBuildState createState() => _BodyBuildState();
}
class _BodyBuildState extends State<BodyBuild> {
int changeColor(int colorNumber) {
return colorNumber = Random().nextInt(245) + 1;
}
#override
Widget build(BuildContext context) {
return Expanded(child:
FlatButton(color: Color(colorNumber),
onPressed: () {
setState(() {
changeColor(1);
});
}, child: Text('press anywhere'),));
}
}
First of all, I recommend checking the documentation of Flutter for using color const. Color const has color from the lower 32 bits of an int. 1-255 values are very rare values for Color. I suggest you find a hexadecimal value by examining the link I gave. https://api.flutter.dev/flutter/dart-ui/Color-class.html .
Also, you can give an integer value. But it should be a huge number to change the color of the Flatbutton. For Example, 4123423412341242342 would change to color kind of turquoise blue.
I'm making a flutter app. I have a homepage widget which shows two things
- device code;
temperature
At first, these are set to some default values, but the user then goes from the home page to a new route, call this widget the sensor widget. In this new page, it basically connects to a sensor. This sensor sends out the device code and temperature, and I am able to show it inthe sensor widget.
The problem comes when i want to show this new information onthe homepage. I made a button where the user can go back to the homepage, but I want a way to update the homepage with the values I have in my sensor widget.
Im making use of the InheritedWidget class to make this happen, but I keep getting a null error when I try to access the variables in the homepage.
Below is the inherited widget class for this.
class TemperatureContext extends InheritedWidget {
final int _deviceCode;
final double _temperature;
int get deviceCode => _deviceCode;
double get temperature => _temperature;
set deviceCode(int d) {_deviceCode = d;}
set temperature(double t) {_temperature = t}
TemperatureContext(this.deviceCode, this.temperature, {Key key, Widget child})
.super(key: key, child:child)
#override
bool updateShouldNotify(Widget oldWidget) {
return (temperature != oldWidget.temperature && deviceCode != oldWidget.deviceCode) }
static TemperatureContext of(BuildContext context) {
return context.inheritFromWidgetOfExactType(TemperatureContext) }
}
I then have the homepage, new_widget is a function that builds a widget based on the
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
static int deviceCode = 0;
static double deviceCode = get_temp_globally();
#override
Widget build(BuildContext context) {
final tempContext = TemperatureContext.of(context);
Widget output = new MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: new Text('Homepage'),
),
body: new Column(
children: <Widget>[
new_widget(tempContext.deviceCode, tempContext.temperature),
new FlatButton(
child: new Text('Set new sensor'),
onPressed: () {
Navigator.of(context).pushNamed('/ChangePage');
})
],
)));
return output;
}
Next is the change page widget where the user is taken to when they press the button in the home page
class SensorWidget extends StatefulWidget {
SensorWidget({Key key, this.title}) : super(key: key);
final String title;
#override
_SensorWidgetState createState() => new _SensorWidgetState();
}
class _SensorWidgetState extends State<SensorWidget> {
static int deviceCode = 0;
static double temperature = get_temp_globally();
/* Some code that gets the deviceCode,
temperature and sets them to the above
variables */
#override
Widget build(BuildContext context) {
output = TemperatureContext(
deviceCode,
temperature,
child: MaterialApp(
home: new Scaffold(
appBar: new AppBar(
title: const Text('Sensor widget'),
actions: _buildActionButtons(),
),
floatingActionButton: _buildScanningButton(),
body: new Container(
child: new FlatButton(
child: new Text("Go back"),
onPressed: () {
Navigator.of(context).pop(true);
}
),
),
),
),
);
return output;
}
}
And this is my main.dart file
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: 'Temperature detector',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new HomePage(),
routes: <String, WidgetBuilder> {
'/HomePage' : (BuildContext context) => new HomePage(),
'/SensorWidget': (BuildContext context) => new SensorWidget(),
},
);
}
}
Basically when I put the new_widget function in my HomePage class (which I didnt put here, but basically builds a widget based on the two arguements provided), I get a "NoSuchMethodError": the getter deviceCode was called on null.
I dont get why this is null since I already initialized it. Any help? Thanks
See https://flutter.io/flutter-for-android/#what-is-the-equivalent-of-startactivityforresult :
The Navigator class handles routing in Flutter and is used to get a result back from a route that you have pushed on the stack. This is done by awaiting on the Future returned by push().
For example, to start a location route that lets the user select their location, you could do the following:
Map coordinates = await Navigator.of(context).pushNamed('/location');
And then, inside your location route, once the user has selected their location you can pop the stack with the result:
Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});
There are two ways to change what a user sees on display: I can push to another page or I can change the state of my stateful widget and rebuild it. Can you tell me, which way is best practice? (And if it depends - what I guess - on what?)
Pushing:
class Pushing extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () => Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondPage())),)
),
);
}
}
Using States
class UsingStates extends StatefulWidget {
#override
State createState() => new _UsingStatesState();
}
class _UsingStatesState extends State<UsingStates> {
bool isPageTwo;
#override
void initState() {
isPageTwo = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: isPageTwo ? Center(child: Text('Page two')) : Center(child: RaisedButton(onPressed: () {
setState(() {
isPageTwo = true;
});
})),
);
}
}
Of course the answer is: It depends.
When to use Navigator:
Routes pushed with the navigator are in a way equivalent to...
Activity and Fragment in Android
A route in Angular or React
A html file of a classic web page
You would use the Navigator to switch between logically separate parts of your app. Think of a StackOverflow app that comes with different pages, e.g. "Question List", "Question Detail", "Ask Question" form and "User Profile".
Navigator takes care of back navigation (hardware back button on android phones + back arrow in AppBar)
Note that a route does not have to overlay the full screen. showDialog also uses Navigator.push() internally (that's why you use Navigator.pop() to dismiss a dialog.
Similar to startActivityForResult on Android, a route can also return a result when pop is called. Think of a dialog that lets you pick a date.
When to use State:
Use State when the screens are a logical unit, e.g.:
When you load a list of items from a server, you would have 4 different states:
Loading
"An error occured..."
Placeholder displayed when the list is empty
The ListView
A form with multiple steps
A screen with multiple tabs (in this case the navigations is handled by the tab bar)
A "Please wait" overlay that blocks the screen while a POST request is sent to the server
After all Navigator is also a StatefulWidget that keeps track of the route history. Stateful widgets are a fundamental building block of Flutter. When your app is really complex and Navigator does not fit your needs, you can always create your own StatefulWidget for full control.
It always helps to look at Flutter's source code (CTRL + B in Android Studio).
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new Page1(title: 'Flutter Demo Home Page'),
);
}
}
class Page1 extends StatefulWidget {
Page1({Key key, this.title}) : super(key: key);
final String title;
#override
Page1State createState() => new Page1State();
}
class Page1State extends State<Page1> {
StreamController<int> streamController = new StreamController<int>();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new RaisedButton(child: new Text("This is Page 1, Press here to go to page 2"),onPressed:()=>streamController.add(2) ,),
),
);
}
#override
void initState() {
streamController.stream.listen((intValue){
print("Page 1 stream : "+intValue.toString());
if(intValue==2){
Navigator.of(context).push(new MaterialPageRoute(builder: (context)=>Page2(title: "Page 2",)));
}
});
super.initState();
}
#override
void dispose() {
streamController.close();
super.dispose();
}
}
class Page2 extends StatefulWidget {
Page2({Key key, this.title}) : super(key: key);
final String title;
#override
Page2State createState() => new Page2State();
}
class Page2State extends State<Page2> {
StreamController<int> streamController = new StreamController<int>();
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new RaisedButton(child: new Text("This is Page 2, Press here to go to page 2"),onPressed:()=> streamController.add(1),),
),
);
}
#override
void initState() {
streamController.stream.listen((intValue){
print("Page 2 stream : "+intValue.toString());
if(intValue==1){
Navigator.of(context).push(new MaterialPageRoute(builder: (context)=>Page1(title: "Page 1",)));
}
});
super.initState();
}
#override
void dispose() {
streamController.close();
super.dispose();
}
}
Sorry for bad formatting. You can run this code without any dependencies. Hope it helped