33 How to Adapt to Different Mobile Screen Resolutions

33 How to Adapt to Different Mobile Screen Resolutions #

Hello, I am Chen Hang.

In the previous article, I shared with you the basic principles of implementing internationalization in Flutter. While native Android and iOS only require providing different directories for internationalization resources, which can adapt to different languages and regions automatically at runtime, Flutter’s internationalization is implemented entirely in code.

That is, through code declaration, all texts that need to be translated in the application are declared as properties of LocalizationsDelegate, and then manually translated and adapted for different languages and regions. Finally, when initializing the application, this delegate is set as the translation callback for internationalization. To simplify this process and separate internationalization resources from code implementation, we usually use arb files to store the mapping relationships for different languages and regions, and use the Flutter i18n plugin to generate code automatically.

It can be said that internationalization provides a unified and standardized experience for users worldwide. Therefore, providing a unified and standardized experience for different sizes and orientations of mobile phones is the problem that screen adaptation needs to solve.

In the world of mobile applications, pages are composed of widgets. If the devices we support are only regular smartphones, we can ensure that the same page and widget have a relatively consistent display effect on different phone screens. However, with the increasing popularity of tablet computers and large-screen smartphones like phablets, many applications that originally only ran on regular smartphones are gradually running on tablets.

But, due to the large size of tablet screens, when displaying interfaces and widgets adapted from regular smartphones, UI abnormalities may occur. For example, for news apps, there are usually two pages: news list and news details. If we simply put these two pages on a tablet, there will be abnormal experiences such as stretched widgets, small and crowded text, insufficient image clarity, and wasted screen space.

On the other hand, even for the same phone or tablet, the screen’s width and height configuration are not fixed. Because of the existence of the accelerometer, when we rotate the screen, the width and height configuration will reverse, meaning the layout behavior in the vertical direction and the horizontal direction will exchange, leading to UI abnormalities such as stretched widgets.

Therefore, in order to provide the best experience for users on different screen width and height configurations, we not only need to adapt the layout for tablets and make full use of the extra available screen space, but also need to rearrange widgets when the screen orientation changes. In other words, we need to optimize the interface layout of the application, provide new features and display new content to replace stretched and distorted interfaces and widgets with more natural layouts, and merge single views into compound views.

In native Android or iOS, we usually prepare multiple layout files to implement different layouts on the same page by determining which set of layouts to use based on the current screen resolution. In Flutter, the principle of screen adaptation is very similar, except that Flutter does not have the concept of layout files, so we need to prepare multiple layouts to achieve this.

So today, let’s take a look at how to adapt to screen rotation and tablets using multiple layouts respectively.

Adaptation to Screen Rotation #

When the screen orientation changes, the screen width and height configuration also invert: from portrait mode to landscape mode, the original width becomes the height (vertical layout space is shortened), and the height becomes the width (horizontal layout space is prolonged).

In most cases, we don’t need to worry about the issue of shortened vertical layout space, as ScrollView and ListView can handle it. If a few control elements are not fully displayed on one screen, the user can still use the same interactive scroll view as in portrait mode to view the rest of the elements. However, when it comes to horizontal layout space being prolonged, the interface and controls are usually severely stretched, and significant adjustments are needed to the original layout and interaction.

The same principle applies when switching from landscape mode back to portrait mode.

To adapt to both portrait and landscape modes, we need to prepare two layout schemes: one for vertical orientation and one for horizontal orientation. When the device orientation changes, Flutter notifies us to rebuild the layout. Flutter provides an OrientationBuilder widget, which can inform us of the current orientation through the builder function callback when the device’s orientation changes. With this, we can identify whether the device is in landscape or portrait mode based on the orientation parameter provided by the callback function and refresh the interface accordingly.

The following code demonstrates the specific usage of OrientationBuilder. In its builder callback function, we accurately identify the device orientation and load different layout methods for landscape and portrait models. _buildVerticalLayout and _buildHorizontalLayout are methods used to create the respective layouts:

@override
Widget build(BuildContext context) {
  return Scaffold(
    // Use the builder mode of OrientationBuilder to detect screen rotation
    body: OrientationBuilder(
      builder: (context, orientation) {
        // Return different layout behaviors based on the screen rotation direction
        return orientation == Orientation.portrait
            ? _buildVerticalLayout()
            : _buildHorizontalLayout();
      },
    ),
  );
}

OrientationBuilder provides the orientation parameter to identify the device orientation. If we want to set some component initialization behaviors based on the device’s rotation direction outside of OrientationBuilder, we can use the orientation method provided by MediaQueryData:

if (MediaQuery.of(context).orientation == Orientation.portrait) {
  // do something
}

Please note that by default, Flutter supports both portrait and landscape modes. If our application does not need to provide landscape mode, we can also use the setPreferredOrientations method provided by SystemChrome to inform Flutter of this, so that Flutter can fix the layout orientation of the view:

SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);

Adapting for Tablets #

When adapting to larger screen sizes, we want the content on the app to accommodate the extra space available on the screen. If we use the same layout as on the phone for tablets, we will waste a lot of visual space. Similar to adapting to screen rotation, the most direct approach is to create two different layouts for phones and tablets. However, considering that tablets and phones offer the same functionality to users, this implementation will introduce a lot of unnecessary duplicate code.

To solve this problem, we can adopt another approach: divide the screen space into multiple panes, using the Fragment and ChildViewController concepts, similar to native Android and iOS, to abstract the visual functionality of independent blocks.

The multi-pane layout can achieve better visual balance on tablets and landscape mode, enhancing the app’s usability and readability. And, we can quickly reuse visual functionality on different sizes of phone screens through independent blocks.

The following figure shows how to use the multi-pane layout to transform the news list and news detail interactions on regular phones, landscape phones, and tablets:

Figure 1: Multi-pane layout diagram

First, we need to create two reusable independent blocks for the news list and news details:

  • For the news list, we can use a callback function to inform the parent widget element index when an element is clicked;
  • For the news details, it is used to display the element index clicked in the news list.

For phones, since the space is small, the news list block and news detail block are separate pages, and we can switch between the news detail pages by clicking on the news element. For tablets (and phone landscape layout), since the space is large enough, we place these two blocks on the same page, and we can refresh the news detail of the same page by clicking on the news element.

The implementation of the page and the block is independent of each other, and we can reduce the work of writing two independent layouts by reusing the blocks:

// List Widget
  class ListWidget extends StatefulWidget {
    final ItemSelectedCallback onItemSelected;
    ListWidget(
      this.onItemSelected, // Callback function when a list item is clicked
    );
    @override
    _ListWidgetState createState() => _ListWidgetState();
  }

  class _ListWidgetState extends State<ListWidget> {
    @override
    Widget build(BuildContext context) {
      // Create a list with 20 items
      return ListView.builder(
        itemCount: 20,
        itemBuilder: (context, position) {
          return ListTile(
              title: Text(position.toString()), // Title is index
              onTap: () => widget.onItemSelected(position), // Callback function when clicked
          );
        },
      );
    }
  }

// Detail Widget
  class DetailWidget extends StatefulWidget {
    final int data; // Index of the item clicked in the news list
    DetailWidget(this.data);
    @override
    _DetailWidgetState createState() => _DetailWidgetState();
  }

  class _DetailWidgetState extends State<DetailWidget> {
    @override
    Widget build(BuildContext context) {
      return Container(
        color: Colors.red, // Container background color
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text(widget.data.toString()), // Center to display the index of the item clicked in the list
            ],
          ),
        ),
      );
    }
  }

Then, we only need to check if the device screen has enough width to display both the list and detail sections. To get the screen width, we can use the size method provided by MediaQueryData.

Here, we set the tablet judgment condition to width greater than 480. This way, there will be enough space on the screen to switch to a multi-pane layout:

if(MediaQuery.of(context).size.width > 480) {
  //tablet
} else {
  //phone
}

Finally, if the width is large enough, we will use the Row widget to wrap the list and detail on the same page. Users can click on the list on the left to refresh the detail on the right. If the width is relatively small, we will only display the list. Users can click on the list to navigate to a new page to display the detail:

class _MasterDetailPageState extends State<MasterDetailPage> {
  var selectedValue = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: OrientationBuilder(builder: (context, orientation) {
        // Tablet or landscape phone, embed the list ListWidget and detail DetailWidget on the page
        if (MediaQuery.of(context).size.width > 480) {
          return Row(children: <Widget>[
            Expanded(
              child: ListWidget((value) { // Refresh the right detail page in the list click callback method
                setState(() {selectedValue = value;});
              }),
            ),
            Expanded(child: DetailWidget(selectedValue)),
          ]);

        } else { // Regular phone, embed list ListWidget on the page
          return ListWidget((value) { // Open detail page DetailWidget in the list click callback method
            Navigator.push(context, MaterialPageRoute(
              builder: (context) {
                return Scaffold(
                  body: DetailWidget(value),
                );
              },
            ));

          });
        }
      }),
    );
  }
}

Run the code, and you can see that our application has fully adapted to different sizes and orientations of device screens.

Figure 2: List and detail for portrait phone

Figure 3: List and detail for landscape phone

Figure 4: List and detail for portrait tablet

Figure 5: List and detail for landscape tablet

Summary #

Alright, that’s all for today’s sharing. Let’s summarize today’s key points.

In Flutter, in order to adapt to different device screens, we need to provide different layout methods. By encapsulating independent visual blocks and providing them with different page presentation forms through the orientation callback parameter provided by OrientationBuilder and the screen size provided by MediaQueryData, we can greatly reduce the repetitive work of writing independent layouts. If your application does not need to support device orientation, you can also force portrait orientation through the setPreferredOrientations method provided by SystemChrome.

In order to do well in application development, we not only need to ensure that the product functions normally, but also need to solve the potential problems caused by fragmentation (including device fragmentation, brand fragmentation, system fragmentation, screen fragmentation, etc.) in order to ensure a good user experience.

Unlike other dimensions of fragmentation that may cause functional deficiencies or even crashes, screen fragmentation does not necessarily render the function completely unusable. However, the display size of controls can easily become deformed without proper adaptation, causing users to see abnormally shaped or incomplete UI information, which affects the product image, so it also needs to be given special attention.

In application development, we can run our program on mainstream models and emulators with different screen sizes to observe whether the UI style and functionality are abnormal, thus writing more robust layout code.

I have packaged the knowledge points involved in today’s sharing into GitHub. You can download it, run it multiple times, and deepen your understanding and memory.

Thought question #

Finally, let me leave you with a thought question.

The setPreferredOrientations method takes effect globally. If your application has two adjacent pages, page A supports only portrait orientation, while page B supports both portrait and landscape orientations, how would you implement this?

Feel free to leave a comment in the comment section to share your views. I will be waiting for you in the next article! Thank you for listening and feel free to share this article with more friends to read together.