一、项目概述

1.1 项目功能

该项目是基于 Flutter 开发的「GitCode 口袋工具」,核心功能包括:

  • 底部 Tab 导航切换首页、仓库、组织、个人中心页面;
  • 首页支持搜索 GitCode 平台的仓库 / 用户,支持下拉刷新、上拉加载更多;
  • 封装通用分页控制器,实现统一的分页加载、状态管理逻辑;
  • 网络请求层封装,支持仓库 / 用户搜索的 API 调用;
  • 数据模型与 JSON 解析,保证数据结构的一致性;
  • 自定义 UI 组件(仓库卡片、用户卡片)展示搜索结果。

1.2 技术栈

  • 核心框架:Flutter(Material3)、Dart;
  • 状态管理:Dart Stream/StreamController(轻量级异步状态管理);
  • 网络请求:http 包(HTTP 请求)、超时处理;
  • 第三方库:pull_to_refresh(下拉刷新 / 上拉加载);

1.3 组织结构

应用入口

  • 核心文件:main.dart
  • 模块职责:应用初始化和全局导航管理,是项目的 “根入口”。
    • 应用初始化
    • 底部导航:通过MainNavigationPage实现 4 个核心页面(首页、仓库、组织、我的)的切换,管理BottomNavigationBar的选中状态;
    • 路由管理:当前为静态路由(直接切换页面),无动态路由配置,可后续扩展。
  • 模块特性
    • 全局唯一:整个应用的根 Widget,控制页面层级;
    • 导航统一:底部导航的样式、切换逻辑全局统一。

页面模块(Pages)

作为用户交互的直接载体,按业务场景拆分为多个页面,分为核心业务页面和占位页面两类:

(1)核心业务页面
  • home_page.dart(首页 / 搜索页):
    • 核心职责:整合搜索功能(仓库 / 用户切换)、搜索框交互、分页结果展示、错误提示;
    • 核心逻辑:监听 Tab 切换(仓库 / 用户)、触发搜索请求、监听控制器的状态流(数据 / 错误)、渲染对应的卡片列表;
    • 依赖关系:依赖控制器模块、UI 组件模块、核心基础模块。
  • repository_list_page.dart(仓库列表页):
    • 核心职责:独立的仓库搜索结果页,复用分页控制器,支持刷新 / 加载更多;
    • 特性:接收外部传入的搜索关键词,专注于仓库数据展示,与首页的仓库搜索逻辑复用同一套控制器和网络请求。
(2)占位页面(待扩展)
  • repository_page.dart(仓库页)、organization_page.dart(组织页)、profile_page.dart(我的页):
    • 核心职责:仅提供基础页面结构(Scaffold+AppBar + 文本),无业务逻辑,为后续功能扩展预留;
    • 特性:StatelessWidget,结构简单,可快速接入核心层能力。

控制器模块(Controllers)

  • 核心文件:pagination_controller.dart
  • 模块职责:封装通用业务逻辑(分页加载),是 “核心层” 与 “页面层” 的桥梁,负责状态管理和逻辑复用。
    • 核心能力:下拉刷新、上拉加载更多、加载状态控制(防止重复加载)、错误状态分发;
    • 状态管理:通过StreamController暴露三个核心流 ——dataStream(数据)、loadingStream(加载状态)、errorStream(错误信息),供页面层监听;
    • 逻辑封装:统一处理 “刷新重置页码、加载更多判断是否有数据、请求异常处理” 等通用逻辑。
  • 依赖关系:依赖核心基础模块(网络、数据模型);被页面模块依赖。
  • 模块特性
    • 泛型设计:支持任意数据类型(仓库 / 用户),可复用于多个页面;

UI 组件模块(Widgets)

  • 核心文件:repository_card.dart、user_card.dart
  • 模块职责:提供可复用的原子级 UI 组件,仅负责数据展示,无业务逻辑。
    • RepositoryCard:仓库卡片组件,接收GitCodeRepository实体类,渲染仓库名、描述、编程语言、星数等信息;
    • UserCard:用户卡片组件,接收GitCodeUser实体类,渲染头像、用户名、用户类型,支持点击事件(当前仅弹窗提示)。
  • 依赖关系:依赖数据模型模块;被页面模块依赖。
  • 模块特性
    • 纯展示型:StatelessWidget,数据驱动渲染,无状态管理;
    • 样式统一:卡片样式、间距、图标等全局统一,便于 UI 迭代。5. 页面模块(Pages)

核心基础模块(Core)

提供数据请求、数据结构定义的核心能力,分为两个子模块:

(1)网络请求子模块(ApiClient)
  • 核心文件:api_client.dart
  • 模块职责:封装 GitCode API 的请求逻辑,是应用与后端交互的唯一入口。
    • 实现两大核心接口:searchRepositories(仓库搜索)、searchUsers(用户搜索);
    • 处理网络请求细节:URL 拼接、参数传递、超时控制(10 秒)、响应状态码判断、JSON 数据解析适配;
    • 异常处理:捕获网络错误、401 Token 失效、非 200 状态码等异常并抛出统一格式的错误信息;
    • 兼容多格式响应:适配 GitCode API 返回的 “纯 List、Map [items]、Map [data]” 三种数据格式,保证解析不崩溃。
  • 依赖关系:依赖配置模块(ApiConfig)、数据模型模块;无上层依赖。
  • 模块特性
    • 职责单一:仅负责网络请求,不涉及 UI、状态管理;
    • 可复用:被首页、仓库列表页等多个页面复用,无需重复编写请求逻辑。
(2)数据模型子模块(Models)
  • 核心文件:gitcode_repository.dart、gitcode_user.dart
  • 模块职责:定义业务数据结构,实现 “JSON 数据→Dart 实体类” 的转换,是数据在应用内的统一载体。
    • GitCodeRepository:仓库数据模型,包含 id、仓库名、描述、星数、更新时间等核心字段,通过fromJson工厂方法兼容不同字段名(如full_name/fullName);
    • GitCodeUser:用户数据模型,包含用户名、头像地址、用户类型等核心字段,同样做了字段兼容和默认值处理。
  • 依赖关系:无任何外部依赖(纯数据结构定义)。
  • 模块特性
    • 数据隔离:UI 层、控制器层仅操作实体类,无需关心 JSON 原始格式;
(3)配置模块(Config)
  • 核心文件:api_config.dart
  • 模块职责:存储项目全局静态配置,是整个应用的 “基础参数中心”。
    • 定义 GitCode API 的个人访问令牌(personalToken),作为接口请求的鉴权凭证;

二、代码结构与核心模块分析

GitCode口袋工具
├── 核心层 (Core)
│   ├── 网络请求 (api_client.dart):封装GitCode API调用(搜索仓库/用户)
│   ├── 配置 (api_config.dart):存储API Token配置
│   └── 数据模型 (Models)
│       ├── gitcode_repository.dart:仓库数据模型(解析JSON、存储字段)
│       └── gitcode_user.dart:用户数据模型(解析JSON、存储字段)
├── 控制器层 (Controllers)
│   └── pagination_controller.dart:分页加载控制器(统一处理下拉刷新/上拉加载)
├── 页面层 (Pages)
│   ├── main.dart:应用入口 + 底部导航(首页/仓库/组织/我的)
│   ├── home_page.dart:首页(搜索框 + 仓库/用户切换 + 结果展示)
│   ├── repository_page.dart:仓库页面(等待实现)
│   ├── organization_page.dart:组织页面(等待实现)
│   ├── profile_page.dart:我的页面(等待实现)
│   └── repository_list_page.dart:仓库列表页(独立搜索结果页,复用分页逻辑)
└── 组件层 (Widgets)
    ├── repository_card.dart:仓库卡片(展示仓库信息)
    └── user_card.dart:用户卡片(展示用户头像/名称等)

2.1 入口与导航(main.dart)

功能定位

项目入口文件,负责初始化 App、配置主题、实现底部 Tab 导航。

// 1. 应用入口
void main() {
  runApp(const MyApp());
}

// 2. 根组件:配置主题、首页
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GitCode口袋工具',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const MainNavigationPage(), // 底部导航页面作为首页
      debugShowCheckedModeBanner: false,
    );
  }
}

// 3. 底部Tab导航实现
class MainNavigationPage extends StatefulWidget {
  @override
  State createState() => _MainNavigationPageState();
}

class _MainNavigationPageState extends State {
  int _selectedIndex = 0; // 当前选中的Tab索引
  // 4个核心页面列表
  static final List<Widget> _pages = [HomePage(), RepositoryPage(), OrganizationPage(), ProfilePage()];

  // Tab切换逻辑
  void _onItemTapped(int index) {
    setState(() => _selectedIndex = index); // 更新选中索引,触发UI重建
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _pages.elementAt(_selectedIndex), // 显示当前选中的页面
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed, // 固定4个Tab(避免动画异常)
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
        items: [/* 4个Tab的图标+文字配置 */],
      ),
    );
  }
}
关键知识点
  • MaterialApp是 Flutter 应用的根组件,负责路由、主题、本地化等全局配置;
  • 底部导航通过BottomNavigationBar实现,结合StatefulWidget维护选中状态;
  • BottomNavigationBarType.fixed:当 Tab 数量≥4 时,使用 fixed 类型避免 Tab 自动折叠。

2.2 分页控制器(pagination_controller.dart)

功能定位

通用分页逻辑封装,负责管理分页数据、加载状态、错误状态,通过 Stream 通知 UI 更新。

class PaginationController<T> {
  // 1. 状态流控制器(广播模式:多监听者)
  final StreamController<List<T>> _dataController = StreamController.broadcast();
  final StreamController<bool> _loadingController = StreamController.broadcast();
  final StreamController<String?> _errorController = StreamController.broadcast();

  // 2. 状态变量
  List<T> _items = []; // 分页数据列表
  int _currentPage = 1; // 当前页码
  bool _hasMore = true; // 是否有更多数据
  bool _isLoading = false; // 是否正在加载

  // 3. 对外暴露的流(只读)
  Stream<List<T>> get dataStream => _dataController.stream;
  Stream<bool> get loadingStream => _loadingController.stream;
  Stream<String?> get errorStream => _errorController.stream;

  // 4. 核心加载方法
  Future<void> loadData({
    required Future<List<T>> Function(int page, int perPage) fetchFn, // 数据获取回调
    bool refresh = false, // 是否刷新(重置页码)
    int perPage = 15, // 每页条数
  }) async {
    // 防重复加载
    if (_isLoading) return;
    // 刷新:重置状态
    if (refresh) {
      _currentPage = 1;
      _hasMore = true;
      _items.clear();
    }
    // 无更多数据则返回
    if (!_hasMore && !refresh) return;

    // 更新加载状态
    _isLoading = true;
    _loadingController.add(true);
    _errorController.add(null);

    try {
      // 调用外部传入的API请求方法
      final newItems = await fetchFn(_currentPage, perPage);
      // 更新数据列表
      if (refresh) {
        _items = newItems;
      } else {
        _items.addAll(newItems);
      }
      // 判断是否有更多数据(当前页数据量≥每页条数则有更多)
      _hasMore = newItems.length >= perPage;
      if (_hasMore) _currentPage++;
      // 通知UI更新数据
      _dataController.add(List<T>.from(_items));
    } catch (e) {
      // 错误处理:发送错误信息
      _errorController.add('加载失败: ${e.toString()}');
      // 刷新失败则清空数据
      if (refresh) {
        _items.clear();
        _dataController.add([]);
      }
    } finally {
      // 恢复加载状态
      _isLoading = false;
      _loadingController.add(false);
    }
  }

  // 5. 资源释放(必须调用,避免内存泄漏)
  void dispose() {
    _dataController.close();
    _loadingController.close();
    _errorController.close();
  }
}
关键知识点
  • 泛型(T):使控制器支持任意类型的数据(仓库 / 用户),提高复用性;
  • StreamController.broadcast():广播流,支持多个 Widget 监听状态变化;
  • 异步状态管理:通过_isLoading防止重复请求,_hasMore控制是否加载更多;
  • 资源释放dispose方法关闭流控制器,避免 Flutter 内存泄漏警告。

2.3 API 请求层(api_client.dart + api_config.dart)

功能定位

封装 GitCode API 的网络请求逻辑,统一处理请求参数、响应解析、异常捕获。

2.3.1 api_config.dart(配置文件)
class ApiConfig {
  // GitCode个人访问令牌(需替换为真实Token)
  static const String personalToken = '';
}
  • 作用:集中管理 API 配置(如 Token、基础 URL),便于维护;
  • 注意:真实环境中建议通过环境变量 / 安全存储管理 Token,避免硬编码。
2.3.2 api_client.dart(核心请求逻辑)
class GitCodeApiClient {
  final String baseUrl = 'https://api.gitcode.com/api/v5'; // API基础地址

  // 1. 搜索仓库API
  Future<List<GitCodeRepository>> searchRepositories({
    required String keyword,
    int page = 1,
    int perPage = 20,
  }) async {
    // 构建请求URL(拼接参数)
    final uri = Uri.parse('$baseUrl/search/repositories').replace(
      queryParameters: {
        'q': keyword,
        'page': page.toString(),
        'per_page': perPage.toString(),
        'access_token': ApiConfig.personalToken,
      },
    );

    try {
      // 发送GET请求,设置10秒超时
      final response = await http.get(uri).timeout(const Duration(seconds: 10));
      if (response.statusCode == 200) {
        // 解析JSON响应(兼容不同格式:List/Map[items]/Map[data])
        final dynamic jsonData = jsonDecode(response.body);
        List<dynamic> items = [];
        if (jsonData is List) {
          items = jsonData;
        } else if (jsonData is Map && jsonData['items'] != null) {
          items = jsonData['items'] as List<dynamic>;
        } else if (jsonData is Map && jsonData['data'] != null) {
          items = jsonData['data'] as List<dynamic>;
        }
        // 转换为仓库模型列表
        return items.map((item) => GitCodeRepository.fromJson(item)).toList();
      } else {
        throw Exception('仓库搜索失败,状态码: ${response.statusCode}');
      }
    } on http.ClientException catch (e) {
      throw Exception('网络连接失败: $e');
    } catch (e) {
      rethrow; // 重新抛出异常,让上层处理
    }
  }

  // 2. 搜索用户API(逻辑与仓库搜索一致,仅返回类型不同)
  Future<List<GitCodeUser>> searchUsers({...}) async {
    // 类似逻辑,最终返回 GitCodeUser.fromJson 转换的列表
  }
}
关键知识点
  • Uri 构建Uri.parse() + replace(queryParameters:) 安全拼接 URL 参数(自动编码特殊字符);
  • HTTP 请求http.get() 发送 GET 请求,timeout设置超时时间,避免请求挂起;
  • JSON 解析兼容:适配 API 返回的不同格式(直接数组 / 带 items/data 的对象);
  • 异常捕获:分层捕获网络异常、业务异常,向上抛出便于上层统一处理。

2.4 数据模型(gitcode_repository.dart + gitcode_user.dart)

功能定位

定义数据结构,实现 JSON 到 Dart 对象的转换(工厂方法),保证数据解析的一致性。

2.4.1 gitcode_repository.dart(仓库模型)
class GitCodeRepository {
  // 1. 定义模型字段(与API返回字段对应)
  final int id;
  final String fullName;
  final String? description;
  final String? language;
  final int stargazersCount;
  final int forksCount;
  final bool isPrivate;
  final DateTime updatedAt;

  // 2. 构造函数(required标记必填字段)
  GitCodeRepository({
    required this.id,
    required this.fullName,
    this.description,
    this.language,
    required this.stargazersCount,
    required this.forksCount,
    required this.isPrivate,
    required this.updatedAt,
  });

  // 3. 工厂方法:从JSON解析为模型对象
  factory GitCodeRepository.fromJson(Map<String, dynamic> json) {
    try {
      return GitCodeRepository(
        id: json['id'] ?? 0, // 空值兜底,避免崩溃
        fullName: json['full_name'] ?? json['fullName'] ?? '未知仓库',
        description: json['description'] ?? json['desc'] ?? '',
        language: json['language'] ?? '',
        stargazersCount: json['stargazers_count'] ?? json['stars_count'] ?? 0,
        forksCount: json['forks_count'] ?? json['forks'] ?? 0,
        isPrivate: json['private'] ?? json['is_private'] ?? false,
        updatedAt: json['updated_at'] != null 
            ? DateTime.parse(json['updated_at'].toString())
            : DateTime.now(),
      );
    } catch (e) {
      // 解析失败时打印日志,返回默认对象(避免应用崩溃)
      print('❌ 解析仓库数据失败: $e');
      return GitCodeRepository(
        id: 0,
        fullName: '解析失败',
        description: '数据解析错误',
        language: '',
        stargazersCount: 0,
        forksCount: 0,
        isPrivate: false,
        updatedAt: DateTime.now(),
      );
    }
  }
}
关键知识点
  • 工厂构造函数(factory):用于复杂的对象创建(如 JSON 解析),返回已初始化的对象;
  • 空值兜底(??):API 返回字段缺失 / 为空时,设置默认值,避免NullReferenceError
  • 异常捕获:解析 JSON 时捕获异常,返回默认对象,保证应用稳定性;
  • DateTime 解析:处理 API 返回的字符串日期,转换为 Dart 的 DateTime 类型。

2.5 核心页面(home_page.dart)

功能定位

项目核心页面,支持:

  • 搜索框输入关键词;
  • Tab 切换(仓库 / 用户);
  • 下拉刷新、上拉加载更多;
  • 流监听展示数据 / 加载状态 / 错误信息;
  • 自定义卡片展示搜索结果。
class HomePage extends StatefulWidget {
  @override
  State createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  // 1. 控制器初始化
  final TextEditingController _searchController = TextEditingController(); // 搜索框
  final RefreshController _refreshController = RefreshController(); // 刷新控制器
  final GitCodeApiClient _apiClient = GitCodeApiClient(); // API客户端
  late PaginationController _paginationController; // 分页控制器
  late TabController _tabController; // Tab控制器
  int _searchType = 0; // 0=仓库,1=用户
  String _currentKeyword = ''; // 当前搜索关键词

  @override
  void initState() {
    super.initState();
    // 初始化Tab控制器(2个Tab,绑定当前State的动画资源)
    _tabController = TabController(length: 2, vsync: this);
    _paginationController = PaginationController<dynamic>(); // 泛型为dynamic,兼容仓库/用户
    // 监听Tab切换:切换时重新搜索
    _tabController.addListener(() {
      if (_tabController.index != _searchType) {
        setState(() => _searchType = _tabController.index);
        if (_currentKeyword.isNotEmpty) _performSearch();
      }
    });
  }

  // 2. 搜索逻辑
  void _performSearch() {
    final keyword = _searchController.text.trim();
    if (keyword.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请输入搜索关键词')));
      return;
    }
    setState(() => _currentKeyword = keyword);
    // 调用分页控制器的加载方法(刷新模式)
    _paginationController.loadData(
      fetchFn: _searchType == 0 ? _fetchRepositories : _fetchUsers,
      refresh: true,
    );
  }

  // 3. 数据获取回调(传给分页控制器)
  Future<List<GitCodeRepository>> _fetchRepositories(int page, int perPage) async {
    return await _apiClient.searchRepositories(keyword: _currentKeyword, page: page, perPage: perPage);
  }
  Future<List<GitCodeUser>> _fetchUsers(int page, int perPage) async {
    return await _apiClient.searchUsers(keyword: _currentKeyword, page: page, perPage: perPage);
  }

  // 4. 构建UI
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('GitCode搜索'),
        bottom: TabBar(controller: _tabController, tabs: [/* 仓库/用户Tab */]),
      ),
      body: Column(
        children: [
          // 搜索框区域
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: _searchController,
                    decoration: InputDecoration(/* 圆角、提示文字等样式 */),
                    onSubmitted: (_) => _performSearch(), // 回车搜索
                  ),
                ),
                ElevatedButton(onPressed: _performSearch, child: const Icon(Icons.search)),
              ],
            ),
          ),
          // 错误提示区域(监听分页控制器的错误流)
          StreamBuilder<String?>(
            stream: _paginationController.errorStream,
            builder: (context, snapshot) {
              if (snapshot.hasData && snapshot.data != null) {
                return Container(/* 红色背景展示错误信息 */);
              }
              return const SizedBox.shrink();
            },
          ),
          // 搜索结果区域(监听分页控制器的数据流)
          Expanded(
            child: StreamBuilder<List<dynamic>>(
              stream: _paginationController.dataStream,
              builder: (context, snapshot) {
                // 分状态展示UI:未搜索/加载中/无结果/有结果
                if (!_currentKeyword.isEmpty && snapshot.hasData && snapshot.data!.isNotEmpty) {
                  // 有结果:展示下拉刷新列表
                  return SmartRefresher(
                    controller: _refreshController,
                    enablePullDown: true, // 下拉刷新
                    enablePullUp: true, // 上拉加载
                    onRefresh: () async {
                      // 刷新逻辑:调用分页控制器的刷新方法
                      await _paginationController.loadData(
                        fetchFn: _searchType == 0 ? _fetchRepositories : _fetchUsers,
                        refresh: true,
                      );
                      _refreshController.refreshCompleted(); // 标记刷新完成
                    },
                    onLoading: () async {
                      // 加载更多逻辑
                      await _paginationController.loadData(
                        fetchFn: _searchType == 0 ? _fetchRepositories : _fetchUsers,
                        refresh: false,
                      );
                      _refreshController.loadComplete(); // 标记加载完成
                    },
                    child: ListView.builder(
                      itemCount: snapshot.data!.length,
                      itemBuilder: (context, index) {
                        // 根据搜索类型展示不同卡片
                        return _searchType == 0 
                            ? RepositoryCard(repository: snapshot.data![index] as GitCodeRepository)
                            : UserCard(user: snapshot.data![index] as GitCodeUser);
                      },
                    ),
                  );
                } else if (_currentKeyword.isEmpty) {
                  // 未搜索:展示默认提示
                  return Center(/* 搜索图标+提示文字 */);
                } else {
                  // 加载中/无结果:展示对应状态
                  return Center(/* 加载动画/无结果提示 */);
                }
              },
            ),
          ),
        ],
      ),
    );
  }

  // 5. 资源释放
  @override
  void dispose() {
    _searchController.dispose();
    _refreshController.dispose();
    _paginationController.dispose();
    _tabController.dispose();
    super.dispose();
  }
}
关键知识点
  • SingleTickerProviderStateMixin:为 TabController 提供动画资源(vsync 参数);
  • StreamBuilder:监听 Stream 变化,自动重建 UI(响应式编程);
  • SmartRefresher:第三方库实现下拉刷新 / 上拉加载,需调用refreshCompleted()/loadComplete()标记状态;
  • 类型转换as GitCodeRepository/as GitCodeUser,将 dynamic 类型转换为具体模型;
  • SnackBar:轻量级提示组件,用于用户操作反馈(如空关键词提示)。

2.6 仓库列表页(repository_list_page.dart)

功能定位

独立的仓库列表页(可通过传参接收搜索关键词),逻辑与首页仓库搜索一致,是分页控制器的另一个使用示例。

核心特点
  • 接收构造函数参数searchKeyword,用于指定搜索关键词;
  • 复用PaginationController<GitCodeRepository>实现分页;
  • 同样使用SmartRefresher实现下拉刷新 / 上拉加载;
  • 代码结构与首页高度相似,体现了分页控制器的复用性。

2.7 基础页面(organization_page.dart/profile_page.dart/repository_page.dart)

功能定位

底部导航的占位页面,仅展示基础 UI 结构,无业务逻辑,用于后续扩展。

2.8 UI 组件(repository_card.dart + user_card.dart)

功能定位

自定义可复用组件,分别展示仓库 / 用户信息,统一 UI 样式。

2.8.1 RepositoryCard(仓库卡片)
  • 使用Card组件作为容器,设置 margin/padding 保证间距;
  • 展示仓库名称(加粗)、描述(最多 2 行)、语言 / 星数 / 分支数等信息;
  • 使用Icon + Text组合展示统计信息,提升可读性;
  • maxLines + overflow: TextOverflow.ellipsis 处理文本溢出。
2.8.2 UserCard(用户卡片)
  • 使用ListTile组件快速实现「头像 + 标题 + 副标题 + 右侧箭头」的布局;
  • CircleAvatar加载网络头像(NetworkImage);
  • 点击事件:通过onTap展示 SnackBar 提示,预留扩展空间。

三、核心功能实现原理

3.1 分页加载逻辑

  1. 初始化:currentPage=1hasMore=trueitems=[]
  2. 首次加载:调用loadData(refresh: true),重置状态后请求第 1 页数据;
  3. 加载更多:调用loadData(refresh: false),请求currentPage页数据,追加到items
  4. 终止条件:当返回数据量 < 每页条数时,设置hasMore=false,停止加载更多;
  5. 防重复加载:isLoading标记,加载中时拒绝新的请求。

3.2 流(Stream)的使用

  • 流的作用:实现控制器与 UI 的解耦,控制器状态变化时主动通知 UI 更新;
  • 广播流:StreamController.broadcast(),支持多个 StreamBuilder 监听(如同时监听数据和加载状态);
  • 数据传递:控制器通过_dataController.add(data)发送数据,UI 通过StreamBuilder接收并重建。

3.3 下拉刷新与上拉加载

  • 依赖pull_to_refresh库的SmartRefresher组件;
  • 下拉刷新:触发onRefresh,调用分页控制器的loadData(refresh: true),完成后调用refreshCompleted()
  • 上拉加载:触发onLoading,调用分页控制器的loadData(refresh: false),完成后调用loadComplete()
  • 状态重置:刷新完成后需调用_refreshController.loadComplete()重置加载更多的状态。

3.4 网络请求与数据解析

  1. 构建请求 URL:拼接基础地址、接口路径、查询参数(含 Token);
  2. 发送请求:http.get() + 超时处理,捕获网络异常;
  3. 响应处理:判断状态码(200 为成功),解析 JSON;
  4. 模型转换:通过工厂方法fromJson将 JSON 转换为 Dart 对象,空值兜底;
  5. 异常抛出:将网络 / 业务异常抛出,由上层分页控制器处理并展示错误信息。

3.5 底部 Tab 导航

  1. 维护_selectedIndex状态,标记当前选中的 Tab;
  2. BottomNavigationBaronTap回调更新_selectedIndex
  3. _pages列表存储所有 Tab 对应的页面,通过_pages.elementAt(_selectedIndex)展示当前页面;
  4. BottomNavigationBarType.fixed:保证 4 个 Tab 同时显示,不折叠。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐