Day 27: Pexels API, .env & Data Models
Get a Pexels API key, test it from the terminal, keep it out of Git with flutter_dotenv (and learn what .env can't protect), then turn Pexels JSON into type-safe Dart models with fromJson and toJson.
ဒီနေ့ကစပြီး Pixly က internet ကနေ တကယ့် data တွေကို ယူသုံးတော့မှာ ဖြစ်ပါတယ်။ Data ယူဖို့ မစခင် ပြင်ဆင်ရမယ့် အရာ ၃ ခု ရှိပါတယ်။
- Pexels API key ရယူပြီး API က ဘယ်လို data ပြန်ပေးလဲ ကြည့်ခြင်း
- API key ကို code ထဲမှာ တိုက်ရိုက် မရေးဘဲ
.envfile နဲ့ ခွဲထားခြင်း - API က ပြန်ပေးတဲ့ JSON ကို Dart object (model) တွေ အဖြစ် ပြောင်းခြင်း
Network request တကယ် ခေါ်တာကိုတော့ မနက်ဖြန် Day 28 မှာ Dio နဲ့ လုပ်ပါမယ်။
Pexels API Key ရယူခြင်း
- pexels.com မှာ account ဖွင့်ပြီး login ဝင်ပါ။
- Pexels API page ကို သွားပြီး လိုအပ်တဲ့ အချက်အလက်တွေ ဖြည့်ကာ API key တောင်းပါ။
- ရလာတဲ့ API key ကို ဘယ်သူ့ကိုမှ မပြပါနဲ့။ GitHub လို public နေရာတွေမှာလဲ မတင်မိအောင် သတိထားပါ။
Terminal ကနေ API ကို စမ်းကြည့်ခြင်း
Code မရေးခင် API က ဘာပြန်ပေးလဲဆိုတာ terminal ကနေ အရင် စမ်းကြည့်တာ အလေ့အကျင့် ကောင်းတခုပါ။ Pexels က API key ကို Authorization header ထဲမှာ ထည့်ပို့ခိုင်းပါတယ်။
curl -H "Authorization: YOUR_API_KEY" \
"https://api.pexels.com/v1/curated?per_page=1"YOUR_API_KEY နေရာမှာ ကိုယ့် key ကို အစားထိုးပါ။ Pexels ရဲ့ documentation အတိုင်း header value မှာ key ကိုပဲ တိုက်ရိုက် ထည့်ရတာ ဖြစ်ပြီး ရှေ့မှာ Bearer လို စာလုံး ခံစရာ မလိုပါဘူး။ ပြန်ရလာတဲ့ JSON ကို ချုံ့ကြည့်မယ်ဆိုရင် ဒီလိုပုံစံ ဖြစ်ပါတယ်။
{
"page": 1,
"per_page": 1,
"photos": [
{
"id": 1001,
"width": 4000,
"height": 6000,
"url": "https://www.pexels.com/photo/...",
"photographer": "...",
"photographer_url": "https://www.pexels.com/@...",
"photographer_id": 500,
"avg_color": "#7A8C6E",
"src": {
"original": "...",
"large2x": "...",
"large": "...",
"medium": "...",
"small": "...",
"portrait": "...",
"landscape": "...",
"tiny": "..."
},
"liked": false,
"alt": "Green hills under a cloudy sky"
}
],
"next_page": "https://api.pexels.com/v1/curated/?page=2&per_page=1"
}ဒီ structure ကနေ သတိထားစရာ ၃ ချက် ရှိပါတယ်။
srcထဲမှာ ပုံတပုံတည်းကို အရွယ်အစား အမျိုးမျိုးနဲ့ ပေးထားပါတယ်။ Grid မှာ thumbnail ပြဖို့mediumလို အသေးကို သုံးပြီး detail screen မှာမှlarge2xလို အကြီးကို သုံးမှာ ဖြစ်ပါတယ်။ Data usage နဲ့ loading speed အတွက် အရမ်း အရေးကြီးပါတယ်။avg_colorက ပုံရဲ့ ပျမ်းမျှ အရောင်ပါ။ ပုံ load မပြီးခင် placeholder အရောင်အနေနဲ့ သုံးလို့ ရပါတယ်။next_pageက နောက်ထပ် page ရှိမှ ပါလာပါတယ်။ Search (/v1/search) ရဲ့ response မှာtotal_results(ရလဒ် စုစုပေါင်း) ပါပေမယ့် documentation ရဲ့ curated ဥပမာ response မှာတော့total_resultsမပါပါဘူး။ ဒါကြောင့် နောက်ထပ် ပုံ ရှိသေးလားဆိုတာကိုtotal_resultsနဲ့ မတွက်ဘဲnext_pageပါမပါနဲ့ ဆုံးဖြတ်တာက endpoint နှစ်ခုလုံးမှာ စိတ်ချရပါတယ်။
API Key ကို .env နဲ့ ခွဲထားခြင်း
API key ကို code ထဲမှာ ဒီလို တိုက်ရိုက် ရေးလိုက်ရင် အလွယ်ဆုံးပါပဲ။
const apiKey = 'abc123...'; // ❌ git commit လုပ်လိုက်တာနဲ့ key ပါသွားပြီဒါပေမယ့် project ကို GitHub ပေါ် တင်လိုက်တာနဲ့ key ပါ ပါသွားမှာ ဖြစ်ပါတယ်။ ဒါကြောင့် key တွေကို .env ဆိုတဲ့ သီးသန့် file ထဲမှာ ထားပြီး အဲ့ဒီ file ကို git ထဲ မထည့်ဘဲ ထားကြပါတယ်။
fvm flutter pub add flutter_dotenvProject root (pubspec.yaml နဲ့ အတူတူ နေရာ) မှာ .env file ဖန်တီးပါ။
PEXELS_API_KEY=your_api_key_hereပြီးရင် .gitignore ထဲမှာ .env ကို ထည့်ပြီး git ထဲ မပါသွားအောင် လုပ်ပါ။
.env⚠️.gitignoreက git က track မလုပ်ရသေးတဲ့ file တွေကိုပဲ လျစ်လျူရှုပေးတာပါ။.envကို commit လုပ်ပြီးမှ.gitignoreထဲ ထည့်ရင် git က ဆက် track လုပ်နေဦးမှာ ဖြစ်ပါတယ်။ ဒီလိုဖြစ်ခဲ့ရင်git rm --cached .envနဲ့ tracking ကို ဖြုတ်ပေးရပါမယ်။ ဒါပေမယ့် အရင် commit တွေထဲမှာ key က ကျန်နေဆဲ ဖြစ်တဲ့အတွက် key ကို GitHub ပေါ် တင်မိခဲ့ပြီးပြီဆိုရင် Pexels မှာ key အသစ် ပြန်ထုတ်ပြီး အဟောင်းကို မသုံးတော့ပါနဲ့။
ဒါဆို project ကို clone လုပ်တဲ့သူ ဘယ် key တွေ လိုလဲ မသိနိုင်တော့ပါဘူး။ ဒီအတွက် key မပါတဲ့ .env.example ကိုတော့ git ထဲ ထည့်ထားပေးပါတယ်။
# Copy this file to .env and paste your own key from https://www.pexels.com/api/
PEXELS_API_KEY=your_api_key_hereflutter_dotenv က .env ကို asset အနေနဲ့ ဖတ်တာ ဖြစ်တဲ့အတွက် Day 26 မှာ လုပ်ခဲ့သလို pubspec.yaml ရဲ့ assets: ထဲမှာ ထည့်ပေးရပါမယ်။
assets:
- .env
- assets/images/
- assets/svg/
- assets/fonts/OFL.txtApp စတင်တဲ့အချိန်မှာ .env ကို load လုပ်ပေးရပါမယ်။
// lib/main.dart
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:pixly/config/constants.dart';
import 'package:pixly/config/theme.dart';
import 'package:pixly/screens/home_screen.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: '.env');
// 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());
}mainကိုFuture<void> main() asyncပြောင်းလိုက်ပါတယ်။ Day 12 မှာ ပြောခဲ့တဲ့awaitကို သုံးဖို့ပါ။WidgetsFlutterBinding.ensureInitialized()-runAppမခေါ်ခင် asset ဖတ်တာလိုမျိုး Flutter engine ကို သုံးရမယ့် အလုပ်တွေ လုပ်ချင်ရင် ဒီ line ကို အရင်ခေါ်ပေးရပါတယ်။await dotenv.load(fileName: '.env')-.envထဲက value တွေကို memory ထဲ ဖတ်ထားပေးပါတယ်။
နောက်ဆုံး key ကို ပြန်ယူသုံးဖို့ class လေးတခု ရေးထားပါမယ်။
// lib/config/env.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// Typed access to values from the `.env` file.
class Env {
static String get pexelsApiKey => dotenv.get('PEXELS_API_KEY');
}dotenv.get က key မရှိရင် error throw လုပ်ပါတယ်။ .env ထည့်ဖို့ မေ့သွားရင် API ကို key အလွတ်နဲ့ ခေါ်ပြီး ဘာဖြစ်မှန်းမသိ ဖြစ်နေမယ့်အစား ချက်ချင်း သိရတဲ့အတွက် ပိုကောင်းပါတယ်။
⚠️ .env က ဘာကို မကာကွယ်ပေးနိုင်လဲ
ဒီအချက်ကို သေချာ နားလည်ထားဖို့ အရမ်း အရေးကြီးပါတယ်။ .env က key ကို git ထဲ မပါအောင် ကာကွယ်ပေးတာပါ။ App ထဲမှာ လျှို့ဝှက်ထားပေးတာ မဟုတ်ပါဘူး။ .env ကို asset အနေနဲ့ ထည့်လိုက်တဲ့အတွက် build လုပ်ထားတဲ့ APK ဒါမှမဟုတ် IPA file ကို ဖြည်ပြီး ကြည့်လိုက်တာနဲ့ key ကို ပြန်ဖတ်လို့ ရနေမှာ ဖြစ်ပါတယ်။
- Pexels key လို အခမဲ့ပြီး rate limit ရှိတဲ့ key အတွက် ဒီ level က လေ့လာဖို့ လုံလောက်ပါတယ်။
- ငွေကြေး ပတ်သက်တဲ့ key, admin key လိုမျိုး တကယ် လျှို့ဝှက်ရမယ့် key တွေကို mobile app ထဲ ဘယ်တော့မှ မထည့်ပါနဲ့။ အဲ့ဒီလို အခြေအနေမှာ ကိုယ့် backend server ကနေ ကြားခံ ခေါ်ပေးရပါမယ်။ App က ကိုယ့် server ကို ခေါ်၊ server က key နဲ့ third-party API ကို ခေါ်တဲ့ ပုံစံပါ။
JSON ကနေ Dart Model တွေ
API က ပြန်ပေးတဲ့ JSON ကို decode လုပ်လိုက်ရင် Map<String, dynamic> ရပါတယ်။ ဒါကို app တခုလုံးမှာ photo['photographer'] လို ဟိုသုံး ဒီသုံး လုပ်နေမယ်ဆိုရင် key နာမည် စာလုံးတလုံး မှားတာနဲ့ run မှ error တက်မှာ ဖြစ်ပြီး ဘယ် field က ဘယ် type လဲဆိုတာကိုလဲ editor က မသိပါဘူး။ ဒါကြောင့် JSON ကို Day 7 မှာ လေ့လာခဲ့တဲ့ class တွေ အဖြစ် ပြောင်းပြီးမှ သုံးပါမယ်။ ဒီလို class တွေကို model လို့ ခေါ်ပါတယ်။
PhotoSrc
// lib/models/photo_src.dart
/// The different image sizes Pexels gives us for one photo.
class PhotoSrc {
final String original;
final String large2x;
final String large;
final String medium;
final String small;
final String portrait;
final String landscape;
final String tiny;
const PhotoSrc({
required this.original,
required this.large2x,
required this.large,
required this.medium,
required this.small,
required this.portrait,
required this.landscape,
required this.tiny,
});
factory PhotoSrc.fromJson(Map<String, dynamic> json) {
return PhotoSrc(
original: json['original'] as String? ?? '',
large2x: json['large2x'] as String? ?? '',
large: json['large'] as String? ?? '',
medium: json['medium'] as String? ?? '',
small: json['small'] as String? ?? '',
portrait: json['portrait'] as String? ?? '',
landscape: json['landscape'] as String? ?? '',
tiny: json['tiny'] as String? ?? '',
);
}
Map<String, dynamic> toJson() => {
'original': original,
'large2x': large2x,
'large': large,
'medium': medium,
'small': small,
'portrait': portrait,
'landscape': landscape,
'tiny': tiny,
};
}factory PhotoSrc.fromJson(...)- Day 7 က named constructor လိုပဲ ဖြစ်ပြီးMapတခုကို လက်ခံပြီးPhotoSrcobject တခု ပြန်ထုတ်ပေးပါတယ်။json['medium'] as String? ?? ''- Day 5 က null safety ကို ပြန်သုံးထားတာပါ။ API က field တခုခု မပို့ခဲ့ရင်nullဖြစ်နိုင်တဲ့အတွက် အရင်String?(null ဖြစ်နိုင်တဲ့ String) အဖြစ် cast လုပ်ပြီး null ဖြစ်ခဲ့ရင်??နဲ့ string အလွတ် default ပေးထားပါတယ်။ ဒါမှ field တခု ပျောက်နေရုံနဲ့ app crash မဖြစ်တော့မှာပါ။toJson()- object ကိုMapပြန်ပြောင်းပေးတာပါ။ Day 35 မှာ bookmark လုပ်ထားတဲ့ ပုံတွေကို device ထဲ သိမ်းတဲ့အခါ ပြန်သုံးမှာ ဖြစ်ပါတယ်။
Photo
// lib/models/photo.dart
import 'package:pixly/models/photo_src.dart';
class Photo {
final int id;
final int width;
final int height;
final String url;
final String photographer;
final String photographerUrl;
final String avgColor;
final String alt;
final PhotoSrc src;
const Photo({
required this.id,
required this.width,
required this.height,
required this.url,
required this.photographer,
required this.photographerUrl,
required this.avgColor,
required this.alt,
required this.src,
});
/// width ÷ height of the original photo, handy for layouts.
double get aspectRatio => height == 0 ? 1 : width / height;
factory Photo.fromJson(Map<String, dynamic> json) {
return Photo(
id: json['id'] as int,
width: json['width'] as int? ?? 0,
height: json['height'] as int? ?? 0,
url: json['url'] as String? ?? '',
photographer: json['photographer'] as String? ?? 'Unknown',
photographerUrl: json['photographer_url'] as String? ?? '',
avgColor: json['avg_color'] as String? ?? '#9E9E9E',
alt: json['alt'] as String? ?? '',
src: PhotoSrc.fromJson(json['src'] as Map<String, dynamic>),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'width': width,
'height': height,
'url': url,
'photographer': photographer,
'photographer_url': photographerUrl,
'avg_color': avgColor,
'alt': alt,
'src': src.toJson(),
};
}သတိထားကြည့်စရာတွေကတော့
- JSON မှာ
photographer_url(snake_case) ဖြစ်ပေမယ့် Dart မှာphotographerUrl(camelCase) လို့ ပြောင်းသုံးထားပါတယ်။ Dart ရဲ့ naming convention အတိုင်း ဖြစ်အောင်ပါ။fromJson/toJsonထဲမှာပဲ JSON key အစစ်ကို သုံးထားတာ ဖြစ်ပါတယ်။ idကတော့ photo တိုင်းမှာ အမြဲ ပါရမယ့် field ဖြစ်တဲ့အတွက် default မပေးဘဲas intနဲ့ တိုက်ရိုက် cast လုပ်ထားပါတယ်။idမပါတဲ့ data ဆိုရင် မှားနေတဲ့ data ဖြစ်တဲ့အတွက် error တက်တာက ပိုကောင်းပါတယ်။srcက nested object ဖြစ်တဲ့အတွက်PhotoSrc.fromJsonကို ဆက်ခေါ်ထားပါတယ်။aspectRatiogetter ကတော့ မူလ ပုံရဲ့ width ÷ height ဖြစ်ပါတယ်။ Day 24 မှာ ပြောခဲ့တဲ့childAspectRatioလိုပဲ width/height ratio ဖြစ်ပြီး detail screen မှာ ပုံကို အချိုးမပျက် ပြဖို့ သုံးမှာပါ။
PhotosPage
API က ပုံတွေကို တခါတည်း အကုန်မပေးဘဲ page လိုက် ခွဲပေးပါတယ်။ page တခုစာ data ကို ကိုယ်စားပြုဖို့ model တခု ထပ်လိုပါတယ်။
// lib/models/photos_page.dart
import 'package:pixly/models/photo.dart';
/// One page of results from /curated or /search.
class PhotosPage {
final int page;
final int perPage;
final List<Photo> photos;
final int? totalResults;
final String? nextPage;
const PhotosPage({
required this.page,
required this.perPage,
required this.photos,
this.totalResults,
this.nextPage,
});
/// Pexels only sends `next_page` when there is another page.
bool get hasMore => nextPage != null;
factory PhotosPage.fromJson(Map<String, dynamic> json) {
final photos = (json['photos'] as List<dynamic>? ?? [])
.map((item) => Photo.fromJson(item as Map<String, dynamic>))
.toList();
return PhotosPage(
page: json['page'] as int? ?? 1,
perPage: json['per_page'] as int? ?? photos.length,
photos: photos,
totalResults: json['total_results'] as int?,
nextPage: json['next_page'] as String?,
);
}
}photoslist ကို ပြောင်းတဲ့နေရာမှာ Day 10 က.map()နဲ့.toList()ကို သုံးထားပါတယ်။ JSON list ထဲက item တခုချင်းစီကိုPhoto.fromJsonနဲ့Photoobject ပြောင်းပေးတာပါ။totalResultsနဲ့nextPageက response မှာ ပါချင်မှ ပါမှာ ဖြစ်တဲ့အတွက် nullable (int?,String?) ထားပါတယ်။hasMoregetter ကnext_pageပါမပါကို ကြည့်ပြီး နောက်ထပ် page ရှိမရှိ ပြောပေးပါတယ်။ Day 31 မှာ infinite scroll လုပ်တဲ့အခါ ဒီ getter ကို အဓိက သုံးမှာ ဖြစ်ပါတယ်။
💡 JSON field များလာတဲ့အခါfromJson/toJsonတွေကို လက်နဲ့ ရေးရတာ ပင်ပန်းပြီး မှားလွယ်ပါတယ်။ ဒါကို code generation နဲ့ အလိုအလျောက် ထုတ်ပေးတဲ့json_serializableနဲ့freezedအကြောင်းကို Day 45 မှာ ဆက်လေ့လာမှာ ဖြစ်ပါတယ်။ အခုတော့ JSON parsing ဘယ်လို အလုပ်လုပ်လဲ နားလည်အောင် လက်နဲ့ အရင် ရေးကြည့်တာ ဖြစ်ပါတယ်။
Model တွေကို စမ်းကြည့်ခြင်း
Network request မရေးရသေးခင် model တွေ မှန်မမှန် စမ်းဖို့ Pexels response ပုံစံအတိုင်း sample JSON file တွေ (test/fixtures/) ပြင်ဆင်ပြီး test လေးတွေ ရေးထားပါတယ်။ (Sample data ထဲက ID, နာမည်, URL တွေက test အတွက် လုပ်ထားတာ ဖြစ်ပြီး Pexels က ပုံအစစ်တွေ မဟုတ်ပါဘူး။)
Fixture file တွေက Pexels response ပုံစံအတိုင်းပါပဲ။ ဥပမာ test/fixtures/curated_last_page.json က နောက်ဆုံး page ဖြစ်တဲ့အတွက် next_page မပါဘဲ prev_page ပဲ ပါပါတယ်။
{
"page": 3,
"per_page": 12,
"photos": [ ... ],
"prev_page": "https://api.pexels.com/v1/curated/?page=2&per_page=12"
}// test/models_test.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:pixly/models/photo.dart';
import 'package:pixly/models/photos_page.dart';
Map<String, dynamic> fixture(String name) =>
json.decode(File('test/fixtures/$name.json').readAsStringSync())
as Map<String, dynamic>;
void main() {
test('parses a curated page (no total_results, has next_page)', () {
final page = PhotosPage.fromJson(fixture('curated_page1'));
expect(page.page, 1);
expect(page.perPage, 12);
expect(page.photos, hasLength(12));
expect(page.totalResults, isNull);
expect(page.hasMore, isTrue);
});
test('last page has no next_page => hasMore is false', () {
final page = PhotosPage.fromJson(fixture('curated_last_page'));
expect(page.hasMore, isFalse);
});
}fixture() helper က test/fixtures/ ထဲက JSON file ကို ဖတ်ပြီး Map ပြန်ပေးတာ ဖြစ်ပါတယ်။ Project ထဲမှာတော့ ဒီ test တွေအပြင် search response နဲ့ toJson/fromJson round trip ကို စစ်တဲ့ test တွေလဲ ထပ်ပါပါတယ်။
fvm flutter test နဲ့ run လိုက်ရင် test အားလုံး pass ဖြစ်ရပါမယ်။ Testing အကြောင်းကို Day 86 မှာ အသေးစိတ် ပြောမှာ ဖြစ်လို့ ဒီနေ့တော့ "model တွေ JSON ကို မှန်မှန်ကန်ကန် ပြောင်းပေးလား" ဆိုတာ စစ်တဲ့ နည်းလမ်းတခုအနေနဲ့ပဲ မှတ်ထားပါ။
ဒီနေ့ပြီးသွားတဲ့အခါ project ထဲမှာ ဒီလို file တွေ ရှိနေပါပြီ။
lib/
├── config/
│ ├── assets.dart
│ ├── constants.dart
│ ├── env.dart ← new
│ └── theme.dart
├── models/ ← new
│ ├── photo.dart
│ ├── photo_src.dart
│ └── photos_page.dart
├── screens/
│ └── home_screen.dart
├── widgets/
│ └── empty_state.dart
└── main.dartSource code: ဒီနေ့ရဲ့ code အပြည့်အစုံကို GitHub မှာ run ကြည့်လို့ ရပါတယ်။
ဒီနေ့ Day 27 အတွက်ကတော့ ဒီလောက်ပဲ ဖြစ်ပါတယ်။ အဆုံးထိ ဖတ်ပေးတဲ့အတွက် အများကြီး ကျေးဇူးတင်ပါတယ်။ နားမလည်တာတွေ အဆင်မပြေတာတွေ ရှိခဲ့ရင်လဲ အောက်မှာပေးထားတဲ့ discord server ထဲမှာ လာရောက်ဆွေးနွေးနိုင်ပါတယ်။ နောက်နေ့တွေမှာလဲ ဆက်လက်ပြီး sharing လုပ်သွားပေးသွားမှာ ဖြစ်တဲ့အတွက် subscribe လုပ်ထားဖို့ ဖိတ်ခေါ်ပါတယ်။
- Youtube: https://www.youtube.com/@arkarmintun
- Newsletter: https://arkar.dev/
- Discord: https://discord.gg/3xUJ6k6dkH