作为一名长期从事跨平台开发的工程师,我最近在探索如何将Flutter应用迁移到OpenHarmony平台。在这个过程中,我选择了一个非常有趣的实战项目——《瑞克和莫蒂》角色列表应用。这个项目不仅涵盖了Flutter开发的核心技术点,还特别适合展示如何在OpenHarmony环境下构建高性能的跨平台应用。
这个应用的主要功能包括:
这个项目特别适合想要学习以下内容的开发者:
项目的核心架构采用经典的MVC模式,但根据Flutter的特点做了适当调整:
code复制lib/
├── models/ # 数据模型
│ └── character.dart
├── services/ # 服务层
│ └── api_service.dart
├── widgets/ # 自定义组件
│ ├── character_card.dart
│ └── loading_indicator.dart
└── screens/ # 页面层
├── main_screen.dart
└── character_detail.dart
这种结构清晰地将业务逻辑、数据获取和UI展示分离,特别适合中等复杂度的Flutter应用。在实际开发中,我发现这种架构有以下优势:
对于状态管理,项目采用了最基础的setState方式,而不是引入像Provider或Bloc这样的状态管理库。这是经过深思熟虑的决定:
不过,我在代码中预留了扩展点,比如将状态相关的代码集中放在单独的类中,这样未来如果需要引入更复杂的状态管理方案,重构成本会很低。
网络请求使用Dart原生的http包实现,没有选择更高级的Dio等库,主要出于以下考虑:
dart复制class RickMortyApi {
static const String _baseUrl = 'https://rickandmortyapi.com/api';
Future<Map<String, dynamic>> getCharacters({
int page = 1,
String? status,
String? name,
}) async {
final params = {
'page': '$page',
if (status != null) 'status': status,
if (name != null) 'name': name,
};
final uri = Uri.parse('$_baseUrl/character').replace(queryParameters: params);
final response = await http.get(uri);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to load characters');
}
}
}
这种实现方式简单直接,对于这个规模的项目完全够用。同时,我也添加了基本的错误处理逻辑,确保应用在API请求失败时不会崩溃。
应用的主页面采用Tab布局,包含三个子页面:角色、地点和剧集。这是通过TabController配合TabBar和TabBarView实现的:
dart复制class _RickMortyScreenState extends State<RickMortyScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('瑞克和莫蒂'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: '角色'),
Tab(text: '地点'),
Tab(text: '剧集'),
],
),
),
body: TabBarView(
controller: _tabController,
children: const [
CharactersTab(),
LocationsTab(),
EpisodesTab(),
],
),
);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
}
关键点说明:
默认情况下,当用户切换Tab时,不活动的Tab页面会被销毁,再次切换回来时会重新创建。这会导致页面状态丢失,用户体验不佳。为了解决这个问题,我们使用AutomaticKeepAliveClientMixin:
dart复制class _CharactersTabState extends State<CharactersTab>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true;
@override
Widget build(BuildContext context) {
super.build(context); // 必须调用super.build
// 其余构建逻辑...
}
}
注意事项:
无限滚动是现代移动应用的标配功能,它通过监听滚动事件,在用户接近列表底部时自动加载更多内容。实现这个功能需要以下几个关键组件:
dart复制class _CharactersTabState extends State<CharactersTab> {
final ScrollController _scrollController = ScrollController();
bool _isLoadingMore = false;
int _page = 1;
int _totalPages = 1;
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 200) {
_loadMore();
}
}
Future<void> _loadMore() async {
if (_isLoadingMore || _page >= _totalPages) return;
setState(() => _isLoadingMore = true);
try {
final data = await _api.getCharacters(page: _page + 1);
setState(() {
_characters.addAll(data['results'] ?? []);
_page++;
_isLoadingMore = false;
});
} catch (e) {
setState(() => _isLoadingMore = false);
}
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
优化技巧:
应用支持两种筛选方式:按状态筛选和关键词搜索。这两种筛选都通过API的参数来实现,而不是在客户端过滤,这样可以保证数据的一致性。
状态筛选实现:
dart复制Widget _buildFilterChip(String label, String? status) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: FilterChip(
label: Text(label),
selected: _statusFilter == status,
onSelected: (selected) {
setState(() => _statusFilter = selected ? status : null);
_loadCharacters();
},
),
);
}
搜索功能实现:
dart复制TextField(
decoration: const InputDecoration(
hintText: '搜索角色...',
prefixIcon: Icon(Icons.search)
),
onSubmitted: (value) {
_searchQuery = value.isEmpty ? null : value;
_loadCharacters();
},
),
性能优化点:
角色列表采用GridView展示,每行显示两个角色卡片。这是通过SliverGridDelegateWithFixedCrossAxisCount实现的:
dart复制GridView.builder(
controller: _scrollController,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.75,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: _characters.length + (_isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _characters.length) {
return const Center(child: CircularProgressIndicator());
}
return CharacterCard(character: _characters[index]);
},
)
参数说明:
性能优化:
角色卡片采用Material Card组件,包含角色图片、状态指示器和基本信息:
dart复制class CharacterCard extends StatelessWidget {
final Map<String, dynamic> character;
const CharacterCard({super.key, required this.character});
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'alive': return Colors.green;
case 'dead': return Colors.red;
default: return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => _navigateToDetail(context),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildImageSection(),
_buildInfoSection(),
],
),
),
);
}
Widget _buildImageSection() {
return Expanded(
flex: 3,
child: Stack(
fit: StackFit.expand,
children: [
Image.network(
character['image'] ?? '',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Icon(Icons.error),
),
Positioned(
top: 8,
right: 8,
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: _getStatusColor(character['status'] ?? ''),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
),
),
],
),
);
}
Widget _buildInfoSection() {
return Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
character['name'] ?? '',
style: const TextStyle(fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
'${character['species']} • ${character['gender']}',
style: TextStyle(color: Colors.grey[600], fontSize: 12),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
void _navigateToDetail(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CharacterDetailScreen(character: character),
),
);
}
}
设计要点:
良好的加载状态处理对用户体验至关重要。我们实现了以下几种加载状态:
dart复制Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _loadCharacters,
child: GridView.builder(
// ... grid view参数
itemCount: _characters.length + (_isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= _characters.length) {
return const Center(child: CircularProgressIndicator());
}
return CharacterCard(character: _characters[index]);
},
),
),
)
最佳实践:
在开发过程中,我特别关注了以下几个性能关键点:
列表滚动性能:
图片加载优化:
内存管理:
网络请求优化:
在实际开发中,我遇到了以下几个典型问题及解决方案:
问题1:Tab切换时页面闪烁
问题2:滚动时卡顿
问题3:图片加载慢
问题4:内存泄漏
在开发这个应用过程中,我总结了一些有用的调试技巧:
Flutter性能面板:
Dart DevTools:
日志调试:
布局边界检查:
将Flutter应用迁移到OpenHarmony平台时,需要注意以下几个关键点:
平台差异处理:
性能考量:
打包与发布:
测试策略:
在实际迁移过程中,我发现大部分Flutter代码可以直接运行在OpenHarmony上,只需要关注少数平台特定的功能适配。这充分证明了Flutter"一次编写,多端运行"的优势。