Bu codelab hakkında
1. Giriş
Flutter, Google'ın tek bir kod tabanından mobil, web ve masaüstü için etkileyici, yerel olarak derlenmiş uygulamalar oluşturmaya yönelik kullanıcı arayüzü araç setidir. Flutter, mevcut kodla çalışır, dünyanın dört bir yanındaki geliştiriciler ve kuruluşlar tarafından kullanılır, ücretsiz ve açık kaynaktır.
Bu codelab'de, Flutter müzik uygulamasını geliştirerek sıkıcı olmaktan güzel hale getireceksiniz. Bu codelab'de, bu amaca ulaşmak için Material 3'te tanıtılan araçlar ve API'ler kullanılır.
Neler öğreneceksiniz?
- Platformlar arasında kullanılabilir ve güzel bir Flutter uygulaması yazma.
- Uygulamanızdaki metinleri, kullanıcı deneyimine katkıda bulunacak şekilde tasarlama
- Doğru renkleri seçme, widget'ları özelleştirme, kendi temanızı oluşturma ve koyu modu hızlıca uygulama.
- Platformlar arası uyarlanabilir uygulamalar oluşturma.
- Tüm ekranlarda iyi görünen uygulamalar oluşturma
- Flutter uygulamanıza hareket ekleyerek uygulamanızın öne çıkmasını sağlama.
Ön koşullar
Bu codelab'de Flutter deneyiminiz olduğu varsayılır. Aksi takdirde, önce temel bilgileri öğrenmeniz önerilir. Aşağıdaki bağlantılar faydalı olabilir:
- Flutter ile kullanıcı arayüzü oluşturma başlıklı makaleyi inceleyin.
- İlk Flutter uygulamanız codelab'ini deneyin
Ne oluşturacaksınız?
Bu codelab, takipçilerin en sevdikleri sanatçılarla ilgili gelişmeleri takip edebilecekleri bir müzik çalar uygulaması olan MyArtist
adlı uygulamanın ana ekranını oluşturma konusunda size yol gösterir. Bu makalede, uygulama tasarımınızı farklı platformlarda güzel görünecek şekilde nasıl değiştirebileceğiniz açıklanmaktadır.
Aşağıdaki videolarda, bu codelab'in tamamlanmasının ardından uygulamanın nasıl çalıştığı gösterilmektedir:
Bu kod laboratuvarından ne öğrenmek istiyorsunuz?
2. Flutter geliştirme ortamınızı kurma
Bu laboratuvarı tamamlamak için Flutter SDK ve bir düzenleyici yazılımına ihtiyacınız vardır.
Aşağıdaki cihazlardan herhangi birini kullanarak kod laboratuvarını çalıştırabilirsiniz:
- Bilgisayarınıza bağlı ve Geliştirici moduna ayarlanmış fiziksel bir Android veya iOS cihaz.
- iOS simülasyon aracı (Xcode araçlarının yüklenmesi gerekir).
- Android Emülatörü (Android Studio'da kurulum gerektirir).
- Tarayıcı (Hata ayıklama için Chrome gerekir).
- Windows, Linux veya macOS masaüstü uygulaması olarak. Uygulamayı dağıtmayı planladığınız platformda geliştirme yapmanız gerekir. Bu nedenle, Windows masaüstü uygulaması geliştirmek istiyorsanız uygun derleme zincirine erişmek için Windows'da geliştirme yapmanız gerekir. docs.flutter.dev/desktop adresinde işletim sistemine özgü gereksinimler ayrıntılı olarak açıklanmıştır.
3. Codelab başlangıç uygulamasını edinme
GitHub'dan kopyalayın
Bu kod laboratuvarını GitHub'dan kopyalamak için aşağıdaki komutları çalıştırın:
git clone https://github.com/flutter/codelabs.git cd codelabs/boring_to_beautiful/step_01/
Her şeyin çalıştığından emin olmak için Flutter uygulamasını aşağıda gösterildiği gibi masaüstü uygulaması olarak çalıştırın. Alternatif olarak, bu projeyi IDE'nizde açıp uygulamayı çalıştırmak için araçlarını kullanabilirsiniz.
flutter run
Başarılı aktarım Sanatçım ana ekranının başlatıcı kodu çalışıyor olmalıdır. Sanatçım ana ekranını görürsünüz. Masaüstünde iyi görünüyor ancak mobilde... Pek iyi değil. Örneğin, çentik için özel bir ayar yoktur. Endişelenmeyin, bu sorunu çözeceksiniz.
Kodu gezme
Ardından, kodun bir turunu yapın.
Aşağıdakileri içeren lib/src/features/home/view/home_screen.dart
dosyasını açın:
lib/src/features/home/view/home_screen.dart
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../../utils/adaptive_components.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(playlists: playlists),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Bu dosya, material.dart
dosyasını içe aktarır ve iki sınıf kullanarak durum bilgisine sahip bir widget uygular:
import
ifadesi, Material Components'i kullanılabilir hale getirir.HomeScreen
sınıfı, görüntülenen sayfanın tamamını temsil eder._HomeScreenState
sınıfınınbuild()
yöntemi, kullanıcı arayüzündeki tüm widget'ların nasıl oluşturulduğunu etkileyen widget ağacının kökünü oluşturur.
4. Yazı biçiminden yararlanın
Metinler her yerdedir. Metin, kullanıcıyla iletişim kurmanın faydalı bir yoludur. Uygulamanız samimi ve eğlenceli mi yoksa güvenilir ve profesyonel mi? En sevdiğiniz bankacılık uygulamasının Comic Sans kullanmamasının bir nedeni vardır. Metnin nasıl sunulduğu, kullanıcıların uygulamanızla ilgili ilk izlenimini belirler. Metni daha dikkatli kullanmanın bazı yollarını aşağıda bulabilirsiniz.
Anlatmayın, gösterin
Mümkün olduğunda "anlatmak" yerine "gösterme". Örneğin, başlangıç uygulamasındaki NavigationRail
'te her ana rota için sekmeler bulunur ancak öndeki simgeler aynıdır:
Kullanıcının her sekmenin metnini okuması gerektiğinden bu yöntem faydalı değildir. Kullanıcının, istediğiniz sekmeyi bulmak için başlıktaki simgelere hızlıca göz atabilmesi için görsel ipuçları ekleyerek başlayın. Bu, yerelleştirme ve erişilebilirlik açısından da faydalıdır.
lib/src/shared/router.dart
alanına her gezinme hedefi (ana sayfa, oynatma listesi ve kullanıcılar) için farklı ön simgeler ekleyin:
lib/src/shared/router.dart
const List<NavigationDestination> destinations = [
NavigationDestination(
label: 'Home',
icon: Icon(Icons.home), // Modify this line
route: '/',
),
NavigationDestination(
label: 'Playlists',
icon: Icon(Icons.playlist_add_check), // Modify this line
route: '/playlists',
),
NavigationDestination(
label: 'Artists',
icon: Icon(Icons.people), // Modify this line
route: '/artists',
),
];
Sorun mu yaşıyorsunuz?
Uygulamanız düzgün çalışmıyorsa yazım hatalarını kontrol edin. Gerekirse aşağıdaki bağlantılardaki kodu kullanarak tekrar ilerlemeye başlayabilirsiniz.
Yazı tiplerini dikkatli bir şekilde seçin
Yazı tipleri, uygulamanızın karakterini belirler. Bu nedenle, doğru yazı tipini seçmek çok önemlidir. Yazı tipi seçerken göz önünde bulundurmanız gereken bazı noktalar aşağıda verilmiştir:
- Sans serif veya serif: Serif yazı tiplerinde harflerin sonunda dekoratif vuruş veya "kuyruk" bulunur ve bu yazı tipleri daha resmi olarak algılanır. Sans serif yazı tiplerinde dekoratif stilde harf sapları bulunmaz ve bu yazı tipleri daha rahat bir tarzda algılanır.
- Büyük harf yazı tipleri: Büyük harf kullanmak, küçük miktarlardaki metinlere (başlıklar gibi) dikkat çekmek için uygundur ancak çok fazla kullanıldığında kullanıcının metni tamamen yoksaymasına neden olacak şekilde bağırarak konuşuyormuş gibi algılanabilir.
- Başlık biçimi veya normal cümle düzeni: Başlık veya etiket eklerken büyük harfleri nasıl kullandığınızı göz önünde bulundurun: Her kelimenin ilk harfi büyük yazılan başlık biçimi ("This Is a Title Case Title") daha resmidir. Yalnızca özel adları ve metindeki ilk kelimeyi büyük harfle yazan normal cümle düzeni ("This is a sentence case title") daha samimi ve rahat bir üsluptur.
- Kerning (her harf arasındaki boşluk), satır uzunluğu (ekrandaki metnin tamamının genişliği) ve satır yüksekliği (her metin satırının yüksekliği): Bu özelliklerin çok fazla veya çok az olması uygulamanızın okunabilirliğini azaltır. Örneğin, büyük ve kesintisiz bir metin bloğunu okurken kaldığınız yeri belirlemek zor olabilir.
Bu nedenle, Google Yazı Tipleri'ne gidip müzik uygulamasının eğlenceli olması gerektiği için Montserrat gibi bir sans serif yazı tipi seçin.
Komut satırından google_fonts
paketini alın. Bu işlem, yazı tiplerini uygulama bağımlılığı olarak eklemek için pubspec.yaml
dosyasını da günceller.
flutter pub add google_fonts
macos/Runner/DebugProfile.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<!-- Make sure the following two lines are present -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
lib/src/shared/extensions.dart
'te yeni paketi içe aktarın:
lib/src/shared/extensions.dart
import 'package:google_fonts/google_fonts.dart'; // Add this line.
Montserrat TextTheme:
ayarlama
TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line
Değişiklikleri etkinleştirmek için dosyasını anında yeniden yükleyin. (IDE'nizdeki düğmeyi kullanın veya komut satırından
r
yazarak anında yeniden yükleyin.):
Montserrat yazı tipinde gösterilen metinle birlikte yeni NavigationRail
simgelerini görürsünüz.
Sorun mu yaşıyorsunuz?
Uygulamanız düzgün çalışmıyorsa yazım hatalarını kontrol edin. Gerekirse aşağıdaki bağlantılardaki kodu kullanarak tekrar ilerlemeye başlayabilirsiniz.
5. Temayı ayarlama
Temalar, belirli bir renk ve metin stili sistemi belirleyerek uygulamaya yapılandırılmış bir tasarım ve tekdüze bir görünüm kazandırır. Temalar, her widget'ın tam rengini belirtme gibi küçük ayrıntılar üzerinde strese girmenize gerek kalmadan hızlı bir şekilde kullanıcı arayüzü uygulamanızı sağlar.
Flutter geliştiricileri genellikle özel temalı bileşenleri iki yoldan biriyle oluşturur:
- Her biri kendi temasına sahip özel widget'lar oluşturun.
- Varsayılan widget'lar için kapsamlı temalar oluşturun.
Bu örnekte, uygulama genelinde tutarlı temalı widget'lar ve renkler oluşturmak için lib/src/shared/providers/theme.dart
içinde bulunan bir tema sağlayıcı kullanılır:
lib/src/shared/providers/theme.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
class NoAnimationPageTransitionsBuilder extends PageTransitionsBuilder {
const NoAnimationPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
class ThemeSettingChange extends Notification {
ThemeSettingChange({required this.settings});
final ThemeSettings settings;
}
class ThemeProvider extends InheritedWidget {
const ThemeProvider({
super.key,
required this.settings,
required this.lightDynamic,
required this.darkDynamic,
required super.child,
});
final ValueNotifier<ThemeSettings> settings;
final ColorScheme? lightDynamic;
final ColorScheme? darkDynamic;
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
Color custom(CustomColor custom) {
if (custom.blend) {
return blend(custom.color);
} else {
return custom.color;
}
}
Color blend(Color targetColor) {
return Color(
Blend.harmonize(
targetColor.toARGB32(),
settings.value.sourceColor.toARGB32(),
),
);
}
Color source(Color? target) {
Color source = settings.value.sourceColor;
if (target != null) {
source = blend(target);
}
return source;
}
ColorScheme colors(Brightness brightness, Color? targetColor) {
final dynamicPrimary = brightness == Brightness.light
? lightDynamic?.primary
: darkDynamic?.primary;
return ColorScheme.fromSeed(
seedColor: dynamicPrimary ?? source(targetColor),
brightness: brightness,
);
}
ShapeBorder get shapeMedium =>
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8));
CardThemeData cardTheme() {
return CardThemeData(
elevation: 0,
shape: shapeMedium,
clipBehavior: Clip.antiAlias,
);
}
ListTileThemeData listTileTheme(ColorScheme colors) {
return ListTileThemeData(
shape: shapeMedium,
selectedColor: colors.secondary,
);
}
AppBarTheme appBarTheme(ColorScheme colors) {
return AppBarTheme(
elevation: 0,
backgroundColor: colors.surface,
foregroundColor: colors.onSurface,
);
}
TabBarThemeData tabBarTheme(ColorScheme colors) {
return TabBarThemeData(
labelColor: colors.secondary,
unselectedLabelColor: colors.onSurfaceVariant,
indicator: BoxDecoration(
border: Border(bottom: BorderSide(color: colors.secondary, width: 2)),
),
);
}
BottomAppBarTheme bottomAppBarTheme(ColorScheme colors) {
return BottomAppBarTheme(color: colors.surface, elevation: 0);
}
BottomNavigationBarThemeData bottomNavigationBarTheme(ColorScheme colors) {
return BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
backgroundColor: colors.surfaceContainerHighest,
selectedItemColor: colors.onSurface,
unselectedItemColor: colors.onSurfaceVariant,
elevation: 0,
landscapeLayout: BottomNavigationBarLandscapeLayout.centered,
);
}
NavigationRailThemeData navigationRailTheme(ColorScheme colors) {
return const NavigationRailThemeData();
}
DrawerThemeData drawerTheme(ColorScheme colors) {
return DrawerThemeData(backgroundColor: colors.surface);
}
ThemeData light([Color? targetColor]) {
final colorScheme = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
colorScheme: colorScheme,
appBarTheme: appBarTheme(colorScheme),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(colorScheme),
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
navigationRailTheme: navigationRailTheme(colorScheme),
tabBarTheme: tabBarTheme(colorScheme),
drawerTheme: drawerTheme(colorScheme),
scaffoldBackgroundColor: colorScheme.surface,
);
}
ThemeData dark([Color? targetColor]) {
final colorScheme = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
colorScheme: colorScheme,
appBarTheme: appBarTheme(colorScheme),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(colorScheme),
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
navigationRailTheme: navigationRailTheme(colorScheme),
tabBarTheme: tabBarTheme(colorScheme),
drawerTheme: drawerTheme(colorScheme),
scaffoldBackgroundColor: colorScheme.surface,
);
}
ThemeMode themeMode() {
return settings.value.themeMode;
}
ThemeData theme(BuildContext context, [Color? targetColor]) {
final brightness = MediaQuery.of(context).platformBrightness;
return brightness == Brightness.light
? light(targetColor)
: dark(targetColor);
}
static ThemeProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ThemeProvider>()!;
}
@override
bool updateShouldNotify(covariant ThemeProvider oldWidget) {
return oldWidget.settings != settings;
}
}
class ThemeSettings {
ThemeSettings({required this.sourceColor, required this.themeMode});
final Color sourceColor;
final ThemeMode themeMode;
}
Color randomColor() {
return Color(Random().nextInt(0xFFFFFFFF));
}
const linkColor = CustomColor(name: 'Link Color', color: Color(0xFF00B0FF));
class CustomColor {
const CustomColor({
required this.name,
required this.color,
this.blend = true,
});
final String name;
final Color color;
final bool blend;
Color value(ThemeProvider provider) {
return provider.custom(this);
}
}
Sağlayıcıyı kullanmak için bir örnek oluşturun ve lib/src/shared/app.dart
içindeki MaterialApp
'te bulunan kapsamlı tema nesnesine iletin. İç içe yerleştirilmiş tüm Theme
nesneleri tarafından devralınır:
lib/src/shared/app.dart
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'playback/bloc/bloc.dart';
import 'providers/theme.dart';
import 'router.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final settings = ValueNotifier(ThemeSettings(
sourceColor: Colors.pink,
themeMode: ThemeMode.system,
));
@override
Widget build(BuildContext context) {
return BlocProvider<PlaybackBloc>(
create: (context) => PlaybackBloc(),
child: DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) => ThemeProvider(
lightDynamic: lightDynamic,
darkDynamic: darkDynamic,
settings: settings,
child: NotificationListener<ThemeSettingChange>(
onNotification: (notification) {
settings.value = notification.settings;
return true;
},
child: ValueListenableBuilder<ThemeSettings>(
valueListenable: settings,
builder: (context, value, _) {
final theme = ThemeProvider.of(context); // Add this line
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
},
),
)),
),
);
}
}
Tema ayarlandıktan sonra uygulama için renkleri seçin.
Doğru renk grubunu seçmek zor olabilir. Birincil renk konusunda bir fikriniz olabilir ancak uygulamanızda birden fazla renk olmasını istiyorsanız. Metin ne renk olmalı? Başlık? İçerik? Bağlantılar? Arka plan rengi ne olacak? Material Theme Builder, uygulamanız için birbirini tamamlayan bir renk grubu seçmenize yardımcı olan web tabanlı bir araçtır (Material 3'te kullanıma sunulmuştur).
Uygulama için bir kaynak renk seçmek üzere Material Theme Builder'ı açın ve kullanıcı arayüzü için farklı renkleri keşfedin. Marka estetiğine veya kişisel tercihinize uygun bir renk seçmeniz önemlidir.
Tema oluşturduktan sonra birincil renk baloncuğunu sağ tıklayın. Bu işlem, birincil rengin onaltılık değerini içeren bir iletişim kutusu açar. Bu değeri kopyalayın. (Bu iletişim kutusunu kullanarak rengi de ayarlayabilirsiniz.)
Birincil rengin onaltılık değerini tema sağlayıcıya iletin. Örneğin, #00cbe6
onaltılık rengi Color(0xff00cbe6)
olarak belirtilir. ThemeProvider
, Material Theme Builder'da önizlediğiniz tamamlayıcı renk grubunu içeren bir ThemeData
oluşturur:
final settings = ValueNotifier(ThemeSettings(
sourceColor: Color(0xff00cbe6), // Replace this color
themeMode: ThemeMode.system,
));
Uygulamayı sıcak olarak yeniden başlatın. Birincil renk eklendiğinde uygulama daha etkileyici bir görünüme kavuşur. Bağlamda temaya referans vererek ve ColorScheme
öğesini kullanarak tüm yeni renklere erişebilirsiniz:
final colors = Theme.of(context).colorScheme;
Belirli bir rengi kullanmak için colorScheme
'de bir renk rolüne erişin. lib/src/shared/views/outlined_card.dart
'e gidin ve OutlinedCard
'a bir kenarlık verin:
lib/src/shared/views/outlined_card.dart
class _OutlinedCardState extends State<OutlinedCard> {
@override
Widget build(BuildContext context) {
return MouseRegion(
cursor: widget.clickable
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: Container(
// Add from here...
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
),
// ... To here.
child: widget.child,
),
);
}
}
Material 3, birbirini tamamlayan ve kullanıcı arayüzünde yeni ifade katmanları eklemek için kullanılabilen ayrıntılı renk rolleri sunar. Bu yeni renk rolleri şunlardır:
Primary
,OnPrimary
,PrimaryContainer
,OnPrimaryContainer
Secondary
,OnSecondary
,SecondaryContainer
,OnSecondaryContainer
Tertiary
,OnTertiary
,TertiaryContainer
,OnTertiaryContainer
Error
,OnError
,ErrorContainer
,OnErrorContainer
Background
,OnBackground
Surface
,OnSurface
,SurfaceVariant
,OnSurfaceVariant
Shadow
,Outline
,InversePrimary
Ayrıca yeni tasarım jetonları hem açık hem de koyu temaları destekler:
Bu renk rolleri, kullanıcı arayüzünün farklı bölümlerine anlam ve vurgu vermek için kullanılabilir. Bir bileşen belirgin olmasa bile dinamik renkten yararlanabilir.
Kullanıcı, uygulama parlaklığını cihazın sistem ayarlarından ayarlayabilir. lib/src/shared/app.dart
'te, cihaz koyu moda ayarlandığında MaterialApp
'a koyu tema ve tema modu döndürülür.
lib/src/shared/app.dart
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: theme.light(settings.value.sourceColor),
darkTheme: theme.dark(settings.value.sourceColor), // Add this line
themeMode: theme.themeMode(), // Add this line
routeInformationParser: appRouter.routeInformationParser,
routerDelegate: appRouter.routerDelegate,
);
Koyu modu etkinleştirmek için sağ üst köşedeki ay simgesini tıklayın.
Sorun mu yaşıyorsunuz?
Uygulamanız düzgün çalışmıyorsa tekrar çalışmaya başlamak için aşağıdaki bağlantıdaki kodu kullanın.
6. Uyarlanabilir tasarım ekleme
Flutter ile neredeyse her yerde çalışacak uygulamalar oluşturabilirsiniz. Ancak bu, her uygulamanın her yerde aynı davranış sergilemesi gerektiği anlamına gelmez. Kullanıcılar farklı platformlardan farklı davranışlar ve özellikler beklemeye başladı.
Material, uyarlanabilir düzenlerle çalışmayı kolaylaştıran paketler sunar. Bu Flutter paketlerini GitHub'da bulabilirsiniz.
Platformlar arası, uyarlanabilir bir uygulama geliştirirken aşağıdaki platform farklılıklarını göz önünde bulundurun:
- Giriş yöntemi: fare, dokunmatik veya oyun kumandası
- Yazı tipi boyutu, cihaz yönü ve izleme mesafesi
- Ekran boyutu ve form faktörü: telefon, tablet, katlanabilir, masaüstü, web
lib/src/shared/views/adaptive_navigation.dart
dosyası, gövdeyi oluşturmak için hedeflerin ve içeriğin listesini sağlayabileceğiniz bir gezinme sınıfı içerir. Bu düzeni birden fazla ekranda kullandığınız için her alt öğeye aktarılacak paylaşılan bir temel düzen vardır. Gezinme rayları masaüstü ve büyük ekranlar için iyidir ancak mobil cihazlarda bunun yerine alt gezinme çubuğu göstererek düzeni mobil uyumlu hale getirin.
lib/src/shared/views/adaptive_navigation.dart
import 'package:flutter/material.dart';
class AdaptiveNavigation extends StatelessWidget {
const AdaptiveNavigation({
super.key,
required this.destinations,
required this.selectedIndex,
required this.onDestinationSelected,
required this.child,
});
final List<NavigationDestination> destinations;
final int selectedIndex;
final void Function(int index) onDestinationSelected;
final Widget child;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, dimens) {
if (dimens.maxWidth >= 600) { // Add this line
return Scaffold(
body: Row(
children: [
NavigationRail(
extended: dimens.maxWidth >= 800,
minExtendedWidth: 180,
destinations: destinations
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
),
)
.toList(),
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
Expanded(child: child),
],
),
);
} // Add this line
// Mobile Layout
// Add from here...
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
destinations: destinations,
selectedIndex: selectedIndex,
onDestinationSelected: onDestinationSelected,
),
);
// ... To here.
},
);
}
}
Tüm ekranlar aynı boyutta değildir. Uygulamanızın masaüstü sürümünü telefonunuzda göstermeye çalıştıysanız her şeyi görmek için gözlerinizden yardım alıp yakınlaştırmanız gerekir. Uygulamanızın, gösterildiği ekrana göre görünümünü değiştirmesini istiyorsunuz. Duyarlı tasarım sayesinde uygulamanızın tüm ekran boyutlarında harika görünmesini sağlayabilirsiniz.
Uygulamanızı duyarlı hale getirmek için birkaç uyarlanabilir durma noktası ekleyin (bu durma noktaları, hata ayıklama durma noktalarıyla karıştırılmamalıdır). Bu kesme noktaları, uygulamanızın düzenini değiştirmesi gereken ekran boyutlarını belirtir.
Daha küçük ekranlar, içeriği küçültmeden büyük ekranlar kadar fazla görüntüleyemez. Uygulamanın küçültülmüş bir masaüstü uygulaması gibi görünmesini önlemek için mobil cihazlar için ayrı bir düzen oluşturun. Bu düzende, içeriği bölmek için sekmeler kullanın. Bu sayede uygulama, mobil cihazlarda daha yerel bir görünüme sahip olur.
Aşağıdaki uzantı yöntemleri (lib/src/shared/extensions.dart
'teki MyArtist
projesinde tanımlanmıştır), farklı hedefler için optimize edilmiş düzenler tasarlarken başlangıç için iyi bir noktadır.
lib/src/shared/extensions.dart
extension BreakpointUtils on BoxConstraints {
bool get isTablet => maxWidth > 730;
bool get isDesktop => maxWidth > 1200;
bool get isMobile => !isTablet && !isDesktop;
}
730 pikselden büyük (en uzun yönde) ancak 1.200 pikselden küçük ekranlar tablet olarak kabul edilir. 1.200 pikselden büyük resimler masaüstü olarak kabul edilir. Tablet veya masaüstü olmayan cihazlar mobil olarak kabul edilir. material.io'daki uyarlanabilir kesme noktaları hakkında daha fazla bilgi edinebilirsiniz.
Ana ekranın duyarlı düzeninde, 12 sütunlu ızgara temel alınarak AdaptiveContainer
ve AdaptiveColumn
kullanılır.
Uyarlanabilir bir düzen için iki düzen gerekir: biri mobil cihazlar için, diğeri daha büyük ekranlar için duyarlı bir düzen. Bu noktada LayoutBuilder
, masaüstü düzenini döndürür. lib/src/features/home/view/home_screen.dart
'te mobil düzeni 4 sekmeli bir TabBar
ve TabBarView
olarak oluşturun.
lib/src/features/home/view/home_screen.dart
import 'package:flutter/material.dart';
import '../../../shared/classes/classes.dart';
import '../../../shared/extensions.dart';
import '../../../shared/providers/providers.dart';
import '../../../shared/views/views.dart';
import '../../../utils/adaptive_components.dart';
import '../../playlists/view/playlist_songs.dart';
import 'view.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
Widget build(BuildContext context) {
final PlaylistsProvider playlistProvider = PlaylistsProvider();
final List<Playlist> playlists = playlistProvider.playlists;
final Playlist topSongs = playlistProvider.topSongs;
final Playlist newReleases = playlistProvider.newReleases;
final ArtistsProvider artistsProvider = ArtistsProvider();
final List<Artist> artists = artistsProvider.artists;
return LayoutBuilder(
builder: (context, constraints) {
// Add from here...
if (constraints.isMobile) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('Good morning'),
actions: const [BrightnessToggle()],
bottom: const TabBar(
isScrollable: true,
tabs: [
Tab(text: 'Home'),
Tab(text: 'Recently Played'),
Tab(text: 'New Releases'),
Tab(text: 'Top Songs'),
],
),
),
body: LayoutBuilder(
builder: (context, constraints) => TabBarView(
children: [
SingleChildScrollView(
child: Column(
children: [
const HomeHighlight(),
HomeArtists(
artists: artists,
constraints: constraints,
),
],
),
),
HomeRecent(playlists: playlists, axis: Axis.vertical),
PlaylistSongs(playlist: topSongs, constraints: constraints),
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
],
),
),
),
);
}
// ... To here.
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(playlists: playlists),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(2),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
},
);
}
}
Sorun mu yaşıyorsunuz?
Uygulamanız düzgün çalışmıyorsa tekrar çalışmaya başlamak için aşağıdaki bağlantıdaki kodu kullanın.
7. Boşluk kullanma
Boşluk, uygulamanız için önemli bir görsel araçtır ve bölümler arasında bir düzen aralığı oluşturur.
Yeterli boşluk olmaması yerine çok fazla boşluk olması daha iyidir. Alana daha fazla sığması için yazı tipinizin veya görsel öğelerinizin boyutunu küçültmek yerine daha fazla boşluk eklemek tercih edilir.
Boşluk olmaması, görme sorunları olan kullanıcılar için zor olabilir. Çok fazla boş alan, kullanıcı arayüzünüzün uyumluluğunu bozabilir ve kötü düzenlenmiş görünmesine neden olabilir. Örneğin, aşağıdaki ekran görüntülerine bakın:
Ardından, ana ekrana daha fazla alan açmak için boşluk eklersiniz. Ardından, boşluklara ince ayar yapmak için düzende daha fazla değişiklik yapabilirsiniz.
Bir widget'ın etrafına boşluk eklemek için widget'ı Padding
nesnesi ile sarın. lib/src/features/home/view/home_screen.dart
içindeki tüm dolgu değerlerini 35'e yükseltin:
lib/src/features/home/view/home_screen.dart
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(playlists: playlists),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35), // Modify this line
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
Uygulamayı sıcak yeniden yükleyin. Uygulama, widget'lar arasında daha fazla boşluk olacak şekilde öncekiyle aynı görünecektir. Ek dolgu daha iyi görünüyor ancak üstteki öne çıkan anlar banner'ı kenarlara çok yakın.
lib/src/features/home/view/home_highlight.dart
dosyasında, banner'daki dolguyu 15 olarak değiştirin:
lib/src/features/home/view/home_highlight.dart
class HomeHighlight extends StatelessWidget {
const HomeHighlight({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(15), // Modify this line
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
),
),
),
],
);
}
}
Uygulamayı anında yeniden yükleyin. Alt kısımdaki iki oynatma listesi arasında boşluk olmadığı için aynı tabloya ait gibi görünüyorlar. Bu durum söz konusu değil. Bunu bir sonraki adımda düzelteceksiniz.
Oynatma listelerini içeren Row
öğesine bir boyut widget'ı ekleyerek oynatma listeleri arasına boşluk ekleyin. lib/src/features/home/view/home_screen.dart
alanına 35 genişliğinde bir SizedBox
ekleyin:
lib/src/features/home/view/home_screen.dart
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(35),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 35), // Add this line
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(35),
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
Uygulamayı sıcak olarak yeniden yükleyin. Uygulama aşağıdaki gibi görünmelidir:
Ana ekran içerikleri için bolca yer var ancak her şey çok ayrı görünüyor ve bölümler arasında uyum yok.
Şimdiye kadar, ana ekrandaki widget'ların tüm dolgularını (hem yatay hem de dikey) EdgeInsets.all(35)
ile 35 olarak ayarladınız. Ancak kenarların her birinin dolgusunu bağımsız olarak da ayarlayabilirsiniz. Alana daha iyi sığması için dolguyu özelleştirin.
EdgeInsets.LTRB()
, soldan, üstten, sağdan ve alttan ayrı ayrı ayarlanır.EdgeInsets.symmetric()
, dikey (üst ve alt) dolgunun eşdeğer, yatay (sol ve sağ) dolgunun da eşdeğer olmasını sağlar.EdgeInsets.only()
yalnızca belirtilen kenarları ayarlar.
lib/src/features/home/view/home_screen.dart
return Scaffold(
body: SingleChildScrollView(
child: AdaptiveColumn(
children: [
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 25, 20, 10), // Modify this line
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'Good morning',
style: context.displaySmall,
),
),
const SizedBox(width: 20),
const BrightnessToggle(),
],
),
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
children: [
const HomeHighlight(),
LayoutBuilder(
builder: (context, constraints) => HomeArtists(
artists: artists,
constraints: constraints,
),
),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric( // Modify from here...
horizontal: 15,
vertical: 10,
), // To here.
child: Text(
'Recently played',
style: context.headlineSmall,
),
),
HomeRecent(playlists: playlists),
],
),
),
AdaptiveContainer(
columnSpan: 12,
child: Padding(
padding: const EdgeInsets.all(15), // Modify this line
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only( // Modify from here...
left: 8,
bottom: 8,
), // To here.
child: Text(
'Top Songs Today',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: topSongs,
constraints: constraints,
),
),
],
),
),
const SizedBox(width: 25), // Modify this line
Flexible(
flex: 10,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only( // Modify from here...
left: 8,
bottom: 8,
), // To here.
child: Text(
'New Releases',
style: context.titleLarge,
),
),
LayoutBuilder(
builder: (context, constraints) =>
PlaylistSongs(
playlist: newReleases,
constraints: constraints,
),
),
],
),
),
],
),
),
),
],
),
),
);
lib/src/features/home/view/home_highlight.dart
dosyasında, banner'ın sol ve sağ dolgusunu 35, üst ve alt dolgusunu ise 5 olarak ayarlayın:
lib/src/features/home/view/home_highlight.dart
class HomeHighlight extends StatelessWidget {
const HomeHighlight({super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: Padding(
// Modify the following line
padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 5),
child: Clickable(
child: SizedBox(
height: 275,
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.asset(
'assets/images/news/concert.jpeg',
fit: BoxFit.cover,
),
),
),
onTap: () => launchUrl(Uri.parse('https://docs.flutter.dev')),
),
),
),
],
);
}
}
Uygulamayı anında yeniden yükleyin. Düzen ve boşluklar çok daha iyi görünüyor. Son rötuş olarak hareket ve animasyon ekleyin.
Sorun mu yaşıyorsunuz?
Uygulamanız düzgün çalışmıyorsa tekrar çalışmaya başlamak için aşağıdaki bağlantıdaki kodu kullanın.
8. Hareket ve animasyon ekleme
Hareket ve animasyon, hareket ve enerji katmanın yanı sıra kullanıcı uygulamayla etkileşime geçtiğinde geri bildirim sağlamanın mükemmel yollarıdır.
Ekranlar arasında animasyonlu geçiş yapma
ThemeProvider
, mobil platformlar (iOS, Android) için ekran geçiş animasyonlarına sahip bir PageTransitionsTheme
tanımlar. Masaüstü kullanıcıları zaten fare veya dokunmatik yüzey tıklamasından geri bildirim aldığından sayfa geçiş animasyonu gerekmez.
Flutter, lib/src/shared/providers/theme.dart
'te gösterildiği gibi hedef platforma göre uygulamanız için yapılandırabileceğiniz ekran geçiş animasyonlarını sağlar:
lib/src/shared/providers/theme.dart
final pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.linux: NoAnimationPageTransitionsBuilder(),
TargetPlatform.macOS: NoAnimationPageTransitionsBuilder(),
TargetPlatform.windows: NoAnimationPageTransitionsBuilder(),
},
);
PageTransitionsTheme
öğesini lib/src/shared/providers/theme.dart
'daki hem açık hem de koyu temalara iletin
lib/src/shared/providers/theme.dart
ThemeData light([Color? targetColor]) {
final colorScheme = colors(Brightness.light, targetColor);
return ThemeData.light().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: colorScheme,
appBarTheme: appBarTheme(colorScheme),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(colorScheme),
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
navigationRailTheme: navigationRailTheme(colorScheme),
tabBarTheme: tabBarTheme(colorScheme),
drawerTheme: drawerTheme(colorScheme),
scaffoldBackgroundColor: colorScheme.surface,
);
}
ThemeData dark([Color? targetColor]) {
final colorScheme = colors(Brightness.dark, targetColor);
return ThemeData.dark().copyWith(
pageTransitionsTheme: pageTransitionsTheme, // Add this line
colorScheme: colorScheme,
appBarTheme: appBarTheme(colorScheme),
cardTheme: cardTheme(),
listTileTheme: listTileTheme(colorScheme),
bottomAppBarTheme: bottomAppBarTheme(colorScheme),
bottomNavigationBarTheme: bottomNavigationBarTheme(colorScheme),
navigationRailTheme: navigationRailTheme(colorScheme),
tabBarTheme: tabBarTheme(colorScheme),
drawerTheme: drawerTheme(colorScheme),
scaffoldBackgroundColor: colorScheme.surface,
);
}
iOS'te animasyonsuz
iOS'te animasyonlu
Sorun mu yaşıyorsunuz?
Uygulamanız düzgün çalışmıyorsa tekrar çalışmaya başlamak için aşağıdaki bağlantıdaki kodu kullanın.
9. Fareyle üzerine gelme durumlarını ekleme
Masaüstü uygulamalarına hareket eklemenin bir yolu, fareyle üzerine gelindiğinde değişen durumlar kullanmaktır. Bu durumda, kullanıcı fareyle widget'ın üzerine geldiğinde widget'ın durumu (ör. renk, şekil veya içerik) değişir.
Varsayılan olarak _OutlinedCardState
sınıfı ("son oynatılanlar" oynatma listesi karoları için kullanılır) bir MouseRegion
döndürür. Bu sınıf, fareyle üzerine gelindiğinde imleci işaretçiye dönüştürür. Ancak daha fazla görsel geri bildirim ekleyebilirsiniz.
lib/src/shared/views/outlined_card.dart
dosyasını açın ve _hovered
durumu eklemek için içeriğini aşağıdaki uygulamayla değiştirin.
lib/src/shared/views/outlined_card.dart
import 'package:flutter/material.dart';
class OutlinedCard extends StatefulWidget {
const OutlinedCard({super.key, required this.child, this.clickable = true});
final Widget child;
final bool clickable;
@override
State<OutlinedCard> createState() => _OutlinedCardState();
}
class _OutlinedCardState extends State<OutlinedCard> {
bool _hovered = false;
@override
Widget build(BuildContext context) {
final borderRadius = BorderRadius.circular(_hovered ? 20 : 8);
const animationCurve = Curves.easeInOut;
return MouseRegion(
onEnter: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = true;
});
},
onExit: (_) {
if (!widget.clickable) return;
setState(() {
_hovered = false;
});
},
cursor: widget.clickable
? SystemMouseCursors.click
: SystemMouseCursors.basic,
child: AnimatedContainer(
duration: kThemeAnimationDuration,
curve: animationCurve,
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colorScheme.outline,
width: 1,
),
borderRadius: borderRadius,
),
foregroundDecoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.onSurface.withAlpha(_hovered ? 30 : 0),
borderRadius: borderRadius,
),
child: TweenAnimationBuilder<BorderRadius>(
duration: kThemeAnimationDuration,
curve: animationCurve,
tween: Tween(begin: BorderRadius.zero, end: borderRadius),
builder: (context, borderRadius, child) => ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius,
child: child,
),
child: widget.child,
),
),
);
}
}
Uygulamayı anında yeniden yükleyin ve fareyle en son oynatılan oynatma listesi karolarından birinin üzerine gelin.
OutlinedCard
, opaklığı değiştirir ve köşeleri yuvarlar.
Son olarak, lib/src/shared/views/hoverable_song_play_button.dart
içinde tanımlanan HoverableSongPlayButton
widget'ını kullanarak oynatma listesindeki şarkı numarasını oynatma düğmesine dönüştürün. lib/src/features/playlists/view/playlist_songs.dart
içinde, Center
widget'ını (şarkı numarasını içeren) HoverableSongPlayButton
ile sarmalayın:
lib/src/features/playlists/view/playlist_songs.dart
rowBuilder: (context, index) => DataRow.byIndex(
index: index,
cells: [
DataCell(
HoverableSongPlayButton( // Modify from here...
hoverMode: HoverMode.overlay,
song: playlist.songs[index],
child: Center(
child: Text(
(index + 1).toString(),
textAlign: TextAlign.center,
),
),
), // To here.
),
DataCell(
Row(
children: [
Padding(
padding: const EdgeInsets.all(2),
child: ClippedImage(playlist.songs[index].image.image),
),
const SizedBox(width: 10),
Expanded(child: Text(playlist.songs[index].title)),
],
),
),
DataCell(Text(playlist.songs[index].length.toHumanizedString())),
],
),
Uygulamayı çalışır durumda yeniden yükleyin ve ardından imleci Bugünün En Popüler Şarkıları veya Yeni Çıkan Şarkılar oynatma listesindeki şarkı numarasının üzerine getirin.
Sayı, tıkladığınızda şarkıyı çalan bir oynat düğmesine dönüşür.
Nihai proje kodunu GitHub'da inceleyin.
10. Tebrikler!
Bu codelab'i tamamladınız. Bir uygulamayı daha güzel, daha erişilebilir, daha yerelleştirilebilir ve birden fazla platforma daha uygun hale getirmek için uygulamaya entegre edebileceğiniz birçok küçük değişiklik olduğunu öğrendiniz. Bu teknikler aşağıdakilerle sınırlı olmamakla birlikte şunları kapsar:
- Yazı tipi: Metin, yalnızca bir iletişim aracı değildir. Kullanıcıların deneyimi ve uygulamanızla ilgili algısı üzerinde olumlu bir etki yaratmak için metnin gösterilme şeklini kullanın.
- Temalar: Her widget için tasarım kararları almak zorunda kalmadan güvenle kullanabileceğiniz bir tasarım sistemi oluşturun.
- Uyumluluk: Kullanıcının uygulamanızı çalıştırdığı cihazı, platformu ve bunların özelliklerini göz önünde bulundurun. Ekran boyutunu ve uygulamanızın nasıl görüntülendiğini göz önünde bulundurun.
- Hareket ve animasyon: Uygulamanıza hareket eklemek kullanıcı deneyimine enerji katar ve daha pratik bir şekilde kullanıcılara geri bildirim sağlar.
Birkaç küçük değişiklikle uygulamanızın sıkıcı görünümünü güzel bir görünüme dönüştürebilirsiniz:
Önce
Sonra
Sonraki adımlar
Flutter'da güzel uygulamalar geliştirme hakkında daha fazla bilgi edindiğinizi umuyoruz.
Burada bahsedilen ipuçlarından veya püf noktalarından herhangi birini uygularsanız (ya da paylaşmak istediğiniz kendi bir ipucu varsa) lütfen bize bildirin. Twitter'da @rodydavis ve @khanhnwin hesaplarından bizimle iletişime geçebilirsiniz.
Aşağıdaki kaynaklardan da yararlanabilirsiniz.
Tema oluşturma
- Material Theme Builder (araç)
Uyumlu ve duyarlı kaynaklar:
- Uyarlanabilir ve duyarlı cihazlarda Flutter'ı kod çözme (video)
- Uyarlanabilir düzenler (The Boring Flutter Development Show videosu)
- Duyarlı ve uyarlanabilir uygulamalar oluşturma (flutter.dev)
- Flutter için uyarlanabilir Material bileşenleri (GitHub'daki kitaplık)
- Uygulamanızı büyük ekranlara hazırlamak için yapabileceğiniz 5 şey (Google I/O 2021'den video)
Genel tasarım kaynakları:
- Küçük şeyler: Efsanevi tasarımcı-geliştirici olma (Flutter Engage'den video)
- Katlanabilir Cihazlar için Materyal Tasarım 3 (material.io)
Ayrıca Flutter topluluğuyla bağlantı kurun.
Uygulama dünyasını güzelleştirmeye devam edin.