Flutter for Jetpack Compose developers
Flutter is a framework for building cross-platform applications that uses the Dart programming language.
Your Jetpack Compose knowledge and experience are highly valuable when building with Flutter.
This document can be used as a reference by jumping around and finding questions that are most relevant to your needs. This guide embeds sample code. By using the "Open in DartPad" button that appears on hover or focus, you can open and run some of the examples on DartPad.
Overview
#Flutter and Jetpack Compose code describe how the UI looks and works. Developers call this type of code a declarative framework.
While there are key differences especially when it comes to interacting with legacy Android code, there are many commonalities between the two frameworks.
Composables vs. Widgets
#Jetpack Compose represents UI components as composable functions, later noted in this document as composables. Composables can be altered or decorated through the use of Modifier objects.
Text("Hello, World!",
modifier: Modifier.padding(10.dp)
)
Text("Hello, World!",
modifier = Modifier.padding(10.dp))Flutter represents UI components as widgets.
Both composables and widgets only exist until they need to change.
These languages call this property immutability.
Jetpack Compose modifies UI component properties using an optional
modifier property backed by a Modifier object.
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 Jetpack Compose and Flutter nest UI components
within one another.
Jetpack Compose nests Composables while Flutter nests Widgets.
Layout process
#Jetpack Compose and Flutter handle layout in similar ways. Both of them lay out the UI in a single pass and parent elements provide layout constraints down to their children. More specifically,
- The parent measures itself and its children recursively providing any constraints from the parent to the child.
- The children try to size themselves using the above methods and provide their own children both their constraints and any that might apply from their ancestor nodes.
- Upon encountering a leaf node (a node with no children), the size and properties are determined based on the provided constraints and the element is placed in the UI.
- With all the children sized and placed, the root nodes can determine their measurement, size, and placement.
In both Jetpack Compose and Flutter, the parent component can override or constrain the child's desired size. The widget cannot have any size it wants. It also cannot usually 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.
To learn how constraints work in Flutter, visit Understanding constraints.
Design system
#Because Flutter targets multiple platforms, your app doesn't need to conform to any design system. While this guide features Material widgets, your Flutter app can use many different design systems:
- Custom Material widgets
- Community built widgets
- Your own custom widgets
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 Jetpack Compose. 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
#For Compose apps, your main entry point will be Activity or one of its descendants, generally ComponentActivity.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
SampleTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}To start your Flutter app, pass an instance of your app to
the runApp function.
void main() {
runApp(const MyApp());
}App is a widget. It's build method describes the part of the
user interface it represents.
It's common to begin your app with a WidgetApp class,
like MaterialApp.
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
);
}
}The widget used in the 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.
Compose has a number of defaults from its ancestor Android Views. Unless otherwise specified, most components "wrap" their size to content meaning they only take up as much space as needed when rendered. That's not always the case with Flutter.
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 Compose, you use the Button composable or one of its variants
to create a button. Button is an alias for FilledTonalButton
when using a Material theme.
Button(onClick = {}) {
Text("Do something")
}To achieve the same result in Flutter,
use the FilledButton class:
FilledButton(
onPressed: () {
// This closure is called when your button is tapped.
},
const Text('Do something'),
),Flutter gives you access to a variety of buttons with pre-defined styles.
Aligning components horizontally or vertically
#Jetpack Compose and Flutter handle horizontal and vertical collections of items similarly.
The following Compose snippet adds a globe image and
text in both Row and Column containers with centering of the items:
Row(horizontalArrangement = Arrangement.Center) {
Image(Icons.Default.Public, contentDescription = "")
Text("Hello, world!")
}
Column(verticalArrangement = Arrangement.Center) {
Image(Icons.Default.Public, contentDescription = "")
Text("Hello, world!")
}Flutter uses Row and Column as well but there are some slight differences for specifying child
widgets and alignment. The following is equivalent to the Compose example.
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.public),
Text('Hello, world!'),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(MaterialIcons.globe),
Text('Hello, world!'),
],
)Row and Column require 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, inversely for Column, the main axis is the vertical axis.
Displaying a list view
#In Compose, you have a couple ways to create a list based on
the size of the list you need to display. For a small number of items
that can all be displayed at once, you can iterate over a collection
inside a Column or Row.
For a list with a large number of items, LazyList has better
performance. It only lays out the components that will be visible
versus all of them.
data class Person(val name: String)
val people = arrayOf(
Person(name = "Person 1"),
Person(name = "Person 2"),
Person(name = "Person 3")
)
@Composable
fun ListDemo(people: List<Person>) {
Column {
people.forEach {
Text(it.name)
}
}
}
@Composable
fun ListDemo2(people: List<Person>) {
LazyColumn {
items(people) { person ->
Text(person.name)
}
}
}To lazily build a list in Flutter, ....
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 conventions for lists:
-
The
ListViewwidget has a builder method. This works like theitemclosure inside a ComposeLazyList. -
The
itemCountparameter of theListViewsets how many items theListViewdisplays. -
The
itemBuilderhas 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
#Constructing a grid in Compose is similar to a
LazyList (LazyColumn or LazyRow). You can use the
same items closure. There are properties on each
grid type to specify how to arrange the items,
whether or not to use adaptive or fixed layout,
amongst others.
val widgets = arrayOf(
"Row 1",
Icons.Filled.ArrowDownward,
Icons.Filled.ArrowUpward,
"Row 2",
Icons.Filled.ArrowDownward,
Icons.Filled.ArrowUpward
)
LazyVerticalGrid (
columns = GridCells.Fixed(3),
contentPadding = PaddingValues(8.dp)
) {
items(widgets) { i ->
if (i is String) {
Text(i)
} else {
Image(i as ImageVector, "")
}
}
}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(Icons.arrow_downward),
Icon(Icons.arrow_upward),
Text('Row 2'),
Icon(Icons.arrow_downward),
Icon(Icons.arrow_upward),
];
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,
),
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.
Jetpack Compose's LazyHorizontalGrid, LazyVerticalGrid, and Flutter's GridView are somewhat
similar. GridView uses a delegate to decide how the grid
should lay out its components. The rows, columns, and other
associated properties on LazyHorizontalGrid \ LazyVerticalGrid serve the same purpose.
Creating a scroll view
#LazyColumn and LazyRow in Jetpack Compose have built-in
support for scrolling.
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
#Adaptive Design in Compose is a complex topic with many viable solutions:
- Using a custom layout
- Using
WindowSizeClassalone - Using
BoxWithConstraintsto control what is shown based on available space - Using the Material 3 adaptive library that uses
WindowSizeClassalong with specialized composable layouts for common layouts
For that reason, you are encouraged to look into the Flutter options directly and see what fits your requirements versus attempting to find something that is a one to one translation.
To create relative views in Flutter, you can use one of two options:
- Get the
BoxConstraintsobject in theLayoutBuilderclass. - 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
#Compose stores state with the remember API and descendants
of the MutableState interface.
Scaffold(
content = { padding ->
var _counter = remember { mutableIntStateOf(0) }
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize().padding(padding)) {
Text(_counter.value.toString())
Spacer(modifier = Modifier.height(16.dp))
FilledIconButton (onClick = { -> _counter.intValue += 1 }) {
Text("+")
}
}
}
)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.
Drawing on the Screen
#In Compose, you use the Canvas composable to draw
shapes, images, and text to the screen.
Flutter has an API based on the Canvas class,
with two classes that help you draw:
-
CustomPaintthat requires a painter:dartCustomPaint( painter: SignaturePainter(_points), size: Size.infinite, ), -
CustomPainterthat implements your algorithm to draw to the canvas.dartclass 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; 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; }
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 Compose, you can control light and dark at any
arbitrary level by wrapping a component with
a Theme composable.
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:
const MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
),
home: HomePage(),
);Styling text
#In Compose, you use the properties on Text for one or two
attributes or construct a TextStyle object to set many at once.
Text("Hello, world!", color = Color.Green,
fontWeight = FontWeight.Bold, fontSize = 30.sp)Text("Hello, world!",
style = TextStyle(
color = Color.Green,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
),
)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: Colors.blue,
),
),Styling buttons
#In Compose, you modify the colors of a button using
the colors property. If left unmodified, they
use the defaults from the current theme.
Button(onClick = {},
colors = ButtonDefaults.buttonColors().copy(
containerColor = Color.Yellow, contentColor = Color.Blue,
)) {
Text("Do something", fontSize = 30.sp, fontWeight = FontWeight.Bold)
}To style button widgets in Flutter, you similarly set the style of its child, or modify properties on the button itself.
FilledButton(
onPressed: (){},
style: FilledButton.styleFrom(backgroundColor: Colors.amberAccent),
child: const Text(
'Do something',
style: TextStyle(
color: Colors.blue,
fontSize: 30,
fontWeight: FontWeight.bold,
)
)
)Bundling assets for use in Flutter
#There is commonly a need to bundle resources for use in your application. They can be animations, vector graphics, images, fonts, or other general files.
Unlike native Android apps that expect a set directory structure under /res/<qualifier>/
where the qualifier could be indicating the type of file, a specific orientation,
or android version, Flutter doesn't require a specific location as long
as the referenced files are listed in the pubspec.yaml file. Below is an excerpt
from a pubspec.yaml referencing several images and a font file.
flutter:
assets:
- assets/my_icon.png
- assets/background.png
fonts:
- family: FiraSans
fonts:
- asset: fonts/FiraSans-Regular.ttfUsing fonts
#In Compose, you have two options for using fonts in your app. You can use a runtime service I to retrieve them Google Fonts. Alternatively, they may be bundled in resource files.
Flutter has similar methods to use fonts, let's discuss them both inline.
Using bundled fonts
#The following are roughly equivalent Compose and Flutter code for using a font file in the /res/ or fonts directory
as listed above.
// Font files bundled with app
val firaSansFamily = FontFamily(
Font(R.font.firasans_regular, FontWeight.Normal),
// ...
)
// Usage
Text(text = "Compose", fontFamily = firaSansFamily, fontWeight = FontWeight.Normal)Text(
'Flutter',
style: TextStyle(
fontSize: 40,
fontFamily: 'FiraSans',
),
),Using a font provider (Google Fonts)
#One point of difference is using fonts from a font provider like Google Fonts. In Compose, the instantiation is done inline with the same approximate code to reference a local file.
After instantiating a provider that references the special strings for the font service,
you would use the same FontFamily declaration.
// Font files bundled with app
val provider = GoogleFont.Provider(
providerAuthority = "com.google.android.gms.fonts",
providerPackage = "com.google.android.gms",
certificates = R.array.com_google_android_gms_fonts_certs
)
val firaSansFamily = FontFamily(
Font(
googleFont = GoogleFont("FiraSans"),
fontProvider = provider,
)
)
// Usage
Text(text = "Compose", fontFamily = firaSansFamily, fontWeight = FontWeight.Light)For Flutter, this is provided by the google_fonts plugin using the name of the font.
import 'package:google_fonts/google_fonts.dart';
//...
Text(
'Flutter',
style: GoogleFonts.firaSans(),
// or
//style: GoogleFonts.getFont('FiraSans')
),Using images
#In Compose, typically image files to the drawable directory
in resources /res/drawable and one uses Image composable to display
the images. Assets are referenced by using the resource locator
in the style of R.drawable.<file name> without the file extension.
In Flutter, the resource location is a listed in pubspec.yaml as shown in the snippet below.
flutter:
assets:
- images/Blueberries.jpgAfter adding your image, you can display it using the Image widget's
.asset() constructor. This constructor:
To review a complete example, check out the Image docs.
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2025-08-04。 查看文档源码 或者 为本页面内容提出建议.