Cupertino Widgets

Before we venture out of Flutter Dart code too much, there is one thing you might have noticed in pubspec.yaml: the dependency on cupertino_icons. We have actually never used what is contained in it in the body of the book, because it doesn’t really allow you to do anything you couldn’t do before in Flutter apps: it contains icons to be used by widgets that look like native Apple widgets and can be used instead of the Material Design widgets.

This means you have the following two choices regarding the look of your app:

  • Make your app look the same on Android and iOS by either using the Material widgets or the Cupertino widgets.

  • Make your app look like other apps built for each platform by using Material widgets on Android and Cupertino widgets on iOS.

Obviously you could choose to show your iOS users the Material widgets and the Android users the Cupertino widgets, but that’s probably not the smartest choice for most people.

The Counter Increase App with Cupertino Widgets

As a way to get an idea of how to use Cupertino widgets, we’re going to rewrite the counter app that Flutter itself gives as an example of basic Flutter UI that we saw in the first chapter, this time using Cupertino widgets instead of Material widgets.

What We’re Going to Build

It will look like the following screenshots, keeping in mind that floating action buttons aren’t part of regular Apple Design:

images/Cupertino/counterapp.png

The alternative would have been to add it to the right of the title in the navigation bar (the Apple app bar), but doing it this way allows us to look at more of the Flutter Cupertino widgets.

Importing the Cupertino Library

Let’s start by changing the imports, we only need to import the Cupertino Flutter library, given that we won’t be using any Material widgets:

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

From MaterialApp to CupertinoApp

The MaterialApp is the next thing that needs to change, to become a CupertinoApp. The name isn’t the only thing that needs to change: the theme needs to be of class CupertinoThemeData and not just ThemeData. The CupertinoThemeData takes a primaryColor instead of a primarySwatch, and it needs to one of the CupertinoColors, which aren’t the regular colors you use with Material apps: Colors.blue will become CupertinoColors.activeBlue, which is the default color.

Material-Based Cupertino Theme Data

Given that you’re probably going to build apps for both Android and iOS using both Material and Cupertino widgets, you might want to create one ThemeData for the Material Design app and derive the styling of the Cupertino version from that Material Design theme. This can be done using MaterialBasedCupertinoThemeData(materialTheme: themeData), which takes as a named argument called materialTheme the ThemeData we want to derive the new CupertinoThemeData from.

Our CupertinoApp

Because of that, the theme argument could be skipped entirely unless you want your app to have a dark background that is supposed to be with an activeOrange primary color. We’re going to stick with a regular light theme with a blue primary color, providing the same arguments that were used in the original Material example to keep the example as close to the original as possible:

 class​ MyApp ​extends​ StatelessWidget {
  @override
  Widget build(BuildContext context) {
 return​ CupertinoApp(
  title: ​'Flutter Demo'​,
  theme: CupertinoThemeData(
  primaryColor: CupertinoColors.activeBlue,
  ),
  home: MyHomePage(title: ​'Flutter Demo Home Page'​),
  );
  }
 }

Unchanged Widgets

StatefulWidgets, StatelessWidgets and States are all the same as with Material apps, and the same goes for Rows, Column, Containers and Center widgets, as well as simple widgets like Icons (even though the IconData used for them should be taken from the CupertinoIcons instead of taking it from the Icons) and Text.

The Scaffold and App Bar

Before getting to them, though, the Scaffold needs to switch to a CupertinoPageScaffold (there is a CupertinoTabScaffold for scaffolds that have bottom navigation, there’s no CupertinoScaffold), which (as I anticipated earlier) takes a CupertinoNavigationBar as the navigationBar argument instead of taking an AppBar as the appBar argument as we did with Material apps. Additionally, the CupertinoNavigationBar takes three peculiarly named arguments: leading, middle and trailing for widgets to place in the navigation bar. Usually the left side is reserved for a back button, the right side for an important action and the middle is where we should put the title of the page:

 return​ ​CupertinoPageScaffold​(
  navigationBar: CupertinoNavigationBar(
  middle: Text(widget.title),
  ),

The CupertinoButton

The button to increase the counter will be a - you guessed it - CupertinoButton. It takes a child widget for which we can use a simple Row:

 CupertinoButton(
  onPressed: _incrementCounter,
  child: Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
  Icon(CupertinoIcons.add_circled_solid),
  Text(​"Increase"​)
  ]
  )
 )

CupertinoIcons.add is the + icon that should be used when it appears alone, CupertinoIcons.add_circled has a circle around it and CupertinoIcons.circled_solid is what’s in the screenshot you saw earlier.

Wrapping Up

The full code, including the things that are the same as the app we saw in the first chapter, is the following:

 import​ ​'package:flutter/cupertino.dart'​;
 
 void​ ​main​() => runApp(MyApp());
 class​ MyApp ​extends​ StatelessWidget {
  @override
  Widget build(BuildContext context) {
 return​ CupertinoApp(
  title: ​'Flutter Demo'​,
  theme: CupertinoThemeData(
  primaryColor: CupertinoColors.activeBlue,
  ),
  home: MyHomePage(title: ​'Flutter Demo Home Page'​),
  );
  }
 }
 
 class​ MyHomePage ​extends​ StatefulWidget {
  MyHomePage({Key key, ​this​.title}) : ​super​(key: key);
 
 final​ ​String​ title;
 
  @override
  _MyHomePageState createState() => _MyHomePageState();
 }
 
 class​ _MyHomePageState ​extends​ State<MyHomePage> {
 int​ _counter = 0;
 
 void​ _incrementCounter() {
  setState(() {
  _counter++;
  });
  }
 
  @override
  Widget build(BuildContext context) {
 return​ CupertinoPageScaffold(
  navigationBar: CupertinoNavigationBar(
  middle: Text(widget.title),
  ),
  child: Center(
  child: Column(
  crossAxisAlignment: CrossAxisAlignment.center,
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
  Text(
 'You have pushed the button this many times:'​,
  ),
  Text(
 '​​$_counter​​'​,
  style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle,
  ),
  CupertinoButton(
  onPressed: _incrementCounter,
  child: Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
  Icon(CupertinoIcons.add_circled_solid),
  Text(​"Increase"​)
  ]
  )
  )
  ],
  ),
  ),
  );
  }
 }

Other Cupertino Widgets

This is meant to be just a quick introduction to how to build iOS-style UIs and is meant as a starting platform, not as a comprehensive guide, so we will only be talking about the most important Cupertino widgets. You can find more of them in detail in a dedicated section of Flutter’s official website.[56]

Cupertino Navigation

Before talking about some of the UI elements that need to be converted to Cupertino widgets, we’ll see how to implement navigation using Cupertino widgets. There are a few differences between Material Design and iOS layouts that need to be clarified: the drawer side navigation menu is a typical Material Design element, so it doesn’t have a Cupertino equivalent and navigation is handled using either a bottom tab bar or simple push/pop navigation.

Tab-Based Navigation Using the CupertinoTabScaffold

Let’s talk about something we briefly mentioned in the previous section: the CupertinoTabScaffold. This widget isn’t actually necessary to have tab-based navigation (since theCupertinoTabBar can be used to call setState and render different data based on the selected tab), but it makes it very easy.

It takes two arguments:

  • The tabBar, which has to be set to a CupertinoTabBar with the needed items list of BottomNavigationBarItems.

  • The tabBuilder which, given the context and the int index, has to return the content of the tab corresponding to that index, usually returning a CupertinoTabView that gives each tab its own navigation history and state, containing a CupertinoPageScaffold for the content of the tab.

An example of how to do that is the following:

 class​ HomePage ​extends​ StatelessWidget {
 
 final​ data = [
  {
 "Train"​,
  CupertinoIcons.train_style_one
  },
  {
 "Search"​,
  CupertinoIcons.search
  },
  {
 "Share"​,
  CupertinoIcons.share
  }
  ];
 
  @override
  Widget build(context) =>
  CupertinoTabScaffold(
  tabBar: CupertinoTabBar(
  items: data.map(
  (el) =>
  BottomNavigationBarItem(
  title: Text(el.first),
  icon: Icon(el.last)
  )
  ).toList(),
  ),
  tabBuilder: (context, i) =>
  CupertinoTabView(
  builder: (context) =>
  CupertinoPageScaffold(
  navigationBar: CupertinoNavigationBar(
  middle: Text(data[i].first),
  ),
  child: Center(
  child: Icon(data[i].last, size: 80.0)
  ),
  )
  )
  );
 }

which produces the following view when opened:

images/Cupertino/tabnav1.png

and it gives us a chance to notice the iOS Share icon, which is very different from the Material Share icon, when tapping on the third tab:

images/Cupertino/tabnav2.png

Push/Pop Navigation with the CupertinoPageRoute

The Navigator itself works just like in Material apps, but we need to replace the MaterialPageRoute with the CupertinoPageRoute, but not necessarily. The MaterialPageRoute actually adapts to the native animations typically used for transitioning between pages (vertical on Android, horizontal on iOS), but it is contained in material.dart. In case you want to use the iOS animation (CupertinoPageTransition) on Android or you don’t want to use widgets from material.dart because the current file only imports cupertino.dart, you can use the CupertinoPageRoute just like you use the MaterialPageRoute, by passing it to Navigator.push or similar methods, passing to the CupertinoPageRoute constructor the usual context and a builder callback.

After pushing a page, the leading widget of the navigation bar (if present) will be a button that allows the user to go back to the previous page.

images/Cupertino/backbutton.png

Replacement for Some UI Elements

There are a few UI elements that are worthy or being discussed as replacements for Material widgets we’ve seen during the course of this book.

CupertinoTextField

One of the widgets that needs to be changed is the TextField, and it doesn’t only require a name change, unlike some other Cupertino widgets: the CupertinoTextField’s decoration is of type BoxDecoration (the same we used with the Container) and not InputDecoration. The InputDecoration is what we used to provide a labelText, which now is an argument of the CupertinoTextField itself called placeholder:

 TextField(
  controller: _controller,
  decoration: InputDecoration(
  labelText: ​"Description"
  ),
  onSubmitted: (val) {
 // do something with value
  }
 )

will become:

 CupertinoTextField(
  controller: _controller,
  placeholder: ​"Description"
  onSubmitted: (val) {
 // do something with value
  }
 )

CupertinoActivityIndicator

The replacement for the CircularProgressIndicator is the CupertinoActivityIndicator, which looks like this:

images/Cupertino/activityindicator.png

CupertinoAlertDialog

The Cupertino library provides the CupertinoAlertDialog to be launched with showCupertinoDialog as a replacement for the Material AlertDialog that is launched using showDialog.

The CupertinoAlertDialog takes three arguments: the title widget (presumably some Text, the content widget and a list of widgets (presumably buttons) called actions to show at the bottom of the dialog:

 CupertinoAlertDialog(
  title: Text(​"Dialog title"​),
  content: Text(​"Content of the dialog, usually longer than the title"​),
  actions: <Widget>[
  CupertinoButton(
  child: Text(​"Action 1"​),
  onPressed: () {}
  ),
  CupertinoButton(
  child: Text(​"Action 2"​),
  onPressed: () {}
  )
  ],
 )

It will look like this when launched with showCupertinoDialog(context: context, builder: (_) => CupertinoAlertDialog(...)):

images/Cupertino/dialog.png

Automatically Switching Between Cupertino and Material

If we want our app to be able to change what it displays based on what OS it runs on, we need to be able to know what platform we’re running on from the Dart code. This is possible thanks to the Platform class in the dart:io library, which can be used in the following way:

 if​(Platform.isIOS) {
 // render Cupertino widgets
 } ​else​ {
 // render Material widgets
 }

It also provides Platform.isAndroid, Platform.isFuchsia, Platform.isMacOS, Platform.isWindows and Platform.isLinux, but you probably won’t need the latter three given that at the time of writing Flutter on the desktop won’t work for most apps.

The easiest way to take advantage of that is to have different UI trees for different platforms, but it isn’t really the most elegant solution. That’s why many Flutter developers choose to implement generic classes that have factory methods to render different widgets for different platforms.

The Flutter Platform Widgets Package

The implementation of that, though, can be repetitive and it is suited to the creation of a library, which does indeed exist: the flutter_platform_widgets package. This package provides classes like PlatformApp, PlatformScaffold, PlatformAppBar, PlatformButton and PlatformPageRoute (full list and usage available on the package’s API reference[57]) that will automatically use the Material or Cupertino equivalents on each platform.

It also provides a generic PlatformWidget that takes an ios and an android argument that you can use for widgets not provided by the library with a dedicated class or if the choice of widgets made by the authors of the library isn’t what you want or need for your app.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset