บทนำ
ยินดีต้อนรับสู่ภาคที่ 2 ของชุดบทความนี้ ในภาคแรก เราเห็นวิธีสร้าง REST API ด้วย Upstash, Serverless Framework และ Redis
ในภาคนี้ เราจะสร้าง Mobile Application ด้วย Flutter เพื่อ Consume REST API Endpoints ของเรา
มาเริ่มกันเลย 🙃
ขั้นแรก คุณต้องมี Flutter ติดตั้งและรันบนคอมพิวเตอร์แล้ว
- Flutter
สร้าง Flutter Project ใหม่ใน IDE และตั้งชื่อตามต้องการ
เปิดไฟล์ pubspec.yaml ที่ Root Directory ของโปรเจกต์ Flutter และเพิ่ม Dependencies ภายใต้ dev_dependencies
timeago: ^3.1.0
shared_preferences: ^2.0.6
http: ^0.13.4
สุดท้ายจะมีหน้าตาประมาณนี้:
dev_dependencies:
flutter_test:
sdk: flutter
timeago: ^3.1.0
shared_preferences: ^2.0.6
http: ^0.13.4
Library timeago แปลง Unix Timestamps (เช่น 1636824843) ให้เป็นรูปแบบมนุษย์อ่านได้ เช่น a minute ago, 5 mins ago ฯลฯ
shared_preferences ใช้เก็บ userId และรายละเอียดเล็กน้อยหลังสร้างบัญชีผู้ใช้ http ใช้สำหรับ HTTP Calls
สร้างผู้ใช้ (Create User)
หน้าจอแรกคือ Create User Screen ซึ่ง Consume Create User Endpoint
หน้าจอจะมีหน้าตาประมาณนี้ (รูป Bunny เป็น Placeholder สำหรับ ImageView)
สร้างโฟลเดอร์ account ภายใน lib แล้วสร้างไฟล์ create_profile_screen.dart ภายในโฟลเดอร์ account
โครงสร้างโฟลเดอร์ lib สุดท้ายจะประมาณนี้
เพื่อสร้างผู้ใช้ใหม่ ต้องมี:
- Profile Pic URL
- First Name
- Last Name
- Username
- Endpoint
โค้ด Create User Profile
static const String CREATE_USER_PROFILE_URL = "https://5vafvrk8kj.execute-api.us-east-1.amazonaws.com/dev/user";
bool _loading = false;
Future<void> createUserProfile() async {
setState(() { _loading = true; });
print(usernameController.text);
print(firstNameController.text);
print(lastNameController.text);
print(profilePicUrl);
await http.post(Uri.parse(CREATE_USER_PROFILE_URL),
body: convert.jsonEncode({
'username': usernameController.text,
"firstName": firstNameController.text,
"lastName": lastNameController.text,
"profilePic": profilePicUrl
})
).then((response) async {
var jsonResponse = convert.jsonDecode(response.body) as Map<String, dynamic>;
setState(() { _loading = false; });
if (response.statusCode == 400) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
padding: EdgeInsets.all(10),
backgroundColor: Colors.red,
content: Text(jsonResponse['message'])
));
} else if (response.statusCode == 200) {
print('user id is :' + jsonResponse['userId']);
await saveUserId(jsonResponse['userId']);
Navigator.push(context, MaterialPageRoute(builder: (context) { return HomeScreen(); }));
}
});
}
Future เป็น Core Dart Class สำหรับ Asynchronous Operations แทนค่าหรือข้อผิดพลาดที่จะพร้อมใช้งานในอนาคต http.Response มีข้อมูลที่ได้จาก HTTP Call ที่สำเร็จ
โค้ดข้างต้นใช้ http.post ส่ง POST Request ไปยัง create user endpoint แล้วรอ Response หาก Status Code 200 แสดงว่าสำเร็จ จะบันทึก UserId ใน Shared Preferences แล้วไปยัง HomeScreen
ดู Source Code เต็มได้ที่ Create Profile Screen
สร้างโพสต์ (Create a Post)
Endpoint หนึ่งอนุญาตให้ผู้ใช้สร้างโพสต์ หน้าจอจะมีหน้าตาประมาณนี้
เพื่อสร้างโพสต์ ผู้ใช้ต้องมี:
- UserId
- Text
- Image URL
เพื่อการสาธิต เราใช้ Image URL สำเร็จรูป ในแอปจริง คุณต้องให้ผู้ใช้เลือกภาพ อัปโหลดไป Server รับ Image URL แล้วค่อยสร้างโพสต์
Method CreatePost คล้าย CreateUser:
Future<void> createPost(String userId) async {
await http.post(Uri.parse(CREATE_USER_POST_URL),
body: convert.jsonEncode({
'userId': userId,
"postText": postTextController.text,
"postImage": _postPicUrl[i]
})
).then((response) async {
var jsonResponse = convert.jsonDecode(response.body) as Map<String, dynamic>;
setState(() { _loading = false; });
if (response.statusCode == 400) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
padding: EdgeInsets.all(10),
backgroundColor: Colors.red,
content: Text(jsonResponse['message'])
));
} else if (response.statusCode == 200) {
print('post id is :' + jsonResponse['id']);
Navigator.of(context).pop();
}
});
}
แสดงรายการโพสต์ทั้งหมด (List All Posts)
Home Screen จะแสดงรายการโพสต์ทั้งหมดที่สร้างขึ้น
เพื่อดึงโพสต์ทั้งหมดอย่างราบรื่น เราต้องสร้าง Custom Dart Object ที่แทนโพสต์เดี่ยว
class Post {
String? postText;
String? userId;
String? createdOn;
String? id;
String? postImage;
PostAdmin? postAdmin;
Post({this.postText, this.userId, this.createdOn, this.id, this.postImage, this.postAdmin});
Post.fromJson(Map<String, dynamic> json) {
postText = json['postText'];
userId = json['userId'];
createdOn = json['createdOn'];
id = json['id'];
postImage = json['postImage'];
postAdmin = json['postAdmin'] != null ? PostAdmin.fromJson(json['postAdmin']) : null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['postText'] = this.postText;
data['userId'] = this.userId;
data['createdOn'] = this.createdOn;
data['id'] = this.id;
data['postImage'] = this.postImage;
if (this.postAdmin != null) { data['postAdmin'] = this.postAdmin!.toJson(); }
return data;
}
}
class PostAdmin {
String? timestamp;
String? userId;
String? username;
String? firstName;
String? lastName;
String? profilePic;
PostAdmin({this.timestamp, this.userId, this.username, this.firstName, this.lastName, this.profilePic});
PostAdmin.fromJson(Map<String, dynamic> json) {
timestamp = json['timestamp'];
userId = json['userId'];
username = json['username'];
firstName = json['firstName'];
lastName = json['lastName'];
profilePic = json['profilePic'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['timestamp'] = this.timestamp;
data['userId'] = this.userId;
data['username'] = this.username;
data['firstName'] = this.firstName;
data['lastName'] = this.lastName;
data['profilePic'] = this.profilePic;
return data;
}
}
จากนั้นแปลง http.Response ให้เป็น Custom Dart Object นี้
List<Post> parsePosts(String responseBody) {
final parsed = jsonDecode(responseBody).cast<Map<String, dynamic>>();
return parsed.map<Post>((json) => Post.fromJson(json)).toList();
}
Future<List<Post>> fetchPosts(http.Client client) async {
final response = await client.get(Uri.parse(GET_POSTS));
return compute(parsePosts, response.body);
}
Return Type ของ fetchPosts คือ Future<List<Post>>
หากรัน fetchPosts() บนอุปกรณ์ช้า อาจเห็นแอปค้างสักครู่ขณะ Parse/Convert JSON (Jank) เราแก้ไขด้วยการย้าย Parsing ไป Background โดยใช้ Function compute
compute(parsePosts, response.body);
compute() รันฟังก์ชันที่ทốnทรัพยากรใน Background Isolate และส่งผลลัพธ์กลับมา
ในไฟล์ Home Screen เราใช้ Widget FutureBuilder เพื่อดึงโพสต์ทั้งหมดแบบ Asynchronous
ต้องระบุ 2 Parameters:
- Future ที่ต้องการทำงาน (จาก
fetchPosts()) - Builder Function บอก Flutter ให้ Render อะไรตาม State ของ Future: Loading, Success, Error
snapshot.hasDataคืนค่า True ก็ต่อเมื่อ Snapshot มี Non-null Data เนื่องจากfetchPostsคืนค่า Non-null เท่านั้น ฟังก์ชันควร Throw Exception แม้กรณี "404 Not Found" เพื่อให้snapshot.hasErrorเป็น True และแสดงข้อความ Error มิฉะนั้นจะแสดง Spinner
Expanded(child: FutureBuilder<List<Post>>(
future: _posts,
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Post>? posts = snapshot.data;
if (posts != null) {
return ListView.builder(itemBuilder: (context, index) {
return Card(
child: Container(
padding: EdgeInsets.all(10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(1000),
child: Image.network(
posts[index].postAdmin!.profilePic!,
fit: BoxFit.cover, height: 40, width: 40,
),
),
Expanded(
child: Container(
padding: EdgeInsets.only(left: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(posts[index].postAdmin!.username!, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
Text(posts[index].postText!),
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
posts[index].postImage!,
fit: BoxFit.cover, height: 150, width: size.width,
),
),
],
),
),
),
],
),
),
);
}, itemCount: posts.length);
}
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// Default: Loading Spinner
return Container(
height: 40, width: 40,
child: Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).colorScheme.secondary)))
);
},
))
ใน initState เราเรียก fetchPosts:
late Future<List<Post>> _posts;
@override
void initState() {
super.initState();
_posts = fetchPosts(http.Client());
}
เหตุผลที่เรียกใน initState ไม่ใช่ build คือ Flutter เรียก build() บ่อยมาก ทุกครั้งที่ต้องเปลี่ยน View การวาง Fetch Call ใน build() จะทำให้ API ถูกเรียกซ้ำมากและช้า
ดู Complete Source Code ได้
ยังมี Endpoint อื่นๆ ที่ต้องสร้าง Interface แต่บทช่วยสอนที่ดีต้องมี Exercises 😂
สรุป
ในชุดบทความนี้ เราดูวิธีสร้าง Serverless REST API ด้วย Upstash และ Consume ผ่าน Mobile Application
ผมอยากเห็นว่าคุณจะสร้างอะไรต่อกับ Upstash หรือปรับปรุง Tutorial นี้ให้เข้ากับ Use Case ของคุณอย่างไร
ถ้าช่วยได้ โปรดแชร์บน Social Media | มีคำถาม? คอมเมนต์ได้เลย | พบข้อผิดพลาด? แจ้งมาให้ ผมจะแก้ ASAP
Happy Coding ✌🏿
อ้างอิง
- Upstash Docs
- Redis
- Flutter
- Fetching data from the internet