Hello guys, welcome to my new article on Flutter

Flutter allows you to build your application for different target platforms, but since different devices have different screen sizes, it is important to build responsive and adaptive designs for a consistent look of your application.

The entire code discussed can be found in the repository given below.

Here is a little preview of what we will be building today

Mobile View
Web View

1) What is a responsive design?

A responsive app is basically one that renders the same design for different device sizes on the same operating system. For instance, even in Android, there are different devices with different screens. Thus, the app should look the same on large and small devices alike.

To build a responsive app, the following widgets are recommended to be used.

Aspect RatioA widget that attempts to size the child to a specific aspect ratio.
CustomSingleChildLayoutA widget that defers the layout of its single child to a delegate.
CustomMultiChildLayoutA widget that uses a delegate to size and position multiple children.
FittedBoxScales and positions its child within itself according to fit.
FractionallySizedBoxA widget that sizes its child to a fraction of the total available space. For more details about the layout algorithm, see RenderFractionallySizedOverflowBox.
LayoutBuilderBuilds a widget tree that can depend on the parent widget’s size.
MediaQueryEstablishes a subtree in which media queries resolve to the given data.
MediaQueryDataInformation about a piece of media (e.g., a window).
OrientationBuilderBuilds a widget tree that can depend on the parent widget’s orientation (distinct from the device orientation).

For our app, we shall discuss the use of some of these widgets in the section below.

2) Building a responsive app

Before building a responsive app, you should predetermine and target the different device sizes that you want to work on. If you are primarily aiming for mobile and desktop views, then you should target those specific size dimensions to work with.

Thus in a class like Dimens in our code base, you can create one for yourself where you can have the sizes and dimensions you want. It is a good practice to do so.

class Dimens{
  static const maxWidth = 800;
  static const size500 = 'w500/';
  static const size400 = 'w400/';
  static const size300 = 'w300/';
}

We have a static const maxWidth variable which has a value of 800. It means that if the device’s width is less than maxWidth, then most probably it is an android or a tablet device.

Now, let’s create a stateless widget ResponsiveScreen.

class ResponsiveScreen extends StatelessWidget {
  Widget mobileBody;
  Widget desktopBody;
  ResponsiveScreen({required this.mobileBody, required this.desktopBody, Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
        builder: (context, constraints){
          if(constraints.maxWidth < Dimens.maxWidth){
            return mobileBody;
          }
          else{
            return desktopBody;
          }
        }
    );
  }
}

Here, we return a LayoutBuilder. It gives us access to constraints of the device sizes and different configurations.

The ResponsiveScreen accepts 2 widgets in its constructors. The mobileBody and the desktopBody. We check for the maxWidth using the constraints provided to us by the layout builder. If the width is less than maxWidth, we return mobileBody and if it is greater than maxWidth, we return a desktopBody.

Looking further into our widgets.

home: ResponsiveScreen(
     mobileBody: const MobileScreen(),
     desktopBody: const DesktopView()
)

DesktopView( ) looks like this:

Row(
    children: [
      Container(
         alignment: Alignment.center,
           width: 250,
              child: ListView(
                children: [
                  ListTile(
                    title: Text(
                     "Movies",
                     style: screenProvider.screen == 1? 
                     const TextStyle(color: Colors.brown, fontSize: 20) : 
                     const TextStyle(color: Colors.black),
                     ),
                     onTap: (){
                       screenProvider.movieScreen();
                     },
                    ),
                    ListTile(
                      title: Text(
                       "Shows",
                       style: screenProvider.screen == 2? 
                       const TextStyle(
                             color: Colors.brown,
                             fontSize: 20) 
                        : const TextStyle(color: Colors.black),),
                        onTap: (){
                          screenProvider.showScreen();
                        },
                      ),
                    ],
                  ),
                ),
            Expanded(
                child: screenProvider.screen == 1 ?
                const Movies() : screenProvider.screen == 2 ?
                const Shows() : const Center(child: 
                CircularProgressIndicator(),)
            )
          ],
        ),

In our DeskTopView( ), we use a container of a constant size of 250. We want the rest of the screen to be occupied by our components, thus we use an Expanded widget which makes the Movies( ), or the Shows( ) widget occupy the remaining space.

The GridView used in Movies( ) or Shows( ) widgets, also uses MediaQuery to automatically pad the limits of the grid’s scrollable to avoid partial obstruction.

We can also use the MediaQuery widget to access the device’s configurations. Which will be discussed further in the article below.

3) What is an adaptive design?

An Adaptive app not only looks consistent on different devices but also renders similar functionality on different devices with different operating systems.

To look into our application, we render the movie data on pages. For this, we have implemented pagination.

Now, pagination is implemented differently in mobile devices and desktop apps. Thus, we adapted our app to render this functionality in the section below.

4) Building an adaptive app

Remember how we can use MediaQuery to access the device’s configurations, we do the same below.

For a mobile device, we use a RefreshIndicator to load new pages and for a desktop, we use a NumberPaginator to implement pagination.

Let us look into Movies( ) widget

if(MediaQuery.of(context).size.width > Dimens.maxWidth){
    return Column(
       children: <Widget>[
           Expanded(
               child: GridView.builder(
               gridDelegate: const 
                    SliverGridDelegateWithMaxCrossAxisExtent(
                      maxCrossAxisExtent: 300,
                      mainAxisSpacing: 10,
                      crossAxisSpacing: 10,
                      mainAxisExtent: 300),
                 itemBuilder: (ctx, index){
                    return Image.network(
                      returnImageSource(
                        Dimens.size400,
                        movies[index].posterPath),
                        fit: BoxFit.cover,
                          );
                        },
                        itemCount: movies.length,
                      ),
                    ),
           Container(
              color: Colors.black26,
              child: NumberPaginator(
                numberPages: 10,
                onPageChange: (int index){
                    setState(() {
                       page = index+1;
                    });
              },
           ),
        )
      ]
    );
 }

Here, we return our data in a GirdView, where we return an Image with a bigger size if the size of the width is greater than the maxWidth. Thus, NumberPagination is used to paginate data in desktop view.

Similarly, for a mobile view

else{
    return RefreshIndicator(
        onRefresh: loadMore,
        child: GridView.builder(
            gridDelegate: const 
            SliverGridDelegateWithMaxCrossAxisExtent(
                    maxCrossAxisExtent: 300,
                    mainAxisSpacing: 10,
                    crossAxisSpacing: 10,
                    mainAxisExtent: 300),
            itemBuilder: (ctx, index){
               return Image.network(
                  returnImageSource(
                       Dimens.size300,
                       movies[index].posterPath),
                  fit: BoxFit.scaleDown,
               );
            },
           itemCount: movies.length,
           )
        );
     }

We return a GridView wrapped in a RefreshIndicator to implement pagination using pull to refresh in mobile view.

Thus, pagination is achieved by implementing different approaches for different device sizes and configuration constraints.

Here is a little working preview of what we have built.

Mobile view.

5) Project code and resources

  1. Responsive and Adaptive apps – Flutter
  2. Design cookbook – Flutter

Prithviraj Kapil


Hi! I’m Prithviraj Kapil, a passionate coder, app developer, and technical creative writer. I work with Android and Flutter. I like to develop mobile apps and create art. I am currently an undergraduate student and I have started contributing to Developers Breach with a motive to share my knowledge with others.

Here We Go Again : (

if (article == helpful) {
    println("Like and subscribe to blog newsletter.")
} else {
    println("Let me know what i should blog on.")
}

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.