Responsive Layouts: Mit Flutter von der Mobile-App zur Web-App

Seite 2: Die Navigation

Inhaltsverzeichnis

Die Navigation ist in der mobilen Ansicht wegen des geringen Platzes auf ein Logo und ein Hamburger-Icon beschränkt. Das Hamburger-Icon triggert onTap, also nach Berührung, das Öffnen des Menüs in einem Drawer-Widget. Damit sind die Navigationselemente aufgeräumt. Nutzer erreichen diese aber durch das von vielen Anwendungen gelernte Drawer/Sidebar-Verhalten, bei dem eine Navigation nach Tap auf ein Menü-Icon von rechts oder links in den Viewport fährt und die verfügbaren Optionen präsentiert.

Um dieses Layout umzusetzen, können sich Entwickler an einem Row-Widget bedienen, das ein Logo und Hamburger-Icon als Children erhält. Da Row- und Column-Widgets in Flutter ein CSS-Flexbox-ähnliches Verhalten haben, können Children in diesen Widgets in der gewünschten Reihenfolge übergeben werden und das Parent-Widget verteilt die Children je nach aktuellem cross- oder mainAxisAlignment-Setting. Dabei nehmen die Children jeweils nur ihre nötige Mindestbreite an. Der restliche Platz bleibt frei. MainAxisAlignment.spaceBetween sorgt dafür, dass der gesamte freie Platz in der Row zwischen den Children aufgezogen wird. Dadurch lässt sich das Logo am linken Ende der Row positionieren, das Hamburger-Icon am rechten Ende.

class MobileNavbar extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return Container(
     padding: EdgeInsets.symmetric(horizontal: 30),
     child: Row(
       mainAxisAlignment: MainAxisAlignment.spaceBetween,
       children: [
         SvgPicture.asset('images/logo.svg'),
         IconButton(
           icon: Icon(Icons.menu),
           onPressed: () => Scaffold.of(context).openEndDrawer(),
         ),
       ],
     ),
   );
 }
}

Der Drawer selbst muss dem Scaffold-Widget der Page übergeben werden. Da er von rechts in den Viewport fahren soll, ist er als endDrawer zu definieren. Dabei stellt drawerEnableOpenDragGesture sicher, dass der Drawer nur mobil zum Einsatz kommt.

Scaffold(
 backgroundColor: Color(0xFFFFFFFF),
 drawerEnableOpenDragGesture: !isDesktop,
 endDrawer: isDesktop
   ? null
   : Drawer(
     child: SafeArea(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Learn'),
            SizedBox(height: 40),
            Text('Inspire'),
            SizedBox(height: 40),
            Text('About'),
            SizedBox(height: 40),
            Text('Info'),
            SizedBox(height: 40),
            Text('Help'),
          ],
        ),
      ),
    ),
  …
)

Die Desktop-Ansicht zeigt nun eine klassische Navigation an, die alle Elemente horizontal nebeneinander einreiht.

class DesktopNavbar extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   return Container(
     constraints: BoxConstraints(
       maxWidth: 1200,
     ),
     decoration: BoxDecoration(
       border: Border(
         bottom: BorderSide(
           color: Color(0xFFF3F3F4),
         ),
       ),
     ),
     child: Row(
       children: [
         SvgPicture.asset('images/logo.svg'),
         SizedBox(width: 20),
         Row(
           children: [
             Text('Learn'),
             SizedBox(width: 20),
             Text('Inspire'),
             SizedBox(width: 20),
             Text('About'),
             SizedBox(width: 20),
             Text('Info'),
             SizedBox(width: 20),
             Text('Help'),
           ],
         ),
         Spacer(),
         IconButton(icon: Icon(Icons.search), onPressed: () {}),
         LoginButton(),
         RegisterButton(),
       ],
     ),
   );
 }
}

Beim Größer- und Kleinerziehen der Seite in der Desktopansicht fällt auf, dass sich nur der Whitespace zwischen den Navigationselementen und den Buttons am rechten Ende sowie der Whitespace zwischen Navigationsbereich und rechtem und linkem Bildschirmrand verändert. Dabei ist der Navigationsbereich im Viewport immer horizontal zentriert.

Dieses Verhalten kennen die Besucher von Websites, die mit CSS-Frameworks wie Bootstrap aufgesetzt wurden (s. Abb. 2). Dort gibt es in der Regel einen zentrierten Inhaltsbereich, der eine gewisse maximale Breite annimmt, danach jedoch nicht weiter wächst. Das verhindert, dass beim Verarbeiten der Inhalte die Augen vom linken bis zum rechten Bildschirmrand wandern müssen. Vor allem textlastige Websites machen sich das zunutze, um die Lesbarkeit der Teaser und Artikel zu erhöhen.

Medium-Website mit von Bootstrap bekanntem Spaltenaufbau (Abb. 2)

(Bild: medium.com)

Dieses Verhalten lässt sich in Flutter mit einfachen Basis-Widgets umsetzen. Mit einem Blick in den Scaffold fällt im Body eine Column auf, die je nach Mobile- oder Desktopansicht die entsprechenden Widgets als Children erhält. crossAxisAligment dient dazu, die Children horizontal zu zentrieren.


Scaffold(
 …
  body: SafeArea(
    child: SingleChildScrollView(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.center,
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
    isDesktop ? _buildDesktopPage() : _buildMobilePage(context)
        ],
      ),
    ),
  ),
)

Die DesktopNavbar wiederum enthält als erstes Child einen Container, dessen Breite auf 1200 Pixel begrenzt ist.

class DesktopNavbar extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      constraints: BoxConstraints(
        maxWidth: 1200,
      ),
    );
  }
}

Damit wird der Haupt-Content horizontal zentriert und wächst nur bis zu einer gewissen Breite. Doch warum wächst der Whitespace zwischen den Navigationselementen und den Buttons am rechten Ende der Navigation? Die Children der Navbar-Row geben Aufschluss darüber:

Row(
  children: [
    SvgPicture.asset('images/logo.svg'),
    SizedBox(width: 20),
    Row(
      children: [
        Text('Learn'),
        SizedBox(width: 20),
        Text('Inspire'),
        SizedBox(width: 20),
        Text('About'),
        SizedBox(width: 20),
        Text('Info'),
        SizedBox(width: 20),
        Text('Help'),
      ],
    ),
    Spacer(),
    IconButton(icon: Icon(Icons.search), onPressed: () {}),
    LoginButton(),
    RegisterButton(),
  ],
)

Dort befindet sich an der betreffenden Stelle ein Spacer-Widget. Damit lässt sich ein Anker platzieren, an dem sich der restliche Whitespace bei mehreren Children an einer Stelle konzentrieren kann. Daher ordnen sich in diesem Fall Logo und Navigationspunkte links und die Buttons rechts an, während der gesamte übrige Whitespace zwischen den beiden Bereichen gesammelt wird. Dieser Whitespace wächst, je breiter der vorhandene Platz ist.

Warum aber ist die Navbar in ein Desktop- und ein Mobile-Widget getrennt? Diese Frage ist durchaus berechtigt, da die einzelnen Elemente der Navigation auch einfach in ein Navbar-Widget zusammengefasst und die Widgets darin je nach Desktop- oder Mobile-Ansicht aus- und eingeblendet werden könnten. Um die Komplexität in den einzelnen Widgets möglichst gering zu halten, wurde die Unterscheidung im Code frühzeitig gesetzt. So muss auch die Viewport-Größe seltener geprüft werden. Der in Kauf genommene Nachteil (Trade-off) ist natürlich der potenziell erhöhte Wartungsaufwand durch die doppelte Verwendung von Widgets.

Allerdings kann es bei anderen Widgets dazu kommen, dass die Reihenfolge der Children responsiv angepasst werden muss. Erhöht sich dabei die Komplexität durch Desktop-/Mobile-Unterscheidungen noch weiter, kann es zu Komplikationen bei der Erweiterung, Anpassung oder Wartung der Komponente kommen.