@tencent-rtc/mcp 1.3.6 → 1.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,865 @@
1
+ `LiveListStore` is the `AtomicXCore` core module for management of live room list, creation, joining and room status maintenance. With `LiveListStore`, you can build complete lifecycle management for your application.
2
+
3
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/e2538748f6d111f09fbf525400370dda.png)
4
+
5
+
6
+ ## Core Features
7
+ - **Get live room list**: Obtain the current list of all public live rooms, supporting load by page.
8
+
9
+ - **Live stream lifecycle management**: Provides a complete process API for creation, going live, joining, leaving, and ending live stream.
10
+
11
+ - **Live stream information update**: Anchors can update publicly available information in the live streaming room at any time, such as cover and notice.
12
+
13
+ - **Real-time event monitoring**: Listen to key events such as live streaming end and users being kicked out.
14
+
15
+ - **Custom business data**: Powerful custom metadata (`MetaData`) capacity allows you to store and synchronize any business-relevant information in the room, such as live status, music info, and custom role.
16
+
17
+
18
+ ## Core Concepts
19
+
20
+ Before starting integration, let's learn about several core concepts related to `LiveListStore` in the following table:
21
+ <table>
22
+ <tr>
23
+ <td rowspan="1" colSpan="1" >**Core Concepts**</td>
24
+
25
+ <td rowspan="1" colSpan="1" >**Type**</td>
26
+
27
+ <td rowspan="1" colSpan="1" >**Core Responsibilities and Description**</td>
28
+ </tr>
29
+
30
+ <tr>
31
+ <td rowspan="1" colSpan="1" >[LiveInfo](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveInfo-class.html)</td>
32
+
33
+ <td rowspan="1" colSpan="1" >`class`</td>
34
+
35
+ <td rowspan="1" colSpan="1" >Represents the information model of a live room. It contains live ID (`liveID`), name (`liveName`), room owner information (`liveOwner`) and custom metadata (`metaData`).</td>
36
+ </tr>
37
+
38
+ <tr>
39
+ <td rowspan="1" colSpan="1" >[LiveListState](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListState-class.html)</td>
40
+
41
+ <td rowspan="1" colSpan="1" >`class`</td>
42
+
43
+ <td rowspan="1" colSpan="1" >Represents the current status of the live list module. Its core attribute `liveList` is a `ValueListenable<List<LiveInfo>>` data type, storing the fetched live stream list. `currentLive` represents the live room information where the user currently resides.</td>
44
+ </tr>
45
+
46
+ <tr>
47
+ <td rowspan="1" colSpan="1" >[LiveListListener](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListListener-class.html)</td>
48
+
49
+ <td rowspan="1" colSpan="1" >`class`</td>
50
+
51
+ <td rowspan="1" colSpan="1" >Represents the global event listener for a live streaming room. It includes two callbacks, `onLiveEnded` (live streaming end) and `onKickedOutOfLive` (kicked out of live), used to process key room status updates.</td>
52
+ </tr>
53
+
54
+ <tr>
55
+ <td rowspan="1" colSpan="1" >[LiveListStore](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListStore-class.html)</td>
56
+
57
+ <td rowspan="1" colSpan="1" >`class`</td>
58
+
59
+ <td rowspan="1" colSpan="1" >This is the core management class for interaction with the live list and room lifecycle. It is a Global Singleton (`shared`), responsible for all live streaming room operations such as create, join, and information update.</td>
60
+ </tr>
61
+ </table>
62
+
63
+
64
+ ## Implementation Steps
65
+
66
+ ### Step 1: Integrating the Component
67
+ - **Live streaming**: Refer to [quick integration](https://write.woa.com/document/200455640177692672) with **AtomicXCore**, and complete the feature implementation for [Anchor starts live streaming](https://write.woa.com/document/200455640177692672) and [audience viewing](https://write.woa.com/document/200455640177692672).
68
+
69
+ - **Voice chat room**: Refer to [quick integration](https://write.woa.com/document/200555778169073664) with **AtomicXCore**, and complete the feature implementation for [Anchor starts live streaming](https://write.woa.com/document/200555778169073664) and [audience listening](https://write.woa.com/document/200555778169073664).
70
+
71
+
72
+ ### Step 2: Audience Entering the Live Room Implementation
73
+
74
+ Create a page to display a live broadcast list. This page uses `GridView` to layout live streaming room cards. When a user clicks a certain card, get the `liveID` of the room and navigate to the audience viewing page.
75
+ ``` java
76
+ import 'package:flutter/material.dart';
77
+ import 'package:atomic_x_core/atomicxcore.dart';
78
+
79
+ /// Live stream list page
80
+ class LiveListPage extends StatefulWidget {
81
+ const LiveListPage({super.key});
82
+
83
+ @override
84
+ State<LiveListPage> createState() => _LiveListPageState();
85
+ }
86
+
87
+ class _LiveListPageState extends State<LiveListPage> {
88
+ final _liveListStore = LiveListStore.shared;
89
+ final ScrollController _scrollController = ScrollController();
90
+ bool _isLoading = false;
91
+
92
+ @override
93
+ void initState() {
94
+ super.initState();
95
+ _liveListStore.liveState.liveList.addListener(_onLiveListChanged);
96
+ _scrollController.addListener(_onScroll);
97
+ _fetchLiveList();
98
+ }
99
+
100
+ void _onLiveListChanged() {
101
+ setState(() {});
102
+ }
103
+
104
+ void _onScroll() {
105
+ // Load more when scrolling to the bottom
106
+ if (_scrollController.position.pixels >=
107
+ _scrollController.position.maxScrollExtent - 200) {
108
+ _loadMore();
109
+ }
110
+ }
111
+
112
+ Future<void> _fetchLiveList() async {
113
+ if (_isLoading) return;
114
+ setState(() => _isLoading = true);
115
+
116
+ final result = await _liveListStore.fetchLiveList(cursor: '', count: 20);
117
+ if (!result.isSuccess) {
118
+ debugPrint('List live stream failed: ${result.message}');
119
+ }
120
+
121
+ setState(() => _isLoading = false);
122
+ }
123
+
124
+ Future<void> _loadMore() async {
125
+ if (_isLoading) return;
126
+
127
+ final cursor = _liveListStore.liveState.liveListCursor.value;
128
+ if (cursor.isEmpty) return; // no more data
129
+
130
+ setState(() => _isLoading = true);
131
+ await _liveListStore.fetchLiveList(cursor: cursor, count: 20);
132
+ setState(() => _isLoading = false);
133
+ }
134
+
135
+ Future<void> _refresh() async {
136
+ _liveListStore.reset();
137
+ await _fetchLiveList();
138
+ }
139
+
140
+ // When user click certain card in the list
141
+ void _onLiveCardTap(LiveInfo liveInfo) {
142
+ // Create an audience viewing webpage and input the liveID
143
+ Navigator.push(
144
+ context,
145
+ MaterialPageRoute(
146
+ builder: (context) => YourAudiencePage(liveID: liveInfo.liveID),
147
+ ),
148
+ );
149
+ }
150
+
151
+ @override
152
+ void dispose() {
153
+ _liveListStore.liveState.liveList.removeListener(_onLiveListChanged);
154
+ _scrollController.dispose();
155
+ super.dispose();
156
+ }
157
+
158
+ @override
159
+ Widget build(BuildContext context) {
160
+ final liveList = _liveListStore.liveState.liveList.value;
161
+
162
+ return Scaffold(
163
+ appBar: AppBar(title: const Text('Live Stream List')),
164
+ body: RefreshIndicator(
165
+ onRefresh: _refresh,
166
+ child: liveList.isEmpty
167
+ ? Center(
168
+ child: _isLoading
169
+ ? const CircularProgressIndicator()
170
+ const Text('No live stream')
171
+ )
172
+ : GridView.builder(
173
+ controller: _scrollController,
174
+ padding: const EdgeInsets.all(8),
175
+ gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
176
+ crossAxisCount: 2,
177
+ childAspectRatio: 0.75,
178
+ crossAxisSpacing: 8,
179
+ mainAxisSpacing: 8,
180
+ ),
181
+ itemCount: liveList.length + (_isLoading ? 1 : 0),
182
+ itemBuilder: (context, index) {
183
+ if (index >= liveList.length) {
184
+ return const Center(child: CircularProgressIndicator());
185
+ }
186
+ return _buildLiveCard(liveList[index]);
187
+ },
188
+ ),
189
+ ),
190
+ );
191
+ }
192
+
193
+ Widget _buildLiveCard(LiveInfo liveInfo) {
194
+ return GestureDetector(
195
+ onTap: () => _onLiveCardTap(liveInfo),
196
+ child: Card(
197
+ clipBehavior: Clip.antiAlias,
198
+ child: Column(
199
+ crossAxisAlignment: CrossAxisAlignment.start,
200
+ children: [
201
+ Cover image
202
+ Expanded(
203
+ child: Stack(
204
+ fit: StackFit.expand,
205
+ children: [
206
+ Image.network(
207
+ liveInfo.coverURL,
208
+ fit: BoxFit.cover,
209
+ errorBuilder: (context, error, stackTrace) {
210
+ return Container(color: Colors.grey[300]);
211
+ },
212
+ ),
213
+ // Live stream tag
214
+ Positioned(
215
+ top: 8,
216
+ left: 8,
217
+ child: Container(
218
+ padding: const EdgeInsets.symmetric(
219
+ horizontal: 6,
220
+ vertical: 2,
221
+ ),
222
+ decoration: BoxDecoration(
223
+ color: Colors.red,
224
+ borderRadius: BorderRadius.circular(4),
225
+ ),
226
+ child: const Text(
227
+ Live streaming
228
+ style: TextStyle(color: Colors.white, fontSize: 10),
229
+ ),
230
+ ),
231
+ ),
232
+ viewers
233
+ Positioned(
234
+ bottom: 8,
235
+ right: 8,
236
+ child: Container(
237
+ padding: const EdgeInsets.symmetric(
238
+ horizontal: 6,
239
+ vertical: 2,
240
+ ),
241
+ decoration: BoxDecoration(
242
+ color: Colors.black54,
243
+ borderRadius: BorderRadius.circular(4),
244
+ ),
245
+ child: Row(
246
+ mainAxisSize: MainAxisSize.min,
247
+ children: [
248
+ const Icon(
249
+ Icons.visibility,
250
+ color: Colors.white,
251
+ size: 12,
252
+ ),
253
+ const SizedBox(width: 4),
254
+ Text(
255
+ '${liveInfo.totalViewerCount}',
256
+ style: const TextStyle(
257
+ color: Colors.white,
258
+ fontSize: 10,
259
+ ),
260
+ ),
261
+ ],
262
+ ),
263
+ ),
264
+ ),
265
+ ],
266
+ ),
267
+ ),
268
+ // Live streaming information
269
+ Padding(
270
+ padding: const EdgeInsets.all(8),
271
+ child: Column(
272
+ crossAxisAlignment: CrossAxisAlignment.start,
273
+ children: [
274
+ Text(
275
+ liveInfo.liveName,
276
+ maxLines: 1,
277
+ overflow: TextOverflow.ellipsis,
278
+ style: const TextStyle(fontWeight: FontWeight.bold),
279
+ ),
280
+ const SizedBox(height: 4),
281
+ Row(
282
+ children: [
283
+ CircleAvatar(
284
+ radius: 10,
285
+ backgroundImage: NetworkImage(
286
+ liveInfo.liveOwner.avatarURL,
287
+ ),
288
+ ),
289
+ const SizedBox(width: 4),
290
+ Expanded(
291
+ child: Text(
292
+ liveInfo.liveOwner.userName,
293
+ maxLines: 1,
294
+ overflow: TextOverflow.ellipsis,
295
+ style: TextStyle(
296
+ fontSize: 12,
297
+ color: Colors.grey[600],
298
+ ),
299
+ ),
300
+ ),
301
+ ],
302
+ ),
303
+ ],
304
+ ),
305
+ ),
306
+ ],
307
+ ),
308
+ ),
309
+ );
310
+ }
311
+ }
312
+
313
+ /// Audience viewing webpage (placeholder)
314
+ class YourAudiencePage extends StatelessWidget {
315
+ final String liveID;
316
+
317
+ const YourAudiencePage({super.key, required this.liveID});
318
+
319
+ @override
320
+ Widget build(BuildContext context) {
321
+ return Scaffold(
322
+ appBar: AppBar(title: const Text('Live Streaming Room')),
323
+ body: Center(child: Text('Live Streaming Room: $liveID')),
324
+ );
325
+ }
326
+ }
327
+ ```
328
+
329
+ **LiveInfo Parameter Description**
330
+ <table>
331
+ <tr>
332
+ <td rowspan="1" colSpan="1" >**Parameter Name**</td>
333
+
334
+ <td rowspan="1" colSpan="1" >**Type**</td>
335
+
336
+ <td rowspan="1" colSpan="1" >**Description**</td>
337
+ </tr>
338
+
339
+ <tr>
340
+ <td rowspan="1" colSpan="1" >`liveID`</td>
341
+
342
+ <td rowspan="1" colSpan="1" >`String`</td>
343
+
344
+ <td rowspan="1" colSpan="1" >Unique identifier of the live streaming room</td>
345
+ </tr>
346
+
347
+ <tr>
348
+ <td rowspan="1" colSpan="1" >`liveName`</td>
349
+
350
+ <td rowspan="1" colSpan="1" >`String`</td>
351
+
352
+ <td rowspan="1" colSpan="1" >Title of the live streaming room</td>
353
+ </tr>
354
+
355
+ <tr>
356
+ <td rowspan="1" colSpan="1" >`coverURL`</td>
357
+
358
+ <td rowspan="1" colSpan="1" >`String`</td>
359
+
360
+ <td rowspan="1" colSpan="1" >Thumbnail URL of the live streaming room</td>
361
+ </tr>
362
+
363
+ <tr>
364
+ <td rowspan="1" colSpan="1" >`liveOwner`</td>
365
+
366
+ <td rowspan="1" colSpan="1" >`LiveUserInfo`</td>
367
+
368
+ <td rowspan="1" colSpan="1" >Room owner's personal info</td>
369
+ </tr>
370
+
371
+ <tr>
372
+ <td rowspan="1" colSpan="1" >`totalViewerCount`</td>
373
+
374
+ <td rowspan="1" colSpan="1" >`int`</td>
375
+
376
+ <td rowspan="1" colSpan="1" >Total viewers in the live streaming room</td>
377
+ </tr>
378
+
379
+ <tr>
380
+ <td rowspan="1" colSpan="1" >`categoryList`</td>
381
+
382
+ <td rowspan="1" colSpan="1" >`List<int>`</td>
383
+
384
+ <td rowspan="1" colSpan="1" >Category tag list of the live streaming room</td>
385
+ </tr>
386
+
387
+ <tr>
388
+ <td rowspan="1" colSpan="1" >`notice`</td>
389
+
390
+ <td rowspan="1" colSpan="1" >`String`</td>
391
+
392
+ <td rowspan="1" colSpan="1" >Announcement information in the live streaming room</td>
393
+ </tr>
394
+
395
+ <tr>
396
+ <td rowspan="1" colSpan="1" >`metaData`</td>
397
+
398
+ <td rowspan="1" colSpan="1" >`Map<String, String>`</td>
399
+
400
+ <td rowspan="1" colSpan="1" >Custom metadata for complex business scenarios</td>
401
+ </tr>
402
+ </table>
403
+
404
+
405
+ ## Advanced Features
406
+
407
+ ### Scenario One: Categorize Live Broadcast List
408
+
409
+ On the live square page of the App, the top section features category tags such as "Popular," "Music," and "Gaming." When users click different tags, the live stream list below dynamically filters to show only live streaming rooms of the corresponding category, thereby helping users quickly identify content of interest.
410
+
411
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/e254a211f6d111f09af3525400a31896.png)
412
+
413
+
414
+ #### Implementation Method
415
+
416
+ The core is to use the `categoryList` property in the `LiveInfo` model. When the anchor starts live streaming and sets the category, the `LiveInfo` object returned by `fetchLiveList` will contain this classification information. After your App obtains the complete live list, just perform a simple filter on the client according to the user's selected category, then refresh the UI.
417
+
418
+ #### Sample Code
419
+
420
+ The following example shows how to create a `LiveListManager` to handle data and filter logic.
421
+ ``` java
422
+ import 'package:flutter/material.dart';
423
+ import 'package:atomic_x_core/atomicxcore.dart';
424
+
425
+ /// Live stream list data manager
426
+ class LiveListManager extends ChangeNotifier {
427
+ final _liveListStore = LiveListStore.shared;
428
+ List<LiveInfo> _fullLiveList = [];
429
+ List<LiveInfo> _filteredLiveList = [];
430
+ int? _selectedCategoryId;
431
+
432
+ List<LiveInfo> get filteredLiveList => _filteredLiveList;
433
+ int? get selectedCategoryId => _selectedCategoryId;
434
+ late final VoidCallback _liveListChangedListener = _onLiveListChanged;
435
+
436
+ LiveListManager() {
437
+ _liveListStore.liveState.liveList.addListener(_liveListChangedListener);
438
+ }
439
+
440
+ void _onLiveListChanged() {
441
+ _fullLiveList = _liveListStore.liveState.liveList.value;
442
+ _applyFilter();
443
+ }
444
+
445
+ Future<void> fetchFirstPage() async {
446
+ _liveListStore.reset();
447
+ await _liveListStore.fetchLiveList(cursor: '', count: 20);
448
+ }
449
+
450
+ /// Filter live stream list by category
451
+ void filterLiveList({int? categoryId}) {
452
+ _selectedCategoryId = categoryId;
453
+ _applyFilter();
454
+ }
455
+
456
+ void _applyFilter() {
457
+ if (_selectedCategoryId == null) {
458
+ // If categoryId is null, display the complete list
459
+ _filteredLiveList = List.from(_fullLiveList);
460
+ } else {
461
+ // Filter live streaming rooms by specified category
462
+ _filteredLiveList = _fullLiveList.where((liveInfo) {
463
+ return liveInfo.categoryList.contains(_selectedCategoryId);
464
+ }).toList();
465
+ }
466
+ notifyListeners();
467
+ }
468
+
469
+ @override
470
+ void dispose() {
471
+ _liveListStore.liveState.liveList.removeListener(_liveListChangedListener);
472
+ super.dispose();
473
+ }
474
+ }
475
+ ```
476
+ ``` java
477
+ /// Live stream list webpage with category filtering
478
+ class CategoryLiveListPage extends StatefulWidget {
479
+ const CategoryLiveListPage({super.key});
480
+
481
+ @override
482
+ State<CategoryLiveListPage> createState() => _CategoryLiveListPageState();
483
+ }
484
+
485
+ class _CategoryLiveListPageState extends State<CategoryLiveListPage> {
486
+ final _manager = LiveListManager();
487
+ late final VoidCallback _managerChangedListener = _onManagerChanged;
488
+
489
+ // Classification list (example)
490
+ final List<Map<String, dynamic>> _categories = [
491
+ {'id': null, 'name': 'All'},
492
+ {'id': 1, 'name': 'Music'},
493
+ {'id': 2, 'name': 'Game'},
494
+ {'id': 3, 'name': 'Chat'},
495
+ ];
496
+
497
+ @override
498
+ void initState() {
499
+ super.initState();
500
+ _manager.addListener(_managerChangedListener);
501
+ _manager.fetchFirstPage();
502
+ }
503
+
504
+ void _onManagerChanged() {
505
+ setState(() {});
506
+ }
507
+
508
+ // When the user clicks the top category tag
509
+ void _onCategoryTap(int? categoryId) {
510
+ _manager.filterLiveList(categoryId: categoryId);
511
+ }
512
+
513
+ @override
514
+ void dispose() {
515
+ _manager.removeListener(_managerChangedListener);
516
+ _manager.dispose();
517
+ super.dispose();
518
+ }
519
+
520
+ @override
521
+ Widget build(BuildContext context) {
522
+ return Scaffold(
523
+ appBar: AppBar(title: const Text('Live Streaming Plaza')),
524
+ body: Column(
525
+ children: [
526
+ // Category tag bar
527
+ SizedBox(
528
+ height: 50,
529
+ child: ListView.builder(
530
+ scrollDirection: Axis.horizontal,
531
+ padding: const EdgeInsets.symmetric(horizontal: 8),
532
+ itemCount: _categories.length,
533
+ itemBuilder: (context, index) {
534
+ final category = _categories[index];
535
+ final isSelected = _manager.selectedCategoryId == category['id'];
536
+ return Padding(
537
+ padding: const EdgeInsets.symmetric(horizontal: 4),
538
+ child: ChoiceChip(
539
+ label: Text(category['name']),
540
+ selected: isSelected,
541
+ onSelected: (_) => _onCategoryTap(category['id']),
542
+ ),
543
+ );
544
+ },
545
+ ),
546
+ ),
547
+ // Live stream list
548
+ Expanded(
549
+ child: _manager.filteredLiveList.isEmpty
550
+ ? const Center(child: Text('No live stream'))
551
+ : GridView.builder(
552
+ padding: const EdgeInsets.all(8),
553
+ gridDelegate:
554
+ const SliverGridDelegateWithFixedCrossAxisCount(
555
+ crossAxisCount: 2,
556
+ childAspectRatio: 0.75,
557
+ crossAxisSpacing: 8,
558
+ mainAxisSpacing: 8,
559
+ ),
560
+ itemCount: _manager.filteredLiveList.length,
561
+ itemBuilder: (context, index) {
562
+ final liveInfo = _manager.filteredLiveList[index];
563
+ return _buildLiveCard(liveInfo);
564
+ },
565
+ ),
566
+ ),
567
+ ],
568
+ ),
569
+ );
570
+ }
571
+
572
+ Widget _buildLiveCard(LiveInfo liveInfo) {
573
+ return Card(
574
+ clipBehavior: Clip.antiAlias,
575
+ child: Column(
576
+ crossAxisAlignment: CrossAxisAlignment.start,
577
+ children: [
578
+ Expanded(
579
+ child: Image.network(
580
+ liveInfo.coverURL,
581
+ fit: BoxFit.cover,
582
+ width: double.infinity,
583
+ errorBuilder: (context, error, stackTrace) {
584
+ return Container(color: Colors.grey[300]);
585
+ },
586
+ ),
587
+ ),
588
+ Padding(
589
+ padding: const EdgeInsets.all(8),
590
+ child: Text(
591
+ liveInfo.liveName,
592
+ maxLines: 1,
593
+ overflow: TextOverflow.ellipsis,
594
+ ),
595
+ ),
596
+ ],
597
+ ),
598
+ );
599
+ }
600
+ }
601
+ ```
602
+
603
+ ### Scenario Two: Show Product Information in Live Shopping
604
+
605
+ In an ecommerce live stream, the Anchor is explaining a product. At this point, a product card pops up at the bottom of the live stream for ALL audience, displaying the product image, name and price in real time. When the Anchor switches to the next product, the card on the client side updates automatically and synchronously.
606
+
607
+ ![](https://write-document-release-1258344699.cos.ap-guangzhou.tencentcos.cn/100032457995/e24959b0f6d111f0b34b525400ecee81.png)
608
+
609
+
610
+ #### Implementation Method
611
+
612
+ The Anchor sets the current product's structured information (recommend using **JSON** format) to a custom key (such as "`product_info`") through the `updateLiveMetaData` API setting. **AtomicXCore** will synchronize this update to ALL audience in real time. The client just needs to subscribe to `LiveListState.currentLive`, listen for changes in `metaData`, and once `product_info` is found updated, parse the JSON data and refresh the product card UI.
613
+
614
+ #### Sample Code
615
+ ``` java
616
+ import 'dart:convert';
617
+ import 'package:flutter/material.dart';
618
+ import 'package:atomic_x_core/atomicxcore.dart';
619
+
620
+ /// Product Information Model
621
+ class Product {
622
+ final String id;
623
+ final String name;
624
+ final String price;
625
+ final String imageUrl;
626
+
627
+ Product({
628
+ required this.id,
629
+ required this.name,
630
+ required this.price,
631
+ required this.imageUrl,
632
+ });
633
+
634
+ factory Product.fromJson(Map<String, dynamic> json) {
635
+ return Product(
636
+ id: json['id'] ?? '',
637
+ name: json['name'] ?? '',
638
+ price: json['price'] ?? '',
639
+ imageUrl: json['imageUrl'] ?? '',
640
+ );
641
+ }
642
+
643
+ Map<String, dynamic> toJson() {
644
+ return {
645
+ 'id': id,
646
+ 'name': name,
647
+ 'price': price,
648
+ 'imageUrl': imageUrl,
649
+ };
650
+ }
651
+ }
652
+
653
+ /// Anchor: Push product information
654
+ class HostProductController {
655
+ final _liveListStore = LiveListStore.shared;
656
+
657
+ Future<void> pushProductInfo(Product product) async {
658
+ final jsonString = jsonEncode(product.toJson());
659
+ final metaData = {'product_info': jsonString};
660
+
661
+ // Use Global Singleton liveListStore to update metaData
662
+ final result = await _liveListStore.updateLiveMetaData(metaData);
663
+ if (result.isSuccess) {
664
+ debugPrint('Product ${product.name} pushed successfully');
665
+ } else {
666
+ debugPrint('Product information sending failed: ${result.message}');
667
+ }
668
+ }
669
+ }
670
+
671
+ /// Client: Product card component
672
+ class ProductCardWidget extends StatefulWidget {
673
+ const ProductCardWidget({super.key});
674
+
675
+ @override
676
+ State<ProductCardWidget> createState() => _ProductCardWidgetState();
677
+ }
678
+
679
+ class _ProductCardWidgetState extends State<ProductCardWidget> {
680
+ final _liveListStore = LiveListStore.shared;
681
+ Product? _currentProduct;
682
+ String? _lastProductInfo;
683
+ late final VoidCallback _currentLiveChangedListener = _onCurrentLiveChanged;
684
+
685
+ @override
686
+ void initState() {
687
+ super.initState();
688
+ _liveListStore.liveState.currentLive.addListener(_currentLiveChangedListener);
689
+ }
690
+
691
+ void _onCurrentLiveChanged() {
692
+ final currentLive = _liveListStore.liveState.currentLive.value;
693
+ final productInfo = currentLive.metaData['product_info'];
694
+
695
+ // Check whether changed
696
+ if (productInfo != _lastProductInfo) {
697
+ _lastProductInfo = productInfo;
698
+ _parseProductInfo(productInfo);
699
+ }
700
+ }
701
+
702
+ void _parseProductInfo(String? jsonString) {
703
+ if (jsonString == null || jsonString.isEmpty) {
704
+ setState(() => _currentProduct = null);
705
+ return;
706
+ }
707
+
708
+ try {
709
+ final json = jsonDecode(jsonString) as Map<String, dynamic>;
710
+ final product = Product.fromJson(json);
711
+ setState(() => _currentProduct = product);
712
+ } catch (e) {
713
+ debugPrint('Failed to parse product information: $e');
714
+ }
715
+ }
716
+
717
+ @override
718
+ void dispose() {
719
+ _liveListStore.liveState.currentLive.removeListener(_currentLiveChangedListener);
720
+ super.dispose();
721
+ }
722
+
723
+ @override
724
+ Widget build(BuildContext context) {
725
+ if (_currentProduct == null) {
726
+ return const SizedBox.shrink();
727
+ }
728
+
729
+ return Container(
730
+ margin: const EdgeInsets.all(16),
731
+ padding: const EdgeInsets.all(12),
732
+ decoration: BoxDecoration(
733
+ color: Colors.white,
734
+ borderRadius: BorderRadius.circular(12),
735
+ boxShadow: [
736
+ BoxShadow(
737
+ color: Colors.black.withOpacity(0.1),
738
+ blurRadius: 8,
739
+ offset: const Offset(0, 2),
740
+ ),
741
+ ],
742
+ ),
743
+ child: Row(
744
+ children: [
745
+ product image
746
+ ClipRRect(
747
+ borderRadius: BorderRadius.circular(8),
748
+ child: Image.network(
749
+ _currentProduct!.imageUrl,
750
+ width: 80,
751
+ height: 80,
752
+ fit: BoxFit.cover,
753
+ errorBuilder: (context, error, stackTrace) {
754
+ return Container(
755
+ width: 80,
756
+ height: 80,
757
+ color: Colors.grey[300],
758
+ child: const Icon(Icons.image),
759
+ );
760
+ },
761
+ ),
762
+ ),
763
+ const SizedBox(width: 12),
764
+ product information
765
+ Expanded(
766
+ child: Column(
767
+ crossAxisAlignment: CrossAxisAlignment.start,
768
+ mainAxisSize: MainAxisSize.min,
769
+ children: [
770
+ Text(
771
+ _currentProduct!.name,
772
+ style: const TextStyle(
773
+ fontSize: 14,
774
+ fontWeight: FontWeight.bold,
775
+ ),
776
+ maxLines: 2,
777
+ overflow: TextOverflow.ellipsis,
778
+ ),
779
+ const SizedBox(height: 4),
780
+ Text(
781
+ '¥${_currentProduct!.price}',
782
+ style: const TextStyle(
783
+ fontSize: 16,
784
+ color: Colors.red,
785
+ fontWeight: FontWeight.bold,
786
+ ),
787
+ ),
788
+ ],
789
+ ),
790
+ ),
791
+ purchase button
792
+ ElevatedButton(
793
+ onPressed: () {
794
+ // Implement purchase logic
795
+ debugPrint('Purchase goods: ${_currentProduct!.name}');
796
+ },
797
+ style: ElevatedButton.styleFrom(
798
+ backgroundColor: Colors.orange,
799
+ foregroundColor: Colors.white,
800
+ ),
801
+ child: const Text('Buy now'),
802
+ ),
803
+ ],
804
+ ),
805
+ );
806
+ }
807
+ }
808
+ ```
809
+
810
+ ## API documentation
811
+
812
+ For detailed information about ALL public interfaces, properties and methods of [LiveListStore](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListStore-class.html) and its related classes, please refer to the official API documentation of the [AtomicXCore](https://tencent-rtc.github.io/TUIKit_Flutter/index.html) framework. The related Store used in this document is as follows:
813
+ <table>
814
+ <tr>
815
+ <td rowspan="1" colSpan="1" >**Store/Component**</td>
816
+
817
+ <td rowspan="1" colSpan="1" >**Feature Description**</td>
818
+
819
+ <td rowspan="1" colSpan="1" >**API Reference**</td>
820
+ </tr>
821
+
822
+ <tr>
823
+ <td rowspan="1" colSpan="1" >**LiveCoreWidget**</td>
824
+
825
+ <td rowspan="1" colSpan="1" >The core view component for live video stream display and interaction: responsible for video stream rendering and view widget processing, supporting live streaming, audience mic connection, Anchor Connection (TUILiveKit), and other scenarios.</td>
826
+
827
+ <td rowspan="1" colSpan="1" >[API documentation](https://tencent-rtc.github.io/TUIKit_Flutter/api_view_live_live_core_widget/LiveCoreWidget-class.html)</td>
828
+ </tr>
829
+
830
+ <tr>
831
+ <td rowspan="1" colSpan="1" >**LiveCoreController**</td>
832
+
833
+ <td rowspan="1" colSpan="1" >Controller for LiveCoreWidget: used to set up live streaming ID, control preview, and perform other operations.</td>
834
+
835
+ <td rowspan="1" colSpan="1" >[API documentation](https://tencent-rtc.github.io/TUIKit_Flutter/api_view_live_live_core_widget/LiveCoreController-class.html)</td>
836
+ </tr>
837
+
838
+ <tr>
839
+ <td rowspan="1" colSpan="1" >**LiveListStore**</td>
840
+
841
+ <td rowspan="1" colSpan="1" >Full lifecycle management of live streaming room: create/join/leave/terminate room, query room list, modify live information (name, notice), listen to live streaming status (for example being kicked out, ended).</td>
842
+
843
+ <td rowspan="1" colSpan="1" >[API documentation](https://tencent-rtc.github.io/TUIKit_Flutter/api_live_live_list_store/LiveListStore-class.html)</td>
844
+ </tr>
845
+ </table>
846
+
847
+
848
+ ## FAQs
849
+
850
+ ### What Are the Limitations and Rules to Note When Using `UpdateLiveMetaData`?
851
+
852
+ To ensure system stability and efficiency, the usage of `metaData` follows the following rules:
853
+ - **Permission**: Only the room owner and admin can call the API `updateLiveMetaData`. General audience has no permission.
854
+
855
+ - **Quantity and size limit: **
856
+
857
+ - A single room supports up to **10** keys.
858
+
859
+ - The length of each key does not exceed **50 bytes**, and the length of each value does not exceed **2KB**.
860
+
861
+ - The total size of ALL values in a single room is no more than **16KB**.
862
+
863
+ - **Conflict resolution**: The update mechanism for `metaData` is "last write wins". If multiple administrators modify the same key in a short time frame, the last modification will take effect. It is advisable to avoid multiple users simultaneously adjusting the same key information in service design.
864
+
865
+