เปลี่ยนแอป Flutter ได้ง่ายๆ ให้เป็นแอปที่สวยงาม

เปลี่ยนแอป Flutter ได้ง่ายๆ ให้เป็นแอปที่สวยงาม

เกี่ยวกับ Codelab นี้

subjectอัปเดตล่าสุดเมื่อ พ.ค. 13, 2024
account_circleเขียนโดย The Flutter Team

1 บทนำ

Flutter คือชุดเครื่องมือ UI ของ Google สำหรับการสร้างแอปพลิเคชันที่สวยงามซึ่งรวบรวมไว้ภายในอุปกรณ์เคลื่อนที่ เว็บ และเดสก์ท็อปจากฐานของโค้ดเดียว Flutter ใช้งานได้กับโค้ดที่มีอยู่ ซึ่งเป็นโค้ดที่นักพัฒนาซอฟต์แวร์และองค์กรทั่วโลกใช้ โดยเป็นโค้ดฟรีและเป็นโอเพนซอร์ส

ใน Codelab นี้ คุณจะได้ปรับปรุงแอปพลิเคชันเพลง Flutter โดยเปลี่ยนจากสิ่งที่น่าเบื่อให้กลายเป็นความสวยงาม Codelab นี้ใช้เครื่องมือและ API ที่นำเสนอใน Material 3 เพื่อให้บรรลุเป้าหมายนี้

สิ่งที่คุณจะได้เรียนรู้

  • วิธีเขียนแอป Flutter ให้ใช้งานได้และสวยงามในแพลตฟอร์มต่างๆ
  • วิธีออกแบบข้อความในแอปเพื่อให้แน่ใจว่าข้อความจะมีประโยชน์ต่อประสบการณ์ของผู้ใช้
  • วิธีเลือกสีที่เหมาะสม ปรับแต่งวิดเจ็ต สร้างธีมของคุณเอง และใช้งานโหมดมืดได้อย่างรวดเร็วและง่ายดาย
  • วิธีสร้างแอปที่ปรับเปลี่ยนได้ข้ามแพลตฟอร์ม
  • วิธีสร้างแอปที่ดูดีบนหน้าจอใดก็ได้
  • วิธีเพิ่มการเคลื่อนไหวให้แอป Flutter เพื่อทำให้แอปโดดเด่น

สิ่งที่ต้องมีก่อน

Codelab นี้จะสมมติว่าคุณมีประสบการณ์การใช้งาน Flutter อยู่บ้าง หากไม่ คุณอาจต้องเรียนรู้ข้อมูลเบื้องต้นก่อน ลิงก์ต่อไปนี้มีประโยชน์

สิ่งที่คุณจะสร้าง

Codelab นี้จะแนะนำขั้นตอนการสร้างหน้าจอหลักสำหรับแอปพลิเคชันชื่อ MyArtist ซึ่งเป็นแอปโปรแกรมเล่นเพลงที่แฟนๆ สามารถติดตามข่าวสารเกี่ยวกับศิลปินคนโปรด โดยจะกล่าวถึงวิธีที่คุณสามารถปรับแต่งการออกแบบแอปให้ดูสวยงามในแพลตฟอร์มต่างๆ

วิดีโอต่อไปนี้แสดงวิธีการทำงานของแอปเมื่อ Codelab เสร็จสมบูรณ์

คุณต้องการเรียนรู้อะไรจาก Codelab นี้

2 ตั้งค่าสภาพแวดล้อมในการพัฒนาซอฟต์แวร์ Flutter

ห้องทดลองนี้ต้องมีซอฟต์แวร์ 2 ประเภท ได้แก่ Flutter SDK และเครื่องมือแก้ไข

คุณเรียกใช้ Codelab ได้โดยใช้อุปกรณ์ต่อไปนี้

  • อุปกรณ์ Android หรือ iOS ที่เชื่อมต่อกับคอมพิวเตอร์และตั้งค่าเป็นโหมดนักพัฒนาซอฟต์แวร์
  • เครื่องมือจำลอง iOS (ต้องติดตั้งเครื่องมือ Xcode)
  • โปรแกรมจำลอง Android (ต้องตั้งค่าใน Android Studio)
  • เบราว์เซอร์ (การแก้ไขข้อบกพร่องต้องใช้ Chrome)
  • เป็นแอปพลิเคชัน Windows, Linux หรือ macOS บนเดสก์ท็อป คุณต้องพัฒนาบนแพลตฟอร์มที่คุณวางแผนจะทำให้ใช้งานได้ ดังนั้นหากต้องการพัฒนาแอป Windows บนเดสก์ท็อป คุณต้องพัฒนาบน Windows เพื่อเข้าถึงเชนบิลด์ที่เหมาะสม มีข้อกำหนดเฉพาะระบบปฏิบัติการที่ครอบคลุมรายละเอียดใน docs.flutter.dev/desktop

3 ดาวน์โหลดแอปเริ่มต้นสำหรับ Codelab

โคลนไฟล์จาก GitHub

หากต้องการโคลน Codelab นี้จาก GitHub ให้เรียกใช้คำสั่งต่อไปนี้

git clone https://github.com/flutter/codelabs.git
cd codelabs/boring_to_beautiful/step_01/

เพื่อให้แน่ใจว่าทุกอย่างทำงานได้ดี ให้เรียกใช้แอปพลิเคชัน Flutter เป็นแอปพลิเคชันเดสก์ท็อปดังที่แสดงด้านล่าง อีกทางเลือกหนึ่งคือเปิดโปรเจ็กต์นี้ใน IDE แล้วใช้เครื่องมือของโปรเจ็กต์เพื่อเรียกใช้แอปพลิเคชัน

a3c16fc17be25f6c.png เรียกใช้แอป

สำเร็จ! รหัสเริ่มต้นสำหรับหน้าจอหลักของ MyArtist ควรทำงานอยู่ คุณควรเห็นหน้าจอหลักของ MyArtist แม้จะดูดีในเดสก์ท็อป แต่อุปกรณ์เคลื่อนที่กลับ... ไม่ค่อยดี อย่างหนึ่งก็คือ มันไม่ได้ให้รอยบากนั้นหรอก ไม่ต้องกังวล คุณจะแก้ไขปัญหานี้ได้

1e67c60667821082.png d1139cde225de452.png

ทัวร์ชมโค้ด

ต่อไป ทัวร์ชมโค้ด

เปิด lib/src/features/home/view/home_screen.dart ซึ่งจะมีข้อมูลต่อไปนี้

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

ไฟล์นี้นำเข้า material.dart และใช้วิดเจ็ตการเก็บสถานะโดยใช้ 2 คลาส ดังนี้

  • คำสั่ง import แสดงคอมโพเนนต์เนื้อหาที่พร้อมใช้งาน
  • คลาส HomeScreen จะแสดงหน้าเว็บที่ปรากฏทั้งหน้า
  • เมธอด build() ของคลาส _HomeScreenState จะสร้างรูทของโครงสร้างวิดเจ็ต ซึ่งจะส่งผลต่อวิธีสร้างวิดเจ็ตทั้งหมดใน UI

4 ใช้ประโยชน์จากการพิมพ์

ข้อความปรากฏอยู่ทุกที่ ข้อความเป็นวิธีที่มีประโยชน์ในการสื่อสารกับผู้ใช้ แอปของคุณมีเจตนาให้ใช้งานง่ายและสนุก หรืออาจเชื่อถือได้และเป็นมืออาชีพหรือไม่ มีเหตุผลว่าทำไมแอปธนาคารที่คุณชื่นชอบถึงไม่ใช้ Comic Sans ลักษณะการนำเสนอข้อความจะเป็นตัวกำหนดความประทับใจแรกของผู้ใช้ต่อแอปของคุณ ต่อไปนี้คือวิธีการใช้ข้อความอย่างรอบคอบ

แสดงให้เห็นแทนการบอก

"แสดง" หากเป็นไปได้ แทนที่จะใช้ "บอก" ตัวอย่างเช่น NavigationRail ในแอปเริ่มต้นมีแท็บสำหรับเส้นทางหลักแต่ละเส้นทาง แต่ไอคอนนำหน้าจะเหมือนกัน ดังนี้

86c5f73b3aa5fd35.png

กรณีนี้ไม่มีประโยชน์เนื่องจากผู้ใช้ยังคงต้องอ่านข้อความของแต่ละแท็บ เริ่มด้วยการเพิ่มสัญลักษณ์ที่เป็นภาพ เพื่อให้ผู้ใช้สามารถมองผ่านไอคอนนำเพื่อค้นหาแท็บที่ต้องการได้อย่างรวดเร็ว ซึ่งจะช่วยในด้านการแปลและการช่วยเหลือพิเศษด้วย

a3c16fc17be25f6c.png ใน lib/src/shared/router.dart ให้เพิ่มไอคอนนำทางที่แตกต่างกันสำหรับปลายทางการนำทางแต่ละรายการ (หน้าแรก เพลย์ลิสต์ และผู้คน) ดังนี้

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

หากพบปัญหา

หากแอปทำงานไม่ปกติ ให้มองหาการพิมพ์ผิด หากจำเป็น ให้ใช้โค้ดในลิงก์ต่อไปนี้เพื่อกลับสู่เส้นทาง

เลือกแบบอักษรอย่างรอบคอบ

แบบอักษรจะเป็นตัวกำหนดลักษณะเฉพาะของแอปพลิเคชันของคุณ ดังนั้นการเลือกแบบอักษรที่เหมาะสมจึงเป็นสิ่งสำคัญ สิ่งที่ควรคำนึงถึงเมื่อเลือกแบบอักษรมีดังนี้

  • Sans-serif หรือ serif: แบบอักษร Serif มีเส้นตกแต่งหรือ "tail" ที่ส่วนท้ายของตัวอักษรและมองว่าเป็นทางการมากขึ้น แบบอักษร Sans Serif ไม่มีลายเส้นตกแต่งและมักมองว่าเป็นแบบไม่เป็นทางการมากกว่า 34bf54e4cad90101.png A Sans Serif ตัวพิมพ์ใหญ่ T และ Serif อักษรตัวพิมพ์ใหญ่ T
  • แบบอักษรตัวพิมพ์ใหญ่ทั้งหมด: การใช้ตัวพิมพ์ใหญ่ทั้งหมดเหมาะสำหรับการดึงดูดความสนใจไปยังข้อความจำนวนน้อย (นึกถึงบรรทัดแรก) แต่หากใช้มากเกินไป ระบบอาจมองว่าเป็นการตะโกนที่ทำให้ผู้ใช้ไม่ต้องสนใจข้อความนั้น
  • ลักษณะตัวพิมพ์ของชื่อเรื่องหรือประโยคตัวอย่าง: เมื่อเพิ่มชื่อหรือป้ายกำกับ ให้พิจารณาวิธีใช้ตัวพิมพ์ใหญ่: ลักษณะตัวพิมพ์ของชื่อเรื่อง ซึ่งอักษรตัวแรกของแต่ละคำเป็นตัวพิมพ์ใหญ่ ("This Is a Title Case Title") เป็นทางการมากขึ้น ขึ้นต้นประโยคด้วยตัวพิมพ์ใหญ่ ซึ่งจะใช้อักษรตัวพิมพ์ใหญ่เฉพาะคำนามและคำแรกในข้อความ ("This is a sentence case title") มีความเป็นบทสนทนาและไม่เป็นทางการมากกว่า
  • การจัดบรรทัด (ระยะห่างระหว่างตัวอักษรแต่ละตัวอักษร) ความยาวบรรทัด (ความกว้างของข้อความเต็มทั้งหน้าจอ) และความสูงของบรรทัด (ความสูงของข้อความแต่ละบรรทัด): ข้อความเหล่านี้มากเกินไปหรือน้อยเกินไปจะทำให้แอปอ่านน้อยลง เช่น คุณอาจเสียตำแหน่งเมื่ออ่านบล็อกข้อความขนาดใหญ่ที่อ่านไม่แตกได้ง่าย

ดังนั้นไปที่ Google Fonts และเลือกแบบอักษร Sans-Serif เช่น Montserrat เนื่องจากแอปเพลงมีวัตถุประสงค์เพื่อสร้างความบันเทิงและสนุกสนาน

a3c16fc17be25f6c.png ดึงข้อมูลแพ็กเกจ google_fonts จากบรรทัดคำสั่ง ซึ่งจะอัปเดตไฟล์ pubspec เพื่อเพิ่มแบบอักษรเป็นทรัพยากร Dependency ของแอป

$ 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 ให้นำเข้าแพ็กเกจใหม่ดังนี้

lib/src/shared/extensions.dart

import 'package:google_fonts/google_fonts.dart';  // Add this line.

a3c16fc17be25f6c.png ตั้งค่ามอนต์เซอร์รัต TextTheme:

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

a3c16fc17be25f6c.png รีโหลดร้อน 7f9a9e103c7b5e5.png เพื่อเปิดใช้งานการเปลี่ยนแปลง (ใช้ปุ่มใน IDE หรือ จากบรรทัดคำสั่ง ให้ป้อน r เพื่อ Hotโหลดซ้ำ):

1e67c60667821082.png

คุณจะเห็นไอคอน NavigationRail ใหม่ พร้อมกับข้อความที่แสดงในแบบอักษร Montserrat

หากพบปัญหา

หากแอปทำงานไม่ปกติ ให้มองหาการพิมพ์ผิด หากจำเป็น ให้ใช้โค้ดในลิงก์ต่อไปนี้เพื่อกลับสู่เส้นทาง

5 กำหนดธีม

ธีมช่วยให้แอปมีการออกแบบที่มีโครงสร้างและเป็นแบบเดียวกันด้วยการระบุระบบชุดสีและรูปแบบข้อความ ธีมช่วยให้คุณติดตั้ง UI ได้อย่างรวดเร็วโดยไม่ต้องเน้นรายละเอียดเล็กๆ น้อยๆ เช่น การระบุสีที่แน่ชัดสำหรับวิดเจ็ตทุกรายการ

โดยปกติแล้วนักพัฒนาแอป Flutter จะสร้างคอมโพเนนต์ที่มีธีมที่กำหนดเองด้วย 1 ใน 2 วิธีต่อไปนี้

  • สร้างวิดเจ็ตที่กำหนดเองโดยแต่ละวิดเจ็ตมีธีมของตัวเอง
  • สร้างธีมที่กำหนดขอบเขตสำหรับวิดเจ็ตเริ่มต้น

ตัวอย่างนี้ใช้ผู้ให้บริการธีมที่อยู่ใน lib/src/shared/providers/theme.dart เพื่อสร้างวิดเจ็ตและสีที่มีธีมสอดคล้องกันทั่วทั้งแอป

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.pngหากต้องการใช้ผู้ให้บริการ ให้สร้างอินสแตนซ์และส่งไปยังออบเจ็กต์ธีมที่กำหนดขอบเขตใน MaterialApp ซึ่งอยู่ใน lib/src/shared/app.dart โดยจะรับค่าโดยออบเจ็กต์ Theme ที่ฝังอยู่:

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

เมื่อตั้งค่าธีมแล้ว ให้เลือกสีสำหรับแอปพลิเคชัน

การเลือกชุดสีที่เหมาะสมไม่ใช่เรื่องง่ายเสมอไป คุณอาจพอนึกออกว่าสีหลักจะเป็นสีอะไร แต่น่าจะอยากให้แอปมีมากกว่า 1 สีเหมือนกันนะ ข้อความควรเป็นสีอะไร ชื่ออะไร คอนเทนต์ ลิงก์ใช่ไหม แล้วสีพื้นหลังล่ะ Material Theme Builder เป็นเครื่องมือบนเว็บ (เปิดตัวใน Material 3) ซึ่งจะช่วยให้คุณเลือกชุดสีเสริมสำหรับแอปได้

a3c16fc17be25f6c.pngหากต้องการเลือกสีต้นฉบับของแอปพลิเคชัน ให้เปิด Material Theme Builder และสำรวจสีต่างๆ ของ UI สิ่งสำคัญคือการเลือกสีที่เหมาะกับความสวยงามของแบรนด์และ/หรือความชอบส่วนตัวของคุณ

หลังจากสร้างธีมแล้ว ให้คลิกขวาลูกโป่งสีหลัก ซึ่งจะเปิดกล่องโต้ตอบที่มีค่าเลขฐาน 16 ของสีหลัก คัดลอกค่านี้ (คุณสามารถตั้งค่าสีโดยใช้กล่องโต้ตอบนี้)

a3c16fc17be25f6c.pngส่งค่าเลขฐานสิบหกของสีหลักไปยังผู้ให้บริการธีม ตัวอย่างเช่น สีแบบเลขฐาน 16 #00cbe6 จะระบุเป็น Color(0xff00cbe6) ThemeProvider จะสร้าง ThemeData ที่มีชุดสีเสริมที่คุณดูตัวอย่างใน Material Theme Builder:

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

Hot รีสตาร์ทแอป แอปจะเริ่มสื่ออารมณ์มากขึ้นเมื่อใส่สีหลักเรียบร้อยแล้ว เข้าถึงสีใหม่ทั้งหมดโดยอ้างอิงธีมในบริบทและเลือก ColorScheme:

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

a3c16fc17be25f6c.pngหากต้องการใช้สีใดสีหนึ่ง ให้ไปที่บทบาทสีในcolorScheme ไปที่ lib/src/shared/views/outlined_card.dart แล้วใส่เส้นขอบให้ OutlinedCard:

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

Material 3 นำเสนอบทบาทสีต่างๆ ที่แตกต่างกันซึ่งช่วยส่งเสริมกันและกันและนำมาใช้ได้ตลอดทั้ง UI เพื่อเพิ่มการแสดงสีหน้าใหม่ๆ บทบาทใหม่เกี่ยวกับสีเหล่านี้ ได้แก่

  • 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

นอกจากนี้ โทเค็นการออกแบบใหม่ยังรองรับทั้งธีมสว่างและธีมมืดอีกด้วย

7b51703ed96196a4.png

โดยจะใช้บทบาทสีเหล่านี้เพื่อกําหนดความหมายและเน้นส่วนต่างๆ ของ UI ได้ แม้ว่าคอมโพเนนต์จะไม่โดดเด่น แต่คอมโพเนนต์ก็ยังใช้ประโยชน์จากสีแบบไดนามิกได้

a3c16fc17be25f6c.png ผู้ใช้จะตั้งความสว่างของแอปได้ในการตั้งค่าระบบของอุปกรณ์ ใน lib/src/shared/app.dart เมื่อตั้งค่าอุปกรณ์เป็นโหมดมืด ให้เปลี่ยนธีมมืดและโหมดธีมกลับไปเป็น MaterialApp

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

คลิกไอคอนดวงจันทร์ที่มุมขวาบนเพื่อเปิดใช้โหมดมืด

หากพบปัญหา

หากแอปของคุณทำงานไม่ถูกต้อง ให้ใช้โค้ดที่ลิงก์ต่อไปนี้เพื่อกลับสู่การทำงาน

6 เพิ่มการออกแบบที่ปรับเปลี่ยนได้

Flutter จะช่วยให้คุณสร้างแอปที่ทำงานได้เกือบทุกที่ แต่ก็ไม่ได้หมายความว่าทุกแอปควรทำงานเหมือนกันในทุกที่ ปัจจุบันผู้ใช้คาดหวังพฤติกรรมและฟีเจอร์ที่แตกต่างกันจากแพลตฟอร์มต่างๆ

Material มีแพ็กเกจที่ช่วยให้ใช้งานเลย์เอาต์แบบปรับได้ได้ง่ายขึ้น โดยหาแพ็กเกจ Flutter เหล่านี้ได้ใน GitHub

คำนึงถึงความแตกต่างของแพลตฟอร์มต่อไปนี้เมื่อสร้างแอปพลิเคชันแบบปรับเปลี่ยนได้ข้ามแพลตฟอร์ม

  • วิธีการป้อนข้อมูล: เมาส์ การแตะ หรือเกมแพด
  • ขนาดแบบอักษร การวางแนวของอุปกรณ์ และระยะการดู
  • ขนาดหน้าจอและรูปแบบของอุปกรณ์: โทรศัพท์ แท็บเล็ต แบบพับได้ เดสก์ท็อป เว็บ

a3c16fc17be25f6c.png ไฟล์ lib/src/shared/views/adaptive_navigation.dart มีคลาสการนำทางที่คุณให้รายการปลายทางและเนื้อหาสำหรับแสดงผลเนื้อความได้ เนื่องจากคุณใช้เลย์เอาต์นี้ในหลายหน้าจอ จึงมีเลย์เอาต์พื้นฐานที่ใช้ร่วมกันเพื่อส่งผ่านไปยังแต่ละหน้าจอย่อย แถบนำทางเหมาะสำหรับเดสก์ท็อปและหน้าจอขนาดใหญ่ แต่ทำให้เลย์เอาต์เหมาะกับอุปกรณ์เคลื่อนที่โดยแสดงแถบนำทางด้านล่างบนอุปกรณ์เคลื่อนที่แทน

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

หน้าจอแต่ละหน้าจอไม่ได้มีขนาดเท่ากัน หากพยายามแสดงแอปเวอร์ชันเดสก์ท็อปในโทรศัพท์ คุณต้องหรี่ตาและซูมหน้าจอร่วมกันจึงจะเห็นทุกอย่าง คุณต้องการให้แอปเปลี่ยนวิธีแสดงผลตามหน้าจอที่แสดงแอป การออกแบบที่ปรับเปลี่ยนตามอุปกรณ์จะช่วยให้แอปดูดีในหน้าจอทุกขนาด

ในการทำให้แอปตอบสนองตามอุปกรณ์ ให้ใช้เบรกพอยท์ที่ปรับเปลี่ยนได้ 2-3 จุด (อย่าสับสนกับเบรกพอยท์การแก้ไขข้อบกพร่อง) เบรกพอยท์เหล่านี้จะระบุขนาดหน้าจอที่แอปของคุณควรเปลี่ยนเลย์เอาต์

หน้าจอขนาดเล็กไม่สามารถแสดงได้มากเท่ากับหน้าจอขนาดใหญ่ หากไม่ย่อเนื้อหาลง หากต้องการป้องกันไม่ให้แอปดูเหมือนแอปบนเดสก์ท็อปที่ลดขนาดลง ให้สร้างเลย์เอาต์แยกสำหรับอุปกรณ์เคลื่อนที่ซึ่งใช้แท็บในการแบ่งเนื้อหา วิธีนี้ทำให้แอปดูกลมกลืนกับการใช้งานบนอุปกรณ์เคลื่อนที่มากขึ้น

การใช้วิธีการขยายเวลาต่อไปนี้ (ตามที่กำหนดไว้ในโปรเจ็กต์ MyArtist ใน lib/src/shared/extensions.dart) เป็นจุดเริ่มต้นที่ดีเมื่อออกแบบเลย์เอาต์ที่เพิ่มประสิทธิภาพสำหรับเป้าหมายที่แตกต่างกัน

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 พิกเซล (ในทิศทางที่ยาวที่สุด) แต่เล็กกว่า 1,200 พิกเซลจะถือว่าเป็นแท็บเล็ต สิ่งที่มีขนาดใหญ่กว่า 1,200 พิกเซลจะถือว่าเป็นเดสก์ท็อป หากอุปกรณ์ไม่ใช่แท็บเล็ตหรือเดสก์ท็อป จะถือว่าเป็นอุปกรณ์เคลื่อนที่ ดูข้อมูลเพิ่มเติมเกี่ยวกับเบรกพอยท์แบบปรับเปลี่ยนได้ใน material.io คุณอาจลองใช้แพ็กเกจ adaptive_breakpoints

เลย์เอาต์ที่ปรับเปลี่ยนตามอุปกรณ์ของหน้าจอหลักใช้ AdaptiveContainer และ AdaptiveColumn ตามตารางกริดแบบ 12 คอลัมน์ โดยใช้แพ็กเกจ adaptive_components และ adaptive_breakpoints เพื่อใช้เลย์เอาต์ตารางกริดที่ปรับเปลี่ยนตามอุปกรณ์ในดีไซน์ Material

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.pngเลย์เอาต์แบบปรับอัตโนมัติต้องใช้ 2 เลย์เอาต์ ได้แก่ เลย์เอาต์สำหรับอุปกรณ์เคลื่อนที่ และเลย์เอาต์ที่ปรับเปลี่ยนตามอุปกรณ์สำหรับหน้าจอขนาดใหญ่ ปัจจุบัน LayoutBuilder แสดงผลเฉพาะเลย์เอาต์บนเดสก์ท็อปเท่านั้น ใน lib/src/features/home/view/home_screen.dart ให้สร้างเลย์เอาต์อุปกรณ์เคลื่อนที่เป็น TabBar และ TabBarView โดยมี 4 แท็บ

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

หากพบปัญหา

หากแอปของคุณทำงานไม่ถูกต้อง ให้ใช้โค้ดที่ลิงก์ต่อไปนี้เพื่อกลับสู่การทำงาน

ใช้ช่องว่าง

ช่องว่างคือเครื่องมือแสดงข้อมูลทางภาพที่สำคัญของแอป โดยจะช่วยสร้างตัวแบ่งส่วนต่างๆ ในองค์กร

ควรมีพื้นที่ว่างมากเกินไป ขอแนะนำให้เพิ่มช่องว่างเพื่อลดขนาดของแบบอักษรหรือองค์ประกอบภาพเพื่อให้เข้ากับพื้นที่ว่างมากขึ้น

การขาดช่องว่างอาจเป็นความยากลำบากสำหรับผู้ที่มีปัญหาเกี่ยวกับการมองเห็น ช่องว่างมากเกินไปอาจทำให้ไม่สอดคล้องกัน และทำให้ UI ดูไม่เป็นระเบียบ ตัวอย่างเช่น โปรดดูภาพหน้าจอต่อไปนี้

7f5e3514a7ee1750.png

d5144a50f5b4142c.png

ถัดไป คุณจะเพิ่มช่องว่างในหน้าจอหลักเพื่อเพิ่มพื้นที่ จากนั้นคุณจะปรับแต่งเลย์เอาต์เพิ่มเติมเพื่อปรับแต่งระยะห่าง

a3c16fc17be25f6c.png รวมวิดเจ็ตด้วยออบเจ็กต์ Padding เพื่อเพิ่มช่องว่างรอบๆ วิดเจ็ตนั้น เพิ่มค่า Padding ทั้งหมดที่ตอนนี้อยู่ใน lib/src/features/home/view/home_screen.dart เป็น 35 ดังนี้

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 Hot โหลดแอปซ้ำ ซึ่งควรมีลักษณะเหมือนเดิม แต่มีช่องว่างระหว่างวิดเจ็ตมากขึ้น ระยะห่างจากขอบเพิ่มเติมดูดีขึ้น แต่แบนเนอร์ไฮไลต์ที่ด้านบนยังคงอยู่ใกล้กับขอบมากเกินไป

a3c16fc17be25f6c.png ใน lib/src/features/home/view/home_highlight.dart ให้เปลี่ยนระยะห่างจากขอบในแบนเนอร์เป็น 35 ดังนี้

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 Hot โหลดแอปซ้ำ เพลย์ลิสต์ 2 รายการด้านล่างไม่มีช่องว่างระหว่างเพลย์ลิสต์ จึงดูเหมือนว่าจะอยู่ในตารางเดียวกัน ไม่ได้เป็นเช่นนั้นและจะดำเนินการแก้ไขในขั้นตอนต่อไป

df1d9af97d039cc8.png

a3c16fc17be25f6c.png เพิ่มช่องว่างระหว่างเพลย์ลิสต์โดยแทรกวิดเจ็ตขนาดลงใน Row ที่มีวิดเจ็ตเหล่านั้น ใน lib/src/features/home/view/home_screen.dart ให้เพิ่ม SizedBox ที่มีความกว้าง 35 ดังนี้

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 Hot โหลดแอปซ้ำ แอปควรมีลักษณะดังต่อไปนี้

d8b2a3d47736dbab.png

ตอนนี้มีพื้นที่มากมายสำหรับเนื้อหาในหน้าจอหลัก แต่ทุกอย่างดูแยกส่วนเกินไปและส่วนต่างๆ ไม่กลมกลืนกัน

a3c16fc17be25f6c.png จนถึงตอนนี้ คุณตั้งค่าระยะห่างจากขอบทั้งหมด (ทั้งแนวนอนและแนวตั้ง) สำหรับวิดเจ็ตบนหน้าจอหลักเป็น 35 ด้วย EdgeInsets.all(35) แต่คุณก็ตั้งค่าระยะห่างจากขอบของแต่ละขอบแยกกันได้ด้วย ปรับแต่งระยะห่างจากขอบให้พอดีกับพื้นที่มากขึ้น

  • EdgeInsets.LTRB() ตั้งค่าด้านซ้าย บน ขวา และด้านล่างแยกกัน
  • EdgeInsets.symmetric() กำหนดระยะห่างจากขอบสำหรับแนวตั้ง (ด้านบนและด้านล่าง) ให้เท่ากัน และแนวนอน (ซ้ายและขวา) ให้เท่ากัน
  • EdgeInsets.only() จะตั้งค่าขอบที่ระบุเท่านั้น
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 ให้ตั้งค่าระยะห่างจากขอบด้านซ้ายและขวาในแบนเนอร์เป็น 35 และตั้งค่าระยะห่างจากขอบด้านบนและด้านล่างเป็น 5 ดังนี้

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 Hot โหลดแอปซ้ำ การจัดวางและระยะห่างจะดูดีขึ้นมาก เพิ่มการเคลื่อนไหวและภาพเคลื่อนไหวลงไปสักเล็กน้อยเพื่อให้ขั้นตอนเสร็จสมบูรณ์

7f5e3514a7ee1750.png

หากพบปัญหา

หากแอปของคุณทำงานไม่ถูกต้อง ให้ใช้โค้ดที่ลิงก์ต่อไปนี้เพื่อกลับสู่การทำงาน

7 เพิ่มการเคลื่อนไหวและภาพเคลื่อนไหว

การเคลื่อนไหวและภาพเคลื่อนไหวเป็นวิธีที่ดีในการนำเสนอการเคลื่อนไหวและการให้พลังงาน และเป็นการให้ข้อเสนอแนะเมื่อผู้ใช้โต้ตอบกับแอป

เคลื่อนไหวระหว่างหน้าจอ

ThemeProvider กำหนด PageTransitionsTheme ด้วยภาพเคลื่อนไหวการเปลี่ยนหน้าจอสำหรับแพลตฟอร์มอุปกรณ์เคลื่อนที่ (iOS, Android) ผู้ใช้เดสก์ท็อปได้รับความคิดเห็นจากการคลิกเมาส์หรือแทร็กแพดอยู่แล้ว จึงไม่จำเป็นต้องใช้ภาพเคลื่อนไหวการเปลี่ยนหน้า

Flutter มีภาพเคลื่อนไหวการเปลี่ยนหน้าจอที่คุณกําหนดค่าสําหรับแอปตามแพลตฟอร์มเป้าหมายได้ ดังที่แสดงใน lib/src/shared/providers/theme.dart

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 ส่ง PageTransitionsTheme ไปยังทั้งธีมสว่างและธีมมืดใน lib/src/shared/providers/theme.dart

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

มีภาพเคลื่อนไหวใน iOS

หากพบปัญหา

หากแอปของคุณทำงานไม่ถูกต้อง ให้ใช้โค้ดที่ลิงก์ต่อไปนี้เพื่อกลับสู่การทำงาน

เพิ่มสถานะการวางเมาส์เหนือ

วิธีหนึ่งในการเพิ่มการเคลื่อนไหวให้กับแอปบนเดสก์ท็อปคือการใช้สถานะเมื่อชี้เมาส์ ซึ่งวิดเจ็ตจะเปลี่ยนสถานะ (เช่น สี รูปร่าง หรือเนื้อหา) เมื่อผู้ใช้วางเคอร์เซอร์เหนือวิดเจ็ต

โดยค่าเริ่มต้น คลาส _OutlinedCardState (ใช้สำหรับการ์ดเพลย์ลิสต์ "เล่นล่าสุด") จะแสดง MouseRegion ซึ่งจะเปลี่ยนลูกศรของเคอร์เซอร์ให้เป็นตัวชี้เมื่อวางเมาส์ไว้ด้านบน แต่คุณสามารถเพิ่มความคิดเห็นที่เป็นภาพเพิ่มเติมได้

a3c16fc17be25f6c.png เปิด lib/src/shared/views/outlined_card.dart และแทนที่เนื้อหาด้วยการใช้งานต่อไปนี้เพื่อเริ่มใช้สถานะ _hovered

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 โหลดแอปซ้ำโดยด่วน จากนั้นวางเมาส์เหนือการ์ดเพลย์ลิสต์ที่เล่นล่าสุด

OutlinedCard จะเปลี่ยนความทึบแสงและจะทำมุมให้กลมมน

a3c16fc17be25f6c.png สุดท้าย เปลี่ยนหมายเลขเพลงในเพลย์ลิสต์ให้เป็นปุ่มเล่นโดยใช้วิดเจ็ต HoverableSongPlayButton ที่กำหนดไว้ใน lib/src/shared/views/hoverable_song_play_button.dart ใน lib/src/features/playlists/view/playlist_songs.dart ให้รวมวิดเจ็ต Center (ซึ่งมีหมายเลขเพลง) ด้วย HoverableSongPlayButton ดังนี้

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.pngโหลดแอปซ้ำทันที จากนั้นวางเคอร์เซอร์เหนือหมายเลขเพลงบนเพลงยอดนิยมวันนี้หรือเพลย์ลิสต์มาใหม่

ตัวเลขจะเคลื่อนไหวไปที่ปุ่มเล่น ซึ่งจะเล่นเพลงเมื่อคุณคลิก

ดูรหัสโปรเจ็กต์สุดท้ายใน GitHub

8 ยินดีด้วย

คุณดำเนินการ Codelab นี้เสร็จแล้ว คุณได้เรียนรู้ว่ามีการเปลี่ยนแปลงเล็กๆ น้อยๆ มากมายที่คุณสามารถผสานรวมเข้ากับแอปเพื่อทำให้แอปดูสวยงามยิ่งขึ้น เข้าถึงได้มากขึ้น แปลได้มากขึ้น และเหมาะสำหรับหลายแพลตฟอร์มมากขึ้น เทคนิคเหล่านี้รวมถึงแต่ไม่จำกัดเพียงรายการต่อไปนี้

  • การออกแบบตัวอักษร: ข้อความเป็นมากกว่าเครื่องมือสื่อสาร ใช้วิธีการแสดงข้อความเพื่อสร้างผลกระทบเชิงบวกต่อผู้ใช้ ประสบการณ์และการรับรู้แอปของคุณ
  • การกำหนดธีม: สร้างระบบการออกแบบที่คุณสามารถใช้ได้อย่างน่าเชื่อถือ โดยไม่ต้องตัดสินใจเรื่องการออกแบบสำหรับวิดเจ็ตทั้งหมด
  • การปรับตัว: พิจารณาอุปกรณ์และแพลตฟอร์มที่ผู้ใช้กำลังใช้แอปและความสามารถของแอป พิจารณาขนาดหน้าจอและลักษณะการแสดงผลแอป
  • การเคลื่อนไหวและภาพเคลื่อนไหว: การเพิ่มการเคลื่อนไหวลงในแอปจะเพิ่มพลังให้กับประสบการณ์ของผู้ใช้และจะให้ความคิดเห็นแก่ผู้ใช้ในทางปฏิบัติมากขึ้น

เพียงปรับเปลี่ยนเล็กๆ น้อยๆ แอปของคุณก็สามารถเปลี่ยนจากสิ่งที่น่าเบื่อให้กลายเป็นความสวยงามได้ ดังนี้

ก่อน

1e67c60667821082.png

หลัง

ขั้นตอนถัดไป

เราหวังว่าคุณได้เรียนรู้เกี่ยวกับการสร้างแอปที่สวยงามใน Flutter มากขึ้นแล้ว

หากคุณนำกลเม็ดเคล็ดลับใดๆ ที่กล่าวถึงในที่นี้ (หรือมีเคล็ดลับของคุณเองเพื่อแบ่งปัน) เราก็ยินดีรับฟังความคิดเห็นจากคุณ ติดต่อเราได้ทาง Twitter ที่ @rodydavis และ @khanhnwin

แหล่งข้อมูลต่อไปนี้อาจเป็นประโยชน์สำหรับคุณ

ธีม

ทรัพยากรแบบปรับได้และปรับเปลี่ยนตามอุปกรณ์

แหล่งข้อมูลเกี่ยวกับการออกแบบทั่วไป

และเชื่อมต่อกับชุมชน Flutter ด้วย

ออกไปปรับแต่งแอปให้สวยงาม