HarmonyOS开发:迁移实战——完整Android应用迁移鸿蒙
HarmonyOS开发:迁移实战——完整Android应用迁移鸿蒙
📌 核心要点:完整应用迁移不是逐文件翻译,而是"先分析→再拆解→逐层迁移→集成验证"的系统工程,这篇文章带你走一遍全流程。
背景与动机
前面9篇文章,我们讲了架构对比、组件映射、状态管理、网络层、数据层……每一层都单独讲清楚了。但真正的迁移不是一层一层孤立进行的——你要把一个完整的Android应用搬到鸿蒙上,UI、状态、网络、数据、导航、动画,所有东西纠缠在一起。
这篇文章,我们拿一个真实的Android应用——一个"技术博客"App,从零开始完整迁移到鸿蒙。不是讲概念,不是贴片段,而是全流程、全代码、全踩坑。
这个App包含:
- 用户登录/注册
- 文章列表(分页加载)
- 文章详情(Markdown渲染)
- 收藏/点赞
- 个人中心
- 离线缓存
麻雀虽小,五脏俱全。迁移完这个App,你就掌握了完整迁移的方法论。
核心原理
迁移全流程
graph TB
A[1. 分析原应用] --> B[2. 拆解模块]
B --> C[3. 搭建鸿蒙项目]
C --> D[4. 数据层迁移]
D --> E[5. 网络层迁移]
E --> F[6. 状态管理迁移]
F --> G[7. UI层迁移]
G --> H[8. 导航迁移]
H --> I[9. 集成测试]
I --> J[10. 性能优化]
classDef stepStyle fill:#0A59F7,stroke:#0A3CC7,color:#fff,font-weight:bold
classDef milestoneStyle fill:#FF6D00,stroke:#BF360C,color:#fff,font-weight:bold
class A,B,C stepStyle
class D,E,F milestoneStyle
class G,H,I,J stepStyle
迁移优先级原则
为什么先迁移数据层?因为数据层最独立、最稳定、最不容易出错。网络层依赖数据层的模型定义,状态层依赖数据层和网络层,UI层依赖状态层。自底向上迁移,每一层都能独立验证。
| 层级 | 迁移顺序 | 理由 |
|---|---|---|
| 数据模型 | 1 | 纯数据结构,无依赖 |
| 数据库 | 2 | 依赖数据模型 |
| 网络层 | 3 | 依赖数据模型 |
| 状态管理 | 4 | 依赖数据层+网络层 |
| UI组件 | 5 | 依赖状态管理 |
| 导航 | 6 | 依赖UI组件 |
| 动画/细节 | 7 | 最后打磨 |
代码实战
第一步:分析原应用
原Android应用的项目结构:
TechBlog/
├── model/ # 数据模型
│ ├── User.kt
│ ├── Article.kt
│ └── Comment.kt
├── api/ # 网络层
│ ├── ApiService.kt
│ └── RetrofitClient.kt
├── db/ # 数据库
│ ├── AppDatabase.kt
│ └── ArticleDao.kt
├── viewmodel/ # ViewModel
│ ├── LoginViewModel.kt
│ ├── ArticleListViewModel.kt
│ └── ProfileViewModel.kt
├── ui/ # UI
│ ├── LoginActivity.kt
│ ├── ArticleListFragment.kt
│ ├── ArticleDetailActivity.kt
│ └── ProfileFragment.kt
└── util/ # 工具类
├── PreferenceHelper.kt
└── DateUtils.kt
第二步:数据模型迁移
// Android: Article数据模型
data class Article(
val id: String,
val title: String,
val content: String,
val author: String,
val authorAvatar: String,
val category: String,
val tags: List<String>,
val likeCount: Int,
val commentCount: Int,
val isLiked: Boolean,
val isFavorite: Boolean,
val createdAt: Long,
val updatedAt: Long
)
迁移到ArkTS:
// 鸿蒙: 数据模型
export interface ArticleInfo {
id: string
title: string
content: string
author: string
authorAvatar: string
category: string
tags: string[] // 数组类型,数据库中存为JSON字符串
likeCount: number
commentCount: number
isLiked: boolean
isFavorite: boolean
createdAt: number
updatedAt: number
}
export interface UserInfo {
id: string
username: string
email: string
avatar: string
token: string
favoriteCount: number
articleCount: number
}
export interface CommentInfo {
id: string
articleId: string
userId: string
username: string
userAvatar: string
content: string
createdAt: number
}
第三步:数据库迁移
// 鸿蒙: 数据库管理
import { relationalStore } from '@kit.ArkData'
class DatabaseManager {
private static instance: DatabaseManager
private store: relationalStore.RdbStore | null = null
static getInstance(): DatabaseManager {
if (!DatabaseManager.instance) {
DatabaseManager.instance = new DatabaseManager()
}
return DatabaseManager.instance
}
async init(context: Context): Promise<void> {
let config: relationalStore.StoreConfig = {
name: 'techblog.db',
securityLevel: relationalStore.SecurityLevel.S1
}
this.store = await relationalStore.getRdbStore(context, config)
await this.createTables()
}
private async createTables(): Promise<void> {
if (!this.store) return
// 文章表(离线缓存)
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS articles (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT DEFAULT '',
author TEXT DEFAULT '',
author_avatar TEXT DEFAULT '',
category TEXT DEFAULT '',
tags TEXT DEFAULT '[]',
like_count INTEGER DEFAULT 0,
comment_count INTEGER DEFAULT 0,
is_liked INTEGER DEFAULT 0,
is_favorite INTEGER DEFAULT 0,
created_at INTEGER DEFAULT 0,
updated_at INTEGER DEFAULT 0
)
`)
// 收藏表
await this.store.executeSql(`
CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
article_id TEXT NOT NULL,
created_at INTEGER DEFAULT 0,
FOREIGN KEY (article_id) REFERENCES articles(id)
)
`)
await this.store.executeSql('CREATE INDEX IF NOT EXISTS idx_favorites_article ON favorites(article_id)')
await this.store.executeSql('CREATE INDEX IF NOT EXISTS idx_articles_category ON articles(category)')
}
getStore(): relationalStore.RdbStore | null {
return this.store
}
}
// 文章Dao
class ArticleDao {
private store: relationalStore.RdbStore | null = null
init(store: relationalStore.RdbStore) {
this.store = store
}
// 缓存文章列表
async cacheArticles(articles: ArticleInfo[]): Promise<void> {
if (!this.store) return
let buckets: relationalStore.ValuesBucket[] = articles.map(a => ({
id: a.id,
title: a.title,
content: a.content,
author: a.author,
author_avatar: a.authorAvatar,
category: a.category,
tags: JSON.stringify(a.tags),
like_count: a.likeCount,
comment_count: a.commentCount,
is_liked: a.isLiked ? 1 : 0,
is_favorite: a.isFavorite ? 1 : 0,
created_at: a.createdAt,
updated_at: a.updatedAt
}))
await this.store.batchInsert('articles', buckets)
}
// 获取缓存的文章
async getCachedArticles(category: string = ''): Promise<ArticleInfo[]> {
if (!this.store) return []
let predicates = new relationalStore.RdbPredicates('articles')
if (category) {
predicates.equalTo('category', category)
}
predicates.orderByDesc('created_at')
let resultSet = await this.store.query(predicates)
let articles: ArticleInfo[] = []
while (resultSet.goToNextRow()) {
articles.push(this.rowToArticle(resultSet))
}
resultSet.close()
return articles
}
// 收藏/取消收藏
async toggleFavorite(articleId: string, isFavorite: boolean): Promise<void> {
if (!this.store) return
let valueBucket: relationalStore.ValuesBucket = {
is_favorite: isFavorite ? 1 : 0
}
let predicates = new relationalStore.RdbPredicates('articles')
predicates.equalTo('id', articleId)
await this.store.update(valueBucket, predicates)
}
private rowToArticle(resultSet: relationalStore.ResultSet): ArticleInfo {
return {
id: resultSet.getString(resultSet.getColumnIndex('id')),
title: resultSet.getString(resultSet.getColumnIndex('title')),
content: resultSet.getString(resultSet.getColumnIndex('content')),
author: resultSet.getString(resultSet.getColumnIndex('author')),
authorAvatar: resultSet.getString(resultSet.getColumnIndex('author_avatar')),
category: resultSet.getString(resultSet.getColumnIndex('category')),
tags: JSON.parse(resultSet.getString(resultSet.getColumnIndex('tags'))) as string[],
likeCount: resultSet.getLong(resultSet.getColumnIndex('like_count')),
commentCount: resultSet.getLong(resultSet.getColumnIndex('comment_count')),
isLiked: resultSet.getLong(resultSet.getColumnIndex('is_liked')) === 1,
isFavorite: resultSet.getLong(resultSet.getColumnIndex('is_favorite')) === 1,
createdAt: resultSet.getLong(resultSet.getColumnIndex('created_at')),
updatedAt: resultSet.getLong(resultSet.getColumnIndex('updated_at'))
}
}
}
第四步:网络层迁移
// 鸿蒙: 网络层封装
import { http } from '@kit.NetworkKit'
// API响应格式
interface ApiResult<T> {
code: number
message: string
data: T
}
// HTTP客户端
class ApiClient {
private baseUrl: string = 'https://api.techblog.com/v1/'
async request<T>(method: http.RequestMethod, path: string, data?: Object): Promise<T> {
let token = AppStorage.get<string>('token') || ''
let httpRequest = http.createHttp()
try {
let response = await httpRequest.request(this.baseUrl + path, {
method: method,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
extraData: data ? JSON.stringify(data) : undefined,
connectTimeout: 15000,
readTimeout: 15000
})
if (response.responseCode === 200) {
let result = JSON.parse(response.result as string) as ApiResult<T>
if (result.code === 0) {
return result.data
}
throw new Error(result.message)
}
if (response.responseCode === 401) {
// Token过期
AppStorage.setOrCreate('token', '')
throw new Error('登录已过期')
}
throw new Error(`HTTP ${response.responseCode}`)
} finally {
httpRequest.destroy()
}
}
async get<T>(path: string, params?: Record<string, string>): Promise<T> {
let url = path
if (params) {
let qs = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&')
url += `?${qs}`
}
return this.request<T>(http.RequestMethod.GET, url)
}
async post<T>(path: string, data?: Object): Promise<T> {
return this.request<T>(http.RequestMethod.POST, path, data)
}
}
const api = new ApiClient()
// API服务
class BlogApi {
// 登录
async login(username: string, password: string): Promise<UserInfo> {
return api.post<UserInfo>('auth/login', { username, password })
}
// 文章列表
async getArticles(page: number, category: string = ''): Promise<ArticleInfo[]> {
let params: Record<string, string> = { page: page.toString(), size: '20' }
if (category) params.category = category
return api.get<ArticleInfo[]>('articles', params)
}
// 文章详情
async getArticleDetail(id: string): Promise<ArticleInfo> {
return api.get<ArticleInfo>(`articles/${id}`)
}
// 点赞
async likeArticle(id: string): Promise<void> {
return api.post<void>(`articles/${id}/like`)
}
// 收藏
async favoriteArticle(id: string): Promise<void> {
return api.post<void>(`articles/${id}/favorite`)
}
// 用户信息
async getProfile(): Promise<UserInfo> {
return api.get<UserInfo>('user/profile')
}
}
const blogApi = new BlogApi()
第五步:状态管理迁移
// 鸿蒙: 全局状态管理
@ObservedV2
export class AppStore {
// 用户状态
@Trace currentUser: UserInfo | null = null
@Trace token: string = ''
@Trace isLoggedIn: boolean = false
// 文章状态
@Trace articles: ArticleInfo[] = []
@Trace currentPage: number = 1
@Trace hasMore: boolean = true
@Trace loadingArticles: boolean = false
// 分类筛选
@Trace currentCategory: string = ''
// 登录
async login(username: string, password: string): Promise<boolean> {
try {
let user = await blogApi.login(username, password)
this.currentUser = user
this.token = user.token
this.isLoggedIn = true
AppStorage.setOrCreate('token', user.token)
return true
} catch (e) {
console.error('登录失败: ' + (e as Error).message)
return false
}
}
// 退出
logout() {
this.currentUser = null
this.token = ''
this.isLoggedIn = false
AppStorage.setOrCreate('token', '')
}
// 加载文章列表
async loadArticles(refresh: boolean = false): Promise<void> {
if (this.loadingArticles) return
if (refresh) {
this.currentPage = 1
this.articles = []
this.hasMore = true
}
if (!this.hasMore) return
this.loadingArticles = true
try {
let newArticles = await blogApi.getArticles(this.currentPage, this.currentCategory)
if (refresh) {
this.articles = newArticles
} else {
this.articles = [...this.articles, ...newArticles]
}
this.hasMore = newArticles.length >= 20
this.currentPage++
// 缓存到本地
if (articleDao) {
await articleDao.cacheArticles(newArticles)
}
} catch (e) {
console.error('加载文章失败: ' + (e as Error).message)
// 网络失败时尝试读取缓存
if (articleDao && this.articles.length === 0) {
let cached = await articleDao.getCachedArticles(this.currentCategory)
this.articles = cached
}
} finally {
this.loadingArticles = false
}
}
// 点赞
async likeArticle(articleId: string): Promise<void> {
try {
await blogApi.likeArticle(articleId)
this.articles = this.articles.map((a: ArticleInfo) => {
if (a.id === articleId) {
return { ...a, isLiked: !a.isLiked, likeCount: a.isLiked ? a.likeCount - 1 : a.likeCount + 1 }
}
return a
})
} catch (e) {
console.error('点赞失败: ' + (e as Error).message)
}
}
// 收藏
async favoriteArticle(articleId: string): Promise<void> {
try {
await blogApi.favoriteArticle(articleId)
this.articles = this.articles.map((a: ArticleInfo) => {
if (a.id === articleId) {
return { ...a, isFavorite: !a.isFavorite }
}
return a
})
if (articleDao) {
let article = this.articles.find((a: ArticleInfo) => a.id === articleId)
if (article) {
await articleDao.toggleFavorite(articleId, article.isFavorite)
}
}
} catch (e) {
console.error('收藏失败: ' + (e as Error).message)
}
}
// 切换分类
async switchCategory(category: string): Promise<void> {
this.currentCategory = category
await this.loadArticles(true)
}
}
第六步:UI层迁移——文章列表页
// 鸿蒙: 文章列表页
@Entry
@Component
struct ArticleListPage {
@Local store: AppStore = new AppStore()
private navStack: NavPathStack = new NavPathStack()
aboutToAppear() {
this.store.loadArticles(true)
}
build() {
Navigation(this.navStack) {
Column() {
// 分类筛选栏
this.CategoryBar()
// 文章列表
List() {
ForEach(this.store.articles, (article: ArticleInfo) => {
ListItem() {
this.ArticleCard(article)
}
}, (article: ArticleInfo) => article.id)
// 加载更多
if (this.store.hasMore && !this.store.loadingArticles) {
ListItem() {
Row() {
Text('加载更多')
.fontSize(14)
.fontColor('#666666')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(16)
.onClick(() => this.store.loadArticles())
}
}
if (this.store.loadingArticles) {
ListItem() {
Row() {
LoadingProgress()
.width(24)
.height(24)
.color('#0A59F7')
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
.margin({ left: 8 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(16)
}
}
}
.width('100%')
.layoutWeight(1)
.refreshCallback(() => {
this.store.loadArticles(true)
})
}
.width('100%')
.height('100%')
}
.title('技术博客')
.navDestination(this.buildDestination)
}
@Builder
CategoryBar() {
Scroll() {
Row() {
this.CategoryItem('全部', '')
this.CategoryItem('前端', 'frontend')
this.CategoryItem('后端', 'backend')
this.CategoryItem('移动端', 'mobile')
this.CategoryItem('DevOps', 'devops')
this.CategoryItem('AI', 'ai')
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
}
@Builder
CategoryItem(label: string, category: string) {
Text(label)
.fontSize(14)
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(16)
.backgroundColor(this.store.currentCategory === category ? '#0A59F7' : '#F0F0F0')
.fontColor(this.store.currentCategory === category ? Color.White : '#333333')
.margin({ right: 8 })
.onClick(() => this.store.switchCategory(category))
}
@Builder
ArticleCard(article: ArticleInfo) {
Column() {
// 标题
Text(article.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 摘要
Text(article.content.substring(0, 100))
.fontSize(14)
.fontColor('#666666')
.margin({ top: 8 })
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
// 底部信息
Row() {
// 作者
Image(article.authorAvatar || $r('app.media.ic_default_avatar'))
.width(20)
.height(20)
.borderRadius(10)
Text(article.author)
.fontSize(12)
.fontColor('#999999')
.margin({ left: 4 })
Blank()
// 点赞
Row() {
Image(article.isLiked ? $r('app.media.ic_liked') : $r('app.media.ic_like'))
.width(16)
.height(16)
Text(article.likeCount.toString())
.fontSize(12)
.fontColor('#999999')
.margin({ left: 4 })
}
.onClick(() => this.store.likeArticle(article.id))
// 收藏
Image(article.isFavorite ? $r('app.media.ic_favorited') : $r('app.media.ic_favorite'))
.width(16)
.height(16)
.margin({ left: 12 })
.onClick(() => this.store.favoriteArticle(article.id))
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ left: 16, right: 16, top: 8, bottom: 8 })
.onClick(() => {
this.navStack.pushPath({ name: 'Detail', param: { articleId: article.id } })
})
}
@Builder
buildDestination(name: string, param: Object) {
if (name === 'Detail') {
ArticleDetailPage({ articleId: param['articleId'] as string })
}
}
}
第七步:文章详情页
// 鸿蒙: 文章详情页
@Component
export struct ArticleDetailPage {
@Prop articleId: string = ''
@State article: ArticleInfo | null = null
@State loading: boolean = true
@State isLiking: boolean = false
aboutToAppear() {
this.loadDetail()
}
async loadDetail() {
this.loading = true
try {
this.article = await blogApi.getArticleDetail(this.articleId)
} catch (e) {
console.error('加载详情失败: ' + (e as Error).message)
} finally {
this.loading = false
}
}
build() {
Column() {
if (this.loading) {
Column() {
LoadingProgress()
.width(48)
.height(48)
.color('#0A59F7')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
} else if (this.article) {
// 文章内容
Scroll() {
Column() {
// 标题
Text(this.article.title)
.fontSize(22)
.fontWeight(FontWeight.Bold)
.width('100%')
// 作者信息
Row() {
Image(this.article.authorAvatar || $r('app.media.ic_default_avatar'))
.width(36)
.height(36)
.borderRadius(18)
Column() {
Text(this.article.author)
.fontSize(14)
.fontWeight(FontWeight.Medium)
Text(this.formatDate(this.article.createdAt))
.fontSize(12)
.fontColor('#999999')
.margin({ top: 2 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 8 })
Blank()
// 标签
ForEach(this.article.tags.slice(0, 2), (tag: string) => {
Text(tag)
.fontSize(12)
.fontColor('#0A59F7')
.backgroundColor('#EBF3FF')
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ left: 4 })
}, (tag: string, index: number) => `${tag}_${index}`)
}
.width('100%')
.margin({ top: 16 })
// 分割线
Divider()
.margin({ top: 16, bottom: 16 })
// 正文内容
Text(this.article.content)
.fontSize(16)
.lineHeight(28)
.width('100%')
}
.padding(16)
}
.layoutWeight(1)
// 底部操作栏
Row() {
Row() {
Image(this.article.isLiked ? $r('app.media.ic_liked') : $r('app.media.ic_like'))
.width(20)
.height(20)
Text(this.article.likeCount.toString())
.fontSize(14)
.fontColor('#666666')
.margin({ left: 4 })
}
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.onClick(() => this.toggleLike())
Row() {
Image(this.article.isFavorite ? $r('app.media.ic_favorited') : $r('app.media.ic_favorite'))
.width(20)
.height(20)
Text(this.article.isFavorite ? '已收藏' : '收藏')
.fontSize(14)
.fontColor('#666666')
.margin({ left: 4 })
}
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.onClick(() => this.toggleFavorite())
Blank()
Row() {
Image($r('app.media.ic_share'))
.width(20)
.height(20)
Text('分享')
.fontSize(14)
.fontColor('#666666')
.margin({ left: 4 })
}
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
}
.width('100%')
.padding({ left: 8, right: 8, top: 8, bottom: 8 })
.backgroundColor(Color.White)
.shadow({ radius: 8, color: '#1a000000', offsetY: -2 })
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
private async toggleLike() {
if (this.isLiking || !this.article) return
this.isLiking = true
try {
await blogApi.likeArticle(this.article.id)
this.article = {
...this.article,
isLiked: !this.article.isLiked,
likeCount: this.article.isLiked ? this.article.likeCount - 1 : this.article.likeCount + 1
}
} catch (e) {
console.error('点赞失败')
} finally {
this.isLiking = false
}
}
private async toggleFavorite() {
if (!this.article) return
try {
await blogApi.favoriteArticle(this.article.id)
this.article = { ...this.article, isFavorite: !this.article.isFavorite }
} catch (e) {
console.error('收藏失败')
}
}
private formatDate(timestamp: number): string {
let date = new Date(timestamp)
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
}
}
第八步:登录页
// 鸿蒙: 登录页
@Entry
@Component
struct LoginPage {
@StorageLink('appStore') store: AppStore = new AppStore()
@State username: string = ''
@State password: string = ''
@State isLoading: boolean = false
@State errorMsg: string = ''
build() {
Column() {
// Logo
Text('TechBlog')
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#0A59F7')
.margin({ bottom: 48 })
// 用户名
TextInput({ placeholder: '用户名' })
.width('80%')
.height(48)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.padding({ left: 16 })
.onChange((value: string) => {
this.username = value
this.errorMsg = ''
})
// 密码
TextInput({ placeholder: '密码' })
.type(InputType.Password)
.width('80%')
.height(48)
.borderRadius(8)
.backgroundColor('#F5F5F5')
.padding({ left: 16 })
.margin({ top: 16 })
.onChange((value: string) => {
this.password = value
this.errorMsg = ''
})
// 错误信息
if (this.errorMsg) {
Text(this.errorMsg)
.fontSize(14)
.fontColor('#FF0000')
.margin({ top: 12 })
}
// 登录按钮
Button('登录')
.width('80%')
.height(48)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.backgroundColor('#0A59F7')
.borderRadius(8)
.margin({ top: 32 })
.enabled(!this.isLoading && this.username.length > 0 && this.password.length > 0)
.onClick(() => this.doLogin())
// 注册
Text('还没有账号?立即注册')
.fontSize(14)
.fontColor('#0A59F7')
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.backgroundColor(Color.White)
}
private async doLogin() {
this.isLoading = true
this.errorMsg = ''
let success = await this.store.login(this.username, this.password)
this.isLoading = false
if (!success) {
this.errorMsg = '用户名或密码错误'
}
}
}
踩坑与注意事项
坑1:全局状态初始化顺序
AppStorage中的Store必须在UIAbility的onCreate中初始化,否则组件读取时会是空值。
export default class EntryAbility extends UIAbility {
onCreate(want, launchParam) {
// 必须在这里初始化全局Store
AppStorage.setOrCreate('appStore', new AppStore())
AppStorage.setOrCreate('token', '')
}
}
坑2:列表性能优化
文章列表如果数据量大,必须用@Reusable装饰器优化列表项组件:
@Reusable // 关键!组件复用
@Component
struct ArticleCardItem {
@Prop article: ArticleInfo = null
build() {
// ...
}
}
坑3:网络错误时的降级策略
移动端网络不稳定,必须有离线降级策略。上面的代码已经实现了——网络失败时读取本地缓存。
坑4:图片加载
Android用Glide/Coil加载图片,鸿蒙用Image组件直接加载。但要注意:
- 网络图片需要声明ohos.permission.INTERNET权限
- 大图需要设置.decodingSize()限制解码尺寸
- 列表中的图片建议用.thumbnailSize()设置缩略图大小
坑5:导航传参
NavPathStack传参时,参数类型是Object,需要手动类型转换:
@Builder
buildDestination(name: string, param: Object) {
// param是Object类型,需要手动转换
let articleId = param['articleId'] as string
if (name === 'Detail') {
ArticleDetailPage({ articleId: articleId })
}
}
HarmonyOS 6适配说明
HarmonyOS 6对完整应用迁移有几个关键改进:
-
@ObservedV2深度观测:AppStore中的嵌套对象变化能自动触发UI更新,不需要手动展开。
-
List性能优化:List组件的懒加载和回收机制更成熟,配合@Reusable,万级列表也能流畅滚动。
-
Navigation增强:NavPathStack支持路由拦截和转场动画,导航体验更接近原生。
-
分布式能力:文章详情页可以无缝流转到平板/智慧屏,这是Android完全没有的能力。
-
DevEco Studio 6迁移助手:支持一键分析Android项目结构,生成迁移计划,自动转换简单代码。
总结
完整应用迁移是一项系统工程,核心是"自底向上、逐层验证"。
| 维度 | 迁移难度 | 耗时占比 | 重要程度 |
|---|---|---|---|
| 数据模型 | ⭐ | 5% | ⭐⭐⭐⭐ |
| 数据库层 | ⭐⭐⭐ | 15% | ⭐⭐⭐⭐ |
| 网络层 | ⭐⭐⭐ | 15% | ⭐⭐⭐⭐⭐ |
| 状态管理 | ⭐⭐⭐ | 20% | ⭐⭐⭐⭐⭐ |
| UI层 | ⭐⭐⭐ | 35% | ⭐⭐⭐⭐⭐ |
| 导航 | ⭐⭐⭐⭐ | 5% | ⭐⭐⭐⭐ |
| 测试与优化 | ⭐⭐ | 5% | ⭐⭐⭐⭐⭐ |
迁移一个完整App,UI层占了35%的工作量,但大部分是机械性映射。真正需要动脑子的是状态管理和网络层——它们决定了App的架构质量。
最后一句忠告:别追求一次迁移到位。先让核心功能跑起来,再逐步迁移次要功能,最后打磨细节。敏捷迁移,小步快跑。
- 点赞
- 收藏
- 关注作者
评论(0)