更新测试案例
This commit is contained in:
@@ -1,118 +1,154 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:im_app/app/di/db_provider.dart';
|
||||
import 'package:im_app/data/local/drift/app_database.dart';
|
||||
import 'package:im_app/domain/entities/user.dart';
|
||||
import 'package:im_app/domain/presentation/di/user_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'chat_db_test_view_model.g.dart';
|
||||
|
||||
class TestResult {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String duration;
|
||||
|
||||
TestResult({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.duration,
|
||||
});
|
||||
}
|
||||
|
||||
class ChatDbTestState {
|
||||
final bool testStarted;
|
||||
final List<TestResult> testResults;
|
||||
final List<User> users;
|
||||
final String currentState;
|
||||
final bool hasMore;
|
||||
final int currentPage;
|
||||
final int totalCount;
|
||||
|
||||
const ChatDbTestState({
|
||||
this.testStarted = false,
|
||||
this.testResults = const [],
|
||||
this.users = const [],
|
||||
this.currentState = '',
|
||||
this.hasMore = true,
|
||||
this.currentPage = 0,
|
||||
this.totalCount = 0,
|
||||
});
|
||||
|
||||
ChatDbTestState copyWith({
|
||||
bool? testStarted,
|
||||
List<TestResult>? testResults,
|
||||
List<User>? users,
|
||||
String? currentState,
|
||||
bool? hasMore,
|
||||
int? currentPage,
|
||||
int? totalCount,
|
||||
}) => ChatDbTestState(
|
||||
testStarted: testStarted ?? this.testStarted,
|
||||
testResults: testResults ?? this.testResults,
|
||||
users: users ?? this.users,
|
||||
currentState: currentState ?? this.currentState,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
currentPage: currentPage ?? this.currentPage,
|
||||
totalCount: totalCount ?? this.totalCount,
|
||||
);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ChatDbTestViewModel extends _$ChatDbTestViewModel {
|
||||
final _random = Random();
|
||||
bool _isTesting = false;
|
||||
static const _pageSize = 50;
|
||||
|
||||
@override
|
||||
ChatDbTestState build() {
|
||||
// 这里就是 onInit
|
||||
final List<TestResult> testResults = List.generate(
|
||||
1000,
|
||||
(i) => TestResult(
|
||||
title: '用户 ${Random().nextInt(9999)}',
|
||||
subtitle: 'uid: ${Random().nextInt(999999)}',
|
||||
duration: '${Random().nextInt(500)}ms',
|
||||
),
|
||||
);
|
||||
return ChatDbTestState(testResults: testResults);
|
||||
Future.microtask(() => _loadNextPage(reset: true));
|
||||
return const ChatDbTestState(currentState: '加载中...');
|
||||
}
|
||||
|
||||
// ── 导航(Demo 按钮,正式开发后随 UI 一并替换) ──────────────────────────
|
||||
// ── 分页 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Future<void> _loadNextPage({bool reset = false}) async {
|
||||
if (!state.hasMore && !reset) return;
|
||||
|
||||
final repo = ref.read(userRepositoryProvider);
|
||||
final page = reset ? 0 : state.currentPage;
|
||||
final offset = page * _pageSize;
|
||||
|
||||
final results = await Future.wait([
|
||||
repo.getUsers(offset: offset, limit: _pageSize),
|
||||
repo.countUsers(),
|
||||
]);
|
||||
|
||||
if (!ref.mounted) return;
|
||||
|
||||
final newUsers = results[0] as List<User>;
|
||||
final total = results[1] as int;
|
||||
|
||||
state = state.copyWith(
|
||||
users: reset ? newUsers : [...state.users, ...newUsers],
|
||||
currentPage: page + 1,
|
||||
hasMore: newUsers.length >= _pageSize,
|
||||
totalCount: total,
|
||||
currentState: reset && newUsers.isEmpty ? '暂无数据' : '空闲 (共 $total 条)',
|
||||
);
|
||||
}
|
||||
|
||||
/// Called by ListView when reaching the end
|
||||
void loadMore() {
|
||||
if (!state.hasMore || state.testStarted) return;
|
||||
_loadNextPage();
|
||||
}
|
||||
|
||||
// ── 测试 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// 开始测试
|
||||
void startDBTest(BuildContext context) {
|
||||
_isTesting = true;
|
||||
state = state.copyWith(testStarted: true, currentState: '开始测试');
|
||||
_testDBInsert();
|
||||
}
|
||||
|
||||
/// 结束测试
|
||||
void stopDBTest(BuildContext context) {
|
||||
_isTesting = false;
|
||||
state = state.copyWith(testStarted: false, currentState: '结束测试');
|
||||
}
|
||||
|
||||
Future<void> _testDBInsert() async {
|
||||
final db = ref.read(storageSdkProvider);
|
||||
final repo = ref.read(userRepositoryProvider);
|
||||
const count = 10000;
|
||||
const chunkSize = 50;
|
||||
const chunkSize = 200;
|
||||
final stopwatch = Stopwatch()..start();
|
||||
|
||||
debugPrint('开始测试: $count 条,每批 $chunkSize 条');
|
||||
|
||||
final workingList = List<User>.from(state.users);
|
||||
int completed = 0;
|
||||
|
||||
for (var i = 0; i < count; i += chunkSize) {
|
||||
while (completed < count) {
|
||||
if (!_isTesting) break;
|
||||
|
||||
final chunk = List.generate(
|
||||
chunkSize.clamp(0, count - i),
|
||||
(j) => UsersCompanion.insert(
|
||||
uid: i + j,
|
||||
nickname: Value('User ${i + j}'),
|
||||
min(chunkSize, count - completed),
|
||||
(_) => User(
|
||||
uid: _random.nextInt(999999),
|
||||
nickname: 'User ${_random.nextInt(9999)}',
|
||||
),
|
||||
);
|
||||
|
||||
await db.batchInsertOrReplace<DriftUser>(chunk);
|
||||
await repo.saveUsers(chunk);
|
||||
completed += chunk.length;
|
||||
workingList.addAll(chunk);
|
||||
|
||||
// 让出主线程
|
||||
await Future.delayed(Duration.zero);
|
||||
debugPrint(
|
||||
'已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)',
|
||||
);
|
||||
|
||||
debugPrint('已完成: $completed / $count (${stopwatch.elapsedMilliseconds}ms)');
|
||||
|
||||
// 更新 UI 状态
|
||||
if (ref.mounted) {
|
||||
state = state.copyWith(
|
||||
users: List.unmodifiable(workingList),
|
||||
currentState: '已插入 $completed / $count 条',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('全部完成: ${stopwatch.elapsedMilliseconds}ms');
|
||||
_isTesting = false;
|
||||
final elapsed = stopwatch.elapsedMilliseconds;
|
||||
debugPrint('全部完成: ${elapsed}ms');
|
||||
|
||||
if (ref.mounted) {
|
||||
final total = await repo.countUsers();
|
||||
state = state.copyWith(
|
||||
testStarted: false,
|
||||
currentState: '完成!共 $count 条,耗时 ${stopwatch.elapsedMilliseconds}ms',
|
||||
totalCount: total,
|
||||
currentState: '完成!共 $total 条,耗时 ${elapsed}ms',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,69 +3,111 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:im_app/features/chat/presentation/chat_db_test_view_model.dart';
|
||||
|
||||
import '../../../core/ui/components/app_button.dart';
|
||||
import '../presentation/chat_view_model.dart';
|
||||
|
||||
/// 聊天页(Demo 按钮)
|
||||
///
|
||||
/// 包含五个演示按钮,覆盖 go_router 的常见导航场景:
|
||||
/// - 「切换 Tab」 — go,替换历史,不可返回
|
||||
/// - 「有参 push(extra)」 — push + extra(Dart Record),可返回
|
||||
/// - 「有参 push(路径参数)」— push + URL 内嵌 id,可返回
|
||||
/// - 「无参 push」 — push,可返回
|
||||
/// - 「退出登录」 — 守卫自动重定向到 /login
|
||||
///
|
||||
/// 所有操作通过 [ChatViewModel] 处理,View 不直接调用路由。
|
||||
/// 正式开发后替换为会话列表,按钮相关代码一并清除。
|
||||
class ChatDbTestPage extends ConsumerWidget {
|
||||
class ChatDbTestPage extends ConsumerStatefulWidget {
|
||||
const ChatDbTestPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<ChatDbTestPage> createState() => _ChatDbTestPageState();
|
||||
}
|
||||
|
||||
class _ChatDbTestPageState extends ConsumerState<ChatDbTestPage> {
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 300) {
|
||||
ref.read(chatDbTestViewModelProvider.notifier).loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final vm = ref.read(chatDbTestViewModelProvider.notifier);
|
||||
final state = ref.watch(chatDbTestViewModelProvider);
|
||||
final testStarted = ref.watch(
|
||||
chatDbTestViewModelProvider.select((s) => s.testStarted),
|
||||
);
|
||||
final currentState = ref.watch(
|
||||
chatDbTestViewModelProvider.select((s) => s.currentState),
|
||||
);
|
||||
final users = ref.watch(chatDbTestViewModelProvider.select((s) => s.users));
|
||||
final hasMore = ref.watch(
|
||||
chatDbTestViewModelProvider.select((s) => s.hasMore),
|
||||
);
|
||||
final totalCount = ref.watch(
|
||||
chatDbTestViewModelProvider.select((s) => s.totalCount),
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('测试数据库'), ),
|
||||
appBar: AppBar(title: Text('测试数据库 ($totalCount)')),
|
||||
body: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
spacing: 16,
|
||||
children: [
|
||||
SizedBox(height: 4),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: EdgeInsetsGeometry.symmetric(horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
AppButton.inverse(
|
||||
label: state.testStarted ? '结束' : '开始',
|
||||
onPressed: () => state.testStarted ? vm.stopDBTest(context) : vm.startDBTest(context),
|
||||
label: testStarted ? '结束' : '开始',
|
||||
onPressed: () => testStarted
|
||||
? vm.stopDBTest(context)
|
||||
: vm.startDBTest(context),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.currentState,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
)
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(currentState, textAlign: TextAlign.end)),
|
||||
],
|
||||
)
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: state.testResults.length,
|
||||
controller: _scrollController,
|
||||
itemCount: users.length + (hasMore ? 1 : 0),
|
||||
cacheExtent: 500,
|
||||
itemBuilder: (context, index) {
|
||||
final result = state.testResults[index];
|
||||
return ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
title: Text(result.title),
|
||||
subtitle: Text(result.subtitle),
|
||||
// trailing: Text(result.duration),
|
||||
);
|
||||
if (index == users.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
final user = users[index];
|
||||
return _UserTile(uid: user.uid, nickname: user.nickname);
|
||||
},
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserTile extends StatelessWidget {
|
||||
final int uid;
|
||||
final String? nickname;
|
||||
|
||||
const _UserTile({required this.uid, required this.nickname});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
titleAlignment: ListTileTitleAlignment.center,
|
||||
title: Text(nickname ?? '-'),
|
||||
subtitle: Text('uid: $uid'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:im_app/data/models/user_dto.dart';
|
||||
import 'package:im_app/data/remote/login_request.dart';
|
||||
import 'package:im_app/domain/presentation/di/user_provider.dart';
|
||||
import 'package:networks_sdk/networks_sdk.dart';
|
||||
import 'package:im_app/app/di/db_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
@@ -61,22 +61,25 @@ class LoginViewModel extends _$LoginViewModel {
|
||||
Future<void> demoLogin() async {
|
||||
final storageApi = ref.read(storageSdkProvider);
|
||||
final storageLifeCycle = storageApi as StorageSdkLifecycle;
|
||||
final repositoryProvider = ref.read(userRepositoryProvider);
|
||||
final provider = ref.read(authNotifierProvider);
|
||||
|
||||
// Read mock response from assets
|
||||
final String raw = await rootBundle.loadString('assets/loginData.json');
|
||||
final Map<String, dynamic> json = jsonDecode(raw);
|
||||
|
||||
// Parse into LoginData (nested under 'data' key)
|
||||
// Parse → Domain User directly
|
||||
final loginResponse = LoginResponse.fromJson(json);
|
||||
final user = loginResponse.data.toEntity();
|
||||
|
||||
provider.login();
|
||||
// Open database for the user
|
||||
await storageLifeCycle.openDatabase(user.uid);
|
||||
///TODO: User 和 DTO和数据库之间转换
|
||||
final userCompanion = UserDto.fromEntity(user).toCompanion();
|
||||
storageApi.insert(userCompanion);
|
||||
|
||||
// Save user to DB via repository
|
||||
await repositoryProvider.saveUser(user);
|
||||
|
||||
// Trigger auth state
|
||||
provider.login();
|
||||
}
|
||||
|
||||
/// 执行登录
|
||||
@@ -88,10 +91,9 @@ class LoginViewModel extends _$LoginViewModel {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
final user = await ref.read(loginUseCaseProvider).execute(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
final user = await ref
|
||||
.read(loginUseCaseProvider)
|
||||
.execute(email: email, password: password);
|
||||
|
||||
state = state.copyWith(user: user, isLoading: false);
|
||||
} on FormatException catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user