1. Giriş
Material 3, Google'ın açık kaynaklı tasarım sisteminin en son sürümüdür. Flutter, Material 3 kullanarak güzel uygulamalar oluşturma desteğini genişletiyor. Bu codelab'de boş bir Flutter uygulamasıyla başlayıp Flutter ile Material 3'ü kullanarak tamamen stilize edilmiş ve animasyonlu bir uygulama oluşturacaksınız.
Ne oluşturacaksınız?
Bu codelab'de, sahte bir mesajlaşma uygulaması oluşturacaksınız. Uygulamanız şunları yapabilecek:
- Masaüstünde veya mobil cihazda çalışması için uyarlanabilir tasarım kullanın.
- Animasyon kullanarak farklı düzenler arasında sorunsuz bir şekilde geçiş yapın.
- İfade edici stil için Material 3'ü kullanın.
- Android, iOS, web, Windows, Linux ve macOS'te çalışır.

Bu codelab, Flutter ile Material 3'e odaklanmaktadır. Alakalı olmayan kavramlar ve kod blokları işaretlenmiştir ve kopyalayıp yapıştırmanız için kullanımınıza sunulmuştur.
2. Flutter ortamınızı kurma
İhtiyacınız olanlar
- Flutter SDK'sı
- Bir düzenleyici (ör. VS Code veya Android Studio).
Bu codelab'in Android, iOS, web, Windows, Linux ve macOS'te dağıtımı test edilmiştir. Bu dağıtım hedeflerinden bazılarına dağıtım yapabilmek için ek yazılım yüklenmesi gerekir. Platformunuzun doğru şekilde ayarlanıp ayarlanmadığını anlamanın iyi bir yolu flutter doctor çalıştırmaktır.
$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.32.1, on macOS 15.5 24F74 darwin-arm64, locale en-AU) [✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 16.3) [✓] Chrome - develop for the web [✓] Android Studio (version 2024.2) [✓] IntelliJ IDEA Community Edition (version 2024.3.1.1) [✓] VS Code (version 1.100.3) [✓] Connected device (3 available) [✓] Network resources • No issues found!
Çıkışta, seçtiğiniz dağıtım hedefini etkileyen sorunlar listeleniyorsa daha ayrıntılı bilgi edinmek için flutter doctor -v komutunu çalıştırın. flutter doctor -v tarafından listelenen adımları denedikten sonra sorunu çözemezseniz Flutter topluluğu ile iletişime geçebilirsiniz.
3. Başlarken
Boş bir Flutter uygulaması oluşturma
Çoğu Flutter geliştiricisi, flutter create ile temel bir "düğme tıklamalarını sayma" uygulaması oluşturur ve ardından birkaç dakika içinde ihtiyaç duymadıklarını kaldırır. Bir uygulamayı çalışır duruma getirmek için gereken temel öğeleri içeren boş bir Flutter projesi (--empty parametresini kullanarak) oluşturabilirsiniz.
$ flutter create animated_responsive_layout --empty Creating project animated_responsive_layout... Resolving dependencies in `animated_responsive_layout`... Downloading packages... Got dependencies in `animated_responsive_layout`. Wrote 129 files. All done! You can find general documentation for Flutter at: https://docs.flutter.dev/ Detailed API documentation is available at: https://api.flutter.dev/ If you prefer video documentation, consider: https://www.youtube.com/c/flutterdev In order to run your empty application, type: $ cd animated_responsive_layout $ flutter run Your empty application code is in animated_responsive_layout/lib/main.dart.
Bu kodu, kod düzenleyiciniz aracılığıyla veya doğrudan komut satırından çalıştırabilirsiniz. Hangi araç zincirlerinin yüklü olduğuna ve simülatörlerin veya emülatörlerin çalışıp çalışmadığına bağlı olarak, uygulamayı hangi dağıtım hedefinde çalıştıracağınıza karar vermeniz istenebilir. Örneğin, "Chrome" seçeneğini belirleyerek boş uygulamayı web tarayıcısında çalıştırmayı tercih edebilirsiniz.
$ cd animated_responsive_layout $ flutter run Connected devices: macOS (desktop) • macos • darwin-arm64 • macOS 15.5 24F74 darwin-arm64 Chrome (web) • chrome • web-javascript • Google Chrome 137.0.7151.56 [1]: macOS (macos) [2]: Chrome (chrome) Please choose one (or "q" to quit): 2 Launching lib/main.dart on Chrome in debug mode... Waiting for connection from debug service on Chrome... 6.4s This app is linked to the debug service: ws://127.0.0.1:60848/AM68Aq_ZiB8=/ws Debug service listening on ws://127.0.0.1:60848/AM68Aq_ZiB8=/ws Flutter run key commands. R Hot restart. h List all available interactive commands. d Detach (terminate "flutter run" but leave application running). c Clear the screen q Quit (terminate the application on the device). A Dart VM Service on Chrome is available at: http://127.0.0.1:60848/AM68Aq_ZiB8= The Flutter DevTools debugger and profiler on Chrome is available at: http://127.0.0.1:9100?uri=http://127.0.0.1:60848/AM68Aq_ZiB8= Application finished.
Bu senaryoda, boş uygulamanın Chrome web tarayıcısında çalıştığını görürsünüz. Ayrıca, Android, iOS veya masaüstü işletim sisteminizde çalıştırmayı da seçebilirsiniz.

4. Mesajlaşma uygulaması oluşturma
Avatar oluşturma
Her mesajlaşma uygulamasının kullanıcılarının resimlerine ihtiyacı vardır. Bu resimler kullanıcıları temsil eder ve avatar olarak adlandırılır. Ardından, proje ağacının en üstünde bir öğeler dizini oluşturun ve bu dizini bu codelab'in Git deposundaki bir dizi resimle doldurun. Bunu yapmanın bir yolu, wget komut satırı aracını aşağıdaki gibi kullanmaktır.
$ mkdir assets
$ cd assets
$ for name in avatar_1 avatar_2 avatar_3 avatar_4 \
avatar_5 avatar_6 avatar_7 thumbnail_1; \
do wget https://raw.githubusercontent.com/flutter/codelabs/main/animated-responsive-layout/step_04/assets/$name.png ; \
done
Bu işlem, aşağıdaki resimleri uygulamanızın assets dizinine indirir:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Avatar resim öğelerini aldıktan sonra bunları pubspec.yaml dosyasına aşağıdaki şekilde eklemeniz gerekir:
pubspec.yaml
name: animated_responsive_layout
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
environment:
sdk: ^3.8.0
dependencies:
flutter:
sdk: flutter
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
# Add from here...
assets:
- assets/avatar_1.png
- assets/avatar_2.png
- assets/avatar_3.png
- assets/avatar_4.png
- assets/avatar_5.png
- assets/avatar_6.png
- assets/avatar_7.png
- assets/thumbnail_1.png
# ... to here.
Uygulama, gösterdiği mesajlar için bir veri kaynağı gerektirir. Projenizin lib dizininde bir models alt dizini oluşturun. Bu işlemi komut satırında mkdir ile veya tercih ettiğiniz metin düzenleyicide yapabilirsiniz. lib/models dizininde aşağıdaki içeriğe sahip bir models.dart dosyası oluşturun:
lib/models/models.dart
class Attachment {
const Attachment({required this.url});
final String url;
}
class Email {
const Email({
required this.sender,
required this.recipients,
required this.subject,
required this.content,
this.replies = 0,
this.attachments = const [],
});
final User sender;
final List<User> recipients;
final String subject;
final String content;
final List<Attachment> attachments;
final double replies;
}
class Name {
const Name({required this.first, required this.last});
final String first;
final String last;
String get fullName => '$first $last';
}
class User {
const User({
required this.name,
required this.avatarUrl,
required this.lastActive,
});
final Name name;
final String avatarUrl;
final DateTime lastActive;
}
Verilerin şekliyle ilgili bir tanımınız olduğuna göre, lib/models dizininde aşağıdaki içerikle bir data.dart dosyası oluşturun:
lib/models/data.dart
import 'models.dart';
final User user_0 = User(
name: const Name(first: 'Me', last: ''),
avatarUrl: 'assets/avatar_1.png',
lastActive: DateTime.now(),
);
final User user_1 = User(
name: const Name(first: '老', last: '强'),
avatarUrl: 'assets/avatar_2.png',
lastActive: DateTime.now().subtract(const Duration(minutes: 10)),
);
final User user_2 = User(
name: const Name(first: 'So', last: 'Duri'),
avatarUrl: 'assets/avatar_3.png',
lastActive: DateTime.now().subtract(const Duration(minutes: 20)),
);
final User user_3 = User(
name: const Name(first: 'Lily', last: 'MacDonald'),
avatarUrl: 'assets/avatar_4.png',
lastActive: DateTime.now().subtract(const Duration(hours: 2)),
);
final User user_4 = User(
name: const Name(first: 'Ziad', last: 'Aouad'),
avatarUrl: 'assets/avatar_5.png',
lastActive: DateTime.now().subtract(const Duration(hours: 6)),
);
final List<Email> emails = [
Email(
sender: user_1,
recipients: [],
subject: '豆花鱼',
content: '最近忙吗?昨晚我去了你最爱的那家饭馆,点了他们的特色豆花鱼,吃着吃着就想你了。',
),
Email(
sender: user_2,
recipients: [],
subject: 'Dinner Club',
content:
'I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intrigued by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)\n\nSo',
),
Email(
sender: user_3,
recipients: [],
subject: 'This food show is made for you',
content:
'Ping– you'd love this new food show I started watching. It's produced by a Thai drummer who started getting recognized for the amazing vegan food she always brought to shows.',
attachments: [const Attachment(url: 'assets/thumbnail_1.png')],
),
Email(
sender: user_4,
recipients: [],
subject: 'Volunteer EMT with me?',
content:
'What do you think about training to be volunteer EMTs? We could do it together for moral support. Think about it??',
),
];
final List<Email> replies = [
Email(
sender: user_2,
recipients: [user_3, user_2],
subject: 'Dinner Club',
content:
'I think it's time for us to finally try that new noodle shop downtown that doesn't use menus. Anyone else have other suggestions for dinner club this week? I'm so intrigued by this idea of a noodle restaurant where no one gets to order for themselves - could be fun, or terrible, or both :)\n\nSo',
),
Email(
sender: user_0,
recipients: [user_3, user_2],
subject: 'Dinner Club',
content:
'Yes! I forgot about that place! I'm definitely up for taking a risk this week and handing control over to this mysterious noodle chef. I wonder what happens if you have allergies though? Lucky none of us have any otherwise I'd be a bit concerned.\n\nThis is going to be great. See you all at the usual time?',
),
];
Bu verilerle birlikte, verileri görüntülemek için birkaç widget tanımlamanın zamanı geldi. lib altında widgets adlı bir alt dizin oluşturun. widgets içinde dört dosya oluşturacaksınız ve dördü de oluşturulana kadar düzenleyicinizden bazı uyarılar alacaksınız. Bu codelab'in amacının, uygulamayı Material 3 kullanarak stilize etmek olduğunu unutmayın. Bu nedenle, aşağıdaki dört dosyayı listelenen içeriklerle birlikte ekleyin:
lib/widgets/email_list_view.dart
import 'package:flutter/material.dart';
import '../models/data.dart' as data;
import '../models/models.dart';
import 'email_widget.dart';
import 'search_bar.dart' as search_bar;
class EmailListView extends StatelessWidget {
const EmailListView({
super.key,
this.selectedIndex,
this.onSelected,
required this.currentUser,
});
final int? selectedIndex;
final ValueChanged<int>? onSelected;
final User currentUser;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ListView(
children: [
const SizedBox(height: 8),
search_bar.SearchBar(currentUser: currentUser),
const SizedBox(height: 8),
...List.generate(data.emails.length, (index) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: EmailWidget(
email: data.emails[index],
onSelected: onSelected != null
? () {
onSelected!(index);
}
: null,
isSelected: selectedIndex == index,
),
);
}),
],
),
);
}
}
E-posta listesi görüntüleyebilmek, bir mesajlaşma uygulamasının yapabilmesi gereken bir şey gibi görünüyor. Düzenleyiciden birkaç şikayet alacaksınız ancak bir sonraki dosyayı ekleyerek bazılarını düzeltebilirsiniz email_widget.dart.
lib/widgets/email_widget.dart
import 'package:flutter/material.dart';
import '../models/models.dart';
import 'star_button.dart';
enum EmailType { preview, threaded, primaryThreaded }
class EmailWidget extends StatefulWidget {
const EmailWidget({
super.key,
required this.email,
this.isSelected = false,
this.isPreview = true,
this.isThreaded = false,
this.showHeadline = false,
this.onSelected,
});
final bool isSelected;
final bool isPreview;
final bool showHeadline;
final bool isThreaded;
final void Function()? onSelected;
final Email email;
@override
State<EmailWidget> createState() => _EmailWidgetState();
}
class _EmailWidgetState extends State<EmailWidget> {
late final ColorScheme _colorScheme = Theme.of(context).colorScheme;
late Color unselectedColor = Color.alphaBlend(
_colorScheme.primary.withAlpha(20),
_colorScheme.surface,
);
Color get _surfaceColor => switch (widget) {
EmailWidget(isPreview: false) => _colorScheme.surface,
EmailWidget(isSelected: true) => _colorScheme.primaryContainer,
_ => unselectedColor,
};
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: widget.onSelected,
child: Card(
elevation: 0,
color: _surfaceColor,
clipBehavior: Clip.hardEdge,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (widget.showHeadline) ...[
EmailHeadline(email: widget.email, isSelected: widget.isSelected),
],
EmailContent(
email: widget.email,
isPreview: widget.isPreview,
isThreaded: widget.isThreaded,
isSelected: widget.isSelected,
),
],
),
),
);
}
}
class EmailContent extends StatefulWidget {
const EmailContent({
super.key,
required this.email,
required this.isPreview,
required this.isThreaded,
required this.isSelected,
});
final Email email;
final bool isPreview;
final bool isThreaded;
final bool isSelected;
@override
State<EmailContent> createState() => _EmailContentState();
}
class _EmailContentState extends State<EmailContent> {
late final ColorScheme _colorScheme = Theme.of(context).colorScheme;
late final TextTheme _textTheme = Theme.of(context).textTheme;
Widget get contentSpacer => SizedBox(height: widget.isThreaded ? 20 : 2);
String get lastActiveLabel {
final DateTime now = DateTime.now();
if (widget.email.sender.lastActive.isAfter(now)) throw ArgumentError();
final Duration elapsedTime = widget.email.sender.lastActive
.difference(now)
.abs();
return switch (elapsedTime) {
Duration(inSeconds: < 60) => '${elapsedTime.inSeconds}s',
Duration(inMinutes: < 60) => '${elapsedTime.inMinutes}m',
Duration(inHours: < 24) => '${elapsedTime.inHours}h',
Duration(inDays: < 365) => '${elapsedTime.inDays}d',
_ => throw UnimplementedError(),
};
}
TextStyle? get contentTextStyle => switch (widget) {
EmailContent(isThreaded: true) => _textTheme.bodyLarge,
EmailContent(isSelected: true) => _textTheme.bodyMedium?.copyWith(
color: _colorScheme.onPrimaryContainer,
),
_ => _textTheme.bodyMedium?.copyWith(color: _colorScheme.onSurfaceVariant),
};
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LayoutBuilder(
builder: (context, constraints) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (constraints.maxWidth - 200 > 0) ...[
CircleAvatar(
backgroundImage: AssetImage(
widget.email.sender.avatarUrl,
),
),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 6.0),
),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.email.sender.name.fullName,
overflow: TextOverflow.fade,
maxLines: 1,
style: widget.isSelected
? _textTheme.labelMedium?.copyWith(
color: _colorScheme.onSecondaryContainer,
)
: _textTheme.labelMedium?.copyWith(
color: _colorScheme.onSurface,
),
),
Text(
lastActiveLabel,
overflow: TextOverflow.fade,
maxLines: 1,
style: widget.isSelected
? _textTheme.labelMedium?.copyWith(
color: _colorScheme.onSecondaryContainer,
)
: _textTheme.labelMedium?.copyWith(
color: _colorScheme.onSurfaceVariant,
),
),
],
),
),
if (constraints.maxWidth - 200 > 0) ...[const StarButton()],
],
);
},
),
const SizedBox(width: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.isPreview) ...[
Text(
widget.email.subject,
style: const TextStyle(
fontSize: 18,
).copyWith(color: _colorScheme.onSurface),
),
],
if (widget.isThreaded) ...[
contentSpacer,
Text(
"To ${widget.email.recipients.map((recipient) => recipient.name.first).join(", ")}",
style: _textTheme.bodyMedium,
),
],
contentSpacer,
Text(
widget.email.content,
maxLines: widget.isPreview ? 2 : 100,
overflow: TextOverflow.ellipsis,
style: contentTextStyle,
),
],
),
const SizedBox(width: 12),
widget.email.attachments.isNotEmpty
? Container(
height: 96,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
image: DecorationImage(
fit: BoxFit.cover,
image: AssetImage(widget.email.attachments.first.url),
),
),
)
: const SizedBox.shrink(),
if (!widget.isPreview) ...[const EmailReplyOptions()],
],
),
);
}
}
class EmailHeadline extends StatefulWidget {
const EmailHeadline({
super.key,
required this.email,
required this.isSelected,
});
final Email email;
final bool isSelected;
@override
State<EmailHeadline> createState() => _EmailHeadlineState();
}
class _EmailHeadlineState extends State<EmailHeadline> {
late final TextTheme _textTheme = Theme.of(context).textTheme;
late final ColorScheme _colorScheme = Theme.of(context).colorScheme;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Container(
height: 84,
color: Color.alphaBlend(
_colorScheme.primary.withAlpha(12),
_colorScheme.surface,
),
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 12, 12, 12),
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.email.subject,
maxLines: 1,
overflow: TextOverflow.fade,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w400,
),
),
Text(
'${widget.email.replies.toString()} Messages',
maxLines: 1,
overflow: TextOverflow.fade,
style: _textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
],
),
),
// Display a "condensed" version if the widget in the row are
// expected to overflow.
if (constraints.maxWidth - 200 > 0) ...[
SizedBox(
height: 40,
width: 40,
child: FloatingActionButton(
onPressed: () {},
elevation: 0,
backgroundColor: _colorScheme.surface,
child: const Icon(Icons.delete_outline),
),
),
const Padding(padding: EdgeInsets.only(right: 8.0)),
SizedBox(
height: 40,
width: 40,
child: FloatingActionButton(
onPressed: () {},
elevation: 0,
backgroundColor: _colorScheme.surface,
child: const Icon(Icons.more_vert),
),
),
],
],
),
),
);
},
);
}
}
class EmailReplyOptions extends StatefulWidget {
const EmailReplyOptions({super.key});
@override
State<EmailReplyOptions> createState() => _EmailReplyOptionsState();
}
class _EmailReplyOptionsState extends State<EmailReplyOptions> {
late final ColorScheme _colorScheme = Theme.of(context).colorScheme;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 100) {
return const SizedBox.shrink();
}
return Row(
children: [
Expanded(
child: TextButton(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
_colorScheme.onInverseSurface,
),
),
onPressed: () {},
child: Text(
'Reply',
style: TextStyle(color: _colorScheme.onSurfaceVariant),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextButton(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.all(
_colorScheme.onInverseSurface,
),
),
onPressed: () {},
child: Text(
'Reply All',
style: TextStyle(color: _colorScheme.onSurfaceVariant),
),
),
),
],
);
},
);
}
}
Evet, bu widget'ta çok fazla şey oluyor. Özellikle rengin widget'ta nasıl uygulandığını görmek için bu özelliği ayrıntılı olarak incelemenizi öneririz. Bu durum, tekrarlanan bir tema haline gelecek. Sonraki, search_bar.dart.
lib/widgets/search_bar.dart
import 'package:flutter/material.dart';
import '../models/models.dart';
class SearchBar extends StatelessWidget {
const SearchBar({super.key, required this.currentUser});
final User currentUser;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 56,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
color: Colors.white,
),
padding: const EdgeInsets.fromLTRB(31, 12, 12, 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.search),
const SizedBox(width: 23.5),
Expanded(
child: TextField(
maxLines: 1,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
hintText: 'Search replies',
hintStyle: Theme.of(context).textTheme.bodyMedium,
),
),
),
CircleAvatar(backgroundImage: AssetImage(currentUser.avatarUrl)),
],
),
),
);
}
}
Çok daha basit ve durum bilgisiz bir widget. Ardından, başka bir widget ekleyin: star_button.dart
lib/widgets/star_button.dart
impoimport 'package:flutter/material.dart';
class StarButton extends StatefulWidget {
const StarButton({super.key});
@override
State<StarButton> createState() => _StarButtonState();
}
class _StarButtonState extends State<StarButton> {
bool state = false;
late final ColorScheme _colorScheme = Theme.of(context).colorScheme;
Icon get icon {
final IconData iconData = state ? Icons.star : Icons.star_outline;
return Icon(iconData, color: Colors.grey, size: 20);
}
void _toggle() {
setState(() {
state = !state;
});
}
double get turns => state ? 1 : 0;
@override
Widget build(BuildContext context) {
return AnimatedRotation(
turns: turns,
curve: Curves.decelerate,
duration: const Duration(milliseconds: 300),
child: FloatingActionButton(
elevation: 0,
shape: const CircleBorder(),
backgroundColor: _colorScheme.surface,
onPressed: () => _toggle(),
child: Padding(padding: const EdgeInsets.all(10.0), child: icon),
),
);
}
}
Ardından, gösterinin başrol oyuncusu lib/main.dart'yı güncelleyin. Dosyanın mevcut içeriğini aşağıdakiyle değiştirin.
lib/main.dart
import 'package:flutter/material.dart';
import 'models/data.dart' as data;
import 'models/models.dart';
import 'widgets/email_list_view.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
home: Feed(currentUser: data.user_0),
);
}
}
class Feed extends StatefulWidget {
const Feed({super.key, required this.currentUser});
final User currentUser;
@override
State<Feed> createState() => _FeedState();
}
class _FeedState extends State<Feed> {
late final _colorScheme = Theme.of(context).colorScheme;
late final _backgroundColor = Color.alphaBlend(
_colorScheme.primary.withAlpha(36),
_colorScheme.surface,
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: _backgroundColor,
child: EmailListView(currentUser: widget.currentUser),
),
floatingActionButton: FloatingActionButton(
backgroundColor: _colorScheme.tertiaryContainer,
foregroundColor: _colorScheme.onTertiaryContainer,
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
Nereden başlayacağınızı görmek için uygulamayı çalıştırın.

5. Gezinme çubuğu ekleme
Önceki adımın sonunda, başlangıç uygulamasında mesaj listesi vardı ancak başka pek bir şey yoktu. Bu adımda, NavigationBar ekleyerek görsel olarak daha ilgi çekici hale getireceksiniz. Uygulama, kullanıcı arayüzü taslağından gerçek bir uygulamaya dönüştüğünde gezinme çubuğu, kullanıcının kullanabileceği farklı uygulama alanları sunar.
NavigationBar olması, gidilecek hedefler olduğu anlamına gelir. lib dizininde destinations.dart adlı yeni bir dosya oluşturun ve aşağıdaki kodla doldurun.
lib/destinations.dart
import 'package:flutter/material.dart';
class Destination {
const Destination(this.icon, this.label);
final IconData icon;
final String label;
}
const List<Destination> destinations = <Destination>[
Destination(Icons.inbox_rounded, 'Inbox'),
Destination(Icons.article_outlined, 'Articles'),
Destination(Icons.messenger_outline_rounded, 'Messages'),
Destination(Icons.group_outlined, 'Groups'),
];
Bu durumda uygulama, NavigationBar simgesinin gösterileceği dört hedef belirleyebilir. Ardından, bu hedef listesini lib/main.dart dosyasına aşağıdaki şekilde bağlayın:
lib/main.dart
import 'package:flutter/material.dart';
import 'destinations.dart'; // Add this import
import 'models/data.dart' as data;
import 'models/models.dart';
import 'widgets/email_list_view.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
home: Feed(currentUser: data.user_0),
);
}
}
class Feed extends StatefulWidget {
const Feed({super.key, required this.currentUser});
final User currentUser;
@override
State<Feed> createState() => _FeedState();
}
class _FeedState extends State<Feed> {
late final _colorScheme = Theme.of(context).colorScheme;
late final _backgroundColor = Color.alphaBlend(
_colorScheme.primary.withAlpha(36),
_colorScheme.surface,
);
int selectedIndex = 0; // Add this variable
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: _backgroundColor,
child: EmailListView(
// Add from here...
selectedIndex: selectedIndex,
onSelected: (index) {
setState(() {
selectedIndex = index;
});
},
// ... to here.
currentUser: widget.currentUser,
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: _colorScheme.tertiaryContainer,
foregroundColor: _colorScheme.onTertiaryContainer,
onPressed: () {},
child: const Icon(Icons.add),
),
// Add from here...
bottomNavigationBar: NavigationBar(
elevation: 0,
backgroundColor: Colors.white,
destinations: destinations.map<NavigationDestination>((d) {
return NavigationDestination(icon: Icon(d.icon), label: d.label);
}).toList(),
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
),
// ...to here.
);
}
}
Her hedef için farklı içerikler tanımlamak yerine, NavigationBar bölümünde seçilen hedefi yansıtacak şekilde tek tek iletilerin durumunu değiştirin. Tutarlılık için bu işlem tersine de çalışır: Bir ileti seçildiğinde NavigationBar bölümünde ilgili hedef gösterilir. Bu değişiklikleri doğrulamak için uygulamayı çalıştırın:

Bu, dar bir yapılandırmada makul görünür ancak pencereyi genişletirseniz veya telefon simülatörünü yatay olarak döndürürseniz biraz garip görünür. Bu sorunu düzeltmek için uygulama yeterince geniş olduğunda ekranın sol tarafına bir NavigationRail ekleyin. Bu işlem sonraki adımda ele alınır.
6. NavigationRail ekleme
Bu adım, başvurunuza NavigationRail ekler. Buradaki amaç, ekran boyutuna bağlı olarak iki gezinme widget'ından yalnızca birini göstermektir. Bu nedenle, gerektiğinde NavigationBar simgesini gizlemeniz veya göstermeniz gerekir. lib/widgets dizininde bir disappearing_bottom_navigation_bar.dart dosyası oluşturun ve aşağıdaki kodu ekleyin:
lib/widgets/disappearing_bottom_navigation_bar.dart
import 'package:flutter/material.dart';
import '../destinations.dart';
class DisappearingBottomNavigationBar extends StatelessWidget {
const DisappearingBottomNavigationBar({
super.key,
required this.selectedIndex,
this.onDestinationSelected,
});
final int selectedIndex;
final ValueChanged<int>? onDestinationSelected;
@override
Widget build(BuildContext context) {
return NavigationBar(
elevation: 0,
backgroundColor: Colors.white,
destinations: destinations.map<NavigationDestination>((d) {
return NavigationDestination(icon: Icon(d.icon), label: d.label);
}).toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
);
}
}
Aynı dizine aşağıdaki kodu içeren disappearing_navigation_rail.dart adlı başka bir dosya ekleyin:
lib/widgets/disappearing_navigation_rail.dart
import 'package:flutter/material.dart';
import '../destinations.dart';
class DisappearingNavigationRail extends StatelessWidget {
const DisappearingNavigationRail({
super.key,
required this.backgroundColor,
required this.selectedIndex,
this.onDestinationSelected,
});
final Color backgroundColor;
final int selectedIndex;
final ValueChanged<int>? onDestinationSelected;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return NavigationRail(
selectedIndex: selectedIndex,
backgroundColor: backgroundColor,
onDestinationSelected: onDestinationSelected,
leading: Column(
children: [
IconButton(onPressed: () {}, icon: const Icon(Icons.menu)),
const SizedBox(height: 8),
FloatingActionButton(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(15)),
),
backgroundColor: colorScheme.tertiaryContainer,
foregroundColor: colorScheme.onTertiaryContainer,
onPressed: () {},
child: const Icon(Icons.add),
),
],
),
groupAlignment: -0.85,
destinations: destinations.map((d) {
return NavigationRailDestination(
icon: Icon(d.icon),
label: Text(d.label),
);
}).toList(),
);
}
}
Gezinme deyimleri kendi widget'larına göre yeniden düzenlendiğinden lib/main.dart dosyasında bazı değişiklikler yapılması gerekir:
lib/main.dart
import 'package:flutter/material.dart';
// Remove the destination.dart import, it's not required
import 'models/data.dart' as data;
import 'models/models.dart';
import 'widgets/disappearing_bottom_navigation_bar.dart'; // Add import
import 'widgets/disappearing_navigation_rail.dart'; // Add import
import 'widgets/email_list_view.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
home: Feed(currentUser: data.user_0),
);
}
}
class Feed extends StatefulWidget {
const Feed({super.key, required this.currentUser});
final User currentUser;
@override
State<Feed> createState() => _FeedState();
}
class _FeedState extends State<Feed> {
late final _colorScheme = Theme.of(context).colorScheme;
late final _backgroundColor = Color.alphaBlend(
_colorScheme.primary.withAlpha(36),
_colorScheme.surface,
);
int selectedIndex = 0;
// Add from here...
bool wideScreen = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final double width = MediaQuery.of(context).size.width;
wideScreen = width > 600;
}
// ... to here.
@override
Widget build(BuildContext context) {
// Modify from here...
return Scaffold(
body: Row(
children: [
if (wideScreen)
DisappearingNavigationRail(
selectedIndex: selectedIndex,
backgroundColor: _backgroundColor,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
),
Expanded(
child: Container(
color: _backgroundColor,
child: EmailListView(
selectedIndex: selectedIndex,
onSelected: (index) {
setState(() {
selectedIndex = index;
});
},
currentUser: widget.currentUser,
),
),
),
],
),
floatingActionButton: wideScreen
? null
: FloatingActionButton(
backgroundColor: _colorScheme.tertiaryContainer,
foregroundColor: _colorScheme.onTertiaryContainer,
onPressed: () {},
child: const Icon(Icons.add),
),
bottomNavigationBar: wideScreen
? null
: DisappearingBottomNavigationBar(
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
),
);
// ... to here.
}
}
main.dart dosyasında yapılan ilk önemli değişiklik, kullanıcının ekranı yeniden boyutlandırdığı her seferde (ör. tarayıcı penceresini yeniden boyutlandırarak veya telefonu döndürerek) güncellenen bir wideScreen durumunun eklenmesidir. Bir sonraki değişiklik, uygulamanın wideScreen modunda olup olmamasına bağlı olarak NavigationBar ve FloatingActionButton değerlerini değiştirir. Son olarak, ekran yeterince genişse NavigationRail simgesi koşullu olarak sol tarafta gösterilir. Uygulamayı web'de veya masaüstünde çalıştırın ve ekranı iki farklı düzeni gösterecek şekilde yeniden boyutlandırın.
|
|
İki farklı düzenin olması iyi olsa da ikisi arasındaki geçiş pek iyi değil. Çubuğu rayla (veya tam tersi) daha dinamik bir şekilde değiştirmek bu uygulamayı önemli ölçüde iyileştirir. Bu animasyonu sonraki adımda ekleyeceksiniz.
7. Geçişlere animasyon ekleme
Animasyonlu bir deneyim oluşturmak, her bileşenin uygun şekilde koreografisini yaparak bir dizi animasyon oluşturmayı içerir. Bu animasyon için öncelikle lib dizininde, ihtiyacınız olan animasyon eğrilerini içeren animations.dart adlı yeni bir dosya oluşturarak başlayacaksınız.
lib/animations.dart
import 'package:flutter/animation.dart';
class BarAnimation extends ReverseAnimation {
BarAnimation({required AnimationController parent})
: super(
CurvedAnimation(
parent: parent,
curve: const Interval(0, 1 / 5),
reverseCurve: const Interval(1 / 5, 4 / 5),
),
);
}
class OffsetAnimation extends CurvedAnimation {
OffsetAnimation({required super.parent})
: super(
curve: const Interval(
2 / 5,
3 / 5,
curve: Curves.easeInOutCubicEmphasized,
),
reverseCurve: Interval(
4 / 5,
1,
curve: Curves.easeInOutCubicEmphasized.flipped,
),
);
}
class RailAnimation extends CurvedAnimation {
RailAnimation({required super.parent})
: super(
curve: const Interval(0 / 5, 4 / 5),
reverseCurve: const Interval(3 / 5, 1),
);
}
class RailFabAnimation extends CurvedAnimation {
RailFabAnimation({required super.parent})
: super(curve: const Interval(3 / 5, 1));
}
class ScaleAnimation extends CurvedAnimation {
ScaleAnimation({required super.parent})
: super(
curve: const Interval(
3 / 5,
4 / 5,
curve: Curves.easeInOutCubicEmphasized,
),
reverseCurve: Interval(
3 / 5,
1,
curve: Curves.easeInOutCubicEmphasized.flipped,
),
);
}
class ShapeAnimation extends CurvedAnimation {
ShapeAnimation({required super.parent})
: super(
curve: const Interval(
2 / 5,
3 / 5,
curve: Curves.easeInOutCubicEmphasized,
),
);
}
class SizeAnimation extends CurvedAnimation {
SizeAnimation({required super.parent})
: super(
curve: const Interval(
0 / 5,
3 / 5,
curve: Curves.easeInOutCubicEmphasized,
),
reverseCurve: Interval(
2 / 5,
1,
curve: Curves.easeInOutCubicEmphasized.flipped,
),
);
}
Bu eğrilerin geliştirilmesi için yineleme gerekir. Flutter'ın anında yeniden yükleme özelliği bu işlemi çok daha kolay hale getirir. Bu animasyonları kullanmak için bazı geçişler gerekir. lib dizininde transitions adlı bir alt dizin oluşturun ve aşağıdaki kodu içeren bottom_bar_transition.dart adlı bir dosya ekleyin:
lib/transitions/bottom_bar_transition.dart
import 'package:flutter/material.dart';
import '../animations.dart';
class BottomBarTransition extends StatefulWidget {
const BottomBarTransition({
super.key,
required this.animation,
required this.backgroundColor,
required this.child,
});
final Animation<double> animation;
final Color backgroundColor;
final Widget child;
@override
State<BottomBarTransition> createState() => _BottomBarTransition();
}
class _BottomBarTransition extends State<BottomBarTransition> {
late final Animation<Offset> offsetAnimation = Tween<Offset>(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(OffsetAnimation(parent: widget.animation));
late final Animation<double> heightAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(SizeAnimation(parent: widget.animation));
@override
Widget build(BuildContext context) {
return ClipRect(
child: DecoratedBox(
decoration: BoxDecoration(color: widget.backgroundColor),
child: Align(
alignment: Alignment.topLeft,
heightFactor: heightAnimation.value,
child: FractionalTranslation(
translation: offsetAnimation.value,
child: widget.child,
),
),
),
);
}
}
lib/transitions dizinine nav_rail_transition.dart adlı başka bir dosya ekleyin ve aşağıdaki kodu ekleyin:
lib/transitions/nav_rail_transition.dart
import 'package:flutter/material.dart';
import '../animations.dart';
class NavRailTransition extends StatefulWidget {
const NavRailTransition({
super.key,
required this.animation,
required this.backgroundColor,
required this.child,
});
final Animation<double> animation;
final Widget child;
final Color backgroundColor;
@override
State<NavRailTransition> createState() => _NavRailTransitionState();
}
class _NavRailTransitionState extends State<NavRailTransition> {
// The animations are only rebuilt by this method when the text
// direction changes because this widget only depends on Directionality.
late final bool ltr = Directionality.of(context) == TextDirection.ltr;
late final Animation<Offset> offsetAnimation = Tween<Offset>(
begin: ltr ? const Offset(-1, 0) : const Offset(1, 0),
end: Offset.zero,
).animate(OffsetAnimation(parent: widget.animation));
late final Animation<double> widthAnimation = Tween<double>(
begin: 0,
end: 1,
).animate(SizeAnimation(parent: widget.animation));
@override
Widget build(BuildContext context) {
return ClipRect(
child: DecoratedBox(
decoration: BoxDecoration(color: widget.backgroundColor),
child: AnimatedBuilder(
animation: widthAnimation,
builder: (context, child) {
return Align(
alignment: Alignment.topLeft,
widthFactor: widthAnimation.value,
child: FractionalTranslation(
translation: offsetAnimation.value,
child: widget.child,
),
);
},
),
),
);
}
}
Bu iki geçiş widget'ı, görünme ve kaybolma animasyonlarını yapmak için gezinme çubuğu ve çubuk widget'larını sarmalar. Bu iki geçiş widget'ını kullanmak için disappearing_bottom_navigation_bar.dart ile başlayarak iki widget'ı güncelleyin:
lib/widgets/disappearing_bottom_navigation_bar.dart
import 'package:flutter/material.dart';
import '../animations.dart'; // Add this import
import '../destinations.dart';
import '../transitions/bottom_bar_transition.dart'; // Add this import
class DisappearingBottomNavigationBar extends StatelessWidget {
const DisappearingBottomNavigationBar({
super.key,
required this.barAnimation, // Add this parameter
required this.selectedIndex,
this.onDestinationSelected,
});
final BarAnimation barAnimation; // Add this variable
final int selectedIndex;
final ValueChanged<int>? onDestinationSelected;
@override
Widget build(BuildContext context) {
// Modify from here...
return BottomBarTransition(
animation: barAnimation,
backgroundColor: Colors.white,
child: NavigationBar(
elevation: 0,
backgroundColor: Colors.white,
destinations: destinations.map<NavigationDestination>((d) {
return NavigationDestination(icon: Icon(d.icon), label: d.label);
}).toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
);
// ... to here.
}
}
Önceki değişiklik, animasyonlardan birini ekler ve bir geçişi entegre eder. Bu sayede, gezinme çubuğunun nasıl görünüp kaybolacağını kontrol edebilirsiniz.
Ardından, disappearing_navigation_rail.dart öğesini aşağıdaki şekilde değiştirin:
lib/widgets/disappearing_navigation_rail.dart
import 'package:flutter/material.dart';
import '../animations.dart'; // Add this import
import '../destinations.dart';
import '../transitions/nav_rail_transition.dart'; // Add this import
import 'animated_floating_action_button.dart'; // Add this import
class DisappearingNavigationRail extends StatelessWidget {
const DisappearingNavigationRail({
super.key,
required this.railAnimation, // Add this parameter
required this.railFabAnimation, // Add this parameter
required this.backgroundColor,
required this.selectedIndex,
this.onDestinationSelected,
});
final RailAnimation railAnimation; // Add this variable
final RailFabAnimation railFabAnimation; // Add this variable
final Color backgroundColor;
final int selectedIndex;
final ValueChanged<int>? onDestinationSelected;
@override
Widget build(BuildContext context) {
// Delete colorScheme
// Modify from here ...
return NavRailTransition(
animation: railAnimation,
backgroundColor: backgroundColor,
child: NavigationRail(
selectedIndex: selectedIndex,
backgroundColor: backgroundColor,
onDestinationSelected: onDestinationSelected,
leading: Column(
children: [
IconButton(onPressed: () {}, icon: const Icon(Icons.menu)),
const SizedBox(height: 8),
AnimatedFloatingActionButton(
animation: railFabAnimation,
elevation: 0,
onPressed: () {},
child: const Icon(Icons.add),
),
],
),
groupAlignment: -0.85,
destinations: destinations.map((d) {
return NavigationRailDestination(
icon: Icon(d.icon),
label: Text(d.label),
);
}).toList(),
),
);
// ... to here.
}
}
Önceki kodu girerken muhtemelen tanımlanmamış bir widget'la ilgili bir dizi hata uyarısı (FloatingActionButton) almıştınız. Bu sorunu düzeltmek için animated_floating_action_button.dart adlı bir dosyayı lib/widgets konumuna aşağıdaki kodla ekleyin:
lib/widgets/animated_floating_action_button.dart
import 'dart:ui';
import 'package:flutter/material.dart';
import '../animations.dart';
class AnimatedFloatingActionButton extends StatefulWidget {
const AnimatedFloatingActionButton({
super.key,
required this.animation,
this.elevation,
this.onPressed,
this.child,
});
final Animation<double> animation;
final VoidCallback? onPressed;
final Widget? child;
final double? elevation;
@override
State<AnimatedFloatingActionButton> createState() =>
_AnimatedFloatingActionButton();
}
class _AnimatedFloatingActionButton
extends State<AnimatedFloatingActionButton> {
late final ColorScheme _colorScheme = Theme.of(context).colorScheme;
late final Animation<double> _scaleAnimation = ScaleAnimation(
parent: widget.animation,
);
late final Animation<double> _shapeAnimation = ShapeAnimation(
parent: widget.animation,
);
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
child: FloatingActionButton(
elevation: widget.elevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(lerpDouble(30, 15, _shapeAnimation.value)!),
),
),
backgroundColor: _colorScheme.tertiaryContainer,
foregroundColor: _colorScheme.onTertiaryContainer,
onPressed: widget.onPressed,
child: widget.child,
),
);
}
}
Bu değişiklikleri uygulamaya yansıtmak için main.dart dosyasını aşağıdaki şekilde güncelleyin:
lib/main.dart
import 'package:flutter/material.dart';
import 'animations.dart'; // Add this import
import 'models/data.dart' as data;
import 'models/models.dart';
import 'widgets/animated_floating_action_button.dart'; // Add this import
import 'widgets/disappearing_bottom_navigation_bar.dart';
import 'widgets/disappearing_navigation_rail.dart';
import 'widgets/email_list_view.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
home: Feed(currentUser: data.user_0),
);
}
}
class Feed extends StatefulWidget {
const Feed({super.key, required this.currentUser});
final User currentUser;
@override
State<Feed> createState() => _FeedState();
}
class _FeedState extends State<Feed> with SingleTickerProviderStateMixin {
late final _colorScheme = Theme.of(context).colorScheme;
late final _backgroundColor = Color.alphaBlend(
_colorScheme.primary.withAlpha(36),
_colorScheme.surface,
);
// Add from here...
late final _controller = AnimationController(
duration: const Duration(milliseconds: 1000),
reverseDuration: const Duration(milliseconds: 1250),
value: 0,
vsync: this,
);
late final _railAnimation = RailAnimation(parent: _controller);
late final _railFabAnimation = RailFabAnimation(parent: _controller);
late final _barAnimation = BarAnimation(parent: _controller);
// ... to here.
int selectedIndex = 0;
// Remove wideScreen
bool controllerInitialized = false; // Add this variable
@override
void didChangeDependencies() {
super.didChangeDependencies();
final double width = MediaQuery.of(context).size.width;
// Remove wideScreen reference
// Add from here ...
final AnimationStatus status = _controller.status;
if (width > 600) {
if (status != AnimationStatus.forward &&
status != AnimationStatus.completed) {
_controller.forward();
}
} else {
if (status != AnimationStatus.reverse &&
status != AnimationStatus.dismissed) {
_controller.reverse();
}
}
if (!controllerInitialized) {
controllerInitialized = true;
_controller.value = width > 600 ? 1 : 0;
}
// ... to here.
}
// Add from here ...
@override
void dispose() {
_controller.dispose();
super.dispose();
}
// ... to here.
@override
Widget build(BuildContext context) {
// Modify from here ...
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Scaffold(
body: Row(
children: [
DisappearingNavigationRail(
railAnimation: _railAnimation,
railFabAnimation: _railFabAnimation,
selectedIndex: selectedIndex,
backgroundColor: _backgroundColor,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
),
Expanded(
child: Container(
color: _backgroundColor,
child: EmailListView(
selectedIndex: selectedIndex,
onSelected: (index) {
setState(() {
selectedIndex = index;
});
},
currentUser: widget.currentUser,
),
),
),
],
),
floatingActionButton: AnimatedFloatingActionButton(
animation: _barAnimation,
onPressed: () {},
child: const Icon(Icons.add),
),
bottomNavigationBar: DisappearingBottomNavigationBar(
barAnimation: _barAnimation,
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
),
);
},
);
// ... to here.
}
}
Uygulamayı çalıştırın. Başlangıçta, uygulama öncekiyle aynı görünmelidir. Boyuta ve boyutlara bağlı olarak kullanıcı arayüzünün gezinme çubuğu ile gezinme rayı arasında geçiş yaptığını görmek için ekranı yeniden boyutlandırın. Bu geçişlerin hareketi artık akıcı ve eğlenceli görünecek. Uygulamanın görünümünü nasıl değiştirdiğini görmek için kullanılan animasyon eğrilerini değiştirmek üzere anında yeniden yükleme özelliğini kullanın.
8. Ayrıntı görünümü içeren bir liste ekleme
Ek olarak, mesajlaşma uygulamaları, ayrıntılı düzene sahip bir listeyi göstermek için harika bir yerdir ancak yalnızca ekran yeterince genişse. lib/widgets içinde reply_list_view.dart adlı bir dosya ekleyerek başlayın ve dosyayı aşağıdaki kodla doldurun:
lib/widgets/reply_list_view.dart
import 'package:flutter/material.dart';
import '../models/data.dart' as data;
import 'email_widget.dart';
class ReplyListView extends StatelessWidget {
const ReplyListView({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ListView(
children: [
const SizedBox(height: 8),
...List.generate(data.replies.length, (index) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: EmailWidget(
email: data.replies[index],
isPreview: false,
isThreaded: true,
showHeadline: index == 0,
),
);
}),
],
),
);
}
}
Ardından, lib/transitions içinde bir list_detail_transition.dart ekleyin ve aşağıdaki kodla doldurun:
lib/transitions/list_detail_transition.dart
import 'dart:ui';
import 'package:flutter/material.dart';
import '../animations.dart';
class ListDetailTransition extends StatefulWidget {
const ListDetailTransition({
super.key,
required this.animation,
required this.one,
required this.two,
});
final Animation<double> animation;
final Widget one;
final Widget two;
@override
State<ListDetailTransition> createState() => _ListDetailTransitionState();
}
class _ListDetailTransitionState extends State<ListDetailTransition> {
Animation<double> widthAnimation = const AlwaysStoppedAnimation(0);
late final Animation<double> sizeAnimation = SizeAnimation(
parent: widget.animation,
);
late final Animation<Offset> offsetAnimation = Tween<Offset>(
begin: const Offset(1, 0),
end: Offset.zero,
).animate(OffsetAnimation(parent: sizeAnimation));
double currentFlexFactor = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final double width = MediaQuery.of(context).size.width;
double nextFlexFactor = switch (width) {
>= 800 && < 1200 => lerpDouble(1000, 2000, (width - 800) / 400)!,
>= 1200 && < 1600 => lerpDouble(2000, 3000, (width - 1200) / 400)!,
>= 1600 => 3000,
_ => 1000,
};
if (nextFlexFactor == currentFlexFactor) {
return;
}
if (currentFlexFactor == 0) {
widthAnimation = Tween<double>(
begin: 0,
end: nextFlexFactor,
).animate(sizeAnimation);
} else {
final TweenSequence<double> sequence = TweenSequence([
if (sizeAnimation.value > 0) ...[
TweenSequenceItem(
tween: Tween(begin: 0, end: widthAnimation.value),
weight: sizeAnimation.value,
),
],
if (sizeAnimation.value < 1) ...[
TweenSequenceItem(
tween: Tween(begin: widthAnimation.value, end: nextFlexFactor),
weight: 1 - sizeAnimation.value,
),
],
]);
widthAnimation = sequence.animate(sizeAnimation);
}
currentFlexFactor = nextFlexFactor;
}
@override
Widget build(BuildContext context) {
return widthAnimation.value.toInt() == 0
? widget.one
: Row(
children: [
Flexible(flex: 1000, child: widget.one),
Flexible(
flex: widthAnimation.value.toInt(),
child: FractionalTranslation(
translation: offsetAnimation.value,
child: widget.two,
),
),
],
);
}
}
lib/main.dart öğesini aşağıdaki şekilde güncelleyerek bu içeriği uygulamaya entegre edin:
lib/main.dart
import 'package:flutter/material.dart';
import 'animations.dart';
import 'models/data.dart' as data;
import 'models/models.dart';
import 'transitions/list_detail_transition.dart'; // Add import
import 'widgets/animated_floating_action_button.dart';
import 'widgets/disappearing_bottom_navigation_bar.dart';
import 'widgets/disappearing_navigation_rail.dart';
import 'widgets/email_list_view.dart';
import 'widgets/reply_list_view.dart'; // Add import
void main() {
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.light(),
home: Feed(currentUser: data.user_0),
);
}
}
class Feed extends StatefulWidget {
const Feed({super.key, required this.currentUser});
final User currentUser;
@override
State<Feed> createState() => _FeedState();
}
class _FeedState extends State<Feed> with SingleTickerProviderStateMixin {
late final _colorScheme = Theme.of(context).colorScheme;
late final _backgroundColor = Color.alphaBlend(
_colorScheme.primary.withAlpha(36),
_colorScheme.surface,
);
late final _controller = AnimationController(
duration: const Duration(milliseconds: 1000),
reverseDuration: const Duration(milliseconds: 1250),
value: 0,
vsync: this,
);
late final _railAnimation = RailAnimation(parent: _controller);
late final _railFabAnimation = RailFabAnimation(parent: _controller);
late final _barAnimation = BarAnimation(parent: _controller);
int selectedIndex = 0;
bool controllerInitialized = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final double width = MediaQuery.of(context).size.width;
final AnimationStatus status = _controller.status;
if (width > 600) {
if (status != AnimationStatus.forward &&
status != AnimationStatus.completed) {
_controller.forward();
}
} else {
if (status != AnimationStatus.reverse &&
status != AnimationStatus.dismissed) {
_controller.reverse();
}
}
if (!controllerInitialized) {
controllerInitialized = true;
_controller.value = width > 600 ? 1 : 0;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Scaffold(
body: Row(
children: [
DisappearingNavigationRail(
railAnimation: _railAnimation,
railFabAnimation: _railFabAnimation,
selectedIndex: selectedIndex,
backgroundColor: _backgroundColor,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
),
Expanded(
child: Container(
color: _backgroundColor,
// Update from here ...
child: ListDetailTransition(
animation: _railAnimation,
one: EmailListView(
selectedIndex: selectedIndex,
onSelected: (index) {
setState(() {
selectedIndex = index;
});
},
currentUser: widget.currentUser,
),
two: const ReplyListView(),
),
// ... to here.
),
),
],
),
floatingActionButton: AnimatedFloatingActionButton(
animation: _barAnimation,
onPressed: () {},
child: const Icon(Icons.add),
),
bottomNavigationBar: DisappearingBottomNavigationBar(
barAnimation: _barAnimation,
selectedIndex: selectedIndex,
onDestinationSelected: (index) {
setState(() {
selectedIndex = index;
});
},
),
);
},
);
}
}
Her şeyin bir araya getirildiğini görmek için uygulamayı çalıştırın. Gerçek bir uygulamayı temsil eden bir uygulamada Material 3 stili ve farklı düzenler arasında animasyon kullanıyorsunuz. Şu şekilde görünmelidir:

9. Tebrikler
Tebrikler, ilk Material 3 Flutter uygulamanızı başarıyla oluşturdunuz.
Bu codelab'deki tüm adımları kod olarak incelemek için Flutter codelab'leri GitHub deposunda inceleyin.
Yapabilecekleriniz
Şu codelab'lere göz atın:
- Flutter ve Flame ile oyun geliştirme
- Flutter uygulamanızı sıkıcı olmaktan çıkarıp güzelleştirme
- Flutter eklentisinde FFI kullanma
Daha fazla bilgi
- Flutter'daki Material 3 güncellemeleri hakkında daha fazla bilgi edinin.
- Flutter Material 3 belgelerine bakın.
- Tüm Material 3 kaynaklarını inceleyin.









