Material Design e Cupertino no Flutter: o que são e como usar

Material Design e Cupertino no Flutter: o que são e como usar

Você já construiu um aplicativo com Flutter? Se sim, me diz: para qual plataforma você desenvolveu seu app?

Talvez você ache essa pergunta confusa e pense: "Como assim? O Flutter não é multiplataforma?"

É verdade! O Flutter é um framework que permite criar aplicativos para diferentes plataformas com uma única base de código.

Mas quando pergunto para qual plataforma você desenvolveu, estou falando da aparência do seu aplicativo.

Neste artigo, vamos explorar como usar o Material e o Cupertino para desenvolver aplicativos que respeitem o padrão visual recomendado para Android e iOS. Vamos ver também como ajustar os widgets para que eles se adaptem automaticamente à plataforma onde o aplicativo estiver rodando.

Para começar, vamos entender o que é; e como surgiu o MaterialApp.

O que é Material App?

É um sistema de design que oferece widgets e elementos visuais com estilos prontos, lançado em 2014 pela Google.

O Material Design disponibiliza para você um catálogo de design com elementos pré-prontos:

  • Componentes (botões, ícones, menus, formulários);
  • Tipografia;
  • Cores;
  • Elevação.

Alguns widgets do Flutter aplicam o Material Design por padrão. Ou seja, com poucas linhas de código, você produz telas com um layout bem bonito.

Veja um exemplo de botões disponibilizados pelo Material Design:

Diversos botões do Material Design.

Esses botões podem ser criados com widgets do Material Design como o ElevatedButton.

Atualmente, esse catálogo está em sua terceira versão, o Material Design 3. Você pode utilizá-lo desde o Flutter 3.16.

É uma mão na roda no desenvolvimento mobile! Você pode focar no código e deixar para lá a preocupação com o design de um projeto.

Banner da Escola de Mobile: Matricula-se na escola de Mobile. Junte-se a uma comunidade de mais de 500 mil estudantes. Na Alura você tem acesso a todos os cursos em uma única assinatura; tem novos lançamentos a cada semana; desafios práticos. Clique e saiba mais!

Por que usar o Material App?

Há algum tempo, boa parte dos aplicativos Android apresentavam um design pouco atrativo. Isso acontecia porque não existia uma "linha de design" oficial, ou seja, cada desenvolvedor criava o app do jeito que preferia.

Se o desenvolvedor tinha suporte de uma equipe de design ou possuía conhecimentos nessa área, o app geralmente ficava bem feito.

Mas, na ausência de suporte, o resultado era, muitas vezes, um app pouco atraente em termos visuais.

Para resolver essa situação, a Google lançou, em 2014, o Material Design, que utiliza um estilo visual mais minimalista e limpo.

Como é natural na tecnologia, o Material Design está em constante atualização. Logo, é bom se ligar nisso!

Como usar o Material Design no Flutter?

Se você criou aplicativos Flutter, talvez tenha usado os widgets Scaffold e a AppBar, que vêm com estilos e comportamentos padrão graças ao Material Design!

Que tal relembrar esses widgets na prática? Para isso, vamos construir uma tela de Login:

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    debugShowCheckedModeBanner: false,
    theme: ThemeData.dark(),
    home: LoginScreen(),
  ));
}

class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'Welcome',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 24),
              CustomizeFormField(placeholder: 'Name'),
              SizedBox(height: 16),
              CustomizeFormField(placeholder: 'Password'),
              SizedBox(height: 24),
            ],
          ),
        ),
      ),
    );
  }
}

class CustomizeFormField extends StatelessWidget {
  const CustomizeFormField({super.key, required this.placeholder});

  final String placeholder;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: TextFormField(
        style: TextStyle(color: Colors.black),
        decoration: InputDecoration(
          border: OutlineInputBorder(),
          fillColor: Colors.white,
          filled: true,
          hintText: placeholder,
          hintStyle: TextStyle(color: Colors.grey),
          contentPadding: EdgeInsets.symmetric(vertical: 15, horizontal: 10),
        ),
      ),
    );
  }
}

Neste código, usamos o MaterialApp como a base do aplicativo, a nossa tela de login ficará com esse visual:

Tela de login com o texto "Welcome", um campo para inserir o nome e um campo para inserir a senha.

A LoginScreen exibe uma mensagem de boas-vindas e dois campos de formulário (nome e senha) por meio de um widget separado, o CustomizeFormField, que facilita a criação de campos de entrada reutilizáveis.

Nossa tela de login está pronta! Mas será que ela tem o design ideal para todas as plataformas?

A resposta é não. Embora o MaterialApp ofereça bons widgets com estilos prontos, ele segue o padrão visual do Android.

E por que isso importa? Se você quiser publicar o seu aplicativo na App Store (iOS), a Apple exige que ele tenha uma aparência familiar para usuários de iOS, o sistema operacional da Apple.

Por exemplo, a fonte de um iPhone é bastante familiar para seus usuários. Um aplicativo com uma fonte característica do Android não cairia bem em um aplicativo iOS (e seria reprovado na App Store).

Então, se a ideia é criar um aplicativo focado em Android, podemos usar o Material Design.

Mas e se quisermos um visual voltado para iOS? Nesse caso, o Cupertino é a melhor escolha!

O que é o Cupertino?

O Cupertino é a linha de design usada para o desenvolvimento de aplicativos no iOS.

Ele surgiu como uma coleção de widgets e padrões visuais que seguem as diretrizes da Apple, criando uma identidade visual própria e familiar para usuários de dispositivos Apple.

Tela de alerta do iOS.

Acima, temos um exemplo de caixa de diálogo feita com um widget Cupertino, o CupertinoAlertDialog.

O Flutter disponibiliza vários outros widgets Cupertino que desenham elementos visuais de acordo com os padrões de design da Apple.

Vamos ver outro exemplo?

Como usar o Cupertino no Flutter?

Podemos usar os widgets cupertino, que são vários. Assim como fizemos com o Material, vamos fazer uma tela de login, desta vez com o Cupertino. Veja o código a seguir:

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(CupertinoApp(
    debugShowCheckedModeBanner: false,
    home: CupertinoLoginScreen(),
  ));
}

class CupertinoLoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const CupertinoPageScaffold(
      backgroundColor: Color.fromRGBO(21, 21, 21, 1),
      child: Center(
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'Welcome',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.white),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 24),
              CupertinoCustomizeFormField(placeholder: 'Name'),
              SizedBox(height: 16),
              CupertinoCustomizeFormField(placeholder: 'Password'),
              SizedBox(height: 24),
            ],
          ),
        ),
      ),
    );
  }
}

class CupertinoCustomizeFormField extends StatelessWidget {
  const CupertinoCustomizeFormField({required this.placeholder});

  final String placeholder;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(height: 8),
          CupertinoTextField(
            placeholder: placeholder,
            padding: EdgeInsets.symmetric(vertical: 15, horizontal: 10),
            style: TextStyle(color: Colors.black), 
            placeholderStyle: TextStyle(color: Colors.grey), 
            decoration: BoxDecoration(
              color: Colors.white, 
              borderRadius: BorderRadius.circular(8.0),
            ),
          ),
        ],
      ),
    );
  }
}

Aqui, configuramos o aplicativo com CupertinoApp, nosso aplicativo ficará com a seguinte aparência:

Tela de login com o texto "Welcome", um campo para inserir o nome e um campo para inserir a senha.

A tela CupertinoLoginScreen usa o CupertinoPageScaffold e campos de texto adaptados ao iOS.

Perceba que um campo de formulário no Android tem a mesma função que um campo de formulário no iOS.

No aplicativo que desenvolvemos, temos leve arredondamento das bordas dos campos de formulário na versão iOS e, são detalhes como esse que fazem toda a diferença, pois a soma desses pequenos toques contribui para a construção da identidade visual única do seu aplicativo.

A App Store tende a reprovar a publicação de aplicativos com aparência de "Android" e o mesmo vale para a Play Store em relação ao visual de iOS. Logo, é importante atentar-se ao padrão visual no qual você está desenvolvendo o projeto.

Aprendemos a criar apps com Material Design para Android e com Cupertino para iOS e vimos a importância disso para a aparência do nosso projeto.

Cupertino x Material Design: qual utilizar no meu projeto?

Em linhas gerais, o indicado é usar o Cupertino para aplicativos iOS e o Material Design em aplicativos Android.

Mas, vamos pensar juntos.

Queremos que o nosso aplicativo seja usado em várias plataformas e, para isso, ele precisa adaptar a aparência conforme o sistema em que está sendo executado.

Se usarmos o Cupertino, o aplicativo fica com uma aparência iOS. Mas queremos publicar o aplicativo também no Android. Será preciso escolher um deles? Há como contornar esse problema?

Essa é uma dúvida super comum.

A boa notícia é que existe uma solução! Veremos como fazê-la na sequência.

Adaptando o aplicativo Flutter com Material Design e Cupertino para Android e iOS

Sabemos que o Flutter é multiplataforma, mas o visual do nosso aplicativo precisa se ajustar a cada sistema para oferecer uma experiência mais padronizada. Como, então, adaptar a aparência do nosso app para Android e iOS?

Existem algumas maneiras de fazer isso. A primeira é utilizar widgets adaptáveis.

Widgets adaptáveis

No Flutter, os widgets adaptáveis são componentes que ajustam sua aparência e comportamento com base na plataforma em que estão sendo executados.

Por exemplo, você cria um botão que terá layout de iOS em sistemas iOS (e uma aparência Android em dispositivos desse sistema).

Para facilitar a criação de widgets que se adaptam automaticamente, você pode usar a biblioteca flutter_platform_widgets, que oferece uma coleção de widgets que se alteram de acordo com a plataforma.

Veja o passo a passo a seguir.

Para começar, adicione a biblioteca flutter_platform_widgets ao seu pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_platform_widgets: ^7.0.1

Após instalar a biblioteca no projeto, podemos recriar nossa tela de login usando widgets adaptáveis para que ela se ajuste automaticamente ao estilo da plataforma em que está sendo executada:

import 'package:flutter/material.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';

void main() {
  runApp(PlatformApp(
    debugShowCheckedModeBanner: false,
    home: PlatformLoginScreen(),
  ));
}

class PlatformLoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return PlatformScaffold(
      body: Center(
        child: Padding(
          padding: EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'Welcome',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 24),
              PlatformTextField(hintText: 'Username'),
              SizedBox(height: 16),
              PlatformTextField(hintText: 'Password', obscureText: true),
              SizedBox(height: 24),
            ],
          ),
        ),
      ),
    );
  }
}

Neste código, usamos widgets como PlatformApp, PlatformScaffold, PlatformTextField e PlatformElevatedButton que, graças à biblioteca flutter_platform_widgets, mudam automaticamente seu estilo conforme a plataforma.

Os widgets adaptáveis são ótimos para uma abordagem geral, mas e se você precisar de uma personalização ainda maior? Nesse caso, eles não são uma boa alternativa.

Suponha que você queira adicionar um ícone de maçã antes da mensagem "Welcome" somente no iOS. Você não consegue fazer isso com widgets adaptáveis!

Para esse ajuste específico, verificar a plataforma pode ser uma boa opção.

Verificando a plataforma

Podemos usar o pacote dart:io para verificar a plataforma em que o app está rodando e ajustar o design de acordo:

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';

void main() {
  runApp(MaterialApp(
    theme: ThemeData.dark(),
    debugShowCheckedModeBanner: false,
    home: LoginScreenWithIcon(),
  ));
}

class CupertinoCustomizeFormField extends StatelessWidget {
  const CupertinoCustomizeFormField({required this.placeholder});

  final String placeholder;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: CupertinoTextField(
        placeholder: placeholder,
        padding: EdgeInsets.symmetric(vertical: 15, horizontal: 10),
      ),
    );
  }
}

class CustomizeFormField extends StatelessWidget {
  const CustomizeFormField({required this.placeholder});

  final String placeholder;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: TextFormField(
        decoration: InputDecoration(
          border: OutlineInputBorder(),
          fillColor: Colors.white,
          filled: true,
          hintText: placeholder,
          hintStyle: TextStyle(color: Colors.grey),
          contentPadding: EdgeInsets.symmetric(vertical: 15, horizontal: 10),
        ),
      ),
    );
  }
}

class LoginScreenWithIcon extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  if (Platform.isIOS) Icon(Icons.apple, size: 24),
                  Text(
                    'Welcome',
                    style: TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                    textAlign: TextAlign.center,
                  ),
                ],
              )
              /** campos de formulário */
            ],
          ),
        ),
      ),
    );
  }
}

Verificamos a plataforma usando Platform.isIOS.

Se o dispositivo for iOS, o aplicativo exibe o ícone da maçã ao lado da mensagem "Welcome":

Tela com símbolo de maçã e mensagem “Welcome”.

Caso contrário, remove o ícone da maçã:

Tela com mensagem “Welcome”.

Usando o padrão de fábrica

Apesar de ser uma solução mais específica, um problema que pode surgir ao verificar a plataforma é a repetição do código em vários locais do seu projeto.

Afinal, em cada widget que quisermos essa flexibilidade de plataforma, teremos que fazer essa verificação. Ou seja, um trabalho árduo e ineficiente.

Para resolver esse problema, podemos utilizar um padrão de projeto chamado de fábrica.

O nome é autoexplicativo: a ideia é criar algo realmente parecido com uma fábrica. No nosso caso, a fábrica produz um widget de acordo com a plataforma que estamos utilizando e nos entrega.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main(){
  runApp(
    MaterialApp(
      theme: ThemeData.dark(),
      debugShowCheckedModeBanner: false,
      home: LoginScreen(),
    )
  );
}

class CupertinoCustomizeFormField extends StatelessWidget {
  const CupertinoCustomizeFormField({required this.placeholder, this.obscureText = false});

  final String placeholder;
  final bool obscureText;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: CupertinoTextField(
        placeholder: placeholder,
        obscureText: obscureText,
        padding: EdgeInsets.symmetric(vertical: 15, horizontal: 10),
      ),
    );
  }
}

class CustomizeFormField extends StatelessWidget {
  const CustomizeFormField({required this.placeholder});

  final String placeholder;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8.0),
      child: TextFormField(
        decoration: InputDecoration(
          border: OutlineInputBorder(),
          fillColor: Colors.white,
          filled: true,
          hintText: placeholder,
          hintStyle: TextStyle(color: Colors.grey),
          contentPadding: EdgeInsets.symmetric(vertical: 15, horizontal: 10),
        ),
      ),
    );
  }
}

Widget customizeFieldFactory({required String placeholder}) {
      if (Platform.isIOS) {
    return CupertinoCustomizeFormField(placeholder:placeholder);
  } else {
    return CustomizeFormField(placeholder: placeholder);
  }
}

Criamos o customizeFieldFactory, que retorna um CupertinoCustomizeFormField se estivermos em um dispositivo iOS ou um CustomizeFormField se estivermos em um dispositivo que não seja iOS.

Agora, vamos utilizar customizeFieldFactory em nossa tela de Login:

class LoginScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Login'),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              Text(
                'Welcome',
                style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                textAlign: TextAlign.center,
              ),
              SizedBox(height: 24),
              customizeFieldFactory(placeholder: 'Username'),
              SizedBox(height: 16),
              customizeFieldFactory(placeholder: 'Password'),
              SizedBox(height: 24),
            ],
          ),
        ),
      ),
    );
  }
}

Conseguimos! Implementamos o padrão de fábrica no nosso projeto e nosso customizeFieldFactory retorna o widget de acordo com a plataforma que estamos utilizando.

Comece seus primeiros passos no Flutter

Acesse gratuitamente as primeiras aulas de Flutter e Android Studio da Formação Flutter da Alura e aprenda mais o que é o Flutter e como construir aplicativos com essa tecnologia!

Conclusão

Finalizamos, mas não pense que acabou! Se você gostou de aprender sobre como manipular as plataformas no Flutter e quer aprender mais sobre o dart:io, recomendamos a leitura da documentação oficial.

Além disso, se você está com vontade de dar um próximo passo, saiba que além da aparência do seu aplicativo se adaptar aos dispositivos, o layout também deve fazer isso!

Aprender sobre responsividade é uma ótima ideia, pois, além de rodar em várias plataformas, o layout do app precisa se ajustar a diferentes tamanhos de tela.

Mas fique tranquilo! Estamos aqui para ajudar você nesse desafio, para isso você pode acessar o curso a seguir:

Referências

Mikael Diniz
Mikael Diniz

Atualmente estou cursando Ciência da Computação na UFMA e, sou apaixonado por programação, games, matemática e basquete.

Veja outros artigos sobre Mobile