开源鸿蒙跨平台:Flutter 搜索与 API 对接
底部 Tab 导航切换首页、仓库、组织、个人中心页面;首页支持搜索 GitCode 平台的仓库 / 用户,支持下拉刷新、上拉加载更多;封装通用分页控制器,实现统一的分页加载、状态管理逻辑;网络请求层封装,支持仓库 / 用户搜索的 API 调用;数据模型与 JSON 解析,保证数据结构的一致性;自定义 UI 组件(仓库卡片、用户卡片)展示搜索结果。
一、项目概述
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 API 的个人访问令牌(

二、代码结构与核心模块分析
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 分页加载逻辑
- 初始化:
currentPage=1,hasMore=true,items=[]; - 首次加载:调用
loadData(refresh: true),重置状态后请求第 1 页数据; - 加载更多:调用
loadData(refresh: false),请求currentPage页数据,追加到items; - 终止条件:当返回数据量 < 每页条数时,设置
hasMore=false,停止加载更多; - 防重复加载:
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 网络请求与数据解析
- 构建请求 URL:拼接基础地址、接口路径、查询参数(含 Token);
- 发送请求:
http.get()+ 超时处理,捕获网络异常; - 响应处理:判断状态码(200 为成功),解析 JSON;
- 模型转换:通过工厂方法
fromJson将 JSON 转换为 Dart 对象,空值兜底; - 异常抛出:将网络 / 业务异常抛出,由上层分页控制器处理并展示错误信息。
3.5 底部 Tab 导航
- 维护
_selectedIndex状态,标记当前选中的 Tab; BottomNavigationBar的onTap回调更新_selectedIndex;_pages列表存储所有 Tab 对应的页面,通过_pages.elementAt(_selectedIndex)展示当前页面;BottomNavigationBarType.fixed:保证 4 个 Tab 同时显示,不折叠。



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




所有评论(0)