Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

สร้าง Full-Stack Serverless Mobile App ด้วย Flutter, Serverless Framework & Upstash Redis – ภาค 2

บทนำ

ยินดีต้อนรับสู่ภาคที่ 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