Flutter插件knock_flutter的使用_knock_flutter 是一个客户端库,用于与用户界面相关的 Knock 功能进行交互,例如信息流(feeds)。

Flutter插件knock_flutter的使用_knock_flutter 是一个客户端库,用于与用户界面相关的 Knock 功能进行交互,例如信息流(feeds)。

文档

knock_flutter 是一个客户端库,用于与用户界面相关的 Knock 功能进行交互,例如信息流(feeds)。

注意: 这是一个底层库,设计用于构建基于其上的用户界面。

iOS

要通过 APNS(Apple Push Notification Service)接收推送通知,您需要在应用中启用推送通知功能。您可以在这里找到具体说明: Apple 官方文档

示例应用

要在示例应用中测试 APNS 集成,您需要在 Xcode 中打开 <code>example/ios</code> 并选择您的开发团队和唯一的 Bundle Identifier。

Android

要通过 FCM(Firebase Cloud Messaging)接收推送通知,您需要向您的应用添加 Firebase 和相关依赖项。您可以在这里找到具体说明: Firebase 安装指南

示例应用

要在示例应用中测试 FCM 集成,您需要创建一个 Firebase 项目,并将适当的 google-services.json 文件添加到 <code>example/app/google-services.json</code>

包开发

代码生成

代码生成仅限于支持 API 消息的 JSON 序列化/反序列化。如果您需要调整生成的代码,可以运行以下命令:

flutter pub run build_runner build

如果需要更新生成的平台通道消息,可以运行以下命令:

flutter pub run pigeon --input pigeons/messages.dart

注意: 生成的文件被提交到版本控制系统中,因为它们是发布的包的一部分。

发布(内部)

手动更新 changelog.md 文件。 更新 pubspec.yaml 中的发布版本。 创建 PR。 在您的 PR 合并后,运行 /release status knock-flutter 在 Slack 中以启动发布流程。

运行示例应用

在终端中打开命令面板。

转到查看 > 命令面板或按 Command + Shift + P

键入 flutter

选择 Flutter: Select Device

如果没有设备正在运行,此命令会提示您启用设备。

Select Device 提示符中选择目标设备。

选择目标后,启动应用。转到运行 > 开始调试或按 F5

等待应用启动。

您可以在调试控制台视图中查看启动进度。

当应用构建完成后,您的设备上会显示您的应用。

示例代码

以下是使用 knock_flutter 的示例代码:

import 'dart:async';
import 'dart:developer' as developer;

import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:knock_flutter/knock_flutter.dart';

// Knock: 示例用户和信息流数据
const _exampleUserId = '1';
const _exampleUserToken = null;
const _exampleFeedChannelId = '495a74d0-3ac1-43f6-9906-344f9e7d94d9';
const _exampleApnsChannelId = 'c5c4fd65-20de-4ab5-bcda-8f8d077f528e';
const _exampleFcmChannelId = '54268be3-1d12-416a-81a5-3dc7681f2408';

void main() => runApp(const _ExampleKnockApp());

class _ExampleKnockApp extends StatelessWidget {
  const _ExampleKnockApp();

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Example Knock App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const _KnockPage(),
    );
  }
}

class _KnockPage extends StatefulWidget {
  const _KnockPage();

  [@override](/user/override)
  State<_KnockPage> createState() => _KnockPageState();
}

class _KnockPageState extends State<_KnockPage> {
  late Knock knock;

  int _selectedTabIndex = 0;

  [@override](/user/override)
  void initState() {
    super.initState();

    /// Knock: 我们建议不要在应用程序中硬编码 API 密钥。相反,我们建议您在运行或构建应用程序时将其作为环境变量提供:
    ///  1) `--dart-define=KNOCK_API_KEY=your_knock_public_api_key_value`
    ///  2) `--dart-define-from-file="config.json"`
    ///     其中 `config.json` 添加到 `.gitignore` 中并且看起来像:
    ///     ```
    ///     {
    ///        "KNOCK_API_KEY": "your_knock_public_api_key_value"
    ///     }
    ///     ```
    ///
    /// 有关更多详细信息,请参阅:
    /// https://dart.dev/guides/environment-declarations#flutter 和
    /// https://codewithandrea.com/articles/flutter-api-keys-dart-define-env-files/#new-in-flutter-37-use---dart-define-from-file
    knock = Knock(const String.fromEnvironment("KNOCK_API_KEY"));
    knock.authenticate(_exampleUserId, _exampleUserToken);
  }

  [@override](/user/override)
  void dispose() {
    // Knock: 确保在不再使用时释放 Knock 实例!
    knock.dispose();
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    final tabPages = [
      _FeedWidget(
        knock: knock,
        feedChannelId: _exampleFeedChannelId,
      ),
      _PreferencesWidget(
        knock: knock,
      ),
      _UserWidget(
        knock: knock,
      ),
      _ChannelWidget(
        knock: knock,
      ),
      _NotificationsWidget(
        knock: knock,
      ),
    ];

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Example Knock App'),
        actions: [
          IconButton(
            tooltip: "Logout",
            onPressed: () => knock.logout(),
            icon: const Icon(Icons.logout),
          ),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.list),
            label: 'Feed',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'Preferences',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.person),
            label: 'User',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.notifications),
            label: 'Channel',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.notifications),
            label: 'Notifications',
          ),
        ],
        currentIndex: _selectedTabIndex,
        onTap: (index) => setState(() => _selectedTabIndex = index),
      ),
      body: tabPages[_selectedTabIndex],
    );
  }
}

class _UserWidget extends StatefulWidget {
  final Knock knock;

  const _UserWidget({required this.knock});

  [@override](/user/override)
  State<_UserWidget> createState() => _UserWidgetState();
}

class _UserWidgetState extends State<_UserWidget> {
  User? _user;

  final _nameController = TextEditingController();

  [@override](/user/override)
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      // Knock: 获取当前用户
      final user = await widget.knock.user().get();
      setState(() => _user = user);
      _nameController.text = user.name ?? '';
    });
  }

  [@override](/user/override)
  void dispose() {
    super.dispose();
    _nameController.dispose();
  }

  void _onChangeName() async {
    String name = _nameController.text;

    // Knock: 更新定义和自定义用户属性
    final user = await widget.knock.user().identify(
      name: name,
      properties: {
        'timeZoneName': DateTime.now().timeZoneName,
      },
    );
    setState(() => _user = user);
    _nameController.text = user.name ?? '';
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _nameController,
                ),
              ),
              const SizedBox(width: 16),
              OutlinedButton(
                onPressed: _onChangeName,
                child: const Text('Change Name'),
              ),
            ],
          ),
          const SizedBox(height: 16),
          Text(_user?.toString() ?? 'null'),
        ],
      ),
    );
  }
}

class _NotificationsWidget extends StatefulWidget {
  const _NotificationsWidget({required this.knock});

  final Knock knock;

  [@override](/user/override)
  State<_NotificationsWidget> createState() => _NotificationsWidgetState();
}

class _NotificationsWidgetState extends State<_NotificationsWidget> {
  String? _fcmToken;
  Object? _fcmError;
  ChannelData? _fcmChannelData;

  String? _apnsToken;
  Object? _apnsError;
  ChannelData? _apnsChannelData;

  [@override](/user/override)
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      try {
        final channelData =
            await widget.knock.user().getChannelData(_exampleFcmChannelId);
        setState(() {
          _fcmChannelData = channelData;
        });
      } catch (error) {
        setState(() {
          _fcmChannelData = null;
        });
      }
    });

    WidgetsBinding.instance.addPostFrameCallback((_) async {
      try {
        final channelData =
            await widget.knock.user().getChannelData(_exampleApnsChannelId);
        setState(() {
          _apnsChannelData = channelData;
        });
      } catch (error) {
        setState(() {
          _apnsChannelData = null;
        });
      }
    });
  }

  Future<void> _getFcmToken() async {
    try {
      final token = await widget.knock.getFcmToken();
      setState(() {
        _fcmToken = token;
        _fcmError = null;
      });
    } catch (error) {
      setState(() {
        _fcmToken = null;
        _fcmError = error;
      });
    }
  }

  Future<void> _registerFcmToken() async {
    final token = _fcmToken;
    if (token != null) {
      final channelData = await widget.knock
          .user()
          .registerTokenForChannel(_exampleFcmChannelId, token);
      setState(() {
        _fcmChannelData = channelData;
      });
    }
  }

  Future<void> _deregisterFcmToken() async {
    final token = _fcmToken;
    if (token != null) {
      final channelData = await widget.knock
          .user()
          .deregisterTokenForChannel(_exampleFcmChannelId, token);
      setState(() {
        _fcmChannelData = channelData;
      });
    }
  }

  Future<void> _getApnsToken() async {
    try {
      final token = await widget.knock.getApnsToken();
      setState(() {
        _apnsToken = token;
        _apnsError = null;
      });
    } catch (error) {
      setState(() {
        _apnsToken = null;
        _apnsError = error;
      });
    }
  }

  Future<void> _registerApnsToken() async {
    final token = _apnsToken;
    if (token != null) {
      final channelData = await widget.knock
          .user()
          .registerTokenForChannel(_exampleApnsChannelId, token);
      setState(() {
        _apnsChannelData = channelData;
      });
    }
  }

  Future<void> _deregisterApnsToken() async {
    final token = _apnsToken;
    if (token != null) {
      final channelData = await widget.knock
          .user()
          .deregisterTokenForChannel(_exampleApnsChannelId, token);
      setState(() {
        _apnsChannelData = channelData;
      });
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Text('FCM channel data: $_fcmChannelData'),
          Text('FCM token: ${_fcmToken ?? _fcmError}'),
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Row(
              children: [
                OutlinedButton(
                  onPressed: _getFcmToken,
                  child: const Text('Get FCM Token'),
                ),
                const SizedBox(width: 8),
                OutlinedButton(
                  onPressed: _registerFcmToken,
                  child: const Text('Register FCM Token'),
                ),
                const SizedBox(width: 8),
                OutlinedButton(
                  onPressed: _deregisterFcmToken,
                  child: const Text('Deregister FCM Token'),
                ),
              ],
            ),
          ),
          const SizedBox(height: 16),
          Text('APNS channel data: $_apnsChannelData'),
          Text('APNS token: ${_apnsToken ?? _apnsError}'),
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Row(
              children: [
                OutlinedButton(
                  onPressed: _getApnsToken,
                  child: const Text('Get APNS Token'),
                ),
                const SizedBox(width: 8),
                OutlinedButton(
                  onPressed: _registerApnsToken,
                  child: const Text('Register APNS Token'),
                ),
                const SizedBox(width: 8),
                OutlinedButton(
                  onPressed: _deregisterApnsToken,
                  child: const Text('Deregister APNS Token'),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class _ChannelWidget extends StatefulWidget {
  final Knock knock;

  const _ChannelWidget({required this.knock});

  [@override](/user/override)
  State<_ChannelWidget> createState() => _ChannelWidgetState();
}

class _ChannelWidgetState extends State<_ChannelWidget> {
  ChannelData? _channelData;
  Object? _error;

  [@override](/user/override)
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      _handleChannelData(() {
        // Knock: 获取频道数据
        return widget.knock.user().getChannelData(_exampleApnsChannelId);
      });
    });
  }

  void _onReplaceChannelData() async {
    _handleChannelData(() {
      // Knock: 设置现有的频道数据
      return widget.knock.user().setChannelData(
          _exampleApnsChannelId,
          ChannelData.forTokens([
            'test-token-${DateTime.now().toIso8601String()}',
          ]));
    });
  }

  void _onAppendChannelData() async {
    _handleChannelData(() {
      // Knock: 将新令牌追加到现有频道数据
      return widget.knock.user().setChannelData(
          _exampleApnsChannelId,
          ChannelData.forTokens([
            ..._channelData?.data.tokens ?? [],
            'test-token-${DateTime.now().toIso8601String()}',
          ]));
    });
  }

  void _handleChannelData(Future<ChannelData> Function() operation) async {
    try {
      final channelData = await operation();
      setState(() {
        _channelData = channelData;
        _error = null;
      });
    } catch (error) {
      setState(() {
        _channelData = null;
        _error = error;
      });
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        mainAxisSize: MainAxisSize.min,
        children: [
          OutlinedButton(
            onPressed: _onReplaceChannelData,
            child: const Text('Replace Channel Data'),
          ),
          const SizedBox(height: 16),
          OutlinedButton(
            onPressed: _onAppendChannelData,
            child: const Text('Append Channel Data'),
          ),
          const SizedBox(height: 16),
          if (_channelData != null) ...[
            Text(_channelData.toString()),
          ],
          if (_error != null) ...[
            Text(_error.toString()),
          ],
        ],
      ),
    );
  }
}

class _PreferencesWidget extends StatefulWidget {
  final Knock knock;

  const _PreferencesWidget({required this.knock});

  [@override](/user/override)
  State<_PreferencesWidget> createState() => _PreferencesWidgetState();
}

class _PreferencesWidgetState extends State<_PreferencesWidget> {
  PreferenceSet? _preference;

  void _setPreferences() async {
    final PreferenceSet preferences = await widget.knock.preferences().set(
          SetPreferencesProperties(
            channelTypes: {
              // Knock: 您可以开启或关闭频道类型
              ChannelType.email: ChannelTypePreference(value: false),
              // Knock: 或者您可以为频道配置条件
              ChannelType.push: ChannelTypePreference(conditions: [
                const PreferenceCondition(
                  variable: 'recipient.muted_dinos',
                  operator: 'not_contains',
                  argument: 'data.dino',
                )
              ])
            },
            categories: {
              // Knock: 您可以开启或关闭类别
              'dinosaur-proximity': WorkflowPreferenceSetting(value: true),
              // Knock: 您还可以为每个频道或整个类别的条件进行配置
              'velociraptor-enclosure-alert': WorkflowPreferenceSetting(
                channelTypePreferences: {
                  ChannelType.inAppFeed: ChannelTypePreference(value: false),
                  ChannelType.push: ChannelTypePreference(conditions: [
                    const PreferenceCondition(
                      variable: 'recipient.muted_dinos',
                      operator: 'not_contains',
                      argument: 'data.dino',
                    )
                  ])
                },
                conditions: [
                  const PreferenceCondition(
                    variable: 'recipient.muted_dinos',
                    operator: 'not_contains',
                    argument: 'data.dino',
                  )
                ],
              )
            },
            workflows: {
              // Knock: 您可以开启或关闭工作流
              'unix-servers': WorkflowPreferenceSetting(value: true),
              // Knock: 您还可以为每个工作流或整个工作流的条件进行配置
              'disable-park-security': WorkflowPreferenceSetting(
                channelTypePreferences: {
                  ChannelType.inAppFeed: ChannelTypePreference(value: false),
                  ChannelType.push: ChannelTypePreference(conditions: [
                    const PreferenceCondition(
                      variable: 'recipient.muted_dinos',
                      operator: 'not_contains',
                      argument: 'data.dino',
                    )
                  ])
                },
                conditions: [
                  const PreferenceCondition(
                    variable: 'recipient.muted_dinos',
                    operator: 'not_contains',
                    argument: 'data.dino',
                  )
                ],
              ),
            },
          ),
        );
    developer.log(preferences.toString());
  }

  [@override](/user/override)
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      // Knock: 获取所有用户的默认偏好设置。
      final preference = await widget.knock.preferences().get();
      setState(() => _preference = preference);
    });
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    final preference = _preference;
    if (preference == null) {
      return Container();
    }

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      child: Column(
        children: [
          OutlinedButton(
            onPressed: _setPreferences,
            child: const Text('Set Preferences'),
          ),
          const SizedBox(height: 8.0),
          Text(preference.toString()),
        ],
      ),
    );
  }
}

class _FeedWidget extends StatefulWidget {
  const _FeedWidget({
    required this.knock,
    required this.feedChannelId,
  });

  final Knock knock;
  final String feedChannelId;

  [@override](/user/override)
  State<_FeedWidget> createState() => _FeedWidgetState();
}

class _FeedWidgetState extends State<_FeedWidget> {
  late final FeedClient _feedClient;
  StreamSubscription? _subscription;

  final _scrollController = ScrollController();

  [@override](/user/override)
  void initState() {
    super.initState();
    _feedClient = widget.knock.feed(
      widget.feedChannelId,
      options: const FeedOptions(
        pageSize: 5,
      ),
    );

    // Knock: 您可以监听特定事件的流。
    _subscription =
        _feedClient.on(BindableFeedEvent.allItemsEvents).listen((event) {
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(
        content: Text('${event.eventType}: ${event.items.length}'),
        duration: const Duration(seconds: 1),
      ));
    });

    _scrollController.addListener(() {
      final position = _scrollController.position;
      if (position.pixels == position.maxScrollExtent) {
        // Knock: 加载下一页的信息流项,在这种情况下是在无限列表中
        _feedClient.fetchNextPage();
      }
    });
  }

  [@override](/user/override)
  void dispose() {
    super.dispose();
    // Knock: 确保取消您正在监听的所有事件流!
    _subscription?.cancel();
    _scrollController.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    // Knock: 使用 StreamBuilder 呈现信息流非常简单。
    return StreamBuilder<Feed>(
      stream: _feedClient.feed,
      builder: (context, snapshot) {
        if (!snapshot.hasData) {
          return Container();
        }

        final feed = snapshot.requireData;
        final items = feed.items;
        return Column(
          children: [
            _HeaderWidget(
              children: [
                _FeedMetadataWidget(feed: feed),
                _FeedActionsWidget(
                  feed: feed,
                  // Knock: 支持对信息流项的批量操作。
                  onAllSeen: () => _feedClient.markAllAsSeen(),
                  onAllRead: () => _feedClient.markAllAsRead(),
                  onAllArchived: () => _feedClient.markAllAsArchived(),
                ),
              ],
            ),
            Expanded(
              child: ListView.separated(
                controller: _scrollController,
                padding: const EdgeInsets.symmetric(vertical: 8.0),
                separatorBuilder: (_, __) => const Divider(),
                itemCount: items.length,
                itemBuilder: (_, index) {
                  final item = items[index];

                  return Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 8.0),
                    child: _FeedItemWidget(
                      item: item,
                      // Knock: 支持对信息流项的单个操作。
                      onSeen: () => _feedClient.markAsSeen([item]),
                      onUnseen: () => _feedClient.markAsUnseen([item]),
                      onRead: () => _feedClient.markAsRead([item]),
                      onUnread: () => _feedClient.markAsUnread([item]),
                      onArchived: () => _feedClient.markAsArchived([item]),
                      onUnarchived: () => _feedClient.markAsUnarchived([item]),
                    ),
                  );
                },
              ),
            ),
          ],
        );
      },
    );
  }
}

class _FeedMetadataWidget extends StatelessWidget {
  final Feed feed;

  const _FeedMetadataWidget({required this.feed});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(
        children: [
          Text(
            'Total: ${feed.metadata.totalCount}',
            textAlign: TextAlign.start,
          ),
          const SizedBox(width: 8),
          Text(
            'Unread: ${feed.metadata.unreadCount}',
            textAlign: TextAlign.center,
          ),
          const SizedBox(width: 8),
          Text(
            'Unseen: ${feed.metadata.unseenCount}',
            textAlign: TextAlign.end,
          ),
          if (feed.requestInFlight)
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 16.0),
              child: SizedBox(
                height: 16.0,
                width: 16.0,
                child: CircularProgressIndicator(),
              ),
            ),
        ],
      ),
    );
  }
}

class _FeedActionsWidget extends StatelessWidget {
  final Feed feed;
  final VoidCallback onAllSeen;
  final VoidCallback onAllRead;
  final VoidCallback onAllArchived;

  const _FeedActionsWidget({
    required this.feed,
    required this.onAllSeen,
    required this.onAllRead,
    required this.onAllArchived,
  });

  [@override](/user/override)
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: Row(
        children: [
          const SizedBox(width: 8),
          OutlinedButton(
            onPressed: onAllSeen,
            child: const Text('Mark All Seen'),
          ),
          const SizedBox(width: 8),
          OutlinedButton(
            onPressed: onAllRead,
            child: const Text('Mark All Read'),
          ),
          const SizedBox(width: 8),
          OutlinedButton(
            onPressed: onAllArchived,
            child: const Text('Mark All Archived'),
          ),
          const SizedBox(width: 8),
        ],
      ),
    );
  }
}

class _FeedItemWidget extends StatelessWidget {
  final FeedItem item;
  final VoidCallback onSeen;
  final VoidCallback onUnseen;
  final VoidCallback onRead;
  final VoidCallback onUnread;
  final VoidCallback onArchived;
  final VoidCallback onUnarchived;

  const _FeedItemWidget({
    required this.item,
    required this.onSeen,
    required this.onUnseen,
    required this.onRead,
    required this.onUnread,
    required this.onArchived,
    required this.onUnarchived,
  });

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.min,
      children: [
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('ID: ${item.id}'),
            for (var activity in item.activities) ...[
              activity.recipient.when(
                user: (user) => Text(
                  'User Recipient: ${user.id} / ${user.email} / ${user.name}',
                ),
                knockObject: (object) => Text(
                  'Knock Object Recipient: ${activity.recipient.toString()}',
                ),
              ),
              if (activity.actor != null)
                activity.actor!.when(
                  user: (user) => Text(
                    'User Actor: ${user.id} / ${user.email} / ${user.name}',
                  ),
                  knockObject: (object) => Text(
                    'Knock Object Actor: ${activity.recipient.toString()}',
                  ),
                ),
            ],
            Text('Inserted at: ${item.insertedAt}'),
            for (var block in item.blocks)
              block.when(
                markdown: (name, content, rendered) => Html(
                  data: rendered,
                  style: {
                    'body': Style(margin: Margins.zero),
                    'p': Style(padding: HtmlPaddings.zero, margin: Margins.zero)
                  },
                ),
                text: (name, content, rendered) => Html(
                  data: content,
                  style: {
                    'body': Style(margin: Margins.zero),
                    'p': Style(padding: HtmlPaddings.zero, margin: Margins.zero)
                  },
                ),
                buttonSet: (name, buttons) => Row(
                  children: [
                    for (var button in buttons)
                      OutlinedButton(
                        onPressed: null,
                        child: Text(button.label),
                      ),
                  ],
                ),
              )
          ],
        ),
        Row(
          children: [
            if (item.seenAt == null)
              OutlinedButton(
                onPressed: onSeen,
                child: const Text('Seen'),
              ),
            if (item.seenAt != null)
              OutlinedButton(
                onPressed: onUnseen,
                child: const Text('Unseen'),
              ),
            const SizedBox(width: 8.0),
            if (item.readAt == null)
              OutlinedButton(
                onPressed: onRead,
                child: const Text('Read'),
              ),
            if (item.readAt != null)
              OutlinedButton(
                onPressed: onUnread,
                child: const Text('Unread'),
              ),
            const SizedBox(width: 8.0),
            if (item.archivedAt == null)
              OutlinedButton(
                onPressed: onArchived,
                child: const Text('Archive'),
              ),
            if (item.archivedAt != null)
              OutlinedButton(
                onPressed: onUnarchived,
                child: const Text('Unarchive'),
              ),
          ],
        )
      ],
    );
  }
}

class _HeaderWidget extends StatelessWidget {
  final List<Widget> children;

  const _HeaderWidget({required this.children});

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Container(
      color: const Color(0xFFC0C0C0),
      child: Column(
        children: [
          ...children,
          const SizedBox(height: 8),
        ],
      ),
    );
  }
}

更多关于Flutter插件knock_flutter的使用_knock_flutter 是一个客户端库,用于与用户界面相关的 Knock 功能进行交互,例如信息流(feeds)。的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter插件knock_flutter的使用_knock_flutter 是一个客户端库,用于与用户界面相关的 Knock 功能进行交互,例如信息流(feeds)。的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在探讨knock_flutter这个假设的Flutter插件时,虽然我们不能确切知道它的具体实现细节(因为这不是一个真实存在的、广为人知的插件,至少在我最后的知识更新中是这样),但我们可以基于插件名称“knock”来推测一些潜在用途,并给出一些假设性的代码案例来展示可能的实现方式。

潜在用途推测

  1. 敲门/敲击检测:插件可能用于检测用户对设备的物理敲击动作,这在一些需要手势控制的应用中可能很有用,比如解锁屏幕、触发特定动作等。

  2. 声音/震动反馈:与敲击检测相结合,插件可能提供声音或震动反馈机制,以增强用户体验。

  3. 安全性增强:通过敲击模式识别,插件可以用于增加应用的安全性,比如设置特定的敲击序列来解锁敏感功能。

假设性代码案例

1. 敲击检测基础实现

假设knock_flutter插件提供了一个简单的敲击检测功能,我们可以通过以下方式使用它:

import 'package:flutter/material.dart';
import 'package:knock_flutter/knock_flutter.dart'; // 假设的包导入

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool isKnocked = false;

  @override
  void initState() {
    super.initState();
    // 假设的敲击监听初始化
    KnockFlutter.listenForKnocks().listen((knockEvent) {
      setState(() {
        isKnocked = true;
        // 这里可以添加其他逻辑,比如解锁屏幕、触发动作等
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Knock Flutter Demo'),
        ),
        body: Center(
          child: Text(
            isKnocked ? 'Device was knocked!' : 'Waiting for knock...',
            style: TextStyle(fontSize: 24),
          ),
        ),
      ),
    );
  }
}

2. 敲击模式识别与安全性增强

假设knock_flutter插件支持敲击模式识别,我们可以实现一个简单的敲击序列解锁功能:

import 'package:flutter/material.dart';
import 'package:knock_flutter/knock_flutter.dart'; // 假设的包导入

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  List<int> unlockSequence = [1, 2, 3]; // 假设的解锁敲击序列
  List<int> currentSequence = [];
  bool isUnlocked = false;

  @override
  void initState() {
    super.initState();
    KnockFlutter.listenForKnocks(enablePatternRecognition: true).listen((knockEvent) {
      currentSequence.add(knockEvent.knockIndex); // 假设knockEvent包含敲击索引
      print('Current sequence: $currentSequence');

      if (currentSequence.length == unlockSequence.length) {
        if (listEquals(currentSequence, unlockSequence)) {
          setState(() {
            isUnlocked = true;
            currentSequence.clear(); // 解锁后清空序列
          });
          // 这里可以添加解锁后的逻辑,比如显示敏感信息
        } else {
          currentSequence.clear(); // 序列不匹配,清空序列
        }
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Knock Pattern Unlock Demo'),
        ),
        body: Center(
          child: Text(
            isUnlocked ? 'Unlocked!' : 'Waiting for unlock sequence...',
            style: TextStyle(fontSize: 24),
          ),
        ),
      ),
    );
  }
}

bool listEquals(List<int> a, List<int> b) {
  if (a.length != b.length) return false;
  for (int i = 0; i < a.length; i++) {
    if (a[i] != b[i]) return false;
  }
  return true;
}

请注意,以上代码是基于假设的插件功能和API设计的,实际使用时需要根据插件的真实API文档进行调整。如果knock_flutter是一个真实存在的插件,请参考其官方文档以获取准确的用法和API信息。

回到顶部