Aplicativos responsivos Flutter

Tempo de leitura: 6 minutes

Descompactando a capacidade de resposta: Flutter e a arte de criar aplicativos que se encaixam como uma luva em celulares, tablets e na Web! 🚀

Iniciando o projeto

Vamos criar um novo projeto, executando o seguinte comando:

flutter create responsive_ui

 

Estrutura

Com seu projeto criado, vamos organizar a estrutura de pastas do projeto:

- lib
  - app // Crie as pastas a partir de agora.
    - modules
      - mobile
        - mobile_scaffold.dart
      - desktop
        - desktop_scaffold.dart
      - tablet
        - tablet_scaffold.dart
    - core/theme/ // No mesmo nível dos "módulos
      - colors.dart 
    // É aqui que nossos widgets globais serão colocados, se necessário 
    // Além do exemplo, você também pode adicionar widgets aqui
    // Especificamente para cada módulo, você pode ter uma pasta 'widgets' 
    // para cada módulo.
    - widgets 
      - app_bar.dart
      - drawer.dart
      - my_box.dart 
      - my_tile.dart
      - responsive_layout.dart

 

Com tudo organizado, vamos começar a codificar 👨‍💻

Em seu arquivo principal main.dart, exclua o conteúdo inicial padrão e insira este código inicial.

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const ResponsiveLayout(
        mobileScreen: MobileScaffold(),
        tabletScreen: TabletScaffold(),
        desktopScreen: DesktopScaffold(),
      ),
    );
  }

Em um main.dart padrão, não se surpreenda com essas classes e com os erros decorrentes de sua ausência; nós as criaremos uma a uma. Primeiro, vamos entender nosso ResponsiveLayout(), que é responsável por verificar o dispositivo que estamos usando e renderizar o layout correto para cada opção. No nosso caso, essas opções são aplicativotabletdesktop, e vamos nos aprofundar nelas em breve.

ResponsiveLayout

Nosso RespondiveLayout é o componente que usaremos em nosso main.dart; ele implementa um LayoutBuilder. Ele tem as propriedades de que precisamos para distinguir o tipo de dispositivo que estamos usando com base no tamanho fornecido pelas restrições. Podemos conseguir isso usando os atributos fornecidos por nossas restrições:
– maxWidth
– minWidth
– minHeight
– maxHeight

 

Criando nosso ResponsiveLayout

Na pasta global widgets, crie um arquivo chamado responsive_layout.dart. Nele, implementaremos o seguinte código:

import 'package:flutter/material.dart';

class ResponsiveLayout extends StatelessWidget {
  const ResponsiveLayout({
    super.key,
    required this.mobileScreen,
    required this.tabletScreen,
    required this.desktopScreen,
  });
  final Widget mobileScreen, tabletScreen, desktopScreen;
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        if (constraints.maxWidth < 600) {
          return mobileScreen;
        } else if (constraints.minWidth < 1100) {
          return tabletScreen;
        } else {
          return desktopScreen;
        }
      },
    );
  }
}

Aqui podemos ver a implementação do LayoutBuilder, o verdadeiro responsável por distinguir nossos layouts, e ele funciona de forma reativa. Ou seja, à medida que você aumenta ou diminui sua página, ele trará automaticamente a página correta definida pelos valores especificados. Você pode ver que comparamos nossas constraints.maxWidth para verificar a largura do dispositivo e atribuir sua respectiva tela. Assim, podemos definir que uma largura menor que 600 é um celular, menor que 1.100 é um tablet e, além disso, definimos como um desktop.

Primeiro, vamos adicionar os valores em seu arquivo core/theme/colors.dart:

import 'package:flutter/material.dart';

abstract class AppColors {
  static final defaultBackground = Colors.grey[300];
}

Agora, podemos prosseguir com a programação de cada uma de nossas páginas.

Criar página para celular

Em nosso módulo móvel, no arquivo mobile_scaffold.dart, vamos inserir o seguinte código:

import 'package:flutter/material.dart';
import 'package:responsive_ui_web_app/app/core/utils/theme/colors.dart';
import 'package:responsive_ui_web_app/app/widgets/app_bar.dart';
import 'package:responsive_ui_web_app/app/widgets/drawer.dart';
import 'package:responsive_ui_web_app/app/widgets/my_box.dart';
import 'package:responsive_ui_web_app/app/widgets/my_tile.dart';

class MobileScaffold extends StatefulWidget {
  const MobileScaffold({super.key});

  @override
  State<MobileScaffold> createState() => _MobileScaffoldState();
}

class _MobileScaffoldState extends State<MobileScaffold> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const MyAppBar(),
      backgroundColor: AppColors.defaultBackground,
      drawer: const MyDrawer(),
      body: Column(
        children: [
          AspectRatio(
            aspectRatio: 1.0,
            child: SizedBox(
              width: double.infinity,
              child: GridView.builder(
                itemCount: 4,
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 2,
                ),
                itemBuilder: (context, index) => const MyBox(),
              ),
            ),
          ),
          Expanded(
            child: ListView.builder(
              primary: true,
              shrinkWrap: true,
              itemCount: 5,
              itemBuilder: (context, index) => const MyTile(),
            ),
          ),
        ],
      ),
    );
  }
}

Aqui temos uma página simples com uma AppBar, uma Drawer, uma Grid e uma List.
Não se assuste com os componentes que ainda não foram mencionados; nós os criaremos agora.

Criar a AppBar

Vá para o arquivo widgets/app_bar.dart e preencha-o com este código:

import 'package:flutter/material.dart';

class MyAppBar extends StatelessWidget implements PreferredSizeWidget {
  const MyAppBar({super.key});

  @override
  Widget build(BuildContext context) {
    return AppBar(
      leading: IconButton(
        onPressed: () => Scaffold.of(context).openDrawer(),
        icon: const Icon(
          Icons.menu,
          color: Colors.white,
        ),
      ),
      backgroundColor: Colors.grey[900],
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(46.0);
}

Aqui temos uma AppBar simples com o ícone de menu para que possamos abrir nossa Drawer.

 

Criar Drawer

Vá para o arquivo widgets/drawer.dart e preencha-o com este código:

import 'package:flutter/material.dart';

class MyDrawer extends StatelessWidget {
  const MyDrawer({super.key});

  @override
  Widget build(BuildContext context) {
    return Drawer(
      backgroundColor: Colors.grey[300],
      child: const Column(
        children: [
          DrawerHeader(
            child: Icon(
              Icons.favorite,
            ),
          ),
          ListTile(
            leading: Icon(
              Icons.home,
            ),
            title: Text('D A S H B O A R D'),
          ),
          ListTile(
            leading: Icon(
              Icons.chat,
            ),
            title: Text('M E S S A G E'),
          ),
          ListTile(
            leading: Icon(
              Icons.settings,
            ),
            title: Text('S E T T I N GS'),
          ),
          ListTile(
            leading: Icon(
              Icons.logout,
            ),
            title: Text('L O G O U T'),
          ),
        ],
      ),
    );
  }
}

Nosso Drawer é bastante simples, com um cabeçalho e vários itens de ListTile que simulam opções com ícones e texto

Criar MyBox e MyTile

Como esses componentes são apenas contêineres coloridos para fins de exemplo, vou examiná-los rapidamente aqui.
Vá para o arquivo widgets/my_box.dart e widgets/my_tile.dart e preencha-o com este código:

// MyBox

import 'package:flutter/material.dart';

class MyBox extends StatelessWidget {
  const MyBox({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Container(
        color: Colors.blue,
      ),
    );
  }
}

// MyTile

import 'package:flutter/material.dart';

class MyTile extends StatelessWidget {
  const MyTile({super.key});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Container(
        color: Colors.green,
        height: 80.0,
      ),
    );
  }
}

Esses objetos vão além de uma explicação e são praticamente idênticos. Agora que todos os nossos widgets estão prontos, você pode executar seu aplicativo.
Se quiser executar o flutter run em um emulador ou em seu telefone, já que até agora só criamos a tela do celular, as outras telas ficarão em branco.

No entanto, recomendo executar o aplicativo na Web para ver o efeito ao redimensionar a janela do navegador horizontalmente.

Use este comando: flutter run -d chrome ou use o navegador da Web de sua preferência.

Então, vamos criar as outras duas telas, o que será mais fácil, pois aproveitaremos quase tudo o que fizemos até agora.

 

Criar página do tablet

Vá para o módulo tablet e, no arquivo tablet_scaffold.dart, usaremos praticamente o mesmo conteúdo do mobile_scaffold.dart com algumas alterações de proporção. Aqui está o código:

import 'package:flutter/material.dart';
import 'package:responsive_ui_web_app/app/core/utils/theme/colors.dart';
import 'package:responsive_ui_web_app/app/widgets/app_bar.dart';
import 'package:responsive_ui_web_app/app/widgets/drawer.dart';
import 'package:responsive_ui_web_app/app/widgets/my_box.dart';
import 'package:responsive_ui_web_app/app/widgets/my_tile.dart';

class TabletScaffold extends StatefulWidget {
  const TabletScaffold({super.key});

  @override
  State<TabletScaffold> createState() => _TabletScaffoldState();
}

class _TabletScaffoldState extends State<TabletScaffold> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: const MyAppBar(),
      drawer: const MyDrawer(),
      backgroundColor: AppColors.defaultBackground,
      body: Column(
        children: [
          AspectRatio(
            aspectRatio: 4.0,
            child: SizedBox(
              width: double.infinity,
              child: GridView.builder(
                itemCount: 4,
                gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: 4,
                ),
                itemBuilder: (context, index) => const MyBox(),
              ),
            ),
          ),
          Expanded(
            child: ListView.builder(
              primary: true,
              shrinkWrap: true,
              itemCount: 9,
              itemBuilder: (context, index) => const MyTile(),
            ),
          ),
        ],
      ),
    );
  }
}

Observe que aqui temos apenas duas diferenças: nosso aspectRatio antes era 2, agora é 4, e nosso crossAxisCount para nossa grade também mudou de 2 para 4, para acomodar o tamanho maior do tablet.
Bem, como você pode ver, o código é o mesmo; você só precisa ajustar o tamanho, que é diferente para celular, tablet e Web, preenchendo o espaço que ficaria “vazio” se não fosse modificado.

Criar Desktop Page

Aqui faremos algumas alterações no layout, adicionando uma barra lateral extra à direita e mantendo o Drawer aberto.
Para isso, adicionaremos uma Row em nosso corpo, mantendo o Drawer sempre aberto, dividindo a tela horizontalmente em três seções.

import 'package:flutter/material.dart';
import 'package:responsive_ui_web_app/app/core/utils/theme/colors.dart';
import 'package:responsive_ui_web_app/app/widgets/app_bar.dart';
import 'package:responsive_ui_web_app/app/widgets/drawer.dart';
import 'package:responsive_ui_web_app/app/widgets/my_box.dart';
import 'package:responsive_ui_web_app/app/widgets/my_tile.dart';

class DesktopScaffold extends StatefulWidget {
  const DesktopScaffold({super.key});

  @override
  State<DesktopScaffold> createState() => _DesktopScaffoldState();
}

class _DesktopScaffoldState extends State<DesktopScaffold> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: AppColors.defaultBackground,
      appBar: const MyAppBar(),
      body: Row(
        children: [
          //open drawer
          const MyDrawer(),
          Expanded(
            flex: 2,
            child: Column(
              children: [
                AspectRatio(
                  aspectRatio: 4.0,
                  child: SizedBox(
                    width: double.infinity,
                    child: GridView.builder(
                      itemCount: 4,
                      gridDelegate:
                          const SliverGridDelegateWithFixedCrossAxisCount(
                        crossAxisCount: 4,
                      ),
                      itemBuilder: (context, index) => const MyBox(),
                    ),
                  ),
                ),
                Expanded(
                  child: ListView.builder(
                    primary: true,
                    shrinkWrap: true,
                    itemCount: 9,
                    itemBuilder: (context, index) => const MyTile(),
                  ),
                ),
              ],
            ),
          ),
          Expanded(
              child: Column(
            children: [
              Expanded(
                child: Container(
                  color: Colors.pink,
                ),
              ),
            ],
          ))
        ],
      ),
    );
  }
}

Observe que adicionamos widgets Expanded, para que o espaço seja dividido de acordo com o peso (flex) de cada um.

Rodando no navegador

flutter run -d chrome ou seu navegador preferido, você poderá ver esse resultado:

Algumas observações

Tenha cuidado com as importações, especialmente se você também estiver executando o projeto como um aplicativo. Às vezes, eu precisava limpar o projeto depois de executá-lo no navegador e, em seguida, executá-lo como um aplicativo, e vice-versa. Basta usar o flutter clean e executá-lo novamente.