This commit is contained in:
2026-06-04 10:50:24 +08:00
parent 8f9f3a9779
commit 250f21a2b8
67 changed files with 1192 additions and 159 deletions

View File

@@ -13,22 +13,22 @@ A production-ready Flutter quick-start template extracted from real-world projec
## Tech Stack ## Tech Stack
| Category | Package | Purpose | | Category | Package | Purpose |
|---|---|---| | ----------------- | -------------------- | --------------------------------------- |
| State Management | flutter_riverpod | Compile-safe, testable state management | | State Management | flutter_riverpod | Compile-safe, testable state management |
| Networking | dio | HTTP client with interceptor chain | | Networking | dio | HTTP client with interceptor chain |
| Local Cache | shared_preferences | Key-value persistence | | Local Cache | shared_preferences | Key-value persistence |
| Network Monitor | connectivity_plus | Real-time connectivity tracking | | Network Monitor | connectivity_plus | Real-time connectivity tracking |
| Permissions | permission_handler | Runtime permission requests | | Permissions | permission_handler | Runtime permission requests |
| Screen Adaptation | flutter_screenutil | Design-dimension-based layout | | Screen Adaptation | flutter_screenutil | Design-dimension-based layout |
| Image Loading | cached_network_image | Network image caching | | Image Loading | cached_network_image | Network image caching |
| SVG | flutter_svg | SVG rendering | | SVG | flutter_svg | SVG rendering |
| Pull to Refresh | pull_to_refresh | Refresh and load-more | | Pull to Refresh | pull_to_refresh | Refresh and load-more |
| Loading HUD | flutter_easyloading | Toast and loading indicator | | Loading HUD | flutter_easyloading | Toast and loading indicator |
| Device Info | device_info_plus | Device metadata | | Device Info | device_info_plus | Device metadata |
| Package Info | package_info_plus | App version info | | Package Info | package_info_plus | App version info |
| URL Launcher | url_launcher | Open external URLs | | URL Launcher | url_launcher | Open external URLs |
| Linting | flutter_lints | Recommended Dart lint rules | | Linting | flutter_lints | Recommended Dart lint rules |
## Directory Structure ## Directory Structure
@@ -72,7 +72,7 @@ lib/
## Getting Started ## Getting Started
```bash ```bash
cd flutter-template cd record-tool
flutter pub get flutter pub get
flutter analyze flutter analyze
flutter test flutter test

View File

@@ -13,22 +13,22 @@
## 技术栈 ## 技术栈
| 类别 | 依赖 | 用途 | | 类别 | 依赖 | 用途 |
|---|---|---| | -------- | -------------------- | -------------------------- |
| 状态管理 | flutter_riverpod | 编译安全、可测试的状态管理 | | 状态管理 | flutter_riverpod | 编译安全、可测试的状态管理 |
| 网络请求 | dio | HTTP 客户端,支持拦截器链 | | 网络请求 | dio | HTTP 客户端,支持拦截器链 |
| 本地缓存 | shared_preferences | KV 持久化存储 | | 本地缓存 | shared_preferences | KV 持久化存储 |
| 网络监听 | connectivity_plus | 实时网络状态监测 | | 网络监听 | connectivity_plus | 实时网络状态监测 |
| 权限申请 | permission_handler | 运行时权限请求 | | 权限申请 | permission_handler | 运行时权限请求 |
| 屏幕适配 | flutter_screenutil | 设计稿尺寸适配 | | 屏幕适配 | flutter_screenutil | 设计稿尺寸适配 |
| 图片加载 | cached_network_image | 网络图片缓存 | | 图片加载 | cached_network_image | 网络图片缓存 |
| SVG | flutter_svg | SVG 渲染 | | SVG | flutter_svg | SVG 渲染 |
| 下拉刷新 | pull_to_refresh | 下拉刷新 / 上拉加载 | | 下拉刷新 | pull_to_refresh | 下拉刷新 / 上拉加载 |
| 加载提示 | flutter_easyloading | Toast 和 loading | | 加载提示 | flutter_easyloading | Toast 和 loading |
| 设备信息 | device_info_plus | 设备元数据 | | 设备信息 | device_info_plus | 设备元数据 |
| 应用信息 | package_info_plus | 版本号等应用信息 | | 应用信息 | package_info_plus | 版本号等应用信息 |
| 链接跳转 | url_launcher | 外部 URL 打开 | | 链接跳转 | url_launcher | 外部 URL 打开 |
| 代码规范 | flutter_lints | Dart 推荐 lint 规则 | | 代码规范 | flutter_lints | Dart 推荐 lint 规则 |
## 目录结构 ## 目录结构
@@ -71,7 +71,7 @@ lib/
## 快速开始 ## 快速开始
```bash ```bash
cd flutter-template cd record-tool
flutter pub get flutter pub get
flutter analyze flutter analyze
flutter test flutter test

View File

@@ -5,8 +5,10 @@ plugins {
id("dev.flutter.flutter-gradle-plugin") id("dev.flutter.flutter-gradle-plugin")
} }
val appPackageName = "com.gdfw.fxjk"
android { android {
namespace = "com.example.flutter_template" namespace = appPackageName
compileSdk = flutter.compileSdkVersion compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion ndkVersion = flutter.ndkVersion
@@ -20,8 +22,7 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = appPackageName
applicationId = "com.example.flutter_template"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion minSdk = flutter.minSdkVersion

View File

@@ -0,0 +1,9 @@
package com.gdfw.fxjk
object AppConstants {
const val PACKAGE_NAME = "com.gdfw.fxjk"
const val RECORDING_METHOD_CHANNEL = "$PACKAGE_NAME/recording"
const val RECORDING_EVENT_CHANNEL = "$PACKAGE_NAME/recording_events"
const val RECORDING_ACTION_START = "$PACKAGE_NAME.recording.START"
const val RECORDING_ACTION_STOP = "$PACKAGE_NAME.recording.STOP"
}

View File

@@ -1,8 +1,8 @@
package com.example.flutter_template package com.gdfw.fxjk
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import com.example.flutter_template.recording.RecordingPlatformHandler import com.gdfw.fxjk.recording.RecordingPlatformHandler
import com.example.flutter_template.recording.RecordingPreviewFactory import com.gdfw.fxjk.recording.RecordingPreviewFactory
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine

View File

@@ -1,4 +1,4 @@
package com.example.flutter_template.recording package com.gdfw.fxjk.recording
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent

View File

@@ -1,4 +1,4 @@
package com.example.flutter_template.recording package com.gdfw.fxjk.recording
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context

View File

@@ -1,4 +1,4 @@
package com.example.flutter_template.recording package com.gdfw.fxjk.recording
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log

View File

@@ -1,4 +1,4 @@
package com.example.flutter_template.recording package com.gdfw.fxjk.recording
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel import android.app.NotificationChannel
@@ -15,7 +15,8 @@ import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService
import com.example.flutter_template.MainActivity import com.gdfw.fxjk.AppConstants
import com.gdfw.fxjk.MainActivity
class RecordingForegroundService : LifecycleService() { class RecordingForegroundService : LifecycleService() {
private var wakeLock: PowerManager.WakeLock? = null private var wakeLock: PowerManager.WakeLock? = null
@@ -29,7 +30,7 @@ class RecordingForegroundService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
when (intent?.action) { when (intent?.action) {
ACTION_START -> { AppConstants.RECORDING_ACTION_START -> {
acquireWakeLock() acquireWakeLock()
val notification = buildNotification("正在录制") val notification = buildNotification("正在录制")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -43,7 +44,7 @@ class RecordingForegroundService : LifecycleService() {
} }
isRunning = true isRunning = true
} }
ACTION_STOP -> { AppConstants.RECORDING_ACTION_STOP -> {
releaseWakeLock() releaseWakeLock()
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
isRunning = false isRunning = false
@@ -143,8 +144,6 @@ class RecordingForegroundService : LifecycleService() {
companion object { companion object {
const val CHANNEL_ID = "recording_foreground" const val CHANNEL_ID = "recording_foreground"
const val NOTIFICATION_ID = 1001 const val NOTIFICATION_ID = 1001
const val ACTION_START = "com.example.flutter_template.recording.START"
const val ACTION_STOP = "com.example.flutter_template.recording.STOP"
private const val WAKE_LOCK_TAG = "record_tool:recording_wake_lock" private const val WAKE_LOCK_TAG = "record_tool:recording_wake_lock"
@Volatile @Volatile
@@ -156,7 +155,7 @@ class RecordingForegroundService : LifecycleService() {
fun start(context: Context) { fun start(context: Context) {
val intent = val intent =
Intent(context, RecordingForegroundService::class.java).apply { Intent(context, RecordingForegroundService::class.java).apply {
action = ACTION_START action = AppConstants.RECORDING_ACTION_START
} }
ContextCompatStart.startForegroundService(context, intent) ContextCompatStart.startForegroundService(context, intent)
} }
@@ -164,7 +163,7 @@ class RecordingForegroundService : LifecycleService() {
fun stop(context: Context) { fun stop(context: Context) {
val intent = val intent =
Intent(context, RecordingForegroundService::class.java).apply { Intent(context, RecordingForegroundService::class.java).apply {
action = ACTION_STOP action = AppConstants.RECORDING_ACTION_STOP
} }
context.startService(intent) context.startService(intent)
} }

View File

@@ -1,4 +1,4 @@
package com.example.flutter_template.recording package com.gdfw.fxjk.recording
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
@@ -7,7 +7,8 @@ import android.os.Looper
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import com.example.flutter_template.MainActivity import com.gdfw.fxjk.AppConstants
import com.gdfw.fxjk.MainActivity
import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
@@ -18,9 +19,9 @@ class RecordingPlatformHandler(
messenger: BinaryMessenger, messenger: BinaryMessenger,
) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler { ) : MethodChannel.MethodCallHandler, EventChannel.StreamHandler {
private val methodChannel = private val methodChannel =
MethodChannel(messenger, "com.example.flutter_template/recording") MethodChannel(messenger, AppConstants.RECORDING_METHOD_CHANNEL)
private val eventChannel = private val eventChannel =
EventChannel(messenger, "com.example.flutter_template/recording_events") EventChannel(messenger, AppConstants.RECORDING_EVENT_CHANNEL)
private val mainHandler = Handler(Looper.getMainLooper()) private val mainHandler = Handler(Looper.getMainLooper())
private var eventSink: EventChannel.EventSink? = null private var eventSink: EventChannel.EventSink? = null

View File

@@ -1,9 +1,9 @@
package com.example.flutter_template.recording package com.gdfw.fxjk.recording
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import androidx.camera.view.PreviewView import androidx.camera.view.PreviewView
import com.example.flutter_template.MainActivity import com.gdfw.fxjk.MainActivity
import io.flutter.plugin.common.StandardMessageCodec import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory import io.flutter.plugin.platform.PlatformViewFactory

View File

@@ -1,4 +1,4 @@
package com.example.flutter_template.recording package com.gdfw.fxjk.recording
import android.content.Context import android.content.Context
import androidx.lifecycle.LifecycleService import androidx.lifecycle.LifecycleService

View File

@@ -1,4 +1,4 @@
package com.example.flutter_template.recording package com.gdfw.fxjk.recording
enum class RecordingState { enum class RecordingState {
IDLE, IDLE,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 721 B

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@@ -410,6 +410,12 @@ private final class RecordingCameraController: NSObject, AVCaptureFileOutputReco
} }
} }
private enum RecordingChannelNames {
static let packageName = "com.gdfw.fxjk"
static let method = "\(packageName)/recording"
static let events = "\(packageName)/recording_events"
}
final class RecordingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { final class RecordingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private let controller = RecordingCameraController.shared private let controller = RecordingCameraController.shared
private var eventSink: FlutterEventSink? private var eventSink: FlutterEventSink?
@@ -421,13 +427,13 @@ final class RecordingPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
registrar.register(RecordingPreviewFactory(), withId: "recording-camera-preview") registrar.register(RecordingPreviewFactory(), withId: "recording-camera-preview")
let methodChannel = FlutterMethodChannel( let methodChannel = FlutterMethodChannel(
name: "com.example.flutter_template/recording", name: RecordingChannelNames.method,
binaryMessenger: messenger binaryMessenger: messenger
) )
registrar.addMethodCallDelegate(plugin, channel: methodChannel) registrar.addMethodCallDelegate(plugin, channel: methodChannel)
let eventChannel = FlutterEventChannel( let eventChannel = FlutterEventChannel(
name: "com.example.flutter_template/recording_events", name: RecordingChannelNames.events,
binaryMessenger: messenger binaryMessenger: messenger
) )
eventChannel.setStreamHandler(plugin) eventChannel.setStreamHandler(plugin)

View File

@@ -1,16 +1,46 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_template/app/config/app_config.dart'; import 'package:recording_tool/app/config/app_config.dart';
import 'package:flutter_template/app/router/app_navigator.dart'; import 'package:recording_tool/app/router/app_navigator.dart';
import 'package:flutter_template/app/theme/app_theme.dart'; import 'package:recording_tool/app/theme/app_theme.dart';
import 'package:flutter_template/features/recording/recording_page.dart'; import 'package:recording_tool/features/recording/recording_page.dart';
import 'package:recording_tool/features/recording/view-model/view_model_recording.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
class FlutterTemplateApp extends StatelessWidget { class FlutterTemplateApp extends ConsumerStatefulWidget {
const FlutterTemplateApp({super.key}); const FlutterTemplateApp({super.key});
@override
ConsumerState<FlutterTemplateApp> createState() => _FlutterTemplateAppState();
}
class _FlutterTemplateAppState extends ConsumerState<FlutterTemplateApp>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(recordingViewModelProvider.notifier).getClipboardContent();
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
ref.read(recordingViewModelProvider.notifier).getClipboardContent();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ScreenUtilInit( return ScreenUtilInit(

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/app/app.dart'; import 'package:recording_tool/app/app.dart';
import 'package:flutter_template/app/config/app_config.dart'; import 'package:recording_tool/app/config/app_config.dart';
import 'package:flutter_template/core/cache/app_storage.dart'; import 'package:recording_tool/core/cache/app_storage.dart';
import 'package:flutter_template/core/logging/app_logger.dart'; import 'package:recording_tool/core/logging/app_logger.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
class AppBootstrapper { class AppBootstrapper {

View File

@@ -1,9 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_template/core/network/api_exception.dart'; import 'package:recording_tool/core/network/api_exception.dart';
import 'package:flutter_template/core/network/api_response.dart'; import 'package:recording_tool/core/network/api_response.dart';
import 'package:flutter_template/core/network/http_method.dart'; import 'package:recording_tool/core/network/http_method.dart';
typedef JsonParser<T> = T Function(dynamic json); typedef JsonParser<T> = T Function(dynamic json);

View File

@@ -1,10 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_template/app/config/app_config.dart'; import 'package:recording_tool/app/config/app_config.dart';
import 'package:flutter_template/core/cache/app_storage.dart'; import 'package:recording_tool/core/cache/app_storage.dart';
import 'package:flutter_template/core/cache/storage_keys.dart'; import 'package:recording_tool/core/cache/storage_keys.dart';
import 'package:flutter_template/core/utils/device_utils.dart'; import 'package:recording_tool/core/utils/device_utils.dart';
class HeaderInterceptor extends Interceptor { class HeaderInterceptor extends Interceptor {
@override @override

View File

@@ -3,7 +3,7 @@ import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_template/core/network/network_state.dart'; import 'package:recording_tool/core/network/network_state.dart';
class NetworkMonitor { class NetworkMonitor {
final _controller = StreamController<NetworkState>.broadcast(); final _controller = StreamController<NetworkState>.broadcast();

View File

@@ -1,7 +1,7 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_template/core/network/network_monitor.dart'; import 'package:recording_tool/core/network/network_monitor.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart'; import 'package:recording_tool/core/network/offline_queue/offline_queue_manager.dart';
import 'package:flutter_template/core/network/offline_queue/offline_request.dart'; import 'package:recording_tool/core/network/offline_queue/offline_request.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class OfflineQueueInterceptor extends Interceptor { class OfflineQueueInterceptor extends Interceptor {

View File

@@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_template/core/logging/app_logger.dart'; import 'package:recording_tool/core/logging/app_logger.dart';
import 'package:flutter_template/core/network/network_monitor.dart'; import 'package:recording_tool/core/network/network_monitor.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart'; import 'package:recording_tool/core/network/offline_queue/offline_queue_state.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart'; import 'package:recording_tool/core/network/offline_queue/offline_queue_storage.dart';
import 'package:flutter_template/core/network/offline_queue/offline_request.dart'; import 'package:recording_tool/core/network/offline_queue/offline_request.dart';
class OfflineQueueManager { class OfflineQueueManager {
OfflineQueueManager({ OfflineQueueManager({

View File

@@ -1,6 +1,6 @@
import 'package:flutter_template/core/cache/app_storage.dart'; import 'package:recording_tool/core/cache/app_storage.dart';
import 'package:flutter_template/core/cache/storage_keys.dart'; import 'package:recording_tool/core/cache/storage_keys.dart';
import 'package:flutter_template/core/network/offline_queue/offline_request.dart'; import 'package:recording_tool/core/network/offline_queue/offline_request.dart';
class OfflineQueueStorage { class OfflineQueueStorage {
Future<List<OfflineRequest>> loadQueue() async { Future<List<OfflineRequest>> loadQueue() async {

View File

@@ -1,11 +1,11 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/app/config/app_config.dart'; import 'package:recording_tool/app/config/app_config.dart';
import 'package:flutter_template/core/network/api_client.dart'; import 'package:recording_tool/core/network/api_client.dart';
import 'package:flutter_template/core/network/header_interceptor.dart'; import 'package:recording_tool/core/network/header_interceptor.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_interceptor.dart'; import 'package:recording_tool/core/network/offline_queue/offline_queue_interceptor.dart';
import 'package:flutter_template/core/network/providers/network_providers.dart'; import 'package:recording_tool/core/network/providers/network_providers.dart';
import 'package:flutter_template/core/network/providers/offline_queue_providers.dart'; import 'package:recording_tool/core/network/providers/offline_queue_providers.dart';
final dioProvider = Provider<Dio>((ref) { final dioProvider = Provider<Dio>((ref) {
final dio = Dio( final dio = Dio(

View File

@@ -1,6 +1,6 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/core/network/network_monitor.dart'; import 'package:recording_tool/core/network/network_monitor.dart';
import 'package:flutter_template/core/network/network_state.dart'; import 'package:recording_tool/core/network/network_state.dart';
final networkMonitorProvider = Provider<NetworkMonitor>((ref) { final networkMonitorProvider = Provider<NetworkMonitor>((ref) {
final monitor = NetworkMonitor()..start(); final monitor = NetworkMonitor()..start();

View File

@@ -1,8 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_manager.dart'; import 'package:recording_tool/core/network/offline_queue/offline_queue_manager.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_state.dart'; import 'package:recording_tool/core/network/offline_queue/offline_queue_state.dart';
import 'package:flutter_template/core/network/offline_queue/offline_queue_storage.dart'; import 'package:recording_tool/core/network/offline_queue/offline_queue_storage.dart';
import 'package:flutter_template/core/network/providers/network_providers.dart'; import 'package:recording_tool/core/network/providers/network_providers.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
final offlineQueueStorageProvider = Provider<OfflineQueueStorage>((ref) { final offlineQueueStorageProvider = Provider<OfflineQueueStorage>((ref) {

View File

@@ -0,0 +1,44 @@
/// 剪切板内容数据模型
class ClipboardRecordingModel {
final String title;
final int startTimestamp;
final int endTimestamp;
final String address;
ClipboardRecordingModel({
required this.title,
required this.startTimestamp,
required this.endTimestamp,
required this.address,
});
factory ClipboardRecordingModel.fromJson(Map<String, dynamic> json) {
return ClipboardRecordingModel(
title: _readString(json, 'title'),
startTimestamp: _readInt(json, 'startTimestamp'),
endTimestamp: _readInt(json, 'endTimestamp'),
address: _readString(json, 'address'),
);
}
Map<String, dynamic> toJson() {
return {
'title': title,
'startTimestamp': startTimestamp,
'endTimestamp': endTimestamp,
'address': address,
};
}
static String _readString(Map<String, dynamic> json, String key) {
final value = json[key];
if (value is String) return value;
throw FormatException('Clipboard field "$key" must be a String.');
}
static int _readInt(Map<String, dynamic> json, String key) {
final value = json[key];
if (value is int) return value;
throw FormatException('Clipboard field "$key" must be an int.');
}
}

View File

@@ -0,0 +1,26 @@
import 'package:recording_tool/features/recording/model/model_clipboard.dart';
class RecordingModel {
/// 剪切板内容
final ClipboardRecordingModel clipboardRecordingModel;
RecordingModel({required this.clipboardRecordingModel});
factory RecordingModel.fromJson(Map<String, dynamic> json) {
return RecordingModel(
clipboardRecordingModel: ClipboardRecordingModel.fromJson(
json['clipboardRecordingModel'],
),
);
}
Map<String, dynamic> toJson() {
return {'clipboardRecordingModel': clipboardRecordingModel.toJson()};
}
RecordingModel copyWith({ClipboardRecordingModel? clipboardRecordingModel}) {
return RecordingModel(
clipboardRecordingModel:
clipboardRecordingModel ?? this.clipboardRecordingModel,
);
}
}

View File

@@ -0,0 +1,5 @@
abstract final class RecordingChannelNames {
static const packageName = 'com.gdfw.fxjk';
static const method = '$packageName/recording';
static const events = '$packageName/recording_events';
}

View File

@@ -3,11 +3,12 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/features/recording/recording_platform.dart'; import 'package:recording_tool/features/recording/recording_platform.dart';
import 'package:flutter_template/features/recording/recording_session_controller.dart'; import 'package:recording_tool/features/recording/recording_session_controller.dart';
import 'package:flutter_template/features/recording/widgets/camera_preview_widget.dart'; import 'package:recording_tool/features/recording/view-model/view_model_recording.dart';
import 'package:flutter_template/features/recording/widgets/recording_touch_lock_overlay.dart'; import 'package:recording_tool/features/recording/widgets/camera_preview_widget.dart';
import 'package:flutter_template/shared/widgets/widgets.dart'; import 'package:recording_tool/features/recording/widgets/recording_touch_lock_overlay.dart';
import 'package:recording_tool/shared/widgets/widgets.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
class RecordingPage extends ConsumerStatefulWidget { class RecordingPage extends ConsumerStatefulWidget {
@@ -27,6 +28,7 @@ class _RecordingPageState extends ConsumerState<RecordingPage> {
} }
Future<void> _bootstrap() async { Future<void> _bootstrap() async {
await ref.read(recordingViewModelProvider.notifier).getClipboardContent();
await _enterRecordingMode(); await _enterRecordingMode();
// Allow PlatformView to attach before binding CameraX preview. // Allow PlatformView to attach before binding CameraX preview.
await Future<void>.delayed(const Duration(milliseconds: 400)); await Future<void>.delayed(const Duration(milliseconds: 400));

View File

@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:recording_tool/features/recording/recording_channel_names.dart';
enum RecordingState { enum RecordingState {
idle, idle,
@@ -47,10 +48,10 @@ class RecordingPlatform {
RecordingPlatform._(); RecordingPlatform._();
static const MethodChannel _channel = MethodChannel( static const MethodChannel _channel = MethodChannel(
'com.example.flutter_template/recording', RecordingChannelNames.method,
); );
static const EventChannel _events = EventChannel( static const EventChannel _events = EventChannel(
'com.example.flutter_template/recording_events', RecordingChannelNames.events,
); );
static bool get isSupported => static bool get isSupported =>

View File

@@ -3,8 +3,8 @@ import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_template/core/permission/permission_service.dart'; import 'package:recording_tool/core/permission/permission_service.dart';
import 'package:flutter_template/features/recording/recording_platform.dart'; import 'package:recording_tool/features/recording/recording_platform.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
class RecordingSessionState { class RecordingSessionState {
@@ -74,8 +74,8 @@ class RecordingSessionState {
final recordingSessionControllerProvider = final recordingSessionControllerProvider =
NotifierProvider<RecordingSessionController, RecordingSessionState>( NotifierProvider<RecordingSessionController, RecordingSessionState>(
RecordingSessionController.new, RecordingSessionController.new,
); );
class RecordingSessionController extends Notifier<RecordingSessionState> { class RecordingSessionController extends Notifier<RecordingSessionState> {
StreamSubscription<RecordingStatus>? _statusSubscription; StreamSubscription<RecordingStatus>? _statusSubscription;
@@ -161,9 +161,7 @@ class RecordingSessionController extends Notifier<RecordingSessionState> {
if (!shouldRetry) { if (!shouldRetry) {
rethrow; rethrow;
} }
await Future<void>.delayed( await Future<void>.delayed(Duration(milliseconds: 150 * (attempt + 1)));
Duration(milliseconds: 150 * (attempt + 1)),
);
} }
} }
throw StateError('initializePreview retry exhausted'); throw StateError('initializePreview retry exhausted');

View File

@@ -0,0 +1,56 @@
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
import 'package:recording_tool/core/logging/app_logger.dart';
import 'package:recording_tool/features/recording/model/model_clipboard.dart';
import 'package:recording_tool/features/recording/model/model_recording.dart';
final recordingViewModelProvider =
StateNotifierProvider<RecordingViewModel, RecordingModel>((ref) {
return RecordingViewModel(ref);
});
class RecordingViewModel extends StateNotifier<RecordingModel> {
RecordingViewModel(this.ref)
: super(
RecordingModel(
clipboardRecordingModel: ClipboardRecordingModel(
title: '',
startTimestamp: 0,
endTimestamp: 0,
address: '',
),
),
);
final Ref ref;
/// 从剪切板获取内容
Future<void> getClipboardContent() async {
try {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
final text = clipboardData?.text;
AppLogger.debug('读取剪切板内容:$text');
if (text == null || text.trim().isEmpty) {
AppLogger.info('剪切板内容为空,跳过录制信息解析');
return;
}
final decoded = jsonDecode(text);
if (decoded is! Map<String, dynamic>) {
AppLogger.warning('剪切板内容不是 JSON 对象,跳过录制信息解析');
return;
}
final clipboardRecordingModel = ClipboardRecordingModel.fromJson(decoded);
state = state.copyWith(clipboardRecordingModel: clipboardRecordingModel);
AppLogger.info('剪切板录制信息解析成功:${clipboardRecordingModel.toJson()}');
} on FormatException catch (error) {
AppLogger.warning('剪切板录制信息格式错误:$error');
} catch (error, stackTrace) {
AppLogger.debug('读取剪切板录制信息失败', error: error, stackTrace: stackTrace);
}
}
}

View File

@@ -1,3 +1,3 @@
import 'package:flutter_template/app/bootstrap.dart'; import 'package:recording_tool/app/bootstrap.dart';
Future<void> main() => AppBootstrapper.bootstrap(); Future<void> main() => AppBootstrapper.bootstrap();

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_template/shared/widgets/app_network_image.dart'; import 'package:recording_tool/shared/widgets/app_network_image.dart';
class AppAvatar extends StatelessWidget { class AppAvatar extends StatelessWidget {
const AppAvatar({super.key, this.imageUrl, this.initials, this.size = 40}); const AppAvatar({super.key, this.imageUrl, this.initials, this.size = 40});

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_template/app/theme/app_theme.dart'; import 'package:recording_tool/app/theme/app_theme.dart';
class AppCard extends StatelessWidget { class AppCard extends StatelessWidget {
const AppCard({ const AppCard({

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_template/shared/widgets/app_button.dart'; import 'package:recording_tool/shared/widgets/app_button.dart';
class AppErrorView extends StatelessWidget { class AppErrorView extends StatelessWidget {
const AppErrorView({ const AppErrorView({

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_template/core/utils/rate_limiter.dart'; import 'package:recording_tool/core/utils/rate_limiter.dart';
class AppSearchBar extends StatefulWidget { class AppSearchBar extends StatefulWidget {
const AppSearchBar({ const AppSearchBar({

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_template/shared/widgets/app_empty_view.dart'; import 'package:recording_tool/shared/widgets/app_empty_view.dart';
import 'package:flutter_template/shared/widgets/app_error_view.dart'; import 'package:recording_tool/shared/widgets/app_error_view.dart';
import 'package:flutter_template/shared/widgets/app_loading_view.dart'; import 'package:recording_tool/shared/widgets/app_loading_view.dart';
enum AppViewStatus { loading, empty, error, content } enum AppViewStatus { loading, empty, error, content }

View File

@@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_template/app/router/app_navigator.dart'; import 'package:recording_tool/app/router/app_navigator.dart';
class AppToast { class AppToast {
AppToast._(); AppToast._();

View File

@@ -1,8 +1,8 @@
name: flutter_template name: recording_tool
description: "A reusable Flutter quick-start template for Android and iOS." description: "A recording tool for Android and iOS."
# The following line prevents the package from being accidentally published to # The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages. # pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application. # The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43 # A version number is three numbers separated by dots, like 1.2.43
@@ -66,7 +66,6 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
# The following line ensures that the Material Icons font is # The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in # included with your application, so that you can use the icons in
# the material Icons class. # the material Icons class.

View File

@@ -1,10 +1,10 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:permission_handler/permission_handler.dart'; // ignore: depend_on_referenced_packages
import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart'; import 'package:permission_handler_platform_interface/permission_handler_platform_interface.dart';
import 'package:flutter_template/core/permission/permission_service.dart'; import 'package:recording_tool/core/permission/permission_service.dart';
void main() { void main() {
group('PermissionService.requestMissing', () { group('PermissionService.requestMissing', () {
@@ -42,37 +42,42 @@ void main() {
expect(result[Permission.microphone], PermissionStatus.granted); expect(result[Permission.microphone], PermissionStatus.granted);
}); });
test('preserves permanently denied permissions without requesting them', test(
() async { 'preserves permanently denied permissions without requesting them',
final platform = FakePermissionHandlerPlatform( () async {
statuses: <Permission, PermissionStatus>{ final platform = FakePermissionHandlerPlatform(
Permission.camera: PermissionStatus.permanentlyDenied, statuses: <Permission, PermissionStatus>{
Permission.microphone: PermissionStatus.denied, Permission.camera: PermissionStatus.permanentlyDenied,
}, Permission.microphone: PermissionStatus.denied,
requestResults: <Permission, PermissionStatus>{ },
Permission.microphone: PermissionStatus.granted, requestResults: <Permission, PermissionStatus>{
}, Permission.microphone: PermissionStatus.granted,
); },
PermissionHandlerPlatform.instance = platform; );
PermissionHandlerPlatform.instance = platform;
final result = await PermissionService.requestMissing(<Permission>[ final result = await PermissionService.requestMissing(<Permission>[
Permission.camera, Permission.camera,
Permission.microphone, Permission.microphone,
]); ]);
expect(platform.requestCalls, <List<Permission>>[ expect(platform.requestCalls, <List<Permission>>[
<Permission>[Permission.microphone], <Permission>[Permission.microphone],
]); ]);
expect(result[Permission.camera], PermissionStatus.permanentlyDenied); expect(result[Permission.camera], PermissionStatus.permanentlyDenied);
expect(result[Permission.microphone], PermissionStatus.granted); expect(result[Permission.microphone], PermissionStatus.granted);
}); },
);
}); });
group('iOS permission configuration', () { group('iOS permission configuration', () {
test('Podfile enables camera and microphone permission macros', () { test('Podfile enables camera and microphone permission macros', () {
final podfile = File('ios/Podfile').readAsStringSync(); final podfile = File('ios/Podfile').readAsStringSync();
expect(podfile, contains("flutter_additional_ios_build_settings(target)")); expect(
podfile,
contains('flutter_additional_ios_build_settings(target)'),
);
expect(podfile, contains("'PERMISSION_CAMERA=1'")); expect(podfile, contains("'PERMISSION_CAMERA=1'"));
expect(podfile, contains("'PERMISSION_MICROPHONE=1'")); expect(podfile, contains("'PERMISSION_MICROPHONE=1'"));
}); });

View File

@@ -0,0 +1,41 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:recording_tool/features/recording/model/model_clipboard.dart';
void main() {
group('ClipboardRecordingModel', () {
const clipboardJson = {
'title': '王东方 丨李想 空中格斗赛',
'startTimestamp': 1717334400,
'endTimestamp': 1717334400,
'address': '广州市番禺区·粤港澳大湾区青年人才双创小镇',
};
test('parses mini program clipboard JSON', () {
final model = ClipboardRecordingModel.fromJson(clipboardJson);
expect(model.title, '王东方 丨李想 空中格斗赛');
expect(model.startTimestamp, 1717334400);
expect(model.endTimestamp, 1717334400);
expect(model.address, '广州市番禺区·粤港澳大湾区青年人才双创小镇');
expect(model.toJson(), clipboardJson);
});
test('throws FormatException when required field is missing', () {
final json = Map<String, dynamic>.from(clipboardJson)..remove('title');
expect(
() => ClipboardRecordingModel.fromJson(json),
throwsA(isA<FormatException>()),
);
});
test('throws FormatException when required field has wrong type', () {
final json = {...clipboardJson, 'startTimestamp': '1717334400'};
expect(
() => ClipboardRecordingModel.fromJson(json),
throwsA(isA<FormatException>()),
);
});
});
}

View File

@@ -1,5 +1,5 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_template/features/recording/recording_platform.dart'; import 'package:recording_tool/features/recording/recording_platform.dart';
void main() { void main() {
group('RecordingPlatform support', () { group('RecordingPlatform support', () {

View File

@@ -0,0 +1,148 @@
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:recording_tool/features/recording/view-model/view_model_recording.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const defaultClipboardTitle = '';
const validClipboardText =
'{"title":"王东方 丨李想 空中格斗赛","startTimestamp":1717334400,"endTimestamp":1717334400,"address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}';
Future<void> setClipboardText(String? text) async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.getData') {
return text == null ? null : <String, dynamic>{'text': text};
}
return null;
});
}
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, null);
});
group('RecordingViewModel.getClipboardContent', () {
test(
'updates state when clipboard contains valid mini program JSON',
() async {
await setClipboardText(validClipboardText);
final container = ProviderContainer();
addTearDown(container.dispose);
await container
.read(recordingViewModelProvider.notifier)
.getClipboardContent();
final clipboardModel = container
.read(recordingViewModelProvider)
.clipboardRecordingModel;
expect(clipboardModel.title, '王东方 丨李想 空中格斗赛');
expect(clipboardModel.startTimestamp, 1717334400);
expect(clipboardModel.endTimestamp, 1717334400);
expect(clipboardModel.address, '广州市番禺区·粤港澳大湾区青年人才双创小镇');
},
);
test('keeps default state when clipboard is empty', () async {
await setClipboardText('');
final container = ProviderContainer();
addTearDown(container.dispose);
await container
.read(recordingViewModelProvider.notifier)
.getClipboardContent();
expect(
container
.read(recordingViewModelProvider)
.clipboardRecordingModel
.title,
defaultClipboardTitle,
);
});
test('keeps default state when clipboard is not JSON', () async {
await setClipboardText('hello');
final container = ProviderContainer();
addTearDown(container.dispose);
await container
.read(recordingViewModelProvider.notifier)
.getClipboardContent();
expect(
container
.read(recordingViewModelProvider)
.clipboardRecordingModel
.title,
defaultClipboardTitle,
);
});
test('keeps default state when clipboard JSON is not an object', () async {
await setClipboardText('[1,2,3]');
final container = ProviderContainer();
addTearDown(container.dispose);
await container
.read(recordingViewModelProvider.notifier)
.getClipboardContent();
expect(
container
.read(recordingViewModelProvider)
.clipboardRecordingModel
.title,
defaultClipboardTitle,
);
});
test(
'keeps default state when clipboard JSON misses required fields',
() async {
await setClipboardText('{"title":"王东方 丨李想 空中格斗赛"}');
final container = ProviderContainer();
addTearDown(container.dispose);
await container
.read(recordingViewModelProvider.notifier)
.getClipboardContent();
expect(
container
.read(recordingViewModelProvider)
.clipboardRecordingModel
.title,
defaultClipboardTitle,
);
},
);
test(
'keeps default state when clipboard JSON has wrong field type',
() async {
await setClipboardText(
'{"title":"王东方 丨李想 空中格斗赛","startTimestamp":"1717334400","endTimestamp":1717334400,"address":"广州市番禺区·粤港澳大湾区青年人才双创小镇"}',
);
final container = ProviderContainer();
addTearDown(container.dispose);
await container
.read(recordingViewModelProvider.notifier)
.getClipboardContent();
expect(
container
.read(recordingViewModelProvider)
.clipboardRecordingModel
.title,
defaultClipboardTitle,
);
},
);
});
}

View File

@@ -1,14 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_template/app/app.dart'; import 'package:recording_tool/app/app.dart';
void main() { void main() {
testWidgets('template app renders demo page', (tester) async { testWidgets('recording app renders recording page', (tester) async {
await tester.pumpWidget(const ProviderScope(child: FlutterTemplateApp())); await tester.pumpWidget(const ProviderScope(child: FlutterTemplateApp()));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('Flutter Template'), findsOneWidget); expect(find.byIcon(Icons.fiber_manual_record), findsOneWidget);
expect(find.text('通用 Flutter 快速开发模板'), findsOneWidget);
expect(find.text('增加计数'), findsOneWidget);
}); });
} }