Flutter: Managing Orientation through Navigation changes

loading
[object Object]

There are lots of different tools you can use when you need a mobile app.  You can, of course, write a different version of the app for each platform – this gives you the greatest power and ability to leverage each mobile operating system, but at a cost of having to create at least two versions of your app.  There are also options to “Write once, run anywhere”, but those also have limitations – you may have to compromise some goals, but you can also move quickly and with a lower cost.

It was (and is) a very simple app – Open with a screen that allows you to select any number of images from the device, then go to a second screen that would let you advance between images.

To solve the first problem, I pulled in a library called file_picker. After wrangling with differing expectations between the default Flutter settings for the SDK level in Android from a “flutter create”, and what file_picker needs (hint: change compileSDKVersion to 29), the first step was largely done. Note that this could be enhanced with BLOC or any actual state management – no one would want an app that is doing asynchronous loading out in the wild without some busy indicators, and maybe I’ll add that in bit, but for now, my main window looked like this:

import ‘dart:io’; import ‘package:file_picker/file_picker.dart’; import ‘package:flutter/material.dart’; import ‘package:flutter/widgets.dart’;

class FileSelectPage extends StatefulWidget { @override State createState() => _FileSelectPageState(); }

class _FileSelectPageState extends State { List selectedFiles;

_addFiles() async { selectedFiles = await FilePicker.getMultiFile( type: FileType.image, ); if (selectedFiles.length > 0) { Navigator.push( context, MaterialPageRoute(builder: (context) => ShowImagePage(selectedFiles)) ); } }

@override Widget build(BuildContext context) { SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); return Scaffold( appBar: AppBar( title: Text(“Image Review”), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ RaisedButton( child: Text(‘Please select images’), onPressed: _addFiles ), ], ), ), ); } }

And, of course, adding an image that goes full screen is easy with a BoxDecoration:

import ‘dart:io’;

import ‘package:flutter/widgets.dart’;

class ShowImagePage extends StatefulWidget {

  final List<File> images;

  ShowImagePage(this.images);

  @override

  State<ShowImagePage> createState() => _ShowImagePageState();

}

class _ShowImagePageState extends State<ShowImagePage> {

  int currentImage = 0;

  void _advanceImage(int count) {

    if (count == 0) return;

    setState(() {

      currentImage += count;

      if (currentImage < 0) {

        currentImage = widget.images.length – 1;

      }

      else if (currentImage >= widget.images.length) {

        currentImage = 0;

      }

    });

  }

  @override

  Widget build(BuildContext context) {

    return Stack(

      children: <Widget>[

        ShowCurrentImage(widget.images[currentImage]),

      ]

    );

  }

}

 

class ShowCurrentImage extends StatelessWidget {

  final File image;

  ShowCurrentImage(this.image);

 

  @override

  Widget build(BuildContext context) {

    return Container(

      decoration: BoxDecoration(

        image: DecorationImage(

          image: FileImage(image),

          fit: BoxFit.cover,

        )

      )

    );

  }

}

 

To add the ability to change images, all that was needed was to add a couple Positioned GestureDetectors to the Stack, and a MediaDetector to calculate the sizes.  So the new build function looked like this:

  @override

  Widget build(BuildContext context) {

    MediaQueryData mediaData = MediaQuery.of(context);

 

    return Stack(

      children: <Widget>[

        ShowCurrentImage(widget.images[currentImage]),

        Positioned(

          left: 0,

          top: 0,

          width: (mediaData.size.width / 2) – 1,

          height: mediaData.size.height,

          child: GestureDetector(

            onTap: () => _advanceImage(-1),

          )

        ),

        Positioned(

          left: mediaData.size.width / 2,

          top: 0,

          width: (mediaData.size.width / 2) – 1,

          height: mediaData.size.height,

          child: GestureDetector(

            onTap: () => _advanceImage(1),

          )

        ),

      ]

    );

  }

}

So far, so easy, and hey – it’s all done, right?  Well, as it turns out, some of the sets of images that I want to compare are in Landscape mode, but I tend to keep my phone in Portrait mode, so there was an obvious problem:  Once we get the images, we need to force a screen orientation change to match.

This requires importing another flutter library:

import ‘package:flutter/services.dart’;

And then calling setPreferredOrientations.  Except, those File objects we got haven’t been loaded yet, so we also had to do a quick load of the first image to find out what its dimensions are, making the _addFiles function look like this:

text  _addFiles() async {

    selectedFiles = await FilePicker.getMultiFile(

      type: FileType.image,

    );

    if (selectedFiles.length > 0) {

      var fileImage = decodeImage(selectedFiles[0].readAsBytesSync());// Image.file(widget.images[currentImage]);

      var orientations = fileImage.width > fileImage.height ?

        [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight] :

        [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown];

      SystemChrome.setPreferredOrientations(orientations);

 

      Navigator.push(

        context,

        MaterialPageRoute(builder: (context) => ShowImagePage(selectedFiles))

      );

    }

  }

Now it’s perfect, right?  Again, not quite.  This sets Landscape mode properly for the images, but it leaves the app in that orientation when you come back.  I like my main screen in Portrait mode, and wanted it to always reset.

That requires a slightly fancier change, since we now need to know when we come back from the ShowImagePage. To do this, we need to go back to our main, and add a RouteObserver.  Now, technically, we could add this to file_select.dart with no concern, but I tend to hope that my projects will grow over time, and I’ve found that RouteObservers are needed in the most interesting places, so keep the definition in some place obvious. 

Here’s the new line in main:

final RouteObserver<PageRoute> routeObserver = new RouteObserver<PageRoute>();

Now we add RouteAware to our FileSelectPage class:

class _FileSelectPageState extends State<FileSelectPage> with RouteAware {

Next, we wire in the route observer, adding a call to setState to force the widget to rebuild:

  @override

  void didChangeDependencies() {

    super.didChangeDependencies();

    routeObserver.subscribe(this, ModalRoute.of(context));

  }

 

  @override

  void dispose() {

    routeObserver.unsubscribe(this);

    super.dispose();

  }

 

  @override

  void didPopNext() {

    setState(() {});

  }

And finally, we add a setPreferredOrientation as the first line in our build function:

    SystemChrome.setPreferredOrientations(

      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]

    );

And, at last, my simple little project behaves the way I want it to.

Summary:

Flutter gives you excellent control over most aspects of mobile app development (yes, there are a few glaring holes at present <cough>camera<cough>), but for most apps, it is a very quick way to multi-platform support, while providing identical experiences in all platforms, and excellent performance.  Hope you enjoy whatever your current language is, and always be learning new ones!

Full source code is available at https://github.com/bryantgtx/ImageReview

grey dotted shape