Flutter帖子管理插件postor的使用

Flutter帖子管理插件postor的使用

使用 Postor 插件几乎与使用 http 包相同。但是,与每次请求都定义 URL 不同的是,Postor 允许我们设置默认的基本 URL,因此我们只需定义端点即可。

一种最佳实践是在某个地方初始化它的实例,无论是单例还是作为类的实例。

import 'dart:convert';

import 'package:postor/postor.dart';

class MyApi {
  // 注意:我并不推荐使用此 URL,请自行承担风险!
  static const String baseUrl = 'https://fakestoreapi.com';
  static const String usersEndpoint = '/users';

  // 这将创建一个带有默认超时时间为 10 秒和重试次数为 3 次的新 Postor 实例。
  final Postor postor = Postor(baseUrl);

  // 我们也可以用我们自己的请求策略初始化 Postor
  // final Postor postor = Postor(
  //    baseUrl,
  //    defaultTimeout: const Duration(seconds: 1),
  //    retryPolicy: const RetryOptions(maxAttempts: 10),
  // );

  Future<List<User>> getUsers() async {
    final response = await postor.get(usersEndpoint);
    if (response.statusCode != 200) {
      throw transformStatusCodeToException(
        statusCode: response.statusCode,
        responseBody: response.body,
      );
    }
    final rawUsersList = json.decode(response.body) as List;

    return rawUsersList.map((user) {
      return User.fromMap(user as Map<String, dynamic>);
    }).toList();
  }
}

class User {
  const User({
    required this.id,
    required this.username,
  });

  final int id;
  final String username;

  factory User.fromMap(Map<String, dynamic> map) {
    return User(
      id: map['id'],
      username: map['username'],
    );
  }
}

getUsers 方法的主体可以简化为:

// 使用 `get<T>(expectedStatusCode)` 扩展来解析响应体为 T(例如 List 或 Map)。
// 通过这样做,我们也不再需要检查状态码以根据状态码抛出异常。
// 由于扩展已经处理了这一点。
// 注意:默认情况下,[expectedStatusCode] 设置为 200。
// 因此,如果我们期望其他状态码,我们只需要设置它。
// 例如:
// final response = await postor.get(usersEndpoint).get<List>(201);
final response = await postor.get(usersEndpoint).get<List>();
return response.map((u) => User.fromMap(u as Map<String, dynamic>)).toList();

现在基于上述示例,我们可以向 MyApi 类添加另一个方法来取消由 getUsers 发起的请求。

  ...
  void cancelGetUsers() {
    // postor 将使用基本 URL + 端点 + 参数作为取消令牌
    postor.cancel(baseUrl + usersEndpoint);
    // 或者如果我们处于强分析模式下,
    // 它会建议我们使用字符串插值:
    // postor.cancel('$baseUrl$usersEndpoint');
  }
  ...

需要注意的是,我们一次只能执行一个特定的 URL 请求。因此,在再次请求相同的 URL 之前,请务必检查,否则它将抛出 AssertionError

如何检查 getUsers 是否已完成?

有许多可能的方法来实现这一点,但我们将讨论两种可能的解决方案。

示例解决方案 1
final List<User> users = [];
final MyApi myApi = MyApi();

void main() {
  requestUsers();
  requestUsers();
}

// 我们可以在 [requestUsers] 方法中稍后检查的布尔变量
bool isGetUsersRunning = false;

Future<void> requestUsers() async {
  if (isGetUsersRunning) return;
  isGetUsersRunning = true;
  final usersList = await myApi.getUsers();
  users.addAll(usersList);
}
示例解决方案 2
// 我们可以利用 CTManager
// 了解更多关于它,请访问:https://pub.dev/packages/ctmanager
import 'package:ctmanager/ctmanager.dart';

final List<User> users = [];
final MyApi myApi = MyApi();

void main() {
  requestUsers();
  requestUsers();
}

// 而不是创建另一个变量,我们只需要检查
// 是否在 CTManager 中存在获取用户的 URL。
Future<void> requestUsers() async {
  const getUsersURL = MyApi.baseUrl + MyApi.usersEndpoint;
  // 由于我们在 Postor 的 `ctManager` 参数中没有指定任何内容,我们的 Postor 在 MyApi 中将使用 CTManager.I 代替。
  if (CTManager.I.hasTokenOf(getUsersURL)) {
    return;
  }
  final usersList = await myApi.getUsers();
  users.addAll(usersList);
}

创建多部分请求

例如,上传图像等,可以使用 Postor 的 multiPart 方法。

import 'package:postor/postor.dart';

final postor = Postor('https://my-api.com');

void main() {
  postImageAndFields();
}

Future<void> postImageAndFields() async {
  final fields = {
    'name': 'my name',
    'age': '99',
  };
  final files = {
    'avatar': PFileFromPath('path_to_avatar_image', filename: 'avatar.png'),
    'avatar_small': PFileFromPath('path_to_avatar_small_image'),
    // 对于文件字节
    // 'avatar_bytes': PFileFromBytes(avatar_bytes),
  };
  final response = await postor.multiPart(
    '/upload',
    // 如果方法是 POST,则不需要指定方法,因为它是默认值。
    // method: 'POST',
    fields: fields,
    files: files,
  );
  print(response.statusCode);
  print(response.body);
}

要取消它,只需调用:

postor.cancel('https://my-api.com/upload');

或者使用 CTManager:

CTManager.I.cancel('https://my-api.com/upload');

注意:Postor 在隔离中处理文件处理,并且多部分请求和文件处理都是可取消的。

最后,有一个新特性:catchIt。这可能有助于减少具有 try-catch 的方法中的代码行数。

import 'package:postor/postor.dart';
import 'package:postor/error_handler.dart' as eh;

final Postor postor = Postor('my-api.com');

Future<void> getResponse() async {
  try {
    final response = await postor.get('/test').get<List>();
    throw SomeError();
    print('response: $response');
  } catch (error, stackTrace) {
    eh.catchIt(
      error: error,
      stackTrace: stackTrace,
      otherErrorMessage: 'Failed to get response from /test',
      onCatch: _onError,
    );
  }
}

void _onError(String errorMessage) {
  print(errorMessage);
}

在使用 catchIt 之前,不要忘记初始化错误消息处理器,例如在 main.dart 中:

void main() {
  initErrorMessages((Object error, StackTrace? stackTrace, String? otherErrorMessage) {
    if(error is TimeoutException) {
      return 'Operation timeout';
    } else {
      return otherErrorMessage ?? 'An unknown error occurred.';
    }
  });
}

或者可以使用 [defaultErrorMessageHandler]

void main() {
  eh.initErrorMessages(eh.defaultErrorMessageHandler);
}

示例 Demo

import 'dart:async';
import 'dart:convert' show json;

import 'package:postor/error_handler.dart';
import 'package:postor/postor.dart';
// 了解更多关于 CTManager 的信息,请访问:https://pub.dev/packages/ctmanager
import 'package:ctmanager/ctmanager.dart';

final List<User> users = [];
final MyApi myApi = MyApi();

void initErrorMessageHandler() {
  initErrorMessages(defaultErrorMessageHandler);
}

void main() {
  initErrorMessageHandler();
}

// 而不是创建另一个变量,我们只需要检查
// 是否在 CTManager 中存在获取用户的 URL。
Future<void> requestUsers() async {
  const getUsersURL = MyApi.baseUrl + MyApi.usersEndpoint;
  // 由于我们在 Postor 的 `ctManager` 参数中没有指定任何内容,我们的 Postor 在 MyApi 中将使用 CTManager.I 代替。
  if (CTManager.I.hasTokenOf(getUsersURL)) {
    print('getUsers already running!');
    return;
  }
  final usersList = await myApi.getUsers();
  users.addAll(usersList);
  // ignore: avoid_print
  print(users);
}

class MyApi {
  static const String baseUrl = 'https://your-fake-api.com';
  static const String usersEndpoint = '/users';

  // 这将创建一个带有默认超时时间为 10 秒和重试次数为 3 次的新 Postor 实例。
  final Postor postor = Postor(baseUrl);

  // 我们也可以用我们自己的请求策略初始化 Postor
  // final Postor postor = Postor(
  //    baseUrl,
  //    defaultTimeout: const Duration(seconds: 1),
  //    retryPolicy: const RetryOptions(maxAttempts: 10),
  // );

  Future<List<User>> getUsers() async {
    final response = await postor.get(usersEndpoint);
    if (response.statusCode != 200) {
      throw transformStatusCodeToException(
        statusCode: response.statusCode,
        responseBody: response.body,
      );
    }
    final rawUsersList = json.decode(response.body) as List;

    return rawUsersList.map((user) {
      return User.fromMap(user as Map<String, dynamic>);
    }).toList();

    // 或者
    //
    // final response = await postor.get(usersEndpoint).get<List>();
    // return response.map((u) => User.fromMap(u as Map<String, dynamic>)).toList();
  }

  void cancelGetUsers() {
    // postor 将使用基本 URL + 端点 + 参数作为取消令牌
    postor.cancel(baseUrl + usersEndpoint);
    // 或者如果我们处于强分析模式下,
    // 它会建议我们使用字符串插值:
    // postor.cancel('$baseUrl$usersEndpoint');
  }
}

class User {
  const User({
    required this.id,
    required this.username,
  });

  final int id;
  final String username;

  factory User.fromMap(Map<String, dynamic> map) {
    return User(
      id: map['id'] as int,
      username: map['username'] as String,
    );
  }

  [@override](/user/override)
  String toString() => 'User(id: $id, username: $username)';
}

更多关于Flutter帖子管理插件postor的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter帖子管理插件postor的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


虽然postor这个Flutter插件在官方的Flutter插件库中并没有明确的定义或文档,但我们可以基于其名称“postor”进行合理推测,假设它是一个用于帖子管理的插件。以下是一个假想的postor插件的使用示例,这个示例展示了如何在一个Flutter应用中管理帖子(创建、读取、更新和删除帖子)。

假设的postor插件接口

首先,我们假设postor插件提供了以下基本功能:

  • createPost(Post post): 创建一个新的帖子。
  • getPosts(): 获取所有帖子。
  • getPost(int postId): 根据帖子ID获取特定帖子。
  • updatePost(Post post): 更新一个帖子。
  • deletePost(int postId): 删除一个帖子。

示例代码

1. 定义帖子模型

class Post {
  int id;
  String title;
  String content;
  DateTime createdAt;

  Post({required this.title, required this.content, this.id = 0, this.createdAt = const DateTime.now()});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'title': title,
      'content': content,
      'createdAt': createdAt.toIso8601String(),
    };
  }

  factory Post.fromMap(Map<String, dynamic> map) {
    return Post(
      id: map['id'] as int,
      title: map['title'] as String,
      content: map['content'] as String,
      createdAt: DateTime.parse(map['createdAt'] as String),
    );
  }
}

2. 假设的postor插件使用

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:postor/postor.dart' as postor; // 假设的导入路径

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Post Management',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PostManagementScreen(),
    );
  }
}

class PostManagementScreen extends StatefulWidget {
  @override
  _PostManagementScreenState createState() => _PostManagementScreenState();
}

class _PostManagementScreenState extends State<PostManagementScreen> {
  List<Post> posts = [];
  TextEditingController titleController = TextEditingController();
  TextEditingController contentController = TextEditingController();

  @override
  void initState() {
    super.initState();
    loadPosts();
  }

  Future<void> loadPosts() async {
    List<dynamic> result = await postor.getPosts();
    List<Post> loadedPosts = result.map((e) => Post.fromMap(e)).toList();
    setState(() {
      posts = loadedPosts;
    });
  }

  Future<void> createPost() async {
    Post newPost = Post(title: titleController.text, content: contentController.text);
    await postor.createPost(newPost);
    loadPosts();
    titleController.clear();
    contentController.clear();
  }

  Future<void> updatePost(Post post) async {
    await postor.updatePost(post);
    loadPosts();
  }

  Future<void> deletePost(int postId) async {
    await postor.deletePost(postId);
    loadPosts();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Post Management'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            TextField(
              controller: titleController,
              decoration: InputDecoration(labelText: 'Title'),
            ),
            TextField(
              controller: contentController,
              maxLines: 5,
              decoration: InputDecoration(labelText: 'Content'),
            ),
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: createPost,
              child: Text('Create Post'),
            ),
            SizedBox(height: 16),
            Expanded(
              child: ListView.builder(
                itemCount: posts.length,
                itemBuilder: (context, index) {
                  Post post = posts[index];
                  return Card(
                    child: ListTile(
                      title: Text(post.title),
                      subtitle: Text(post.content.substring(0, 50) + '...'),
                      trailing: Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          IconButton(
                            icon: Icon(Icons.edit),
                            onPressed: () async {
                              String? newTitle = await showDialog<String>(
                                context: context,
                                builder: (context) => AlertDialog(
                                  title: Text('Edit Title'),
                                  content: TextField(
                                    controller: TextEditingController(text: post.title),
                                  ),
                                  actions: [
                                    TextButton(
                                      onPressed: (context) => Navigator.pop(context, null),
                                      child: Text('Cancel'),
                                    ),
                                    TextButton(
                                      onPressed: (context) {
                                        String title = (context as BuildContext).findAncestorStateOfType<_AlertDialogState>()!.controller.text;
                                        Post updatedPost = Post(
                                          id: post.id,
                                          title: title,
                                          content: post.content,
                                          createdAt: post.createdAt,
                                        );
                                        updatePost(updatedPost);
                                        Navigator.pop(context, title);
                                      },
                                      child: Text('OK'),
                                    ),
                                  ],
                                ),
                              );
                              if (newTitle != null) {
                                post = Post(
                                  id: post.id,
                                  title: newTitle,
                                  content: post.content,
                                  createdAt: post.createdAt,
                                );
                                setState(() {}); // 更新UI以显示新标题(尽管在这个例子中,updatePost已经触发了重新加载)
                              }
                            },
                          ),
                          IconButton(
                            icon: Icon(Icons.delete),
                            onPressed: () async {
                              await showDialog<void>(
                                context: context,
                                builder: (BuildContext context) {
                                  return AlertDialog(
                                    title: Text('Delete Post'),
                                    content: Text('Are you sure you want to delete this post?'),
                                    actions: <Widget>[
                                      TextButton(
                                        onPressed: () => Navigator.pop(context, null),
                                        child: Text('Cancel'),
                                      ),
                                      TextButton(
                                        onPressed: () {
                                          deletePost(post.id);
                                          Navigator.pop(context);
                                        },
                                        child: Text('Delete'),
                                      ),
                                    ],
                                  );
                                },
                              );
                            },
                          ),
                        ],
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }
}

注意事项

  1. 插件假设:上述代码基于假设的postor插件接口。实际使用时,你需要根据postor插件的实际API进行调整。
  2. 错误处理:示例代码中没有包含错误处理逻辑。在实际应用中,你应该添加适当的错误处理来确保应用的健壮性。
  3. UI设计:示例中的UI设计非常基础,仅用于演示功能。在实际应用中,你可能需要更复杂的UI设计和交互逻辑。
  4. 数据持久化:示例代码没有涉及数据的持久化存储。在实际应用中,你可能需要考虑将帖子数据存储在本地数据库或远程服务器上。

希望这个示例能帮助你理解如何使用一个假设的postor插件进行帖子管理。如果你有更具体的需求或问题,请提供更多细节以便给出更准确的帮助。

回到顶部