asset_screen.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import '../../../core/l10n/app_localizations.dart';
  4. import '../../../core/theme/app_colors.dart';
  5. import '../../../core/utils/number_format.dart';
  6. import '../../../providers/asset_provider.dart';
  7. import '../../../providers/my_copy_trading_provider.dart';
  8. import '../../widgets/common/app_shimmer.dart';
  9. import '../../widgets/common/app_tab_bar.dart';
  10. import '../../widgets/common/network_error_body.dart';
  11. import 'asset_overview_tab.dart';
  12. import 'asset_futures_tab.dart';
  13. import 'asset_copy_trading_tab.dart';
  14. import 'asset_spot_tab.dart';
  15. import 'asset_spot_trading_tab.dart';
  16. import 'asset_staking_tab.dart';
  17. class AssetScreen extends ConsumerStatefulWidget {
  18. const AssetScreen({super.key, this.initialSubTab});
  19. /// 资产页子 Tab 初始索引(0=总览 … 5=锁仓账户),对齐 Web `?tab=staking`
  20. final int? initialSubTab;
  21. @override
  22. ConsumerState<AssetScreen> createState() => _AssetScreenState();
  23. }
  24. class _AssetScreenState extends ConsumerState<AssetScreen>
  25. with SingleTickerProviderStateMixin {
  26. late TabController _tabController;
  27. late PageController _pageController;
  28. List<String> _getLocalizedTabs(BuildContext context) {
  29. final l10n = AppLocalizations.of(context)!;
  30. return [
  31. l10n.assetOverview,
  32. l10n.fund,
  33. l10n.spotTab,
  34. l10n.futures,
  35. l10n.copyTrading,
  36. l10n.assetsLockedStakingAccount,
  37. ];
  38. }
  39. int get _initialTabIndex {
  40. final subTab = widget.initialSubTab;
  41. if (subTab != null && subTab >= 0 && subTab < 6) {
  42. return subTab;
  43. }
  44. return 0;
  45. }
  46. @override
  47. void initState() {
  48. super.initState();
  49. final initialIndex = _initialTabIndex;
  50. _tabController = TabController(
  51. length: 6,
  52. vsync: this,
  53. initialIndex: initialIndex,
  54. );
  55. _pageController = PageController(initialPage: initialIndex);
  56. ref.read(currentAssetSubTabProvider.notifier).state = initialIndex;
  57. // 拖动 PageView → 实时更新指示器 offset(平滑插值)
  58. _pageController.addListener(() {
  59. if (!_pageController.hasClients) return;
  60. final page = _pageController.page!;
  61. final offset = page - _tabController.index;
  62. if (offset.abs() <= 1.0 && !_tabController.indexIsChanging) {
  63. _tabController.offset = offset.clamp(-1.0, 1.0);
  64. }
  65. });
  66. _tabController.addListener(_onTabChanged);
  67. if (initialIndex > 0) {
  68. WidgetsBinding.instance.addPostFrameCallback((_) {
  69. if (!mounted) {
  70. return;
  71. }
  72. _refreshForTab(initialIndex);
  73. });
  74. }
  75. }
  76. @override
  77. void didUpdateWidget(AssetScreen oldWidget) {
  78. super.didUpdateWidget(oldWidget);
  79. final subTab = widget.initialSubTab;
  80. if (subTab == null ||
  81. subTab < 0 ||
  82. subTab >= _tabController.length ||
  83. subTab == oldWidget.initialSubTab) {
  84. return;
  85. }
  86. WidgetsBinding.instance.addPostFrameCallback((_) {
  87. if (!mounted) {
  88. return;
  89. }
  90. _jumpToSubTab(subTab);
  91. });
  92. }
  93. /// 无动画切到指定子 Tab(外部深链进入锁仓账户等)
  94. void _jumpToSubTab(int index) {
  95. if (_tabController.index != index) {
  96. _tabController.index = index;
  97. }
  98. if (_pageController.hasClients) {
  99. _pageController.jumpToPage(index);
  100. }
  101. ref.read(currentAssetSubTabProvider.notifier).state = index;
  102. _refreshForTab(index);
  103. setState(() {});
  104. }
  105. void _onTabChanged() {
  106. if (_tabController.indexIsChanging) {
  107. // Tab 点击 → 驱动 PageView 动画
  108. _pageController.animateToPage(
  109. _tabController.index,
  110. duration: const Duration(milliseconds: 280),
  111. curve: Curves.easeOut,
  112. );
  113. } else {
  114. // Tab 切换完成:更新当前子 tab 索引并按需刷新
  115. final idx = _tabController.index;
  116. final prevIdx = ref.read(currentAssetSubTabProvider);
  117. ref.read(currentAssetSubTabProvider.notifier).state = idx;
  118. if (prevIdx == 2 && idx != 2) {
  119. ref.read(assetProvider.notifier).onSpotTradingTabHidden();
  120. }
  121. WidgetsBinding.instance.addPostFrameCallback((_) {
  122. if (mounted) _refreshForTab(idx);
  123. });
  124. // 仅合约 tab(索引 3)需要定时拉持仓;现货 tab 首次进入 HTTP,后续走 WS
  125. final notifier = ref.read(assetProvider.notifier);
  126. if (idx == 3) {
  127. notifier.startPositionPolling();
  128. } else {
  129. notifier.stopPositionPolling();
  130. }
  131. }
  132. setState(() {});
  133. }
  134. void _refreshForTab(int tabIndex) {
  135. final n = ref.read(assetProvider.notifier);
  136. if (tabIndex == 2) {
  137. n.onSpotTradingTabVisible();
  138. } else {
  139. n.silentRefresh();
  140. }
  141. if (tabIndex == 4) {
  142. // 跟单 tab 额外刷新跟单仓位
  143. ref.read(myCopyTradingProvider.notifier).silentRefresh();
  144. } else if (tabIndex == 5) {
  145. n.loadStakingOrders();
  146. }
  147. }
  148. void _navigateToTab(int index) {
  149. _tabController.animateTo(index);
  150. _pageController.animateToPage(
  151. index,
  152. duration: const Duration(milliseconds: 280),
  153. curve: Curves.easeOut,
  154. );
  155. }
  156. @override
  157. void dispose() {
  158. ref.read(assetProvider.notifier).onSpotTradingTabHidden();
  159. ref.read(assetProvider.notifier).stopPositionPolling();
  160. _tabController.dispose();
  161. _pageController.dispose();
  162. super.dispose();
  163. }
  164. String _getTabLabel(int index) {
  165. const labels = [
  166. 'asset_tab_overview',
  167. 'asset_tab_funds',
  168. 'asset_tab_spot_trading',
  169. 'asset_tab_futures',
  170. 'asset_tab_copy_trading',
  171. 'asset_tab_staking',
  172. ];
  173. return labels[index];
  174. }
  175. @override
  176. Widget build(BuildContext context) {
  177. final state = ref.watch(assetProvider);
  178. final notifier = ref.read(assetProvider.notifier);
  179. return Scaffold(
  180. body: SafeArea(
  181. child: Column(
  182. children: [
  183. // ── Tab 栏 ─────────────────────────────────────
  184. TabBar(
  185. controller: _tabController,
  186. isScrollable: true,
  187. indicator: StretchTabIndicator(
  188. controller: _tabController,
  189. color: AppColors.brand,
  190. height: 3,
  191. borderRadius: 1.5,
  192. ),
  193. indicatorSize: TabBarIndicatorSize.label,
  194. labelStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
  195. unselectedLabelStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
  196. tabAlignment: TabAlignment.start,
  197. dividerColor: Colors.transparent,
  198. labelPadding: const EdgeInsets.symmetric(horizontal: 14),
  199. padding: const EdgeInsets.fromLTRB(4, 8, 16, 0),
  200. tabs: <Widget>[
  201. for (int i = 0; i < 6; i++)
  202. Semantics(
  203. label: _getTabLabel(i),
  204. button: true,
  205. child: Tab(text: _getLocalizedTabs(context)[i]),
  206. ),
  207. ],
  208. ),
  209. // ── Tab 内容 ───────────────────────────────────
  210. Expanded(
  211. child: _buildContent(state, notifier),
  212. ),
  213. ],
  214. ),
  215. ),
  216. );
  217. }
  218. Widget _buildContent(AssetState state, AssetNotifier notifier) {
  219. if (state.isLoading) return const _AssetShimmer();
  220. if (state.errorMessage != null) {
  221. return NetworkErrorBody(onRetry: notifier.loadAssets);
  222. }
  223. return PageView(
  224. controller: _pageController,
  225. physics: const BouncingScrollPhysics(
  226. parent: AlwaysScrollableScrollPhysics(),
  227. ),
  228. onPageChanged: (index) {
  229. if (_tabController.indexIsChanging) return;
  230. _tabController.index = index;
  231. },
  232. children: [
  233. AssetOverviewTab(
  234. state: state,
  235. notifier: notifier,
  236. onNavigateToStaking: () => _navigateToTab(5),
  237. ),
  238. AssetSpotTab(state: state, notifier: notifier),
  239. AssetSpotTradingTab(state: state, notifier: notifier),
  240. AssetFuturesTab(state: state, notifier: notifier),
  241. AssetCopyTradingTab(state: state, notifier: notifier),
  242. AssetStakingTab(state: state, notifier: notifier),
  243. ],
  244. );
  245. }
  246. }
  247. // ── 资产骨架屏 ──────────────────────────────────────────────
  248. class _AssetShimmer extends StatelessWidget {
  249. const _AssetShimmer();
  250. @override
  251. Widget build(BuildContext context) {
  252. return AppShimmer(
  253. child: SingleChildScrollView(
  254. physics: const NeverScrollableScrollPhysics(),
  255. padding: const EdgeInsets.all(16),
  256. child: Column(
  257. crossAxisAlignment: CrossAxisAlignment.start,
  258. children: [
  259. // 资产估值头部
  260. shimmerBox(80, 13),
  261. const SizedBox(height: 10),
  262. shimmerBox(180, 34),
  263. const SizedBox(height: 6),
  264. shimmerBox(120, 13),
  265. const SizedBox(height: 24),
  266. // 账户卡片列表
  267. ...List.generate(3, (_) => Padding(
  268. padding: const EdgeInsets.only(bottom: 12),
  269. child: Container(
  270. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  271. decoration: BoxDecoration(
  272. color: Colors.white,
  273. borderRadius: BorderRadius.circular(12),
  274. ),
  275. child: Row(
  276. children: [
  277. shimmerCircle(22),
  278. const SizedBox(width: 12),
  279. shimmerBox(80, 14),
  280. const Spacer(),
  281. Column(
  282. crossAxisAlignment: CrossAxisAlignment.end,
  283. children: [
  284. shimmerBox(90, 15),
  285. const SizedBox(height: 4),
  286. shimmerBox(60, 12),
  287. ],
  288. ),
  289. const SizedBox(width: 8),
  290. shimmerBox(18, 18, radius: 4),
  291. ],
  292. ),
  293. ),
  294. )),
  295. const SizedBox(height: 8),
  296. // 持仓卡片
  297. Container(
  298. padding: const EdgeInsets.all(16),
  299. decoration: BoxDecoration(
  300. color: Colors.white,
  301. borderRadius: BorderRadius.circular(12),
  302. ),
  303. child: Column(
  304. children: [
  305. Row(
  306. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  307. children: [shimmerBox(100, 14), shimmerBox(60, 14)],
  308. ),
  309. const SizedBox(height: 12),
  310. Row(
  311. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  312. children: List.generate(3, (_) => shimmerBox(70, 40, radius: 6)),
  313. ),
  314. ],
  315. ),
  316. ),
  317. ],
  318. ),
  319. ),
  320. );
  321. }
  322. }
  323. // ══════════════════════════════════════════════════════════════
  324. // 共享组件(供各 Tab 文件使用)
  325. // ══════════════════════════════════════════════════════════════
  326. /// 各 Tab 通用的资产估值头部
  327. class AssetHeader extends StatelessWidget {
  328. const AssetHeader({super.key, required this.state, required this.notifier});
  329. final AssetState state;
  330. final AssetNotifier notifier;
  331. @override
  332. Widget build(BuildContext context) {
  333. final cs = Theme.of(context).colorScheme;
  334. final total = state.totalUsdtValue;
  335. final display = state.obscureBalance ? '******' : formatPrice(total, decimalPlaces: 2);
  336. return Padding(
  337. padding: const EdgeInsets.fromLTRB(16, 20, 16, 0),
  338. child: Column(
  339. crossAxisAlignment: CrossAxisAlignment.start,
  340. children: [
  341. Row(
  342. children: [
  343. Text(AppLocalizations.of(context)!.assetValuation, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 13)),
  344. const SizedBox(width: 6),
  345. Semantics(
  346. label: 'asset_btn_toggle_balance',
  347. button: true,
  348. enabled: true,
  349. onTap: notifier.toggleObscure,
  350. child: GestureDetector(
  351. onTap: notifier.toggleObscure,
  352. child: Icon(
  353. state.obscureBalance ? Icons.visibility_off_outlined : Icons.visibility_outlined,
  354. size: 16, color: cs.onSurface.withAlpha(153),
  355. ),
  356. ),
  357. ),
  358. ],
  359. ),
  360. const SizedBox(height: 8),
  361. Row(
  362. crossAxisAlignment: CrossAxisAlignment.end,
  363. children: [
  364. Semantics(
  365. label: 'asset_text_total_value',
  366. enabled: true,
  367. child: Text(display, style: TextStyle(color: cs.onSurface, fontSize: 32, fontWeight: FontWeight.w700, letterSpacing: -0.5)),
  368. ),
  369. const SizedBox(width: 6),
  370. Semantics(
  371. label: 'asset_text_currency',
  372. enabled: true,
  373. child: Padding(
  374. padding: const EdgeInsets.only(bottom: 5),
  375. child: Text('USDT', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 14)),
  376. ),
  377. ),
  378. ],
  379. ),
  380. ],
  381. ),
  382. );
  383. }
  384. }
  385. /// 合约/跟单持仓卡片
  386. class AssetPositionCard extends StatelessWidget {
  387. const AssetPositionCard({
  388. super.key,
  389. required this.obscure,
  390. this.symbol = 'BTCUSDT',
  391. this.isLong = true,
  392. this.positionType = '',
  393. this.leverage = '20X',
  394. this.pnl = '+24.35',
  395. this.pnlColor,
  396. this.roi = '+5.36%',
  397. this.rows = const [],
  398. this.onShare,
  399. });
  400. final bool obscure;
  401. final String symbol;
  402. final bool isLong;
  403. final String positionType;
  404. final String leverage;
  405. final String pnl;
  406. final Color? pnlColor;
  407. final String roi;
  408. final List<List<String>> rows;
  409. final VoidCallback? onShare;
  410. @override
  411. Widget build(BuildContext context) {
  412. final cs = Theme.of(context).colorScheme;
  413. final isDark = Theme.of(context).brightness == Brightness.dark;
  414. final sideColor = isLong ? AppColors.rise : AppColors.fall;
  415. final effectivePnlColor = pnlColor ?? AppColors.rise;
  416. return Container(
  417. margin: const EdgeInsets.symmetric(horizontal: 16),
  418. padding: const EdgeInsets.all(16),
  419. decoration: BoxDecoration(
  420. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  421. borderRadius: BorderRadius.circular(12),
  422. border: isDark ? null : Border.all(color: AppColors.lightBorder, width: 0.5),
  423. ),
  424. child: Column(
  425. crossAxisAlignment: CrossAxisAlignment.start,
  426. children: [
  427. // 头部:币种 + 标签
  428. Row(
  429. children: [
  430. Container(
  431. width: 32, height: 32,
  432. decoration: BoxDecoration(color: sideColor, borderRadius: BorderRadius.circular(8)),
  433. child: Center(
  434. child: Text(isLong ? AppLocalizations.of(context)!.longLabel : AppLocalizations.of(context)!.shortLabel,
  435. style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w700)),
  436. ),
  437. ),
  438. const SizedBox(width: 10),
  439. Text(symbol, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)),
  440. const SizedBox(width: 8),
  441. _Tag(label: AppLocalizations.of(context)!.perpetual),
  442. const SizedBox(width: 4),
  443. _Tag(label: positionType),
  444. const SizedBox(width: 4),
  445. Container(
  446. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  447. decoration: BoxDecoration(
  448. color: cs.onSurface.withAlpha(20),
  449. borderRadius: BorderRadius.circular(4),
  450. ),
  451. child: Text(leverage, style: TextStyle(color: cs.onSurface, fontSize: 11, fontWeight: FontWeight.w600)),
  452. ),
  453. const Spacer(),
  454. GestureDetector(
  455. onTap: onShare,
  456. child: Icon(Icons.share_outlined, size: 18, color: cs.onSurface.withAlpha(153)),
  457. ),
  458. ],
  459. ),
  460. const SizedBox(height: 14),
  461. // 未实现盈亏 + 收益率
  462. Container(
  463. padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
  464. decoration: const BoxDecoration(),
  465. child: Row(
  466. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  467. children: [
  468. Column(
  469. crossAxisAlignment: CrossAxisAlignment.start,
  470. children: [
  471. Text('${AppLocalizations.of(context)!.unrealizedPnl} (USDT)', style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)),
  472. const SizedBox(height: 2),
  473. Text(
  474. obscure ? '******' : pnl,
  475. style: TextStyle(color: effectivePnlColor, fontSize: 20, fontWeight: FontWeight.w700),
  476. ),
  477. ],
  478. ),
  479. Column(
  480. crossAxisAlignment: CrossAxisAlignment.end,
  481. children: [
  482. Text(AppLocalizations.of(context)!.returnRate, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 11)),
  483. const SizedBox(height: 2),
  484. Text(roi, style: TextStyle(color: effectivePnlColor, fontSize: 16, fontWeight: FontWeight.w600)),
  485. ],
  486. ),
  487. ],
  488. ),
  489. ),
  490. const SizedBox(height: 14),
  491. // 数据网格
  492. _DataGrid(obscure: obscure, rows: rows),
  493. ],
  494. ),
  495. );
  496. }
  497. }
  498. /// 账户行
  499. class AssetAccountRow extends StatelessWidget {
  500. const AssetAccountRow({
  501. super.key,
  502. required this.icon,
  503. required this.label,
  504. required this.amount,
  505. required this.usdAmount,
  506. this.onTransfer,
  507. this.onTap,
  508. });
  509. final IconData icon;
  510. final String label;
  511. final String amount;
  512. final String usdAmount;
  513. final VoidCallback? onTransfer;
  514. final VoidCallback? onTap;
  515. @override
  516. Widget build(BuildContext context) {
  517. final cs = Theme.of(context).colorScheme;
  518. final isDark = Theme.of(context).brightness == Brightness.dark;
  519. return GestureDetector(
  520. onTap: onTap,
  521. behavior: HitTestBehavior.opaque,
  522. child: Container(
  523. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  524. decoration: BoxDecoration(
  525. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  526. borderRadius: BorderRadius.circular(12),
  527. border: isDark ? null : Border.all(color: AppColors.lightBorder, width: 0.5),
  528. ),
  529. child: Row(
  530. children: [
  531. Icon(icon, color: cs.onSurface.withAlpha(153), size: 22),
  532. const SizedBox(width: 12),
  533. Expanded(
  534. child: Text(label,
  535. style: TextStyle(
  536. color: cs.onSurface,
  537. fontSize: 14,
  538. fontWeight: FontWeight.w500)),
  539. ),
  540. if (onTransfer != null) ...[
  541. OutlinedButton(
  542. onPressed: onTransfer,
  543. style: OutlinedButton.styleFrom(
  544. side: const BorderSide(color: AppColors.brand),
  545. foregroundColor: AppColors.brand,
  546. minimumSize: const Size(0, 32),
  547. padding: const EdgeInsets.symmetric(horizontal: 12),
  548. tapTargetSize: MaterialTapTargetSize.shrinkWrap,
  549. ),
  550. child: Text(
  551. AppLocalizations.of(context)!.transfer,
  552. style: const TextStyle(fontSize: 12),
  553. ),
  554. ),
  555. const SizedBox(width: 12),
  556. ],
  557. Column(
  558. crossAxisAlignment: CrossAxisAlignment.end,
  559. children: [
  560. Text(amount, style: TextStyle(color: cs.onSurface, fontSize: 15, fontWeight: FontWeight.w600)),
  561. if (usdAmount.isNotEmpty)
  562. Text(usdAmount, style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 12)),
  563. ],
  564. ),
  565. const SizedBox(width: 8),
  566. Icon(
  567. Icons.chevron_right,
  568. size: 18,
  569. color: cs.onSurface.withAlpha(102),
  570. ),
  571. ],
  572. ),
  573. ),
  574. );
  575. }
  576. }
  577. // ── 内部私有组件 ──────────────────────────────────────────
  578. class _Tag extends StatelessWidget {
  579. const _Tag({required this.label});
  580. final String label;
  581. @override
  582. Widget build(BuildContext context) {
  583. final cs = Theme.of(context).colorScheme;
  584. return Container(
  585. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  586. decoration: BoxDecoration(
  587. border: Border.all(color: cs.onSurface.withAlpha(60)),
  588. borderRadius: BorderRadius.circular(4),
  589. ),
  590. child: Text(label, style: TextStyle(color: cs.onSurface.withAlpha(153), fontSize: 10)),
  591. );
  592. }
  593. }
  594. class _DataGrid extends StatelessWidget {
  595. const _DataGrid({required this.obscure, required this.rows});
  596. final bool obscure;
  597. final List<List<String>> rows;
  598. @override
  599. Widget build(BuildContext context) {
  600. final cs = Theme.of(context).colorScheme;
  601. return Column(
  602. children: rows.map((row) {
  603. return Padding(
  604. padding: const EdgeInsets.only(bottom: 12),
  605. child: Row(
  606. children: [
  607. for (int i = 0; i < row.length; i += 2) ...[
  608. Expanded(
  609. child: Column(
  610. crossAxisAlignment: i == 0
  611. ? CrossAxisAlignment.start
  612. : i + 2 >= row.length
  613. ? CrossAxisAlignment.end
  614. : CrossAxisAlignment.center,
  615. children: [
  616. Text(row[i],
  617. style: TextStyle(color: cs.onSurface.withAlpha(120), fontSize: 11),
  618. textAlign: i == 0
  619. ? TextAlign.left
  620. : i + 2 >= row.length
  621. ? TextAlign.right
  622. : TextAlign.center),
  623. const SizedBox(height: 2),
  624. Text(
  625. obscure ? '***' : row[i + 1],
  626. style: TextStyle(color: cs.onSurface, fontSize: 13, fontWeight: FontWeight.w500),
  627. textAlign: i == 0
  628. ? TextAlign.left
  629. : i + 2 >= row.length
  630. ? TextAlign.right
  631. : TextAlign.center,
  632. ),
  633. ],
  634. ),
  635. ),
  636. ],
  637. ],
  638. ),
  639. );
  640. }).toList(),
  641. );
  642. }
  643. }