> ## Content Index
> Fetch the complete content index at: https://arkar.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Day 30: Photo Grid, Image Caching & Shimmer Loading
- URL: https://arkar.dev/day-30-photo-grid-image-caching-shimmer-loading/
- Published: 2026-11-11T01:30:00.000Z
- Updated: 2026-11-11T01:30:00.000Z
- Description: Pixly finally shows real photos. Build a two-column photo grid, cache images on disk with cached_network_image, use Pexels' avg_color as a placeholder, and replace the spinner with a shimmer skeleton.
- Author: Arkar Min Tun
- Tags: 100 days of flutter, flutter

မနေ့က Day 29 မှာ `PhotosProvider` နဲ့ Pexels က ဓာတ်ပုံ data တွေကို UI ဆီ ရောက်အောင် ပို့ပေးနိုင်ခဲ့ပါပြီ။ ဒါပေမယ့် ပြနေတာက `ListTile` နဲ့ စာသားတွေပဲ ရှိသေးပြီး ဓာတ်ပုံ ကိုယ်တိုင်ကို မပြရသေးပါဘူး။ ဒီနေ့မှာတော့ Pixly ကို တကယ့် photo app တခုလို ဖြစ်လာအောင်

- Day 24 မှာ လေ့လာခဲ့တဲ့ `GridView` နဲ့ ဓာတ်ပုံတွေကို column ၂ ခုနဲ့ ပြမယ်
- ဓာတ်ပုံတွေကို download တခါလုပ်ပြီး device ထဲမှာ cache လုပ်ထားမယ်
- Loading ဖြစ်နေတုန်း spinner အစား Facebook, Instagram, YouTube တို့မှာ မြင်ဖူးတဲ့ shimmer skeleton ကို ပြမယ်

ဆိုတဲ့ အပိုင်းတွေကို လုပ်သွားမှာ ဖြစ်ပါတယ်။

```bash
fvm flutter pub add cached_network_image:^4.0.5 flutter_cache_manager:^3.4.5 shimmer:^4.0.0
```

## ဘာလို့ Image.network မသုံးတာလဲ?

Flutter မှာ `Image.network(url)` နဲ့ internet ကနေ ပုံကို တိုက်ရိုက် ပြလို့ ရပါတယ်။ Flutter ရဲ့ `ImageCache` က decode လုပ်ပြီးသား ပုံတွေကို memory ထဲမှာ ခဏ သိမ်းထားပေးပေမယ့် disk ပေါ်မှာ သိမ်းမထားတဲ့အတွက် app ကို ပိတ်ပြီး ပြန်ဖွင့်ရင် ပုံတွေကို အစကနေ ပြန် download လုပ်ရပါတယ်။

Pixly လို ဓာတ်ပုံ အများကြီး ပြရတဲ့ app မှာဆို user ရဲ့ mobile data တွေ ကုန်တယ်၊ ပုံတွေ ပြန်ပေါ်လာဖို့ စောင့်ရတယ်။ ဒါကြောင့် **`cached_network_image`** package ကို သုံးပါမယ်။

- ပထမဆုံး အကြိမ် download လုပ်ပြီးတဲ့ ပုံကို device ရဲ့ disk ပေါ်မှာ သိမ်းထားပါတယ် (mobile/desktop မှာ)
- နောက်တခါ အဲ့ဒီ URL ကို ပြန်တောင်းရင် disk ကနေ ယူပြီး ပြပေးပါတယ်
- Loading ဖြစ်နေတုန်း ပြမယ့် `placeholder` နဲ့ error ဖြစ်ရင် ပြမယ့် `errorWidget` ကို ကိုယ်တိုင် သတ်မှတ်လို့ ရပါတယ်

နောက်ကွယ်မှာ cache ကို စီမံပေးတာက **`flutter_cache_manager`** package ပါ။ သူ့မှာ သီးခြား သဘောတရား ၂ ခု ရှိပါတယ်။

- **Freshness (server ကို ပြန်မေးရမလား)** \- ပုံကို download လုပ်တဲ့အခါ server ပြန်ပို့တဲ့ HTTP `Cache-Control: max-age` header ကို ကြည့်ပြီး ဘယ်အချိန်အထိ အသုံးပြုလို့ ရလဲ (`validTill`) ဆိုတာ မှတ်ထားပါတယ်။ header မပါရင် ၇ ရက် လို့ ယူပါတယ်။ အဲ့ဒီ အချိန် ကျော်သွားရင်လဲ disk ထဲက ပုံကို အရင် ချက်ချင်း ပြထားပြီး နောက်ကွယ်မှာ server ကို ပြန်မေးပါတယ်။ ပုံ မပြောင်းရင် (ETag နဲ့ စစ်ပြီး `304 Not Modified` ပြန်လာရင်) ရှိပြီးသား file ကိုပဲ ဆက်သုံးပြီး ပြောင်းသွားမှသာ အသစ်ကို download လုပ်ပါတယ်။
- **Cleanup (disk ပေါ်ကနေ ဖျက်မလား)** \- Cache ကို သုံးပြီးတဲ့အခါ ခဏနေရင် cleanup တခါ run ပြီး ကြာကြာ မသုံးတော့တဲ့ file တွေ (default `stalePeriod` ရက် ၃၀) နဲ့ အရေအတွက် များလွန်းနေတဲ့ file တွေ (default `maxNrOfCacheObjects` 200) ကို ဖျက်ပါတယ်။ ဒါပေမယ့် 200 ဆိုတာ "ဘယ်တော့မှ မကျော်ရ" ဆိုတဲ့ တင်းကျပ်တဲ့ limit မဟုတ်ပါဘူး။ ဘယ် file ကို ဘယ်အချိန် ဖျက်မလဲ ဆိုတာ platform ပေါ် မူတည်တဲ့ storage (mobile မှာ SQLite) ရဲ့ အသေးစိတ် logic ပေါ်မှာ မူတည်ပါတယ်။

ဒါကြောင့် app ကို ပြန်ဖွင့်တဲ့အခါ ပုံက cache ထဲမှာ **ရှိနေသေးရင်** disk ကနေ ချက်ချင်း ပြပေးနိုင်တာ ဖြစ်ပါတယ်။ `flutter_cache_manager` ကို direct dependency အနေနဲ့ ထည့်ထားရတာကတော့ test တွေမှာ cache manager ကို အစားထိုးဖို့ သူ့ရဲ့ `BaseCacheManager` type ကို သုံးထားလို့ ဖြစ်ပါတယ်။

## PhotoImage

App ထဲမှာ network ပုံ ပြရမယ့် နေရာတိုင်းက ဒီ widget လေးကိုပဲ သုံးမှာ ဖြစ်ပါတယ်။

```dart
// lib/widgets/photo_image.dart
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';

/// Shows a network photo with disk caching, a colour placeholder while it
/// loads, and a fallback icon if it fails.
class PhotoImage extends StatelessWidget {
  const PhotoImage({
    super.key,
    required this.url,
    required this.placeholderColor,
    this.fit = BoxFit.cover,
  });

  final String url;
  final Color placeholderColor;
  final BoxFit fit;

  /// Tests swap this to serve local files instead of downloading.
  @visibleForTesting
  static BaseCacheManager? cacheManagerOverride;

  @override
  Widget build(BuildContext context) {
    return CachedNetworkImage(
      imageUrl: url,
      cacheManager: cacheManagerOverride,
      fit: fit,
      fadeInDuration: const Duration(milliseconds: 200),
      placeholder: (context, url) => ColoredBox(color: placeholderColor),
      errorWidget: (context, url, error) => ColoredBox(
        color: placeholderColor,
        child: const Center(child: Icon(Icons.broken_image_outlined)),
      ),
    );
  }
}
```

- **`placeholder`** \- ပုံ download မပြီးခင် ပြမယ့် widget ပါ။ ဒီမှာ spinner မပြဘဲ အရောင် box လေး တခုပဲ ပြထားပါတယ်။
- **`errorWidget`** \- URL မှားတာ၊ internet မရှိတာ စတဲ့ အကြောင်းတွေကြောင့် ပုံ မရရင် ပြမယ့် widget ပါ။ မထည့်ထားရင် Flutter ရဲ့ default image error handling ကို သုံးတဲ့အတွက် debug mode မှာ error အသေးစိတ်တွေ ပေါ်နေတတ်ပါတယ်။ ကိုယ်တိုင် သတ်မှတ်ထားတာက user အတွက် ရိုးရှင်းပြီး ကြိုတင် ခန့်မှန်းလို့ရတဲ့ fallback ကို ပြပေးပါတယ်။
- **`fadeInDuration`** \- ပုံ ရောက်လာတဲ့အခါ ရုတ်တရက် ပေါ်မလာဘဲ fade-in နဲ့ ညင်ညင်သာသာ ပေါ်လာအောင်ပါ။
- `**cacheManager**` \- `null` ဆိုရင် package ရဲ့ default cache manager ကို သုံးပါတယ်။ `cacheManagerOverride` ကို test တွေမှာပဲ အစားထိုးပြီး internet မသုံးဘဲ local file တွေကို ပြပေးထားတာ ဖြစ်ပါတယ်။ `@visibleForTesting` annotation က ဒီ field ကို ကြေညာထားတဲ့ file (library) နဲ့ test file တွေ မဟုတ်တဲ့ တခြား code ကနေ သုံးရင် analyzer က သတိပေးပါလိမ့်မယ်။

### avg\_color ကို placeholder အဖြစ်

Day 27 မှာ `Photo` model ထဲ ထည့်ခဲ့တဲ့ `avgColor` ကို မှတ်မိကြမှာပါ။ Pexels documentation မှာကိုက ဒီ field ကို "ပုံ load ဖြစ်နေတုန်း placeholder အဖြစ် သုံးဖို့ အသုံးဝင်တယ်" လို့ ဖော်ပြထားပါတယ်။ ပုံရဲ့ ပျမ်းမျှ အရောင်ကို အရင်ပြထားတဲ့အတွက် ပုံ ရောက်လာတဲ့အခါ အရောင် ရုတ်တရက် ပြောင်းသွားတာ မရှိတော့ဘဲ ပိုပြီး သဘာဝကျကျ မြင်ရပါတယ်။ Pinterest လို app တွေမှာ ပုံ မပေါ်ခင် အရောင် box လေးတွေ အရင် မြင်ရတာ ဒီသဘောပါပဲ။

## PhotoCard

Grid ထဲက ပုံ တပုံချင်းစီအတွက် card ပါ။ ပုံ ပေါ်မှာ photographer နာမည်ကို ထပ်ပြီး ပြထားပါတယ်။

```dart
// lib/widgets/photo_card.dart
import 'package:flutter/material.dart';
import 'package:pixly/config/theme.dart';
import 'package:pixly/models/photo.dart';
import 'package:pixly/utils/color_utils.dart';
import 'package:pixly/widgets/photo_image.dart';

class PhotoCard extends StatelessWidget {
  const PhotoCard({super.key, required this.photo});

  final Photo photo;

  @override
  Widget build(BuildContext context) {
    final overlay = context.pixlyColors.photoOverlay;

    return Card(
      margin: EdgeInsets.zero,
      child: Stack(
        fit: .expand,
        children: [
          PhotoImage(
            url: photo.src.medium,
            placeholderColor: colorFromHex(photo.avgColor),
          ),
          // Dark gradient at the bottom so the white name is readable.
          Positioned(
            left: 0,
            right: 0,
            bottom: 0,
            child: DecoratedBox(
              decoration: BoxDecoration(
                gradient: LinearGradient(
                  begin: .topCenter,
                  end: .bottomCenter,
                  colors: [overlay.withValues(alpha: 0), overlay],
                ),
              ),
              child: Padding(
                padding: const EdgeInsets.fromLTRB(10, 24, 10, 10),
                child: Text(
                  photo.photographer,
                  maxLines: 1,
                  overflow: .ellipsis,
                  style: const TextStyle(
                    color: Colors.white,
                    fontWeight: .w600,
                  ),
                ),
              ),
            ),
          ),
        ],
      ),
    );
  }
}
```

- Day 20 မှာ လေ့လာခဲ့တဲ့ `Stack` နဲ့ `Positioned` ကို သုံးပြီး ပုံ ပေါ်မှာ နာမည်ကို အောက်ခြေမှာ ကပ်ပြထားပါတယ်။ `fit: .expand` က `Stack` ထဲက positioned မဟုတ်တဲ့ child (ဒီမှာ `PhotoImage`) ကို card တခုလုံး အပြည့် ဖြစ်အောင် လုပ်ပေးပါတယ်။
- ဓာတ်ပုံ အဖြူရောင် ဖြစ်နေရင် စာ အဖြူက မမြင်ရတော့ပါဘူး။ ဒါကြောင့် အပေါ်ကနေ အောက်ကို ကြည်ရာကနေ မှောင်လာတဲ့ `LinearGradient` ကို စာရဲ့ နောက်မှာ ခံထားပါတယ်။ Gradient ရဲ့ အရောင်ကို Day 25 မှာ ဆောက်ခဲ့တဲ့ `PixlyColors` theme extension ထဲက `photoOverlay` ကနေ ယူထားတာ ဖြစ်ပြီး dark mode မှာ နည်းနည်း ပိုမှောင်ပါတယ်။
- Card ရဲ့ ထောင့်ကွေးကတော့ Day 25 က `cardTheme` မှာ `clipBehavior: .antiAlias` နဲ့ `borderRadius: 16` သတ်မှတ်ထားလို့ ပုံကပါ ထောင့်ကွေးအတိုင်း ဖြတ်ပြီးသား ဖြစ်နေတာပါ။

### ဘယ် size ကို သုံးမလဲ?

Pexels response ရဲ့ `src` ထဲမှာ ပုံ တပုံတည်းအတွက် size အမျိုးမျိုး ပါလာပါတယ်။ Pexels documentation အရ

| Key       | Size                                     |
| --------- | ---------------------------------------- |
| original  | မူရင်း size အတိုင်း                      |
| large2x   | W 940px × H 650px, DPR 2                 |
| large     | W 940px × H 650px, DPR 1                 |
| medium    | အမြင့် 350px ဖြစ်အောင် အချိုးကျ ချုံ့ထား |
| small     | အမြင့် 130px ဖြစ်အောင် အချိုးကျ ချုံ့ထား |
| portrait  | W 800px × H 1200px ဖြတ်ထား               |
| landscape | W 1200px × H 627px ဖြတ်ထား               |
| tiny      | W 280px × H 200px ဖြတ်ထား                |

Grid ထဲက card သေးသေးလေးတွေမှာ `original` (MB နဲ့ချီတဲ့ ပုံ) ကို သုံးရင် data အလကား ကုန်ပြီး scroll လုပ်တာလဲ လေးလာပါလိမ့်မယ်။ ဒါကြောင့် grid မှာ `medium` ကို သုံးထားပါတယ်။ Pixel density မြင့်တဲ့ ဖုန်းတွေမှာ နည်းနည်း မကြည်သလို ဖြစ်နိုင်ပေမယ့် file size နဲ့ ကြည်လင်မှုကြားမှာ ချိန်ညှိထားတာ ဖြစ်ပါတယ်။ Day 32 မှာ detail screen လုပ်တဲ့အခါကျမှ ပုံ ကြီးကြီး ပြဖို့ ပိုကြီးတဲ့ size ကို သုံးသွားပါမယ်။

## PhotoGrid

```dart
// lib/widgets/photo_grid.dart
import 'package:flutter/material.dart';
import 'package:pixly/models/photo.dart';
import 'package:pixly/widgets/photo_card.dart';

/// The grid layout shared by the photo grid and its shimmer skeleton.
const photoGridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
  crossAxisCount: 2,
  mainAxisSpacing: 12,
  crossAxisSpacing: 12,
  childAspectRatio: 0.75,
);

class PhotoGrid extends StatelessWidget {
  const PhotoGrid({super.key, required this.photos});

  final List<Photo> photos;

  @override
  Widget build(BuildContext context) {
    return GridView.builder(
      padding: const EdgeInsets.all(12),
      itemCount: photos.length,
      gridDelegate: photoGridDelegate,
      itemBuilder: (context, index) => PhotoCard(photo: photos[index]),
    );
  }
}
```

Day 24 မှာ ပြောခဲ့သလိုပဲ

- **`GridView.builder`** \- Day 23, 24 မှာ ပြောခဲ့သလိုပဲ list အကုန်လုံးကို တခါတည်း မဆောက်ဘဲ screen ပေါ်မှာ မြင်ရတဲ့ card တွေနဲ့ scroll cache area (screen ရဲ့ အပေါ်/အောက် နည်းနည်း) ထဲက card တွေကိုပဲ build လုပ်ပါတယ်။ ဒါကြောင့် ပုံ အများကြီး ရှိလာလဲ မလိုအပ်တဲ့ အလုပ်တွေ အများကြီး သက်သာသွားပါတယ်။ ပုံ decode လုပ်တာ၊ render လုပ်တာတွေကတော့ အလုပ် ရှိနေဆဲ ဖြစ်တဲ့အတွက် အပေါ်မှာ ပြောခဲ့တဲ့ ပုံ size ရွေးတာကလဲ အရေးကြီးပါတယ်။
- **`childAspectRatio: 0.75`** \- width : height = 3 : 4 ဖြစ်ပါတယ်။ ဥပမာ - width 390 ရှိတဲ့ screen မှာဆို padding နဲ့ spacing နုတ်ပြီး (390 - 12×2 - 12) ÷ 2 = 177 width ရပြီး height က 177 ÷ 0.75 = 236 ဖြစ်ပါတယ်။ ဓာတ်ပုံ အများစုက ဒေါင်လိုက် ဖြစ်တဲ့အတွက် square ထက် ပိုမြင့်တဲ့ card က ပိုလှပါတယ်။
- Grid delegate ကို `photoGridDelegate` ဆိုပြီး top-level `const` အနေနဲ့ ခွဲထုတ်ထားပါတယ်။ ဘာလို့လဲဆိုတော့ ခဏနေ ရေးမယ့် shimmer skeleton ကလဲ ဒီ layout အတိုင်း တထပ်တည်း ဖြစ်နေဖို့ လိုလို့ပါ။

## Shimmer Loading

Loading ဖြစ်နေတုန်း `CircularProgressIndicator` ကို screen အလယ်မှာ ပြတာထက် ရောက်လာမယ့် content ရဲ့ ပုံစံ (skeleton) ကို အရင်ပြထားရင် user က "ဘာတွေ ပေါ်လာတော့မလဲ" ဆိုတာ ကြိုသိပြီး စောင့်ရတာ ပိုမြန်သလို ခံစားရပါတယ်။ Shimmer ဆိုတာ အဲ့ဒီ skeleton ပေါ်မှာ အလင်းတန်းလေး ဖြတ်သွားနေတဲ့ effect ပါ။

```dart
// lib/widgets/shimmer_grid.dart
import 'package:flutter/material.dart';
import 'package:pixly/config/theme.dart';
import 'package:pixly/widgets/photo_grid.dart';
import 'package:shimmer/shimmer.dart';

/// Grey placeholder tiles with a moving shine, shown while photos load.
class ShimmerGrid extends StatelessWidget {
  const ShimmerGrid({super.key, this.itemCount = 8});

  final int itemCount;

  @override
  Widget build(BuildContext context) {
    final colors = context.pixlyColors;

    return Shimmer.fromColors(
      baseColor: colors.shimmerBase,
      highlightColor: colors.shimmerHighlight,
      child: GridView.builder(
        padding: const EdgeInsets.all(12),
        physics: const NeverScrollableScrollPhysics(),
        itemCount: itemCount,
        gridDelegate: photoGridDelegate,
        itemBuilder: (context, index) => DecoratedBox(
          decoration: BoxDecoration(
            // Any opaque colour works: Shimmer paints its gradient over it.
            color: colors.shimmerBase,
            borderRadius: BorderRadius.circular(16),
          ),
        ),
      ),
    );
  }
}
```

- `**Shimmer.fromColors**` \- `baseColor` (နောက်ခံ အရောင်) နဲ့ `highlightColor` (ဖြတ်သွားမယ့် အလင်းတန်း အရောင်) ကို ပေးရုံနဲ့ shimmer effect ရပါပြီ။ Default အနေနဲ့ ဘယ်ကနေ ညာကို 1500 milliseconds (၁.၅ စက္ကန့်) တခါ အဆုံးမရှိ ပြေးနေမှာ ဖြစ်ပါတယ်။
- Shimmer က child ရဲ့ **ပုံသဏ္ဌာန်** ကိုပဲ ယူပြီး သူ့ gradient နဲ့ ဆေးသုတ်ပါတယ်။ ဒါကြောင့် child box တွေရဲ့ အရောင်က ဘာဖြစ်ဖြစ် opaque (ဖောက်ထွင်း မမြင်ရ) ဖြစ်ဖို့ပဲ လိုပါတယ်။
- အရောင်တွေကို Day 25 မှာ `PixlyColors` ထဲ ကြိုထည့်ထားခဲ့တဲ့ `shimmerBase`, `shimmerHighlight` ကနေ ယူထားတဲ့အတွက် light mode မှာ မီးခိုးနု၊ dark mode မှာ မီးခိုးရင့် အလိုအလျောက် ပြောင်းပါတယ်။ Theme extension ကို အဲ့ဒီတုန်းက ဘာလို့ လုပ်ခဲ့လဲဆိုတာ ဒီနေ့ အကျိုးရှိသွားပါပြီ။
- **`photoGridDelegate`** ကို ပြန်သုံးထားတဲ့အတွက် skeleton tile တွေက တကယ့် card တွေနဲ့ size, spacing တူတူ ဖြစ်ပါတယ်။ ဒါကြောင့် data ရောက်လာတဲ့အခါ layout ခုန်သွားတာ (layout shift) မရှိပါဘူး။
- **`NeverScrollableScrollPhysics`** \- skeleton ကို scroll လုပ်စရာ မလိုလို့ ပိတ်ထားပါတယ်။

## HomeScreen ကို Update လုပ်ခြင်း

Day 29 ရဲ့ `_PhotosBody` ထဲမှာ ပြင်ရမှာ ၂ နေရာပဲ ရှိပါတယ်။ Loading မှာ `ShimmerGrid`, data ရောက်ရင် `PhotoGrid` ကို ပြမယ်။

```dart
// 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/providers/photos_provider.dart';
import 'package:pixly/widgets/empty_state.dart';
import 'package:pixly/widgets/photo_grid.dart';
import 'package:pixly/widgets/shimmer_grid.dart';
import 'package:provider/provider.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: const _PhotosBody(),
    );
  }
}

class _PhotosBody extends StatelessWidget {
  const _PhotosBody();

  @override
  Widget build(BuildContext context) {
    final provider = context.watch<PhotosProvider>();
    final photos = provider.photos;

    if (provider.isLoading && photos.isEmpty) {
      return const ShimmerGrid();
    }

    if (provider.error != null && photos.isEmpty) {
      return EmptyState(
        title: 'Something went wrong',
        message: provider.error!,
        action: FilledButton.icon(
          onPressed: () => context.read<PhotosProvider>().loadCurated(),
          icon: const Icon(Icons.refresh),
          label: const Text('Try again'),
        ),
      );
    }

    if (photos.isEmpty) {
      return const EmptyState(
        title: 'No photos yet',
        message: 'Beautiful photos from Pexels will show up here soon.',
      );
    }

    return PhotoGrid(photos: photos);
  }
}
```

State management နဲ့ UI ကို ခွဲထားခဲ့တဲ့အတွက် `PhotosProvider` ကို လုံးဝ ပြင်စရာ မလိုဘဲ UI ပိုင်းကိုပဲ ပြောင်းလိုက်ရုံနဲ့ app တခုလုံး ပုံစံ ပြောင်းသွားတာကို တွေ့ရမှာပါ။

## ရလဒ်

![Shimmer skeleton while photos load](https://arkar.dev/content/images/2026/10/100dof-day30-day30_shimmer.png)

Shimmer skeleton while photos load

![Photo grid in light mode](https://arkar.dev/content/images/2026/10/100dof-day30-day30_grid_light.png)

Photo grid in light mode

![Photo grid in dark mode](https://arkar.dev/content/images/2026/10/100dof-day30-day30_grid_dark.png)

Photo grid in dark mode

(Screenshot တွေမှာ ပြထားတဲ့ ပုံတွေနဲ့ နာမည်တွေက test အတွက် ပြင်ဆင်ထားတဲ့ sample data ဖြစ်ပါတယ်။ ကိုယ့် API key နဲ့ run ရင်တော့ Pexels က curated photo အစစ်တွေကို မြင်ရမှာပါ။)

Pexels ရဲ့ API guidelines မှာ Pexels ကို link နဲ့ ထင်ထင်ရှားရှား ဖော်ပြဖို့နဲ့ ဖြစ်နိုင်တဲ့ အခါတိုင်း photographer ကို credit ပေးဖို့ တောင်းဆိုထားပါတယ်။ ဒီနေ့ card ပေါ်မှာ photographer နာမည်ကို ပြထားပြီး Pexels link အပါအဝင် attribution အပြည့်အစုံကိုတော့ Day 32 detail screen နဲ့ Day 39 မှာ ဆက်ထည့်သွားပါမယ်။

အခုအချိန်မှာ Pixly က ပထမ page ရဲ့ ပုံ ၂၀ ပဲ ပြနိုင်သေးပါတယ်။ မနက်ဖြန် Day 31 မှာတော့ အောက်ဆုံးထိ scroll ဆွဲရင် နောက် page တွေကို ဆက် load လုပ်ပေးမယ့် infinite scroll နဲ့ အပေါ်က ဆွဲချပြီး refresh လုပ်တဲ့ pull-to-refresh ကို ထည့်သွားမှာ ဖြစ်ပါတယ်။

**Source code:** ဒီနေ့ရဲ့ code အပြည့်အစုံကို [GitHub](https://github.com/arkarmintun1/100-days-of-flutter/tree/main/day30/pixly?ref=arkar.dev) မှာ run ကြည့်လို့ ရပါတယ်။

---

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

- Youtube: [https://www.youtube.com/@arkarmintun](https://www.youtube.com/@arkarmintun?ref=arkar.dev)
- Newsletter: <https://arkar.dev/>
- Discord: [https://discord.gg/3xUJ6k6dkH](https://discord.gg/3xUJ6k6dkH?ref=arkar.dev)