Flutter uygulamanızı hem sıkıcı hem de güzel bir hale getirin

Flutter uygulamanızı sıkıcı olmaktan çıkarıp güzelleştirin

Bu codelab hakkında

subjectSon güncelleme Haz 24, 2025
account_circleYazan: The Flutter Team

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:

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.

1e67c60667821082.pngd1139cde225de452.png

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ın build() 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:

86c5f73b3aa5fd35.png

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',
  ),
];

23278e4f4610fbf4.png

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. Sans serif büyük T ve serif büyük T
  • 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 7f9a9e103c7b5e5.png 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.):

1e67c60667821082.png

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:

7b51703ed96196a4.png

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.
     
},
   
);
 
}
}

a8487a3c4d7890c9.png

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,
                                   
),
                             
),
                           
],
                         
),
                       
),
                     
],
                   
),
                 
),
               
),
             
],
           
),
         
),
       
);
     
},
   
);
 
}
}

377cfdda63a9de54.png

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:

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

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.

df1d9af97d039cc8.png

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:

d8b2a3d47736dbab.png

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.

7f5e3514a7ee1750.png

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

1e67c60667821082.png

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

Uyumlu ve duyarlı kaynaklar:

Genel tasarım kaynakları:

Ayrıca Flutter topluluğuyla bağlantı kurun.

Uygulama dünyasını güzelleştirmeye devam edin.