Flutter screen rotation triggers overflow - flutter

Description : the issue I'am facing is that when I force rotate my screen in Flutter it does paint the layout and THEN rotate the screen. So before the rotation flutter show an overflow bar for some of the widgets (especially when I come back from the screen but it happens also when pushing a new one).
Initially I force the screen to be landscape. Then I rotate it for some menu. Once it is done I just come back to portrait using Navigator.pop(context)
Here is a video of the issue where I slowed down the important fragment : Video of the issue
Here is my code sample:
class ScreenRotator extends StatefulWidget {
const ScreenRotator({
Key? key,
required this.child,
}) : super(key: key);
final Widget child;
#override
State<ScreenRotator> createState() => _ScreenRotatorState();
}
class _ScreenRotatorState extends State<ScreenRotator> {
#override
void initState() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
]);
super.initState();
}
#override
dispose() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
super.dispose();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
Some points about my code :
Refering to the screen rotation it does need te be inside a StateFull widget (calling initState() and dispose() where the screen rotation happens).
I made a special widget called ScreenRotator which calls a StateLess widget (the screen layout).
I couldn't find anything linked to my question and this question refers to older ones which are the following :
Flutter: how to prevent device orientation changes and force portrait?
How to force the application to Portrait mode in flutter
Note : There is a problem with my buttons (TextButtons) but this is not the actual issue i'm asking help for.

Related

Flutter dynamic screen rotation

What is the best way to dynamically do screen rotation in Flutter for few of the screens and not affect to other screens?
So i tried two ways, with mixins. (This way stackoverflow ). It works initially, for the first enter and leave screen, but when I do re-run again some of screen which should be landscape again, it's fixed in portrait and thats all, until i re-run the app.
And tried directly, with setting preferred orientation. (This way stackoverflow This way mess-up whole app even if i just unlock rotation on build/initState (tried both) on one screen, and lock it on same screen on dispose.
For example, i need camera screen rotated with whole UI, and i have screen to edit the image, and i need that screen in the portrait, and there problems shows up.
UPDATE: So here is the link with code with mixins try. I created mixin which enables rotation in that screen, and on dispose, lock rotation again. CODE
And code here:
mixin RotationEnabledStatefulModeMixin<T extends StatefulWidget> on
State<T> {
#override
Widget build(BuildContext context) {
_enableRotation();
return null;
}
#override
void dispose() {
_portraitModeOnly();
print("RotationEnabledStatefulModeMixin -> disposed!");
}
}
void _portraitModeOnly() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
}
void _enableRotation() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
//this is screen where i need landscape only
class TakePictureScreen extends StatefulWidget {
final CameraDescription camera;
TakePictureScreen(
{Key key, #required this.camera, this.report, this.sectionIndex})
: super(key: key);
#override
TakePictureScreenState createState() => TakePictureScreenState();
}
class TakePictureScreenState extends State<TakePictureScreen> with
RotationEnabledStatefulModeMixin<TakePictureScreen>{
CameraController _controller;
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
super.build(context);
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
}

Will my whole widget tree rebuild when a keyboard appears?

I am trying to build a responsive mobile app so I found an approach were i would divide the sreen into definite number of grids and get the grid width and height and then use this width and height to size my widgets
Question:
I would definitly get my screen's size from MediaQuery.of(context) but since i will only use it once to do my calculations will my widget tree rebuild (assuming i did this calculation in my root widget) whenever a keyboard appears or not? And if it will rebuild should i do the calculations in a different place?
No, if you didn't place any set state or callback during that rebuild the widget when you open the keyboard. However, the issue can be easily resolved by putting your main widget "below" the Scaffold in a SingleChildScrollView to avoid rendering issues.
If you absolutely need to perform actions when the keyboard appears you can use a FocusNode in the textField and add a listener to it with the addListener method. By passing a function to add Listener, you can trigger a setState every time you need, causing the widget to rebuild with the new parameters.
This is a very simplified version of what I mean:
class MyWidget extends StatefulWidget{
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
FocusNode _focusNode;
int state=0;
#override
Widget build(BuildContext context) {
return Container(
height: state==0?100:200, //change the height depending on "state"
child: TextField(
focusNode: _focusNode,
),
);
}
void onFocus(){
setState(() {
//Check if the focus node is focused
if(_focusNode.hasFocus) state=1; //Change the value of the state
});
}
#override
void initState() {
super.initState();
_focusNode=FocusNode();
_focusNode.addListener(onFocus); //Here on focus will be called
}
#override
void dispose() {
super.dispose();
_focusNode.dispose();
}
}

Flutter - animation widget, trigger animation only once but get data upates on widget

I came across the following problem already twice within my app:
Inside a StatefulWidget I have a widget that contains an animation and additionally displays other dynamic data. The animation is triggered after the animation widget has been built. However, after the animation (the drawing of a figure) has been played, I want to maintain it in the same drawn state. The rest of the dynamic data of the widget should be always rebuilt when the parent StatefulWidget's build method is called.
Below, there is a simple example of what I try to achieve:
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key, #required this.data}) : super(key: key);
CustomData data;
#override
State<StatefulWidget> createState() => _MyState();
}
Now, I had two ideas of how to implement the state:
1) Initialize the animation widget in initState() and "manually" handle data updates within didUpdateWidget().
class _MyState extends State<MyStatefulWidget> {
AnimationWidget _animationWidget;
#override
void initState() {
super.initState();
_animationWidget = AnimationWidget(displayData: widget.data);
// starts the animation when the widget is built (?)
WidgetsBinding.instance
.addPostFrameCallback((_) => _animationWidget.startAnimation());
}
#override
Widget build(BuildContext context) {
return Column(
children: [
_animationWidget,
... // other content
],
... // column size etc.
);
}
#override
void didUpdateWidget(SackenNotebookContent oldWidget) {
super.didUpdateWidget(oldWidget);
if(dataChanged(oldWidget.data, widget.data))
setState(() {
_animationWidget = AnimationWidget(displayData: widget.data);
});
}
}
2) To create the animation widget within the build() method. But then I already have doubts about when/where to trigger the animation to start. One way would be to trigger it in didUpdateWidget(). But again, the animation would then be triggered on every rebuild of the widget...
class _MyState extends State<MyStatefulWidget> {
#override
Widget build(BuildContext context) {
var animationWidget = AnimationWidget(displayData: widget.data);
// but where to call animationWidget.startAnimation() ?!
// when I call it here I get a null pointer exception...
return Column(
children: [
animationWidget,
... // other content
],
... // column size etc.
);
}
}
Has anyone got an idea of how I could handle such a problem in flutter?

Hero widget inits child widget multiple times

Whenever, I have a Hero widget about a StatefulWidget, the State.initState method is called three times instead of once when navigating to that page.
This obviously only happens when the other page also has a Hero with the same tag.
class Page extends StatelessWidget {
const Page({Key key}) : super(key: key);
#override
Widget build(BuildContext context) => Scaffold(body: Hero(tag: 'tag', child: HeroContent()));
}
class HeroContent extends StatefulWidget {
HeroContent({Key key}) : super(key: key);
#override
createState() => _HeroContentState();
}
class _HeroContentState extends State<HeroContent> {
#override
void initState() {
print('_HeroContentState.initState'); // printed three times with `Hero` widget and once without
super.initState();
}
#override
Widget build(BuildContext context) => Container();
}
Whenever I navigate to Page, _HeroContentState.initState is printed three times (and twice when popping a route).
Fully reproducible example of this as a gist on GitHub.
If I change the build method of Page, to look like this (removing the Hero widget):
#override
Widget build(BuildContext context) => Scaffold(body: HeroContent());
Now, _HeroContentState.initState is only called once as it should be.
How do I avoid Hero inserting my widget three times? How can I ensure that initState is only called once or have a different method that is only called once?
There is nothing you can do about that.
The way Hero works is that it moves in different locations in the widget tree in 3 steps:
the original location
inside Overlay, during the Hero transition
in the new page
Usually, for such issue, you'd use a GlobalKey, but that is not compatible with Hero.
As such, it is probably better to refactor your code such that initState doesn't matter.

Setting different orientations for each screen in Flutter app causes massive lag

I am setting a main menu screen with a portrait orientation which then navigates to a second screen which needs to be landscape orientation.
It feels like these two settings are fighting with each other, and as soon as I set the landscape setting the app goes from instantaneous responses to 2 seconds or screen lag to switch screens.
Code snippets:
class MainMenu extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp,]);
That is how I set my initial orientation.
Navigate to the next screen like so from a button push:
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
class SecondScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
SystemChrome.setEnabledSystemUIOverlays([]);
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft,]);
My orientation for the second screen. If I remove the SystemChrome lines it is responsive. If I add them back in, it lags excessively.
Is there a different method I should be using for sub screens?
Thanks guys.
Edit:
Attempted to implement the theory-based solution from Pete below, which throws piles of:
Another exception was thrown: inheritFromWidgetOfExactType(_InheritedTheme) or inheritFromElement() was called after dispose():
errors in the console (but the app doesn't crash, I just see a split second of a crash dump as the screens navigate back and forth). Code used:
class MainMenu extends StatefulWidget {
#override
_MainMenuParentState createState() => _MainMenuParentState();
}
class _MainMenuParentState extends State<MainMenu> {
#override
void initState() {
super.initState();
SystemChrome.setEnabledSystemUIOverlays([]);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp,]);
}
#override
void dispose(){
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
super.dispose();
}
I have tried in the initState to use Future as well, but the errors each time are identical.
Dispose is called in the onPress:
onPressed: () {
dispose();
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondScreen()),
);
And I do exactly the same the other way, I have converted SecondScreen to a statefulwidget, used initstate to set as landscape and called dispose exactly the same way.
Don't put any call of SystemChrome inside build() method. Make your screen extends from StatefulWidget. Trigger the SystemChrome call inside initState() with a Future.