Cross-platform mobile development is a series of trades. You can hand-build a native app for each platform and get full control at the cost of maintaining two codebases forever. Or you go "write once, run anywhere" and accept that some edges will need extra care. Flutter lands firmly in the second camp - one Dart codebase, near-native performance, and identical behavior on iOS and Android. Most of the time that trade pays off. Screen orientation is one of those edges that needs a little extra care.
Here's a real problem: you want a specific screen to be landscape, then you want the app to snap back to portrait when the user leaves that screen. Flutter can do it, but there's a gotcha or two hiding in the navigation flow. Let's walk through it.
The App That Started It
The app is deliberately boring. Screen one lets you pick a bunch of images off the device. Screen two lets you flip through them one at a time with a tap on the left or right side of the screen. It exists because the stock photo viewer animates between images, and sometimes you need an instant redraw to spot the difference between two near-identical screenshots.
Spin up the skeleton with flutter create, gut the sample counter, and you're left with a home screen that's basically one button.
Picking Files
The pick-images button leans on the file_picker package - it hands you a native, platform-appropriate file selection screen with almost no work. Add it to pubspec.yaml, then the handler is short:
Future<void> _addFiles() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
);
if (result == null) return; // user backed out
if (!mounted) return;
await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => ShowImagePage(files: result.files),
),
);
}
The viewer screen is a StatefulWidget because it tracks which image is currently showing. Two GestureDetectors sit over the image - tap left to go back, tap right to go forward:
GestureDetector(
onTapUp: (details) {
final width = MediaQuery.of(context).size.width;
setState(() {
if (details.localPosition.dx < width / 2) {
_index = (_index - 1).clamp(0, widget.files.length - 1);
} else {
_index = (_index + 1).clamp(0, widget.files.length - 1);
}
});
},
child: Image.file(File(widget.files[_index].path!)),
)
So far, so easy. Except some of the images I want to compare are landscape, and I keep my phone locked in portrait. I can't count on the user (me) unlocking rotation and physically turning the phone. The app has to force the orientation itself.
Forcing Orientation
Flutter exposes this through SystemChrome.setPreferredOrientations in the services library. The catch: to know which orientation you need, you first have to look at the image and see whether it's wider than it is tall. The image package's decodeImage reads the raw bytes and gives you the dimensions:
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:image/image.dart' as img;
Future<void> _lockOrientationFor(String path) async {
final decoded = img.decodeImage(File(path).readAsBytesSync());
final landscape = decoded != null && decoded.width > decoded.height;
await SystemChrome.setPreferredOrientations(
landscape
? [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]
: [DeviceOrientation.portraitUp],
);
}
Call that before you push the viewer route and the screen rotates to fit the picture. Perfect, right?
Not quite.
Snapping It Back
Force landscape and you'll notice the app is still landscape when you return to the file picker. There's a reason I lock rotation in the first place - I want it to reset.
The fix is small because you already have a natural hook: Navigator.push returns a Future that completes when the pushed route pops. Just await it and reset orientation on the way back:
await Navigator.push(
context,
MaterialPageRoute(builder: (_) => ShowImagePage(files: result.files)),
);
// We're back on the file picker - restore portrait.
await SystemChrome.setPreferredOrientations(
[DeviceOrientation.portraitUp],
);
For a two-screen app, awaiting the push is the whole solution. If your navigation is deeper - the viewer pushes another route that pushes another - awaiting one push gets fragile fast, because you only care about landing back on this screen no matter what happened in between. That's what RouteObserver is for: register it in MaterialApp.navigatorObservers, mix RouteAware into your State, and reset orientation in didPopNext. More wiring, but it doesn't care how many screens deep the detour went.
Room To Grow
The little project now behaves exactly the way I want. There's obvious room to improve it.
Hardcoding "reset to portrait" assumes portrait is home. Better: capture the launch orientation once and restore that, so the app respects however the device was actually being held.
final _startOrientation = MediaQuery.of(context).orientation;
Then map that Orientation value back to the matching DeviceOrientation list when you reset. My use case compared similarly-oriented images, so I didn't handle mixed sets in one session - but you could, resetting per image as you page through.
One more note for anyone shipping this: on Android, setPreferredOrientations respects the system's auto-rotate setting on some devices, so test on real hardware, not just the simulator. Orientation is one of those areas where the emulator will happily lie to you.
The Takeaway
Flutter gives you genuine control over the parts of mobile development that usually fight you, and orientation is a good example - a handful of lines from the standard services library, plus the navigation flow you already have. Awaiting Navigator.push isn't just how you get a result back from a screen. It's a clean seam to run teardown logic, orientation reset included.
At Inventive we've been shipping mobile products since 2016, and Flutter is one of the tools we reach for when a client needs one codebase across both platforms without giving up the polish. The polish is in the edges - and the edges are where the trades get interesting.