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:
- The parent view proposes a size to its child view.
- All subsequent child views:
- propose a size to their child’s view
- ask that child what size it wants
- Each parent view renders its child view at the returned size.
Flutter differs somewhat with its process:
- The parent widget passes constraints down to its children. Constraints include minimum and maximum values for height and width.
- 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.
- 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),
)
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:
-
HStack
for horizontal stack views -
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: [
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: [
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 theForEach
within SwiftUI’sList
struct. -
The
itemCount
parameter of theListView
sets how many items theListView
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 theLayoutBuilder
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:
-
CustomPaint
that requires a painter:CustomPaint( painter: SignaturePainter(_points), size: Size.infinite, ),
-
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; }
Navigation
This section explains how to navigate between pages of an app, the push and pop mechanism, and more.
Navigating between pages
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.
-
Name each route in the class passed to the
runApp()
function. The following example usesApp
:// 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 theNavigator
usingpushNamed()
.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, ); }, ); }, ),
-
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 usingModalRoute.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'),
),
Navigating to another app
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 ofCupertinoButton
sets itscolor
. - The
color
property of the childText
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:
- Create a folder called
fonts
in the project’s root directory. This optional step helps to organize your fonts. - Add your
.ttf
,.otf
, or.ttc
font file into thefonts
folder. - Open the
pubspec.yaml
file within the project. - Find the
flutter
section. -
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.
- Add an
images
folder to the root directory. -
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:
- Instantiates the given image using the provided path.
- Reads the image from the assets bundled with your app.
- 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.
- Add the plugin to your app and add the video file to your project.
- Add the asset to your
pubspec.yaml
file. - Use the
VideoPlayerController
class to load and play your video file.
To review a complete walkthrough, check out the video_player example.