Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Redis

Tạo Ứng dụng Di động Full-Stack Serverless với Flutter, Serverless Framework & Upstash Redis – Phần 2

Chào mừng đến với phần 2 của series hướng dẫn này. Ở phần 1, chúng ta đã thấy cách xây dựng REST API sử dụng Upstash, Serverless Framework và Redis.

Trong phần này, chúng ta sẽ xây dựng ứng dụng di động sử dụng Flutter, để tiêu thụ các endpoint REST API của chúng ta.

Hãy bắt đầu 🙃

Đầu tiên, bạn cần cài đặt và chạy Flutter trên máy tính của mình.

  • Flutter

Tạo một dự án Flutter mới trong IDE của bạn và đặt tên theo ý thích.

Mở file pubspec.yaml ở thư mục gốc của dự án Flutter và thêm các dependency này dưới dev_dependencies:

timeago: ^3.1.0
shared_preferences: ^2.0.6
http: ^0.13.4

Vậy nó sẽ trông như sau:

dev_dependencies:
  flutter_test:
    sdk: flutter
  timeago: ^3.1.0
  shared_preferences: ^2.0.6
  http: ^0.13.4

Thư viện timeago chuyển đổi dấu thời gian Unix (1636824843) thành định dạng dễ đọc cho con người như một phút trước, 5 phút trước, v.v.

Khi tạo tài khoản người dùng, chúng ta muốn theo dõi userId và các chi tiết nhỏ khác. Chúng ta sẽ dùng shared_preferences cho việc đó. Sau đó dùng thư viện http` để thực hiện các cuộc gọi HTTP.

Hãy bắt đầu...

Tạo Người dùng

Màn hình đầu tiên chúng ta xây dựng là màn hình tạo người dùng, sẽ tiêu thụ endpoint tạo người dùng.

Màn hình trông như sau:

Tạo Ứng dụng Di động Full-Stack Serverless với Flutter, Serverless Framework & Upstash Redis – Phần 2 Đừng lo về hình con thỏ. Chỉ là placeholder cho imageview.

Tạo thư mục tên account bên trong thư mục lib sau đó tạo file mới tên create_profile_screen.dart bên trong thư mục account.

Cấu trúc thư mục lib cuối cùng của trông như sau: Tạo Ứng dụng Di động Full-Stack Serverless với Flutter, Serverless Framework & Upstash Redis – Phần 2 Để tạo người dùng mới, chúng ta cần:

  • URL ảnh hồ sơ
  • Tên
  • Họ
  • Tên người dùng (username)
  • Endpoint

Hãy xem mã:

static const String CREATE_USER_PROFILE_URL = "https://5vafvrk8kj.execute-api.us-east-1.amazonaws.com/dev/user";
bool _loading = false;

Future 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;
    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 là lớp cốt lõi của Dart để làm việc với các hoạt động bất đồng bộ. Một đối tượng Future đại diện cho một giá trị hoặc lỗi tiềm năng sẽ có sẵn vào một thời điểm nào đó trong tương lai.

Lớp http.Response chứa dữ liệu nhận được từ một cuộc gọi http thành công.

Mã trên sử dụng phương thức http post để gửi yêu cầu post đến endpoint tạo người dùng sau đó chờ phản hồi.

Nếu mã trạng thái phản hồi là 200, yêu cầu thành công, chúng ta lưu UserId vừa tạo trong shared preferences, sau đó chuyển đến màn hình chính (homescreen).

Link mã nguồn hoàn chỉnh cho màn hình này: Create Profile Screen.

Tạo Bài đăng

Một trong các endpoint cho phép người dùng tạo bài đăng. Màn hình trông như sau:

Tạo Ứng dụng Di động Full-Stack Serverless với Flutter, Serverless Framework & Upstash Redis – Phần 2

Để tạo bài đăng, người dùng cần:

  • userId
  • nội dung văn bản (text)
  • imageUrl

Lưu ý rằng, cho mục đích demo, chúng ta dùng imageUrl có sẵn. Trong ứng dụng thực tế, bạn phải cho phép người dùng chọn ảnh, tải lên server, lấy URL ảnh, rồi dùng để tạo bài đăng.

Phương thức CreatePost trông tương tự như phương thức CreateUser.

Future 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;
    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();
    }
  });
}

Liệt kê Tất cả Bài đăng

Màn hình chính của ứng dụng sẽ hiển thị danh sách tất cả bài đăng đã tạo.

Giống như thế này:

Tạo Ứng dụng Di động Full-Stack Serverless với Flutter, Serverless Framework & Upstash Redis – Phần 2 Để lấy tất cả bài đăng một cách dễ dàng, trước tiên chúng ta cần tạo một đối tượng Dart tùy chỉnh đại diện cho một bài đăng đơn lẻ.

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 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 toJson() {
    final Map data = new Map();
    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 json) {
    timestamp = json['timestamp'];
    userId = json['userId'];
    username = json['username'];
    firstName = json['firstName'];
    lastName = json['lastName'];
    profilePic = json['profilePic'];
  }

  Map toJson() {
    final Map data = new Map();
    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;
  }
}

Sau đó, chúng ta chuyển đổi http.Response thành đối tượng Dart tùy chỉnh đó.

List parsePosts(String responseBody) {
  final parsed = jsonDecode(responseBody).cast>();
  return parsed.map((json) => Post.fromJson(json)).toList();
}

Future> fetchPosts(http.Client client) async {
  final response = await client.get(Uri.parse(GET_POSTS));
  return compute(parsePosts, response.body);
}

Kiểu trả về cho phương thức fetchPosts là Future>.

Nếu bạn chạy hàm fetchPosts() trên thiết bị chậm, bạn có thể nhận thấy ứng dụng bị đơ một chút khi nó phân tích và chuyển đổi JSON. Đây là jank (giật lag), và bạn muốn loại bỏ nó.

Chúng ta loại bỏ jank bằng cách di chuyển việc phân tích và chuyển đổi sang nền sử dụng hàm compute.

compute(parsePosts, response.body);

Hàm compute() chạy các hàm tốn kém tài nguyên trong một isolate nền và trả về kết quả.

Trong file màn hình chính, chúng ta sẽ dùng widget FutureBuilder để lấy bất đồng bộ tất cả bài đăng dưới dạng danh sách từ cơ sở dữ liệu.

Chúng ta phải cung cấp hai tham số:

  • Future bạn muốn làm việc. Trong trường hợp này, future trả về từ hàm fetchPosts().

Một hàm builder cho Flutter biết render gì, tùy thuộc vào trạng thái của Future: đang tải, thành công, hoặc lỗi.

Lưu ý snapshot.hasData chỉ trả về true khi snapshot chứa giá trị dữ liệu khác null.

Vì fetchPosts chỉ có thể trả về giá trị khác null, hàm nên ném exception ngay cả khi phản hồi server là "404 Not Found". Ném exception sẽ đặt snapshot.hasError thành true, có thể dùng để hiển thị thông báo lỗi.

Ngoài ra, spinner tải sẽ được hiển thị.

Expanded(child: FutureBuilder>(
  future: _posts,
  builder: (context, snapshot) {
    if (snapshot.hasData) {
      List? 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}");
    }
    // Mặc định, hiển thị spinner tải.
    return Container(
      height: 40,
      width: 40,
      child: Center(child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.secondary))));
  },
))

Trong phương thức initState, chúng ta gọi fetchPosts:

late Future> _posts;
@override
void initState() {
  super.initState();
  _posts = fetchPosts(http.Client());
}

Lý do chúng ta gọi fetchPosts trong initState thay vì build method là vì Flutter gọi build() mỗi khi cần thay đổi bất cứ thứ gì trong view, và điều này xảy ra thường xuyên. Để cuộc gọi fetch trong build() sẽ làm ngập API bằng các cuộc gọi không cần thiết và làm chậm ứng dụng.

Tự do xem qua mã nguồn hoàn chỉnh.

Vẫn còn một vài endpoint cần tạo giao diện, nhưng một hướng dẫn tốt mà không có bài tập thì sao? 😂

Kết luận

Trong series bài viết này, chúng ta đã xem cách xây dựng serverless REST API với Upstash, đồng thời tiêu thụ chúng thông qua ứng dụng di động.

Tôi rất mong thấy những gì bạn xây dựng tiếp theo với Upstash, hoặc cách bạn cải tiến hướng dẫn này cho phù hợp với trường hợp sử dụng của bạn.

Nếu bạn thấy bài viết hữu ích, hãy chia sẻ trên mạng xã hội.

Có câu hỏi? Để lại bình luận.

Nếu bạn tìm thấy lỗi, bạn biết phải làm gì. Để lại bình luận và tôi sẽ xử lý ngay.

Happy Coding ✌🏿

Tham khảo

  • Upstash Docs
  • Redis
  • Flutter
  • Fetching data from the internet