Responsive en Flutter

- Andrés Cruz

Como dije antes, repasaré los conceptos importantes que se requieren para desarrollar diseños responsivos y luego, usted elige cómo desea implementarlos exactamente en su aplicación.

1. MediaQuery

Puede utilizar para recuperar el tamaño (ancho/alto) y la orientación (vertical/paisaje) de la pantalla.

Un ejemplo de esto es el siguiente:

class HomePage extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   Size screenSize = MediaQuery.of(context).size;
   Orientation orientation = MediaQuery.of(context).orientation;
   return Scaffold(
     body: Container(
       color: CustomColors.android,
       child: Center(
         child: Text(
           'View\n\n' +
               '[MediaQuery width]: ${screenSize.width.toStringAsFixed(2)}\n\n' +
               '[MediaQuery orientation]: $orientation',
           style: TextStyle(color: Colors.white, fontSize: 18),
         ),
       ),
     ),
   );
 }
}

2. LayoutBuilder

Usando la clase, puede obtener el objeto, que puede usarse para determinar el ancho máximo y la altura máxima del widget.

RECUERDE: La principal diferencia entre MediaQuery y LayoutBuilder es que MediaQuery utiliza el contexto completo de la pantalla en lugar de solo el tamaño de su widget en particular, mientras que LayoutBuilder puede determinar el ancho y alto máximos de un widget en particular.

Un ejemplo que demuestra esto es el siguiente:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Size screenSize = MediaQuery.of(context).size;

    return Scaffold(
      body: Row(
        children: [
          Expanded(
            flex: 2,
            child: LayoutBuilder(
              builder: (context, constraints) => Container(
                color: CustomColors.android,
                child: Center(
                  child: Text(
                    'View 1\n\n' +
                        '[MediaQuery]:\n ${screenSize.width.toStringAsFixed(2)}\n\n' +
                        '[LayoutBuilder]:\n${constraints.maxWidth.toStringAsFixed(2)}',
                    style: TextStyle(color: Colors.white, fontSize: 18),
                  ),
                ),
              ),
            ),
          ),
          Expanded(
            flex: 3,
            child: LayoutBuilder(
              builder: (context, constraints) => Container(
                color: Colors.white,
                child: Center(
                  child: Text(
                    'View 2\n\n' +
                        '[MediaQuery]:\n ${screenSize.width.toStringAsFixed(2)}\n\n' +
                        '[LayoutBuilder]:\n${constraints.maxWidth.toStringAsFixed(2)}',
                    style: TextStyle(color: CustomColors.android, fontSize: 18),
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}

3. OrientationBuilder

Para determinar la orientación actual de un widget, puede utilizar la clase.

Un ejemplo que demuestra esto es el siguiente:

class HomePage extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   Orientation deviceOrientation = MediaQuery.of(context).orientation;
   return Scaffold(
     body: Column(
       children: [
         Expanded(
           flex: 2,
           child: Container(
             color: CustomColors.android,
             child: OrientationBuilder(
               builder: (context, orientation) => Center(
                 child: Text(
                   'View 1\n\n' +
                       '[MediaQuery orientation]:\n$deviceOrientation\n\n' +
                       '[OrientationBuilder]:\n$orientation',
                   style: TextStyle(color: Colors.white, fontSize: 18),
                 ),
               ),
             ),
           ),
         ),
         Expanded(
           flex: 3,
           child: OrientationBuilder(
             builder: (context, orientation) => Container(
               color: Colors.white,
               child: Center(
                 child: Text(
                   'View 2\n\n' +
                       '[MediaQuery orientation]:\n$deviceOrientation\n\n' +
                       '[OrientationBuilder]:\n$orientation',
                   style: TextStyle(color: CustomColors.android, fontSize: 18),
                 ),
               ),
             ),
           ),
         ),
       ],
     ),
   );
 }
}

4. Expanded y Flexible

Los widgets que son especialmente útiles dentro de una columna o fila son expandidos y flexibles. El widget expande un elemento secundario de una Fila, Columna o Flex para que el elemento secundario llene el espacio disponible, aunque no necesariamente tiene que llenar todo el espacio disponible.

Un ejemplo que muestra varias combinaciones de Expandido y Flexible es el siguiente:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Column(
          children: [
            Row(
              children: [
                ExpandedWidget(),
                FlexibleWidget(),
              ],
            ),
            Row(
              children: [
                ExpandedWidget(),
                ExpandedWidget(),
              ],
            ),
            Row(
              children: [
                FlexibleWidget(),
                FlexibleWidget(),
              ],
            ),
            Row(
              children: [
                FlexibleWidget(),
                ExpandedWidget(),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

class ExpandedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
        decoration: BoxDecoration(
          color: CustomColors.android,
          border: Border.all(color: Colors.white),
        ),
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            'Expanded',
            style: TextStyle(color: Colors.white, fontSize: 24),
          ),
        ),
      ),
    );
  }
}

class FlexibleWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Flexible(
      child: Container(
        decoration: BoxDecoration(
          color: CustomColors.androidAccent,
          border: Border.all(color: Colors.white),
        ),
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            'Flexible',
            style: TextStyle(color: CustomColors.android, fontSize: 24),
          ),
        ),
      ),
    );
  }
}

5. FractionallySizedBox

El widget ayuda a dimensionar su hijo a una fracción del espacio total disponible. Es especialmente útil dentro de los widgets expandidos o flexibles.

Un ejemplo es el siguiente:

class HomePage extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     backgroundColor: Colors.white,
     body: SafeArea(
       child: Column(
         mainAxisAlignment: MainAxisAlignment.start,
         children: [
           Row(
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [
               FractionallySizedWidget(widthFactor: 0.4),
             ],
           ),
           Row(
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [
               FractionallySizedWidget(widthFactor: 0.6),
             ],
           ),
           Row(
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [
               FractionallySizedWidget(widthFactor: 0.8),
             ],
           ),
           Row(
             crossAxisAlignment: CrossAxisAlignment.start,
             children: [
               FractionallySizedWidget(widthFactor: 1.0),
             ],
           ),
         ],
       ),
     ),
   );
 }
}
class FractionallySizedWidget extends StatelessWidget {
 final double widthFactor;
 FractionallySizedWidget({@required this.widthFactor});
 @override
 Widget build(BuildContext context) {
   return Expanded(
     child: FractionallySizedBox(
       alignment: Alignment.centerLeft,
       widthFactor: widthFactor,
       child: Container(
         decoration: BoxDecoration(
           color: CustomColors.android,
           border: Border.all(color: Colors.white),
         ),
         child: Padding(
           padding: const EdgeInsets.all(16.0),
           child: Text(
             '${widthFactor * 100}%',
             style: TextStyle(color: Colors.white, fontSize: 24),
           ),
         ),
       ),
     ),
   );
 }
}

6. AspectRatio

Puede utilizar el widget para dimensionar al child según una relación de aspecto específica. Este widget primero prueba el ancho más grande permitido por las restricciones de diseño y luego decide la altura aplicando la relación de aspecto dada al ancho.

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Column(
          children: [
            AspectRatioWidget(ratio: '16 / 9'),
            AspectRatioWidget(ratio: '3 / 2'),
          ],
        ),
      ),
    );
  }
}

class AspectRatioWidget extends StatelessWidget {
  final String ratio;

  AspectRatioWidget({@required this.ratio});

  @override
  Widget build(BuildContext context) {
    return AspectRatio(
      aspectRatio: Fraction.fromString(ratio).toDouble(),
      child: Container(
        decoration: BoxDecoration(
          color: CustomColors.android,
          border: Border.all(color: Colors.white),
        ),
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Center(
            child: Text(
              'AspectRatio - $ratio',
              style: TextStyle(color: Colors.white, fontSize: 24),
            ),
          ),
        ),
      ),
    );
  }
}

Hemos analizado la mayoría de los conceptos importantes necesarios para crear un diseño responsivo en Flutter, excepto uno.

Aprendamos el último concepto mientras creamos una aplicación responsiva de muestra.

Construyendo una aplicación responsiva

Ahora, aplicaremos algunos de los conceptos que he descrito en la sección anterior. Junto con esto, también aprenderá otro concepto importante para crear diseños para pantallas grandes: la vista dividida.

Crearemos un diseño de aplicación de chat de muestra llamado Flow.

La aplicación constará principalmente de dos pantallas principales:

  1. Página de inicio (PeopleView, BookmarkView, ContactView)
  2. Página de chat (PeopleView, ChatView)

Página principal


La pantalla principal de la aplicación después del lanzamiento será la página de inicio. Consta de dos tipos de vistas:

  1. HomeViewSmall (que consta de AppBar, Drawer, BottomNavigationBar y DestinationView)
  2. HomeViewLarge (que consta de vista dividida, MenuWidget y DestinationView)
class _HomePageState extends State<HomePage> {
 int _currentIndex = 0;
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: LayoutBuilder(
       builder: (context, constraints) {
         if (constraints.maxWidth < 600) {
           return HomeViewSmall();
         } else {
           return HomeViewLarge();
         }
       },
     ),
   );
 }
}

Aquí, LayoutBuilder sirve para determinar el ancho máximo y cambiar entre los widgets HomeViewSmall y HomeViewLarge.

class _HomeViewSmallState extends State<HomeViewSmall> {
 int _currentIndex = 0;
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
         // ...
     ),
     drawer: Drawer(
         // ...
     ),
     bottomNavigationBar: BottomNavigationBar(
         // ...
     ),
     body: IndexedStack(
       index: _currentIndex,
       children: allDestinations.map<Widget>((Destination destination) {
         return DestinationView(destination);
       }).toList(),
     ),
   );
 }
}

 

IndexedStack con DestinationView se utiliza para cambiar entre las vistas según el elemento seleccionado en BottomNavigationBar.

Si desea saber más, consulte el repositorio de GitHub de esta aplicación de muestra que se encuentra al final de este artículo.

class _HomeViewLargeState extends State<HomeViewLarge> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisAlignment: MainAxisAlignment.start,
        children: [
          Expanded(
            flex: 2,
            child: MenuWidget(
              selectedIndex: _index,
              onTapped: (selectedIndex) {
                setState(() {
                  _index = selectedIndex;
                });
              },
            ),
          ),
          Expanded(
            flex: 3,
            child: IndexedStack(
              index: _index,
              children: allDestinations.map<Widget>((Destination destination) {
                return DestinationView(destination);
              }).toList(),
            ),
          ),
        ],
      ),
    );
  }
}

Para pantallas grandes, mostraremos una vista dividida que contiene MenuWidget y DestinationView. Puedes ver que es realmente fácil crear una vista dividida en Flutter. Sólo tienes que colocarlas una al lado de la otra usando una Fila y luego, para llenar todo el espacio, simplemente envuelve ambas vistas usando el widget Expandido. También puede definir la propiedad flex del widget expandido, que le permitirá especificar qué parte de la pantalla debe cubrir cada widget (de forma predeterminada, flex está establecido en 1).

Pero ahora, si pasa a una pantalla en particular y luego cambia entre vistas, perderá el contexto de la página; es decir, siempre volverás a la primera página que es Chats. Para resolver esto aquí, he usado múltiples funciones de devolución de llamada para devolver la página seleccionada a la página de inicio. En la práctica, debería utilizar una técnica de gestión estatal para manejar este escenario. Como el único propósito de este artículo es enseñarle a crear diseños responsivos, no entraré en ninguna de las complejidades de la gestión estatal.

Modificando HomeViewSmall:

class HomeViewSmall extends StatefulWidget {
 final int currentIndex;
 /// Callback function
 final Function(int selectedIndex) onTapped;
 HomeViewSmall(this.currentIndex, this.onTapped);
 @override
 _HomeViewSmallState createState() => _HomeViewSmallState();
}
class _HomeViewSmallState extends State<HomeViewSmall> {
 int _currentIndex = 0;
 @override
 void initState() {
   super.initState();
   _currentIndex = widget.currentIndex;
 }
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     // ...
     bottomNavigationBar: BottomNavigationBar(
       // ...
       currentIndex: _currentIndex,
       onTap: (int index) {
         setState(() {
           _currentIndex = index;
           // Invoking the callback
           widget.onTapped(_currentIndex);
         });
       },
       items: allDestinations.map((Destination destination) {
         return BottomNavigationBarItem(
           icon: Icon(destination.icon),
           label: destination.title,
         );
       }).toList(),
     ),
   );
 }
}

Modificando HomeViewLarge:

class HomeViewLarge extends StatefulWidget {
 final int currentIndex;
 /// Callback function
 final Function(int selectedIndex) onTapped;
 HomeViewLarge(this.currentIndex, this.onTapped);
 @override
 _HomeViewLargeState createState() => _HomeViewLargeState();
}
class _HomeViewLargeState extends State<HomeViewLarge> {
 int _index = 0;
 @override
 void initState() {
   super.initState();
   _index = widget.currentIndex;
 }
 @override
 Widget build(BuildContext context) {
   return Container(
     child: Row(
       crossAxisAlignment: CrossAxisAlignment.start,
       mainAxisAlignment: MainAxisAlignment.start,
       children: [
         Expanded(
           flex: 2,
           child: MenuWidget(
             selectedIndex: _index,
             onTapped: (selectedIndex) {
               setState(() {
                 _index = selectedIndex;
                 // Invoking the callback
                 widget.onTapped(_index);
               });
             },
           ),
         ),
         // ...
       ],
     ),
   );
 }
}

Modificando la página de inicio:

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  int _currentIndex = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: LayoutBuilder(
        builder: (context, constraints) {
          if (constraints.maxWidth < 600) {
            return HomeViewSmall(_currentIndex, (index) {
              setState(() {
                _currentIndex = index;
              });
            });
          } else {
            return HomeViewLarge(_currentIndex, (index) {
              setState(() {
                _currentIndex = index;
              });
            });
          }
        },
      ),
    );
  }
}

Ahora, su página de inicio totalmente responsiva está completa.

Página de chat

Será similar a la página de inicio, pero constará de las dos vistas siguientes:

  • ChatViewSmall (que consta de AppBar, ChatList y el widget SendWidget)
  • ChatViewLarge (que consta del widget PeopleView, ChatList y SendWidget)
class ChatPage extends StatelessWidget {
 final Color profileIconColor;
 ChatPage(this.profileIconColor);
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     body: OrientationBuilder(
       builder: (context, orientation) => LayoutBuilder(
         builder: (context, constraints) {
           double breakpointWidth = orientation == Orientation.portrait ? 600 : 800;
           if (constraints.maxWidth < breakpointWidth) {
             return ChatViewSmall(profileIconColor);
           } else {
             return ChatViewLarge(profileIconColor);
           }
         },
       ),
     ),
   );
 }
}

Aquí, he usado OrientationBuilder junto con LayoutBuilder para variar el ancho del punto de interrupción según la orientación, ya que no quiero mostrar PeopleView en un móvil de pantalla pequeña mientras está en modo horizontal.

class ChatViewSmall extends StatelessWidget {
 final Color profileIconColor;
 ChatViewSmall(this.profileIconColor);
 @override
 Widget build(BuildContext context) {
   return Scaffold(
     appBar: AppBar(
     ),
     body: Container(
       color: Colors.white,
       child: Column(
         children: [
           Expanded(child: ChatList(profileIconColor)),
           SendWidget(),
         ],
       ),
     ),
   );
 }
}
class ChatViewLarge extends StatelessWidget {
  final Color profileIconColor;
  ChatViewLarge(this.profileIconColor);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Row(
        children: [
          Expanded(
            flex: 2,
            child: SingleChildScrollView(
              child: PeopleView(),
            ),
          ),
          Expanded(
            flex: 3,
            child: Container(
              color: Colors.white,
              child: Column(
                children: [
                  Expanded(child: ChatList(profileIconColor)),
                  SendWidget(),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Conclusión

Hemos creado con éxito una aplicación totalmente responsiva en Flutter. Hay una serie de mejoras que aún puedes realizar en esta aplicación, una de las cuales podría ser definir el tamaño de fuente para que difiera según los diferentes tamaños de pantalla.

Artículo original:

https://blog.codemagic.io/building-responsive-applications-with-flutter/

Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz en Udemy

Acepto recibir anuncios de interes sobre este Blog.