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

1. Giriş

Flutter, Google'ın tek bir kod tabanından mobil, web ve masaüstü için yerel olarak derlenmiş, güzel uygulamalar geliştirmeye yönelik kullanıcı arayüzü araç setidir. Ücretsiz ve açık kaynak olan Flutter, mevcut kodlarla çalışır, dünyanın dört bir yanındaki geliştiriciler ve kuruluşlar tarafından kullanılır.

Bu codelab'de bir Flutter müzik uygulamasını geliştirerek sıkıcı olmaktan güzel görünen bir uygulamaya dönüştüreceksiniz. Bu codelab'de, bunun için Material 3'te kullanıma sunulan araçlar ve API'ler kullanılıyor.

Neler öğreneceksiniz?

  • Farklı platformlarda kullanışlı ve güzel bir Flutter uygulaması yazma.
  • Uygulamanızda metinlerin kullanıcı deneyimine katkıda bulunduğundan emin olmak için nasıl tasarlanacağı.
  • Doğru renkleri seçme, widget'ları özelleştirme, kendi temanızı oluşturma ve koyu modu hızlı ve kolay bir şekilde uygulama.
  • Platformlar arası uyarlanabilir uygulamalar geliştirme.
  • Her ekranda iyi görünen uygulamalar nasıl geliştirilir?
  • Flutter uygulamanızı çarpıcı hale getirmek için ona nasıl hareket ekleyebilirsiniz?

Ön koşullar:

Bu codelab'de Flutter deneyiminiz olduğu varsayılır. Yoksa öncelikle temel bilgileri öğrenmeniz iyi olabilir. Aşağıdaki bağlantılar yararlı olabilir:

Neler oluşturacaksınız?

Bu codelab'de, hayranların en sevdikleri sanatçılarla ilgili gelişmeleri takip edebilecekleri bir müzik çalma uygulaması olan MyArtist adlı uygulamanın ana ekranını oluşturma konusunda yardım alabilirsiniz. Farklı platformlarda güzel görünmek için uygulama tasarımınızı nasıl değiştirebileceğiniz açıklanmaktadır.

Aşağıdaki videolarda, bu codelab'in tamamlanmasının ardından uygulamanın işleyiş şekli gösterilmektedir:

Bu codelab'den ne öğrenmek istersiniz?

Konuyla yeni tanıştım ve iyi bir genel bakış istiyorum. Bu konuyla ilgili bilgim var ancak bilgilerinizi tazelemek istiyorum. Projemde kullanmak için örnek kod arıyorum. Belirli bir konuyla ilgili açıklama arıyorum.

2. Flutter geliştirme ortamınızı kurma

Bu laboratuvarı tamamlamak için iki yazılıma ihtiyacınız vardır: Flutter SDK'sı ve düzenleyici.

Codelab'i aşağıdaki cihazlardan birini kullanarak çalıştırabilirsiniz:

  • Bilgisayarınıza bağlı ve Geliştirici moduna ayarlanmış fiziksel bir Android veya iOS cihaz.
  • iOS simülatörü (Xcode araçlarının yüklenmesini gerektirir).
  • Android Emülatör (Android Studio'da kurulum gerektirir).
  • Tarayıcı (hata ayıklama için Chrome gereklidir).
  • Windows, Linux veya macOS masaüstü uygulaması olarak Uygulamayı dağıtmayı planladığınız platformda gerçekleştirmeniz gerekir. Bu nedenle, bir Windows masaüstü uygulaması geliştirmek istiyorsanız uygun derleme zincirine erişmek için Windows'da geliştirme yapmanız gerekir. İşletim sistemine özgü gereksinimler docs.flutter.dev/desktop sayfasında ayrıntılı olarak açıklanmıştır.

3. codelab başlangıç uygulamasını edinme

GitHub'dan klonlama

Bu codelab'i GitHub'dan klonlamak için şu 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 bir masaüstü uygulaması olarak çalıştırın. Alternatif olarak, bu projeyi IDE'nizde açın ve uygulamayı çalıştırmak için projedeki araçları kullanın.

a3c16fc17be25f6c.png Uygulamayı çalıştırın.

Başarıyla gerçekleştirildi. MyArtist'in ana ekranının başlangıç kodu çalışıyor olmalıdır. MyArtist ana ekranı görünür. Masaüstünde güzel görünüyor, ancak mobilde... Çok iyi değil. Birincisi, kaliteye itibar etmiyor. Endişelenmeyin, bu sorunu düzelteceksiniz.

1e67c60667821082.png d1139cde225de452.png

Kodda gezin

Ardından, kod turuna katılı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:adaptive_components/adaptive_components.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 '../../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 conditional mobile layout

        return Scaffold(
          body: SingleChildScrollView(
            child: AdaptiveColumn(
              children: [
                AdaptiveContainer(
                  columnSpan: 12,
                  child: Padding(
                    padding: const EdgeInsets.all(2), // 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(2), // Modify this line
                        child: Text(
                          'Recently played',
                          style: context.headlineSmall,
                        ),
                      ),
                      HomeRecent(
                        playlists: playlists,
                      ),
                    ],
                  ),
                ),
                AdaptiveContainer(
                  columnSpan: 12,
                  child: Padding(
                    padding: const EdgeInsets.all(2), // 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(2), // Modify this line
                                child: Text(
                                  'Top Songs Today',
                                  style: context.titleLarge,
                                ),
                              ),
                              LayoutBuilder(
                                builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: topSongs,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                        // Add spacer between tables
                        Flexible(
                          flex: 10,
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.start,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Padding(
                                padding:
                                    const EdgeInsets.all(2), // Modify this line
                                child: Text(
                                  'New Releases',
                                  style: context.titleLarge,
                                ),
                              ),
                              LayoutBuilder(
                                builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: newReleases,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );
  }
}

Bu dosya, material.dart öğesini içe aktarır ve iki sınıf kullanarak durum bilgili bir widget uygular:

  • import ifadesi, Malzeme Bileşenlerini kullanıma sunar.
  • HomeScreen sınıfı, görüntülenen sayfanın tamamını temsil eder.
  • _HomeScreenState sınıfının build() yöntemi, widget ağacının kökünü oluşturur. Bu da kullanıcı arayüzündeki tüm widget'ların oluşturulma şeklini etkiler.

4. Tipografiden yararlanın

Metin her yerde. Metin, kullanıcıyla iletişim kurmak için kullanışlı bir yöntemdir. Uygulamanızın amacı arkadaş canlısı ve eğlenceli mi ya da güvenilir ve profesyonel mi? En sevdiğiniz bankacılık uygulamasının Comic Sans'ı kullanmamasının bir nedeni var. Metnin sunulma şekli, kullanıcının uygulamanızla ilgili ilk izlenimini şekillendirir. Metinleri daha dikkatli şekilde kullanmanın bazı yollarını burada bulabilirsiniz.

Anlatmak yerine gösterin

Mümkün olduğunda, "göster" yerine "tell" yazın. Örneğin, başlangıç uygulamasındaki NavigationRail her ana rota için sekmelere sahiptir, ancak baştaki simgeler aynıdır:

86c5f73b3aa5fd35.png

Kullanıcının her sekmedeki metni okuması gerektiği için bu yararlı değildir. Kullanıcıların önde gelen simgelere hızlıca göz atması için görsel ipuçları ekleyerek başlayın. Bu işlem yerelleştirme ve erişilebilirlik açısından da faydalıdır.

a3c16fc17be25f6c.png lib/src/shared/router.dart uygulamasında her gezinme hedefi (ev, oynatma listesi ve kişiler) için başında ayrı 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

Bir sorun mu var?

Uygulamanız düzgün bir şekilde çalışmıyorsa yazım hataları olup olmadığını kontrol edin. Gerekirse bu sorunu çözmek için aşağıdaki bağlantılarda yer alan kodu kullanın.

Yazı tiplerini dikkatlice seçin

Yazı tipleri uygulamanızın kişiliğini belirler. Bu nedenle, doğru yazı tipini seçmek çok önemlidir. Yazı tipi seçerken dikkate almanız gereken birkaç nokta vardır:

  • Sans-serif veya serif: Serif yazı tiplerinde dekoratif hatlar veya "kuyruklar" vardır harflerin sonuna eklenmiş ve daha resmi olduğu düşünülür. Sans-serif yazı tiplerinde dekoratif darbeler yoktur ve bu yazı tipleri daha samimi olarak algılanır. 34bf54e4cad90101.png A sans serif büyük harf T, serif büyük harf T
  • Tümü büyük harf yazı tipleri: Az miktarda metne dikkat çekmek için (başlık gibi) yazıların tamamında büyük harf kullanılması uygundur, ancak aşırı kullanıldığında kullanıcının bunu tamamen göz ardı etmesine neden olacak şekilde bağırarak algılanabilir.
  • İlk harfler büyük veya tümce düzeni: Başlık ya da etiket eklerken büyük harfleri nasıl kullandığınızı göz önünde bulundurun: Başlık harfinde, her kelimenin ilk harfi büyük yazılır ("Bu, İlk Harfler Büyüktür Başlıktır") daha resmidir. Yalnızca özel isimlerin ve metindeki ilk kelimenin büyük harfle yazıldığı Cümle düzeni, daha konuşmaya dayalı ve resmi olmayan bir yaklaşımdır.
  • Kısıtlama (her bir harf arasındaki boşluk), satır uzunluğu (ekrandaki tam metnin genişliği) ve satır yüksekliği (her bir metin satırının yüksekliği): Bunların çok fazla veya çok az olması uygulamanızı daha az okunabilir hale getirir. Örneğin, büyük ve kesintisiz bir metin bloğunu okurken yerinizi kolayca kaybedebilirsiniz.

Bu doğrultuda, Google Fonts'a gidin ve müzik uygulamasının amacı eğlenceli ve eğlenceli olması için tasarlanmış olan Montserrat gibi bir sans-serif yazı tipi seçin.

a3c16fc17be25f6c.png Komut satırından, google_fonts paketini alın. Bu işlem, yazı tiplerini uygulama bağımlılığı olarak eklemek için pubspec 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" "http://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/>
        <!-- Make sure these lines are present from here... -->
        <key>com.apple.security.network.client</key>
        <true/>
        <!-- To here. -->
        <key>com.apple.security.network.server</key>
        <true/>
</dict>
</plist>

a3c16fc17be25f6c.png 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.

a3c16fc17be25f6c.png Montserrat'ı kurun TextTheme:

TextTheme get textTheme => GoogleFonts.montserratTextTheme(theme.textTheme); // Modify this line

a3c16fc17be25f6c.png Değişiklikleri etkinleştirmek için 7f9a9e103c7b5e5.png çalışırken yeniden yükleyin. (IDE'nizdeki düğmeyi kullanın veya çalışır durumda yeniden yüklemek için komut satırında r yazın.):

1e67c60667821082.png

Yeni NavigationRail simgelerini, Montserrat yazı tipiyle yazılmış metinle birlikte görürsünüz.

Bir sorun mu var?

Uygulamanız düzgün bir şekilde çalışmıyorsa yazım hataları olup olmadığını kontrol edin. Gerekirse bu sorunu çözmek için aşağıdaki bağlantılarda yer alan kodu kullanın.

5. Temayı belirleme

Temalar, renk ve metin stillerinden oluşan bir sistem belirleyerek uygulamanın yapılandırılmış bir tasarıma ve tek tip ortama sahip olmasını sağlar. Temalar, her widget için tam rengi belirtmek gibi küçük ayrıntıları strese sokmadan kullanıcı arayüzünü hızla uygulamanıza olanak tanır.

Flutter geliştiricileri, özel temalı bileşenleri genellikle şu iki yöntemden biriyle oluşturur:

  • Her biri kendi temasına sahip, bağımsız özel widget'lar oluşturun.
  • Varsayılan widget'lar için kapsamlı temalar oluşturun.

Bu örnekte, uygulama genelinde tutarlı bir şekilde temalı widget'lar ve renkler oluşturmak için lib/src/shared/providers/theme.dart'te bulunan bir tema sağlayıcı kullanılmaktadı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.value, settings.value.sourceColor.value));
 }

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

 CardTheme cardTheme() {
   return CardTheme(
     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,
   );
 }

 TabBarTheme tabBarTheme(ColorScheme colors) {
   return TabBarTheme(
     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 _colors = colors(Brightness.light, targetColor);
   return ThemeData.light().copyWith(
     pageTransitionsTheme: pageTransitionsTheme,
     colorScheme: _colors,
     appBarTheme: appBarTheme(_colors),
     cardTheme: cardTheme(),
     listTileTheme: listTileTheme(_colors),
     bottomAppBarTheme: bottomAppBarTheme(_colors),
     bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
     navigationRailTheme: navigationRailTheme(_colors),
     tabBarTheme: tabBarTheme(_colors),
     drawerTheme: drawerTheme(_colors),
     scaffoldBackgroundColor: _colors.background,
     useMaterial3: true,
   );
 }

 ThemeData dark([Color? targetColor]) {
   final _colors = colors(Brightness.dark, targetColor);
   return ThemeData.dark().copyWith(
     pageTransitionsTheme: pageTransitionsTheme,
     colorScheme: _colors,
     appBarTheme: appBarTheme(_colors),
     cardTheme: cardTheme(),
     listTileTheme: listTileTheme(_colors),
     bottomAppBarTheme: bottomAppBarTheme(_colors),
     bottomNavigationBarTheme: bottomNavigationBarTheme(_colors),
     navigationRailTheme: navigationRailTheme(_colors),
     tabBarTheme: tabBarTheme(_colors),
     drawerTheme: drawerTheme(_colors),
     scaffoldBackgroundColor: _colors.background,
     useMaterial3: true,
   );
 }

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

// Custom Colors
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);
 }
}

a3c16fc17be25f6c.pngSağlayıcıyı kullanmak için bir örnek oluşturup bunu lib/src/shared/app.dart konumunda bulunan MaterialApp bölgesindeki 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 renk seçebilirsiniz.

Doğru renk grubunu seçmek her zaman kolay değildir. Birincil renk hakkında bir fikre sahip olabilirsiniz, ancak büyük olasılıkla uygulamanızın birden fazla rengine sahip olmasını istersiniz. Metin ne renk olmalı? Başlık mı? İçerik mi? Bağlantılar mı? Peki arka plan rengi nedir? Material Theme Builder, uygulamanız için bir dizi tamamlayıcı renkler seçmenize yardımcı olan web tabanlı bir araçtır (Materyal 3'te kullanıma sunulmuştur).

a3c16fc17be25f6c.pngUygulamanın kaynak rengini seçmek için Material Theme Builder'ı açın ve kullanıcı arayüzünün farklı renklerini keşfedin. Markanın estetiğine ve/veya kişisel tercihinize uygun bir renk seçmek önemlidir.

Tema oluşturduktan sonra, Birincil renk balonunu 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.)

a3c16fc17be25f6c.pngBirincil rengin onaltılık değerini tema sağlayıcıya iletin. Örneğin, #00cbe6 onaltılık rengi Color(0xff00cbe6) olarak belirtilir. ThemeProvider, Materyal Tema Oluşturucu'da önizlediğiniz tamamlayıcı renkler grubunu içeren bir ThemeData oluşturur:

final settings = ValueNotifier(ThemeSettings(
   sourceColor:  Color(0xff00cbe6), // Replace this color
   themeMode: ThemeMode.system,
 ));

Uygulamayı çalışırken yeniden başlatın. Birincil renk yerine koyduğunda uygulama daha etkileyici görünmeye başlar. Temaya bağlam içinde başvurarak ve ColorScheme öğesini alarak yeni renklerin tümüne erişin:

final colors = Theme.of(context).colorScheme;

a3c16fc17be25f6c.pngBelirli bir rengi kullanmak için colorScheme üzerinde renk rolüne erişin. lib/src/shared/views/outlined_card.dart bölümüne gidin ve OutlinedCard öğesine kenarlık ekleyin:

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(
        child: widget.child,
        // Add from here...
        decoration: BoxDecoration(
          border: Border.all(
            color: Theme.of(context).colorScheme.outline,
            width: 1,
          ),
        ),
        // ... To here.
      ),
    );
  }
}

Materyal 3, birbirini tamamlayan ve yeni ifade katmanları eklemek için kullanıcı arayüzü genelinde kullanılabilen incelikli renk rolleri sunar. Bu yeni renk rolleri şunları içerir:

  • 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 atamak için kullanılabilir. Bir bileşen belirgin olmasa bile dinamik renkten yararlanabilir.

a3c16fc17be25f6c.png Kullanıcı, uygulamanın parlaklığını cihazın sistem ayarlarından ayarlayabilir. lib/src/shared/app.dart'da cihaz koyu moda ayarlandığında MaterialApp öğesine koyu tema ve tema modu geri döndürü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.

Bir sorun mu var?

Uygulamanız düzgün çalışmıyorsa sorunu tekrar çözmek için aşağıdaki bağlantıda bulunan kodu kullanın.

6. Uyarlanabilir tasarım ekleme

Flutter ile hemen hemen her yerde çalışan uygulamalar oluşturabilirsiniz. Ancak bu, her uygulamanın her yerde aynı davranması beklendiği anlamına gelmez. Kullanıcılar, farklı platformlardan farklı davranış ve özellikler bekler.

Material, uyarlanabilir düzenlerle çalışmayı kolaylaştıran paketler sunar. Bu Flutter paketlerini GitHub'da bulabilirsiniz.

Platformlar arası, uyarlanabilir uygulama oluştururken aşağıdaki platform farklılıklarını göz önünde bulundurun:

  • Giriş yöntemi: fare, dokunma veya oyun kumandası
  • Yazı tipi boyutu, cihaz yönü ve görüntüleme mesafesi
  • Ekran boyutu ve form faktörü: telefon, tablet, katlanabilir, masaüstü, web

a3c16fc17be25f6c.png lib/src/shared/views/adaptive_navigation.dart dosyası, gövdeyi oluşturmak için hedeflerin ve içeriklerin listesini sağlayabileceğiniz bir gezinme sınıfı içerir. Bu düzeni birden fazla ekranda kullandığınızdan, her alt öğeye aktarılacak ortak bir temel düzen olur. Gezinme rayları, masaüstü ve büyük ekranlar için uygundur. Ancak bunun yerine mobil cihazlarda 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 super.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) {
        // Tablet Layout
        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. Telefonunuzda uygulamanızın masaüstü sürümünü görüntülemeyi denediyseniz her şeyi görmek için gözlerinizi kısma ve yakınlaştırmayı bir arada yapmanız gerekir. Uygulamanızın, gösterildiği ekrana bağlı olarak görünümünü değiştirmesini istersiniz. Duyarlı tasarım sayesinde uygulamanızın tüm ekran boyutlarında mükemmel görünmesini sağlayabilirsiniz.

Uygulamanızı duyarlı hale getirmek için birkaç uyarlanabilir ayrılma noktası ekleyin (hata ayıklama ayrılma noktalarıyla karıştırmayın). Bu ayrılma noktaları, uygulamanızın düzenini değiştirmesi gereken ekran boyutlarını belirtir.

Küçük ekranlar, içeriği küçültmeden büyük ekranlar kadar büyük ekranlar kadar gösterilemez. Uygulamanın küçültülmüş bir masaüstü uygulaması gibi görünmesini önlemek amacıyla, mobil cihazlarda içeriği bölmek için sekmelerin kullanıldığı ayrı bir düzen oluşturun. Bu sayede uygulama, mobil cihazlarda daha yerel bir görünüme sahip olur.

lib/src/shared/extensions.dart projesindeki MyArtist projesinde tanımlanan aşağıdaki uzantı yöntemleri, farklı hedefler için optimize edilmiş düzenler tasarlarken iyi bir başlangıç noktası olabilir.

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 1200 pikselden küçük bir ekran tablet olarak kabul edilir. 1.200 pikselden büyük her şey masaüstü olarak kabul edilir. Bir cihaz tablet veya masaüstü değilse mobil olarak kabul edilir. Uyarlanabilir ayrılma noktaları hakkında daha fazla bilgiyi material.io adresinde bulabilirsiniz. adaptive_breakpoints paketini kullanmayı düşünebilirsiniz.

Ana ekranın duyarlı düzeni, Materyal Tasarım'da duyarlı bir ızgara düzeni uygulamak için adaptive_components ve adaptive_breakpoints paketlerini kullanan 12 sütunlu ızgaraya göre AdaptiveContainer ve AdaptiveColumn kullanır.

return LayoutBuilder(
      builder: (context, constraints) {
        return Scaffold(
          body: SingleChildScrollView(
            child: AdaptiveColumn(
              children: [
                AdaptiveContainer(
                  columnSpan: 12,
                  child: Padding(
                    padding: const EdgeInsets.symmetric(
                      horizontal: 20,
                      vertical: 40,
                    ),
                    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(
                          horizontal: 15,
                          vertical: 20,
                        ),
                        child: Text(
                          'Recently played',
                          style: context.headlineSmall,
                        ),
                      ),
                      HomeRecent(
                        playlists: playlists,
                      ),
                    ],
                  ),
                ),
                AdaptiveContainer(
                  columnSpan: 12,
                  child: Padding(
                    padding: const EdgeInsets.all(15),
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Flexible(
                          flex: 10,
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.start,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Padding(
                                padding:
                                    const EdgeInsets.only(left: 8, bottom: 8),
                                child: Text(
                                  'Top Songs Today',
                                  style: context.titleLarge,
                                ),
                              ),
                              LayoutBuilder(
                                builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: topSongs,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                        const SizedBox(width: 25),
                        Flexible(
                          flex: 10,
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.start,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Padding(
                                padding:
                                    const EdgeInsets.only(left: 8, bottom: 8),
                                child: Text(
                                  'New Releases',
                                  style: context.titleLarge,
                                ),
                              ),
                              LayoutBuilder(
                                builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: newReleases,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
      },
    );

a3c16fc17be25f6c.pngUyarlanabilir düzende, biri mobil, diğeri büyük ekranlar için duyarlı düzen olmak üzere iki düzen gerekir. LayoutBuilder şu anda yalnızca masaüstü düzeni döndürüyor. lib/src/features/home/view/home_screen.dart ürününde mobil düzeni 4 sekmeli TabBar ve TabBarView olarak oluşturun.

lib/src/features/home/view/home_screen.dart

import 'package:adaptive_components/adaptive_components.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 '../../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.symmetric(
                      horizontal: 20,
                      vertical: 40,
                    ),
                    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(
                          horizontal: 15,
                          vertical: 20,
                        ),
                        child: Text(
                          'Recently played',
                          style: context.headlineSmall,
                        ),
                      ),
                      HomeRecent(
                        playlists: playlists,
                      ),
                    ],
                  ),
                ),
                AdaptiveContainer(
                  columnSpan: 12,
                  child: Padding(
                    padding: const EdgeInsets.all(15),
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Flexible(
                          flex: 10,
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.start,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Padding(
                                padding:
                                    const EdgeInsets.only(left: 8, bottom: 8),
                                child: Text(
                                  'Top Songs Today',
                                  style: context.titleLarge,
                                ),
                              ),
                              LayoutBuilder(
                                builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: topSongs,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                        const SizedBox(width: 25),
                        Flexible(
                          flex: 10,
                          child: Column(
                            mainAxisAlignment: MainAxisAlignment.start,
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Padding(
                                padding:
                                    const EdgeInsets.only(left: 8, bottom: 8),
                                child: Text(
                                  'New Releases',
                                  style: context.titleLarge,
                                ),
                              ),
                              LayoutBuilder(
                                builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: newReleases,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );
     },
   );
 }
}

377cfdda63a9de54.png

Bir sorun mu var?

Uygulamanız düzgün çalışmıyorsa sorunu tekrar çözmek için aşağıdaki bağlantıda bulunan kodu kullanın.

Boşluk kullan

Boşluklar, uygulamanız için önemli bir görsel araçtır. Bu araç, bölümler arasında düzenli bir ara oluşturur.

Yeterli boşluk bırakmamaktansa ç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 eklemeniz tercih edilir.

Boşluk olmaması, görme sorunları olanlar için zor olabilir. Çok fazla boşluk olması durumunda tutarlılık engellenebilir ve kullanıcı arayüzü kötü düzenlenmiş görünebilir. Ö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şlukların ince ayarını yapmak için düzende daha fazla ince ayar yaparsınız.

a3c16fc17be25f6c.png Bir widget'ın etrafına boşluk eklemek için widget'ı Padding nesnesiyle sarmalayın. Şu anda lib/src/features/home/view/home_screen.dart alanında bulunan tüm dolgu değerlerini 35'e yükseltin:

lib/src/features/home/view/home_screen.dart

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,
                            ),
                          ),
                        ],
                      ),
                    ),
                    // Add spacer between tables
                    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,
                            ),
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );

a3c16fc17be25f6c.png Uygulamayı çalışırken yeniden yükleyin. Önceki gibi görünmesi gerekir. Ancak widget'lar arasında daha fazla boşluk vardır. Ek dolgu daha iyi görünüyor ancak üst kısımdaki vurgu banner'ı kenarlara hâlâ çok yakın.

a3c16fc17be25f6c.png lib/src/features/home/view/home_highlight.dart içinde banner'ın dolgusunu 35 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(35), // 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: () => launch('https://docs.flutter.dev'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png Uygulamayı çalışırken yeniden yükleyin. Alt kısımdaki iki oynatma listesinin arasında boşluk olmadığından aynı tabloya ait gibi görünürler. Durum bu değil. Bir sonraki adımda bunu düzelteceksiniz.

df1d9af97d039cc8.png

a3c16fc17be25f6c.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'da, 35 genişliğinde bir SizedBox ekleyin:

lib/src/features/home/view/home_screen.dart

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,
              ),
            ),
            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,
              ),
            ),
            PlaylistSongs(
              playlist: newReleases,
              constraints: constraints,
            ),
          ],
        ),
      ),
    ],
  ),
),

a3c16fc17be25f6c.png Uygulamayı çalışırken yeniden yükleyin. Uygulama şu şekilde görünmelidir:

d8b2a3d47736dbab.png

Artık ana ekran içerikleri için bolca yer var ancak her şey çok ayrı görünüyor ve bölümler arasında uyuşmazlık yok.

a3c16fc17be25f6c.png Şu ana kadar EdgeInsets.all(35) kullanarak ana ekrandaki widget'lara ilişkin tüm dolguyu (yatay ve dikey) 35 olarak ayarladınız, ancak kenarların her biri için dolguyu ayrı ayrı da ayarlayabilirsiniz. Dolguyu alana daha iyi sığacak şekilde özelleştirin.

  • EdgeInsets.LTRB() sol, üst, sağ ve alt öğelerini ayrı ayrı ayarlar
  • EdgeInsets.symmetric(), dikey (üst ve alt) dolguyu eşdeğer, yatay (sol ve sağ) dolguyu eşdeğer olacak şekilde ayarlar
  • EdgeInsets.only() yalnızca belirtilen kenarları ayarlar.
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(
                       horizontal: 15,
                       vertical: 10,
                     ), // Modify this line
                     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(left: 8, bottom: 8), // Modify this line
                               child: Text(
                                 'Top Songs Today',
                                 style: context.titleLarge,
                               ),
                             ),
                             LayoutBuilder(
                               builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: topSongs,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                    const SizedBox(width: 25),
                    Flexible(
                      flex: 10,
                      child: Column(
                        mainAxisAlignment: MainAxisAlignment.start,
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          Padding(
                            padding: const EdgeInsets.only(left: 8, bottom: 8), // Modify this line
                            child: Text(
                              'New Releases',
                               style: context.titleLarge,
                            ),
                          ),
                          LayoutBuilder(
                            builder: (context, constraints) =>
                                    PlaylistSongs(
                                  playlist: newReleases,
                                  constraints: constraints,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ],
            ),
          ),
        );

a3c16fc17be25f6c.png lib/src/features/home/view/home_highlight.dart özelliğinde banner'ın sol ve sağ dolgusunu 35, üst ve alt dolgusunu 5 değerine 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 this 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: () => launch('https://docs.flutter.dev'),
            ),
          ),
        ),
      ],
    );
  }
}

a3c16fc17be25f6c.png Uygulamayı çalışırken yeniden yükleyin. Düzen ve boşluk çok daha iyi görünüyor. Son rötuş için biraz hareket ve animasyon ekleyin.

7f5e3514a7ee1750.png

Bir sorun mu var?

Uygulamanız düzgün çalışmıyorsa sorunu tekrar çözmek için aşağıdaki bağlantıda bulunan kodu kullanın.

7. Hareket ve animasyon ekleme

Hareket ve animasyon, hareket ve enerji vermenin yanı sıra kullanıcı uygulamayla etkileşimde bulunduğunda geri bildirim sağlamak için harika yöntemlerdir.

Ekranlar arasında animasyon uygulama

ThemeProvider, mobil platformlar (iOS, Android) için ekran geçişi animasyonlarının bulunduğu bir PageTransitionsTheme tanımlar. Masaüstü kullanıcıları fare veya dokunmatik yüzey tıklamasından zaten geri bildirim aldığından, sayfa geçiş animasyonuna gerek yoktur.

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şi animasyonları 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(),
  },
);

a3c16fc17be25f6c.png lib/src/shared/providers/theme.dart içindeki PageTransitionsTheme öğesini hem açık hem de koyu temaya geçirin.

lib/src/shared/providers/theme.dart

ThemeData light([Color? targetColor]) {
  final _colors = colors(Brightness.light, targetColor);
  return ThemeData.light().copyWith(
    pageTransitionsTheme: pageTransitionsTheme, // Add this line
    colorScheme: ColorScheme.fromSeed(
      seedColor: source(targetColor),
      brightness: Brightness.light,
    ),
    appBarTheme: appBarTheme(_colors),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(),
    tabBarTheme: tabBarTheme(_colors),
    scaffoldBackgroundColor: _colors.background,
  );
}

ThemeData dark([Color? targetColor]) {
  final _colors = colors(Brightness.dark, targetColor);
  return ThemeData.dark().copyWith(
    pageTransitionsTheme: pageTransitionsTheme, // Add this line
    colorScheme: ColorScheme.fromSeed(
      seedColor: source(targetColor),
      brightness: Brightness.dark,
    ),
    appBarTheme: appBarTheme(_colors),
    cardTheme: cardTheme(),
    listTileTheme: listTileTheme(),
    tabBarTheme: tabBarTheme(_colors),
    scaffoldBackgroundColor: _colors.background,
  );
}

iOS'te animasyonsuz

iOS'te animasyonlu

Bir sorun mu var?

Uygulamanız düzgün çalışmıyorsa sorunu tekrar çözmek için aşağıdaki bağlantıda bulunan kodu kullanın.

Fareyle öğelerin üzerine gelerek durumu ekle

Masaüstü uygulamasına hareket eklemenin bir yolu, kullanıcı imleci üzerine getirdiğinde widget'ın durumunu (renk, şekil veya içerik gibi) değiştirdiği fareyle üzerine gelme durumlarıdır.

Varsayılan olarak _OutlinedCardState sınıfı ("son oynatılan" oynatma listesi kutuları için kullanılır), fareyle üzerine gelindiğinde imleç okunu işaretçiye dönüştüren bir MouseRegion döndürür. Ancak daha fazla görsel geri bildirim ekleyebilirsiniz.

a3c16fc17be25f6c.png lib/src/shared/views/outlined_card.dart dosyasını açın ve içeriğini aşağıdaki uygulamayla değiştirerek _hovered durumunu kullanıma sunun.

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.withOpacity(
                _hovered ? 0.12 : 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,
        ),
      ),
    );
  }
}

a3c16fc17be25f6c.png Uygulamayı yeniden yükleyin ve ardından fareyle, son oynatılan oynatma listesi karolarından birinin üzerine gelin.

OutlinedCard, opaklığı değiştirir ve köşeleri yuvarlar.

a3c16fc17be25f6c.png Son olarak, lib/src/shared/views/hoverable_song_play_button.dart içinde tanımlanan HoverableSongPlayButton widget'ını kullanarak bir oynatma listesindeki şarkı numarasına animasyon ekleyin. lib/src/features/playlists/view/playlist_songs.dart'da Center widget'ını (şarkı numarasını içerir) bir HoverableSongPlayButton ile sarmalayın:

lib/src/features/playlists/view/playlist_songs.dart

HoverableSongPlayButton(        // Add this line
  hoverMode: HoverMode.overlay, // Add this line
  song: playlist.songs[index],  // Add this line
  child: Center(                // Modify this line
    child: Text(
      (index + 1).toString(),
       textAlign: TextAlign.center,
       ),
    ),
  ),                            // Add this line

a3c16fc17be25f6c.pngUygulamayı yeniden yükleyin ve imleci Günün En Popüler Şarkıları veya Yeni Çıkanlar şarkı listesindeki şarkı numarasının üzerine getirin.

Sayı, tıkladığınızda şarkıyı çalan bir oynat düğmesine dönüşür.

Projenin son kodunu GitHub'da görün.

8. Tebrikler!

Bu codelab'i tamamladınız. Bir uygulamayı daha güzel, aynı zamanda daha erişilebilir, yerelleştirilebilir ve çeşitli platformlar için daha uygun hale getirmek için uygulamaya entegre edebileceğiniz pek çok küçük değişiklik olduğunu öğrendiniz. Bu teknikler aşağıdakileri kapsar, ancak bunlarla sınırlı değildir:

  • Tipografi: Metin bir iletişim aracından daha fazlasıdır. Kullanıcılar üzerinde olumlu bir etki yaratmak için metnin görüntülenme şeklini kullanın. ve uygulamanızla ilgili algıyı güçlendirir.
  • Tema: Her widget için tasarım kararları vermek zorunda kalmadan, güvenilir bir şekilde kullanabileceğiniz bir tasarım sistemi oluşturun.
  • Uyarlanabilirlik: Kullanıcının uygulamanızı çalıştırdığı cihaz ile platformu ve uygulamanı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 hatta kullanıcılara geri bildirim verir.

Birkaç ufak değişiklikle uygulamanızı sıkıcı olmaktan güzelliğe taşıyabilirsiniz:

Önce

1e67c60667821082.png

Sonra

Sonraki adımlar

Flutter'da güzel uygulamalar geliştirme hakkında daha fazla bilgi edindiğinizi umuyoruz.

Burada belirtilen ipuçları veya püf noktalarından herhangi birini uygularsanız (ya da paylaşmak istediğiniz bir ipucu varsa) görüşlerinizi almak isteriz. Twitter'da @rodydavis ve @khanhnwin üzerinden bize ulaşabilirsiniz.

Aşağıdaki kaynaklardan da yararlanabilirsiniz.

Tema oluşturma

Uyarlanabilir ve duyarlı kaynaklar:

Genel tasarım kaynakları:

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

İlerleyin ve uygulamayı güzelleştirin.