Day 26: Assets, SVGs & Custom Fonts

Give Pixly its own look: register images, SVGs and fonts in pubspec.yaml, render a crisp SVG logo with flutter_svg, serve sharp PNGs with resolution-aware 2.0x/3.0x variants, and bundle a custom font.

မနေ့က Day 25 မှာ Pixly project ကို စတင်ပြီး Material 3 theme ကို တည်ဆောက်ခဲ့ပြီးပါပြီ။ ဒီနေ့မှာတော့ app ကို ကိုယ်ပိုင် identity ရှိလာအောင် logo, illustration, ပုံတွေ, font တွေ ထည့်သွင်းကြပါမယ်။ App တခုမှာ ပါတဲ့ ပုံ၊ icon၊ font၊ JSON file စတဲ့ app နဲ့အတူ တွဲပြီး install လုပ်သွားမယ့် file တွေကို Flutter မှာ assets လို့ ခေါ်ပါတယ်။

ဒီနေ့ ပြီးသွားရင် Pixly ရဲ့ AppBar မှာ SVG logo ပေါ်လာမယ်၊ ဓာတ်ပုံ မရှိသေးတဲ့ အချိန်မှာ ပြမယ့် empty state illustration ရလာမယ်၊ app တခုလုံးလဲ Poppins font နဲ့ ပြောင်းသွားမှာ ဖြစ်ပါတယ်။

pubspec.yaml - Assets ရဲ့ Gatekeeper

Flutter က project folder ထဲမှာ ရှိတဲ့ file တွေကို အလိုအလျောက် app ထဲ ထည့်မပေးပါဘူး။ pubspec.yaml ထဲမှာ ကြေငြာထားတဲ့ asset တွေကိုပဲ app နဲ့အတူ bundle လုပ်ပေးတာ ဖြစ်ပါတယ်။ ပထမဆုံး project root မှာ folder တွေ ဖန်တီးလိုက်ပါမယ်။

pixly/
├── assets/
│   ├── fonts/
│   │   ├── Poppins-Regular.ttf
│   │   ├── Poppins-SemiBold.ttf
│   │   ├── Poppins-Bold.ttf
│   │   └── OFL.txt
│   ├── images/
│   │   ├── placeholder.png
│   │   ├── 2.0x/placeholder.png
│   │   └── 3.0x/placeholder.png
│   └── svg/
│       ├── logo.svg
│       └── empty_photos.svg
├── lib/
└── pubspec.yaml

ပြီးရင် pubspec.yaml ရဲ့ flutter: section အောက်မှာ ကြေငြာပေးပါမယ်။

flutter:
  uses-material-design: true

  assets:
    - assets/images/
    - assets/svg/
    - assets/fonts/OFL.txt

  fonts:
    - family: Poppins
      fonts:
        - asset: assets/fonts/Poppins-Regular.ttf
        - asset: assets/fonts/Poppins-SemiBold.ttf
          weight: 600
        - asset: assets/fonts/Poppins-Bold.ttf
          weight: 700

assets/images/ ဆိုပြီး နောက်ဆုံးမှာ / နဲ့ ရေးလိုက်တာက အဲ့ဒီ folder ထဲမှာ တိုက်ရိုက် ရှိတဲ့ file တွေ အကုန်လုံးကို ထည့်ပါဆိုတဲ့ အဓိပ္ပာယ်ပါ။ file တခုချင်းစီကို စာရင်းလိုက် ရေးနေစရာ မလိုတော့ပါဘူး။ သတိထားရမှာက sub folder တွေကိုတော့ အလိုအလျောက် မထည့်ပေးပါဘူး။ ဥပမာ assets/images/icons/ ဆိုတဲ့ folder ထပ်ရှိရင် အဲ့ဒါကို သီးသန့် တကြောင်း ထပ်ရေးပေးရပါမယ်။ (ခြွင်းချက်ကတော့ အောက်မှာ ပြောမယ့် 2.0x, 3.0x folder တွေပါ။)

⚠️ YAML က indentation (space) ကို အရမ်း အလေးထားပါတယ်။ assets: နဲ့ fonts: က flutter: အောက်မှာ space ၂ ချက်၊ list item တွေက space ၄ ချက် ဖြစ်ရပါမယ်။ space မှားရင် flutter pub get လုပ်တဲ့အခါ error တက်ပါလိမ့်မယ်။

pubspec.yaml ပြင်ပြီးတိုင်း fvm flutter pub get run ပေးရပါမယ်။ Asset အသစ်တွေ ထည့်ပြီးရင် hot reload နဲ့ အမြဲ ပါမလာနိုင်တဲ့အတွက် app ကို stop လုပ်ပြီး အစကနေ ပြန် run ပေးတာ အသေချာဆုံးပါ။

Asset Paths ကို တနေရာတည်းမှာ စုထားခြင်း

// lib/config/assets.dart
/// All asset paths in one place, so a typo is a compile error, not a crash.
class AppAssets {
  static const String logo = 'assets/svg/logo.svg';
  static const String emptyPhotos = 'assets/svg/empty_photos.svg';
  static const String placeholder = 'assets/images/placeholder.png';
}

Asset path တွေကို string အနေနဲ့ နေရာတိုင်းမှာ တိုက်ရိုက် ရေးမယ်ဆိုရင် စာလုံးတလုံး မှားတာနဲ့ compile တုန်းက ဘာမှ မပြဘဲ run နေတုန်းမှ "Unable to load asset" ဆိုပြီး error တက်ပါလိမ့်မယ်။ AppAssets class ထဲမှာ စုထားလိုက်ရင် AppAssets.logoo လို member နာမည် မှားရင် compile error ပြမှာ ဖြစ်ပြီး file နာမည်ပြောင်းချင်ရင်လဲ တနေရာတည်းမှာ ပြင်ရုံပါပဲ။

ဒါပေမယ့် AppAssets ထဲက path string ကိုယ်တိုင် မှားနေရင်တော့ compiler က မသိပါဘူး။ ဒါကြောင့် Pixly project ထဲမှာ path တွေ အကုန်လုံး တကယ် load လို့ရလားဆိုတာ စစ်ပေးတဲ့ test လေးတခု ထည့်ထားပါတယ်။ (Testing အကြောင်းကိုတော့ Day 86 မှာ အသေးစိတ် ပြောပါမယ်။)

// test/assets_test.dart
test('every AppAssets path is bundled', () async {
  for (final path in [
    AppAssets.logo,
    AppAssets.emptyPhotos,
    AppAssets.placeholder,
  ]) {
    final data = await rootBundle.load(path);
    expect(data.lengthInBytes, greaterThan(0), reason: path);
  }
});

PNG vs SVG

App ထဲမှာ သုံးတဲ့ ပုံ format ၂ မျိုးကို နှိုင်းယှဥ်ကြည့်ရအောင်ပါ။

PNG / JPG SVG
ပုံသဏ္ဍာန် Pixel (အစက်) တွေနဲ့ ဖွဲ့စည်းထားတဲ့ raster image Shape, line, color တွေကို ဖော်ပြထားတဲ့ vector (XML)
ချဲ့လိုက်ရင် ဝါးသွား၊ အစက်တွေ မြင်လာ ဘယ်လောက်ချဲ့ချဲ့ ကြည်လင်
သင့်တော်တဲ့ နေရာ ဓာတ်ပုံ၊ အရောင်အသွေး ရှုပ်ထွေးတဲ့ ပုံ Logo, icon, illustration
Flutter မှာ သုံးပုံ Image.asset() built-in ပါပြီးသား flutter_svg package လိုအပ်

Pixly ရဲ့ logo နဲ့ empty state illustration တွေက ရိုးရှင်းတဲ့ shape တွေပဲ ပါတဲ့အတွက် SVG နဲ့ လုပ်ပါမယ်။ Pexels က လာမယ့် ဓာတ်ပုံတွေကတော့ JPG တွေ ဖြစ်ပါတယ်။

SVG ကို flutter_svg နဲ့ ပြခြင်း

Flutter မှာ SVG ကို ပြဖို့ built-in widget မပါတဲ့အတွက် flutter_svg package ကို ထည့်ပေးရပါမယ်။

fvm flutter pub add flutter_svg
SvgPicture.asset(AppAssets.logo, width: 32, height: 32)

SvgPicture.asset ကို Image.asset လိုပဲ သုံးလို့ ရပါတယ်။ ဒီ logo ကို AppBar ရဲ့ title မှာ app name နဲ့ ဘေးချင်းကပ် ထည့်လိုက်ပါမယ်။ Day 25 က theme demo အတွက် ယာယီ ရေးထားခဲ့တဲ့ HomeScreen ကိုတော့ အောက်က code နဲ့ အစားထိုးလိုက်ပါမယ်။ (EmptyState widget ကိုတော့ ဆက်ပြီး ရေးသွားပါမယ်။)

// lib/screens/home_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:pixly/config/assets.dart';
import 'package:pixly/config/constants.dart';
import 'package:pixly/widgets/empty_state.dart';

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Row(
          spacing: 12,
          children: [
            SvgPicture.asset(AppAssets.logo, width: 32, height: 32),
            const Text(AppConstants.appName),
          ],
        ),
      ),
      body: EmptyState(
        title: 'No photos yet',
        message: 'Beautiful photos from Pexels will show up here soon.',
        action: Image.asset(AppAssets.placeholder, width: 48, height: 48),
      ),
    );
  }
}

Day 21 မှာ ပြောခဲ့တဲ့ Row ရဲ့ spacing parameter ကို သုံးပြီး logo နဲ့ စာကြား နေရာ 12 ခြားထားပါတယ်။

💡 flutter_svg က CSS ကို အပြည့်အဝ support မလုပ်ပါဘူး။ Design tool တွေကနေ SVG export လုပ်တဲ့အခါ style တွေကို inline CSS အနေနဲ့ မဟုတ်ဘဲ Presentation Attributes အနေနဲ့ export လုပ်ဖို့ flutter_svg ရဲ့ documentation မှာ အကြံပြုထားပါတယ်။ SVG တခု မှန်မှန်ကန်ကန် မပေါ်ရင် ဒီ setting ကို အရင်ဆုံး စစ်ကြည့်ပါ။

Reusable EmptyState Widget

ဓာတ်ပုံ မရှိသေးတဲ့အခါ၊ search ရလဒ် မတွေ့တဲ့အခါ၊ bookmark မရှိသေးတဲ့အခါ စတဲ့ နေရာ အများကြီးမှာ "ဘာမှ မရှိသေးပါဘူး" ဆိုတဲ့ screen တွေ ပြဖို့ လိုလာမှာပါ။ ဒါကြောင့် Day 16 မှာ လေ့လာခဲ့တဲ့ StatelessWidget နဲ့ ပြန်သုံးလို့ရတဲ့ widget တခု ဆောက်ထားလိုက်ပါမယ်။

// lib/widgets/empty_state.dart
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:pixly/config/assets.dart';

class EmptyState extends StatelessWidget {
  const EmptyState({
    super.key,
    required this.title,
    required this.message,
    this.action,
  });

  final String title;
  final String message;
  final Widget? action;

  @override
  Widget build(BuildContext context) {
    final textTheme = Theme.of(context).textTheme;

    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisSize: .min,
          children: [
            SvgPicture.asset(AppAssets.emptyPhotos, width: 180),
            const SizedBox(height: 24),
            Text(title, style: textTheme.titleLarge, textAlign: .center),
            const SizedBox(height: 8),
            Text(message, style: textTheme.bodyMedium, textAlign: .center),
            if (action != null) ...[const SizedBox(height: 24), action!],
          ],
        ),
      ),
    );
  }
}

action ကို optional (Widget?) ထားပြီး Day 6 မှာ လေ့လာခဲ့တဲ့ collection if နဲ့ spread operator (...) ကို သုံးပြီး action ပါမှ button လိုမျိုး widget ကို ပြပေးတာ ဖြစ်ပါတယ်။ နောက်ရက်တွေမှာ "Retry" button လိုမျိုးတွေ ဒီနေရာမှာ ထည့်သုံးသွားမှာပါ။

Resolution-Aware Images (2.0x, 3.0x)

Phone တွေရဲ့ screen တွေက pixel density မတူကြပါဘူး။ Flutter က layout တွက်တဲ့အခါ logical pixel ကို သုံးပြီး logical pixel 1 ခုမှာ physical pixel ဘယ်နှခု ပါလဲဆိုတာကို device pixel ratio လို့ ခေါ်ပါတယ်။ ဥပမာ iPhone တော်တော်များများရဲ့ device pixel ratio က 3 ဖြစ်ပါတယ်။

PNG ပုံ 48×48 တခုကို width 48 logical pixel နဲ့ 3x screen မှာ ပြလိုက်ရင် physical pixel 144×144 နေရာကို ပုံအစက် 48×48 နဲ့ ချဲ့ဖြည့်ရတဲ့အတွက် ဝါးသွားပါလိမ့်မယ်။ ဒါကို ဖြေရှင်းဖို့ Flutter မှာ resolution variant တွေ ထည့်ပေးလို့ ရပါတယ်။

assets/images/placeholder.png        ← 48×48   (1.0x)
assets/images/2.0x/placeholder.png   ← 96×96   (2.0x)
assets/images/3.0x/placeholder.png   ← 144×144 (3.0x)

File နာမည် အတူတူနဲ့ 2.0x, 3.0x folder တွေထဲမှာ ပိုကြီးတဲ့ ပုံတွေ ထည့်ထားလိုက်ရုံပါပဲ။ code ထဲမှာတော့ ဘာမှ ပြောင်းစရာ မလိုပါဘူး။ Image.asset('assets/images/placeholder.png') လို့ပဲ ခေါ်ရင် Flutter က device ရဲ့ pixel ratio နဲ့ ကိုက်ညီတဲ့ variant ကို အလိုအလျောက် ရွေးပေးပါတယ်။ ratio တိတိကျကျ တူတဲ့ variant ရှိရင် အဲ့ဒါကို ယူပြီး မရှိရင်တော့ အနီးစပ်ဆုံးကို ယူပါတယ်။ ခြွင်းချက်အနေနဲ့ device pixel ratio 2 အောက် (ဥပမာ 1.25) screen တွေမှာတော့ ပုံ ဝါးတာ ပိုသိသာတဲ့အတွက် အနီးဆုံးကို မယူဘဲ ပိုကြီးတဲ့ variant (ဒီဥပမာမှာ 2.0x) ကို ရွေးပေးပါတယ်။ pubspec.yaml မှာလဲ main file (ဒါမှမဟုတ် သူ့ folder) ကို ကြေငြာထားရင် variant folder တွေကို Flutter က အလိုအလျောက် ထည့်ပေးပါတယ်။

ဒီ demo ပုံလေးတွေထဲမှာ variant ကို ခွဲမြင်ရအောင် "1x", "2x", "3x" ဆိုပြီး စာလေးတွေ ရေးထားပါတယ်။ အောက်က screenshot ကို device pixel ratio 2 နဲ့ ရိုက်ထားတဲ့အတွက် 2.0x variant ကို ရွေးသွားတာကို တွေ့ရမှာပါ။

Custom Fonts

Font file တွေကို assets/fonts/ ထဲ ထည့်ပြီး pubspec.yaml မှာ ကြေငြာထားပြီးပြီဆိုတော့ သုံးရုံပါပဲ။

  • family: Poppins - code ထဲမှာ ခေါ်သုံးမယ့် နာမည်
  • weight: 600, weight: 700 - ဒီ file က ဘယ် font weight အတွက်လဲ ဆိုတာ။ weight မပါတဲ့ file က default (400, regular) ဖြစ်ပါတယ်။ ဒါကြောင့် FontWeight.w600 သုံးရင် SemiBold file၊ FontWeight.bold (700) သုံးရင် Bold file ကို Flutter က ရွေးပေးပါတယ်။

App တခုလုံးမှာ ဒီ font ကို သုံးစေချင်တဲ့အတွက် Day 25 က lib/config/theme.dart ထဲက AppTheme._build မှာ တကြောင်းတည်း ထပ်ထည့်လိုက်ပါမယ်။

    final base = ThemeData(colorScheme: colorScheme, fontFamily: 'Poppins');

ThemeData မှာ fontFamily သတ်မှတ်လိုက်ရင် textTheme ထဲက text style အကုန်လုံး (title, body, label စတာတွေ) က Poppins ကို သုံးသွားမှာပါ။ Day 25 မှာ AppBar title ကို style အသစ် မဆောက်ဘဲ text theme ရဲ့ titleLarge ကနေပဲ bold ပြောင်းထားခဲ့တဲ့အတွက် AppBar title ကလဲ Poppins ကို အလိုအလျောက် ရသွားပါတယ်။ widget တခုတည်းမှာပဲ သုံးချင်ရင်တော့ TextStyle(fontFamily: 'Poppins') လို့ ရေးလို့ရပါတယ်။

Font License

Font file တွေကို app ထဲ ထည့်ပြီး ဖြန့်ဝေမယ်ဆိုရင် license ကို စစ်ဖို့ လိုပါတယ်။ Poppins က SIL Open Font License (OFL) နဲ့ ဖြစ်တဲ့အတွက် app ထဲမှာ ထည့်သုံးလို့ ရပါတယ်။ ဒါပေမယ့် OFL.txt ကို folder ထဲမှာ ထားရုံနဲ့ app ထဲ မပါသွားပါဘူး။ အပေါ်က pubspec.yaml မှာ assets/fonts/OFL.txt ကို asset အနေနဲ့ ကြေငြာထားပြီး Flutter ရဲ့ LicenseRegistry မှာ မှတ်ပုံတင်ပေးမှ app ရဲ့ licenses page (showLicensePage) မှာ ပေါ်လာမှာ ဖြစ်ပါတယ်။

// lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pixly/config/constants.dart';
import 'package:pixly/config/theme.dart';
import 'package:pixly/screens/home_screen.dart';

void main() {
  // Show the Poppins font license on Flutter's licenses page.
  LicenseRegistry.addLicense(() async* {
    final license = await rootBundle.loadString('assets/fonts/OFL.txt');
    yield LicenseEntryWithLineBreaks(['Poppins'], license);
  });

  runApp(const PixlyApp());
}

addLicense က license တွေကို Stream (Day 13) အနေနဲ့ ပြန်ပေးတဲ့ function ကို လက်ခံပါတယ်။ async* နဲ့ ရေးထားတဲ့ function က Stream တခုကို ပြန်ပေးပြီး yield လုပ်လိုက်တဲ့ value တွေက အဲ့ဒီ Stream ထဲကို တခုချင်း ဝင်သွားတာ ဖြစ်ပါတယ်။ Flutter က licenses page ကို ဖွင့်တဲ့အချိန်ကျမှ ဒီ function ကို ခေါ်ပြီး file ကို ဖတ်တာ ဖြစ်ပါတယ်။

google_fonts package ကို သုံးပြီးလဲ Google Fonts က font တွေကို လွယ်လွယ်ကူကူ သုံးလို့ ရပါတယ်။ သူက font file ကို asset ထဲမှာ ထည့်ထားရင် အဲ့ဒါကို အရင်သုံးပြီး မရှိရင် device ထဲမှာ cache လုပ်ထားတာကို ရှာ၊ အဲ့ဒါမှ မရှိရင်တော့ app run နေတုန်း internet ကနေ download လုပ်ပါတယ်။ Download လုပ်ရမယ့် အခြေအနေမှာ internet မရှိရင် ပထမဆုံးအကြိမ် ဖွင့်တဲ့အခါ font မပြောင်းသေးတာမျိုး ဖြစ်နိုင်ပါတယ်။ Pixly မှာတော့ font တမျိုးတည်းပဲ သုံးမှာ ဖြစ်တဲ့အတွက် package အပို မထည့်ဘဲ font file တွေကို app ထဲမှာ တိုက်ရိုက် bundle လုပ်လိုက်တာ ဖြစ်ပါတယ်။

ရလဒ်

Pixly with SVG logo, empty state and Poppins - light
Pixly with SVG logo, empty state and Poppins - light
Pixly with SVG logo, empty state and Poppins - dark
Pixly with SVG logo, empty state and Poppins - dark

AppBar မှာ SVG logo၊ အလယ်မှာ empty state illustration၊ စာတွေ အားလုံး Poppins font နဲ့ ပြောင်းသွားတာကို တွေ့ရမှာပါ။ Dark mode မှာလဲ logo နဲ့ illustration တွေ ကြည်ကြည်လင်လင် မြင်ရပါတယ်။

Examples

  • Logo, icon, illustration → SVG + flutter_svg
  • ဓာတ်ပုံ၊ screenshot → PNG/JPG + 2.0x/3.0x variant
  • Brand font → assets/fonts/ + pubspec.yaml fonts: + ThemeData(fontFamily: ...)
  • Asset path တွေ → AppAssets လို class တခုထဲမှာ စုထား

Source code: ဒီနေ့ရဲ့ code အပြည့်အစုံကို GitHub မှာ run ကြည့်လို့ ရပါတယ်။


ဒီနေ့ Day 26 အတွက်ကတော့ ဒီလောက်ပဲ ဖြစ်ပါတယ်။ အဆုံးထိ ဖတ်ပေးတဲ့အတွက် အများကြီး ကျေးဇူးတင်ပါတယ်။ နားမလည်တာတွေ အဆင်မပြေတာတွေ ရှိခဲ့ရင်လဲ အောက်မှာပေးထားတဲ့ discord server ထဲမှာ လာရောက်ဆွေးနွေးနိုင်ပါတယ်။ နောက်နေ့တွေမှာလဲ ဆက်လက်ပြီး sharing လုပ်သွားပေးသွားမှာ ဖြစ်တဲ့အတွက် subscribe လုပ်ထားဖို့ ဖိတ်ခေါ်ပါတယ်။