Flutter for SwiftUI Developers

SwiftUI developers who want to write mobile apps using Flutter should review this guide. It explains how to apply existing SwiftUI knowledge to Flutter.

Flutter is a framework for building cross-platform applications that uses the Dart programming language. To understand some differences between programming with Dart and programming with Swift, see Learning Dart as a Swift Developer and Flutter concurrency for Swift developers.

Your SwiftUI knowledge and experience are highly valuable when building with Flutter.

Flutter also makes a number of adaptations to app behavior when running on iOS and macOS. To learn how, see Platform adaptations.

This document can be used as a cookbook by jumping around and finding questions that are most relevant to your needs. This guide embeds sample code. You can test full working examples on DartPad or view them on GitHub.

Overview

Flutter and SwiftUI code describes how the UI looks and works. Developers call this type of code a declarative framework.

Views vs. Widgets

SwiftUI represents UI components as views. You configure views using modifiers.

Text("Hello, World!") // <-- This is a View
  .padding(10)        // <-- This is a modifier of that View

Flutter represents UI components as widgets.

Both views and widgets only exist until they need to be changed. These languages call this property immutability. SwiftUI represents a UI component property as a View modifier. By contrast, Flutter uses widgets for both UI components and their properties.

Padding(                         // <-- This is a Widget
  padding: EdgeInsets.all(10.0), // <-- So is this
  child: Text("Hello, World!"),  // <-- This, too
)));

To compose layouts, both SwiftUI and Flutter nest UI components within one another. SwiftUI nests Views while Flutter nests Widgets.

Layout process

SwiftUI lays out views using the following process:

  1. The parent view proposes a size to its child view.
  2. All subsequent child views:
    • propose a size to their child’s view
    • ask that child what size it wants
  3. Each parent view renders its child view at the returned size.

Flutter differs somewhat with its process:

  1. The parent widget passes constraints down to its children. Constraints include minimum and maximum values for height and width.
  2. The child tries to decide its size. It repeats the same process with its own list of children:
    • It informs its child of the child’s constraints.
    • It asks its child what size it wishes to be.
  3. The parent lays out the child.
    • If the requested size fits in the constraints, the parent uses that size.
    • If the requested size doesn’t fit in the constraints, the parent limits the height, width, or both to fit in its constraints.

Flutter differs from SwiftUI because the parent component can override the child’s desired size. The widget cannot have any size it wants. It also cannot know or decide its position on screen as its parent makes that decision.

To force a child widget to render at a specific size, the parent must set tight constraints. A constraint becomes tight when its constraint’s minimum size value equals its maximum size value.

In SwiftUI, views may expand to the available space or limit their size to that of its content. Flutter widgets behave in similar manner.

However, in Flutter parent widgets can offer unbounded constraints. Unbounded constraints set their maximum values to infinity.

UnboundedBox(
  child: Container(
      width: double.infinity, height: double.infinity, color: red),
)

If the child expands and it has unbounded constraints, Flutter returns an overflow warning:

UnconstrainedBox(
  child: Container(color: red, width: 4000, height: 50),
)

When parents pass unbounded constraints to children, and the children are expanding, then there is an overflow warning

To learn how constraints work in Flutter, see Understanding constraints.

Design system

Because Flutter targets multiple platforms, your app doesn’t need to conform to any design system. Though this guide features Material widgets, your Flutter app can use many different design systems:

  • Custom Material widgets
  • Community built widgets
  • Your own custom widgets
  • Cupertino widgets that follow Apple’s Human Interface Guidelines

If you’re looking for a great reference app that features a custom design system, check out Wonderous.

UI Basics

This section covers the basics of UI development in Flutter and how it compares to SwiftUI. This includes how to start developing your app, display static text, create buttons, react to on-press events, display lists, grids, and more.

Getting started

In SwiftUI, you use App to start your app.

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            HomePage()
        }
    }
}

Another common SwiftUI practice places the app body within a struct that conforms to the View protocol as follows:

struct HomePage: View {
  var body: some View {
    Text("Hello, World!")
  }
}

To start your Flutter app, pass in an instance of your app to the runApp function.

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

App is a widget. The build method describes the part of the user interface it represents. It’s common to begin your app with a WidgetApp class, like CupertinoApp.

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

  @override
  Widget build(BuildContext context) {
    // Returns a CupertinoApp that, by default,
    // has the look and feel of an iOS app.
    return const CupertinoApp(
      home: HomePage(),
    );
  }
}

The widget used in HomePage might begin with the Scaffold class. Scaffold implements a basic layout structure for an app.

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

  @override
  Widget build(BuildContext context) {
    return const Scaffold(
      body: Center(
        child: Text(
          'Hello, World!',
        ),
      ),
    );
  }
}

Note how Flutter uses the Center widget. SwiftUI renders a view’s contents in its center by default. That’s not always the case with Flutter. Scaffold doesn’t render its body widget at the center of the screen. To center the text, wrap it in a Center widget. To learn about different widgets and their default behaviors, check out the Widget catalog.

Adding Buttons

In SwiftUI, you use the Button struct to create a button.

Button("Do something") {
  // this closure gets called when your
  // button is tapped
}

To achieve the same result in Flutter, use the CupertinoButton class:

        CupertinoButton(
  onPressed: () {
    // This closure is called when your button is tapped.
  },
  child: const Text('Do something'),
)

Flutter gives you access to a variety of buttons with predefined styles. The CupertinoButton class comes from the Cupertino library. Widgets in the Cupertino library use Apple’s design system.

Aligning components horizontally

In SwiftUI, stack views play a big part in designing your layouts. Two separate structures allow you to create stacks:

  1. HStack for horizontal stack views

  2. VStack for vertical stack views

The following SwiftUI view adds a globe image and text to a horizontal stack view:

HStack {
  Image(systemName: "globe")
  Text("Hello, world!")
}

Flutter uses Row rather than HStack:

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: const [
    Icon(CupertinoIcons.globe),
    Text('Hello, world!'),
  ],
),

The Row widget requires a List<Widget> in the children parameter. The mainAxisAlignment property tells Flutter how to position children with extra space. MainAxisAlignment.center positions children in the center of the main axis. For Row, the main axis is the horizontal axis.

Aligning components vertically

The following examples build on those in the previous section.

In SwiftUI, you use VStack to arrange the components into a vertical pillar.

VStack {
  Image(systemName: "globe")
  Text("Hello, world!")
}

Flutter uses the same Dart code from the previous example, except it swaps Column for Row:

Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: const [
    Icon(CupertinoIcons.globe),
    Text('Hello, world!'),
  ],
),

Displaying a list view

In SwiftUI, you use the List base component to display sequences of items. To display a sequence of model objects, make sure that the user can identify your model objects. To make an object identifiable, use the Identifiable protocol.

struct Person: Identifiable {
  var name: String
}

var persons = [
  Person(name: "Person 1"),
  Person(name: "Person 2"),
  Person(name: "Person 3"),
]

struct ListWithPersons: View {
  let persons: [Person]
  var body: some View {
    List {
      ForEach(persons) { person in
        Text(person.name)
      }
    }
  }
}

This resembles how Flutter prefers to build its list widgets. Flutter doesn’t need the list items to be identifiable. You set the number of items to display then build a widget for each item.

class Person {
  String name;
  Person(this.name);
}

var items = [
  Person('Person 1'),
  Person('Person 2'),
  Person('Person 3'),
];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(items[index].name),
          );
        },
      ),
    );
  }
}

Flutter has some caveats for lists:

  • The ListView widget has a builder method. This works like the ForEach within SwiftUI’s List struct.

  • The itemCount parameter of the ListView sets how many items the ListView displays.

  • The itemBuilder has an index parameter that will be between zero and one less than itemCount.

The previous example returned a ListTile widget for each item. The ListTile widget includes properties like height and font-size. These properties help build a list. However, Flutter allows you to return almost any widget that represents your data.

Displaying a grid

When constructing non-conditional grids in SwiftUI, you use Grid with GridRow.

Grid {
  GridRow {
    Text("Row 1")
    Image(systemName: "square.and.arrow.down")
    Image(systemName: "square.and.arrow.up")
  }
  GridRow {
    Text("Row 2")
    Image(systemName: "square.and.arrow.down")
    Image(systemName: "square.and.arrow.up")
  }
}

To display grids in Flutter, use the GridView widget. This widget has various constructors. Each constructor has a similar goal, but uses different input parameters. The following example uses the .builder() initializer:

const widgets = [
  Text('Row 1'),
  Icon(CupertinoIcons.arrow_down_square),
  Icon(CupertinoIcons.arrow_up_square),
  Text('Row 2'),
  Icon(CupertinoIcons.arrow_down_square),
  Icon(CupertinoIcons.arrow_up_square),
];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GridView.builder(
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 3,
          mainAxisExtent: 40.0,
        ),
        itemCount: widgets.length,
        itemBuilder: (context, index) => widgets[index],
      ),
    );
  }
}

The SliverGridDelegateWithFixedCrossAxisCount delegate determines various parameters that the grid uses to lay out its components. This includes crossAxisCount that dictates the number of items displayed on each row.

How SwiftUI’s Grid and Flutter’s GridView differ in that Grid requires GridRow. GridView uses the delegate to decide how the grid should lay out its components.

Creating a scroll view

In SwiftUI, you use ScrollView to create custom scrolling components. The following example displays a series of PersonView instances in a scrollable fashion.

ScrollView {
  VStack(alignment: .leading) {
    ForEach(persons) { person in
      PersonView(person: person)
    }
  }
}

To create a scrolling view, Flutter uses SingleChildScrollView. In the following example, the function mockPerson mocks instances of the Person class to create the custom PersonView widget.

    SingleChildScrollView(
  child: Column(
    children: mockPersons
        .map(
          (person) => PersonView(
            person: person,
          ),
        )
        .toList(),
  ),
),

Responsive and adaptive design

In SwiftUI, you use GeometryReader to create relative view sizes.

For example, you could:

  • Multiply geometry.size.width by some factor to set the width.
  • Use GeometryReader as a breakpoint to change the design of your app.

You can also see if the size class has .regular or .compact using horizontalSizeClass.

To create relative views in Flutter, you can use one of two options:

  • Get the BoxConstraints object in the LayoutBuilder class.
  • Use the MediaQuery.of() in your build functions to get the size and orientation of your current app.

To learn more, check out Creating responsive and adaptive apps.

Managing state

In SwiftUI, you use the @State property wrapper to represent the internal state of a SwiftUI view.

struct ContentView: View {
  @State private var counter = 0;
  var body: some View {
    VStack{
      Button("+") { counter+=1 }
      Text(String(counter))
    }
  }}

SwiftUI also includes several options for more complex state management such as the ObservableObject protocol.

Flutter manages local state using a StatefulWidget. Implement a stateful widget with the following two classes:

  • a subclass of StatefulWidget
  • a subclass of State

The State object stores the widget’s state. To change a widget’s state, call setState() from the State subclass to tell the framework to redraw the widget.

The following example shows a part of a counter app:

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('$_counter'),
            TextButton(
              onPressed: () => setState(() {
                _counter++;
              }),
              child: const Text('+'),
            ),
          ],
        ),
      ),
    );
  }
}

To learn more ways to manage state, check out State management.

Animations

Two main types of UI animations exist.

  • Implicit that animated from a current value to a new target.
  • Explicit that animates when asked.

Implicit Animation

SwiftUI and Flutter take a similar approach to animation. In both frameworks, you specify parameters like duration, and curve.

In SwiftUI, you use the animate() modifier to handle implicit animation.

Button(Tap me!){
   angle += 45
}
.rotationEffect(.degrees(angle))
.animation(.easeIn(duration: 1))

Flutter includes widgets for implicit animation. This simplifies animating common widgets. Flutter names these widgets with the following format: AnimatedFoo.

For example: To rotate a button, use the AnimatedRotation class. This animates the Transform.rotate widget.

AnimatedRotation(
  duration: const Duration(seconds: 1),
  turns: turns,
  curve: Curves.easeIn,
  child: TextButton(
      onPressed: () {
        setState(() {
          turns += .125;
        });
      },
      child: const Text('Tap me!')),
),

Flutter allows you to create custom implicit animations. To compose a new animated widget, use the TweenAnimationBuilder.

Explicit Animation

For explicit animations, SwiftUI uses the withAnimation() function.

Flutter includes explicitly animated widgets with names formatted like FooTransition. One example would be the RotationTransition class.

Flutter also allows you to create a custom explicit animation using AnimatedWidget or AnimatedBuilder.

To learn more about animations in Flutter, see Animations overview.

Drawing on the Screen

In SwiftUI, you use CoreGraphics to draw lines and shapes to the screen.

Flutter has an API based on the Canvas class, with two classes that help you draw:

  1. CustomPaint that requires a painter:

    CustomPaint(
       painter: SignaturePainter(_points),
       size: Size.infinite,
     ),
  2. CustomPainter that implements your algorithm to draw to the canvas.

    class SignaturePainter extends CustomPainter {
       SignaturePainter(this.points);
    
       final List<Offset?> points;
    
       @override
       void paint(Canvas canvas, Size size) {
         final Paint paint = Paint()
           ..color = Colors.black
           ..strokeCap = StrokeCap.round
           ..strokeWidth = 5.0;
         for (int i = 0; i < points.length - 1; i++) {
           if (points[i] != null && points[i + 1] != null) {
             canvas.drawLine(points[i]!, points[i + 1]!, paint);
           }
         }
       }
    
       @override
       bool shouldRepaint(SignaturePainter oldDelegate) =>
           oldDelegate.points != points;
     }

This section explains how to navigate between pages of an app, the push and pop mechanism, and more.

Developers build iOS and macOS apps with different pages called navigation routes.

In SwiftUI, the NavigationStack represents this stack of pages.

The following example creates an app that displays a list of persons. To display a person’s details in a new navigation link, tap on that person.

NavigationStack(path: $path) {
      List {
        ForEach(persons) { person in
          NavigationLink(
            person.name,
            value: person
          )
        }
      }
      .navigationDestination(for: Person.self) { person in
        PersonView(person: person)
      }
    }

If you have a small Flutter app without complex linking, use Navigator with named routes. After defining your navigation routes, call your navigation routes using their names.

  1. Name each route in the class passed to the runApp() function. The following example uses App:

    // Defines the route name as a constant
     // so that it's reusable.
     const detailsPageRouteName = '/details';
    
     class App extends StatelessWidget {
       const App({
         super.key,
       });
    
       @override
       Widget build(BuildContext context) {
         return CupertinoApp(
           home: const HomePage(),
           // The [routes] property defines the available named routes
           // and the widgets to build when navigating to those routes.
           routes: {
             detailsPageRouteName: (context) => const DetailsPage(),
           },
         );
       }
     }

    The following sample generates a list of persons using mockPersons(). Tapping a person pushes the person’s detail page to the Navigator using pushNamed().

    ListView.builder(
       itemCount: mockPersons.length,
       itemBuilder: (context, index) {
         final person = mockPersons.elementAt(index);
         final age = '${person.age} years old';
         return ListTile(
           title: Text(person.name),
           subtitle: Text(age),
           trailing: const Icon(
             Icons.arrow_forward_ios,
           ),
           onTap: () {
             // When a [ListTile] that represents a person is
             // tapped, push the detailsPageRouteName route
             // to the Navigator and pass the person's instance
             // to the route.
             Navigator.of(context).pushNamed(
               detailsPageRouteName,
               arguments: person,
             );
           },
         );
       },
     ),
  2. Define the DetailsPage widget that displays the details of each person. In Flutter, you can pass arguments into the widget when navigating to the new route. Extract the arguments using ModalRoute.of():

    class DetailsPage extends StatelessWidget {
       const DetailsPage({super.key});
    
       @override
       Widget build(BuildContext context) {
         // Read the person instance from the arguments.
         final Person person = ModalRoute.of(
           context,
         )?.settings.arguments as Person;
         // Extract the age.
         final age = '${person.age} years old';
         return Scaffold(
           // Display name and age.
           body: Column(children: [Text(person.name), Text(age)]),
         );
       }
     }

To create more advanced navigation and routing requirements, use a routing package such as go_router.

To learn more, check out Navigation and routing.

Manually pop back

In SwiftUI, you use the dismiss environment value to pop-back to the previous screen.

Button("Pop back") {
        dismiss()
      }

In Flutter, use the pop() function of the Navigator class:

TextButton(
  onPressed: () {
    // This code allows the
    // view to pop back to its presenter.
    Navigator.of(context).pop();
  },
  child: const Text('Pop back'),
),

In SwiftUI, you use the openURL environment variable to open a URL to another application.

@Environment(\.openURL) private var openUrl

// View code goes here

 Button("Open website") {
      openUrl(
        URL(
          string: "https://google.com"
        )!
      )
    }

In Flutter, use the url_launcher plugin.

 CupertinoButton(
   onPressed: () async {
     await launchUrl(
       Uri.parse('https://google.com'),
     );
   },
   child: const Text(
     'Open website',
   ),
),

Themes, styles, and media

You can style Flutter apps with little effort. Styling includes switching between light and dark themes, changing the design of your text and UI components, and more. This section covers how to style your apps.

Using dark mode

In SwiftUI, you call the preferredColorScheme() function on a View to use dark mode.

In Flutter, you can control light and dark mode at the app-level. To control the brightness mode, use the theme property of the App class:

    CupertinoApp(
  theme: CupertinoThemeData(
    brightness: Brightness.dark,
  ),
  home: HomePage(),
);

Styling text

In SwiftUI, you use modifier functions to style text. For example, to change the font of a Text string, use the font() modifier:

Text("Hello, world!")
  .font(.system(size: 30, weight: .heavy))
  .foregroundColor(.yellow)

To style text in Flutter, add a TextStyle widget as the value of the style parameter of the Text widget.

Text(
  'Hello, world!',
  style: TextStyle(
    fontSize: 30,
    fontWeight: FontWeight.bold,
    color: CupertinoColors.systemYellow,
  ),
),

Styling buttons

In SwiftUI, you use modifier functions to style buttons.

Button("Do something") {
    // do something when button is tapped
  }
  .font(.system(size: 30, weight: .bold))
  .background(Color.yellow)
  .foregroundColor(Color.blue)
}

To style button widgets in Flutter, set the style of its child, or modify properties on the button itself.

In the following example:

  • The color property of CupertinoButton sets its color.
  • The color property of the child Text widget sets the button text color.
child: CupertinoButton(
  color: CupertinoColors.systemYellow,
  onPressed: () {},
  padding: const EdgeInsets.all(16),
  child: const Text(
    'Do something',
    style: TextStyle(
      color: CupertinoColors.systemBlue,
      fontSize: 30,
      fontWeight: FontWeight.bold,
    ),
  ),
),

Using custom fonts

In SwiftUI, you can use a custom font in your app in two steps. First, add the font file to your SwiftUI project. After adding the file, use the .font() modifier to apply it to your UI components.

Text("Hello")
  .font(
    Font.custom(
      "BungeeSpice-Regular",
      size: 40
    )
  )

In Flutter, you control your resources with a file named pubspec.yaml. This file is platform agnostic. To add a custom font to your project, follow these steps:

  1. Create a folder called fonts in the project’s root directory. This optional step helps to organize your fonts.
  2. Add your .ttf, .otf, or .ttc font file into the fonts folder.
  3. Open the pubspec.yaml file within the project.
  4. Find the flutter section.
  5. Add your custom font(s) under the fonts section.

     flutter:
       fonts:
         - family: BungeeSpice
           fonts:
             - asset: fonts/BungeeSpice-Regular.ttf
    

After you add the font to your project, you can use it as in the following example:

Text(
  'Cupertino',
  style: TextStyle(
    fontSize: 40,
    fontFamily: 'BungeeSpice',
  ),
)

Bundling images in apps

In SwiftUI, you first add the image files to Assets.xcassets, then use the Image view to display the images.

To add images in Flutter, follow a method similar to how you added custom fonts.

  1. Add an images folder to the root directory.
  2. Add this asset to the pubspec.yaml file.

     flutter:
       assets:
         - images/Blueberries.jpg
    

After adding your image, display it using the Image widget’s .asset() constructor. This constructor:

  1. Instantiates the given image using the provided path.
  2. Reads the image from the assets bundled with your app.
  3. Displays the image on the screen.

To review a complete example, check out the Image docs.

Bundling videos in apps

In SwiftUI, you bundle a local video file with your app in two steps. First, you import the AVKit framework, then you instantiate a VideoPlayer view.

In Flutter, add the video_player plugin to your project. This plugin allows you to create a video player that works on Android, iOS, and on the web from the same codebase.

  1. Add the plugin to your app and add the video file to your project.
  2. Add the asset to your pubspec.yaml file.
  3. Use the VideoPlayerController class to load and play your video file.

To review a complete walkthrough, check out the video_player example.