HarmonyOS开发:迁移实战——完整Android应用迁移鸿蒙

举报
Jack20 发表于 2026/06/28 21:07:30 2026/06/28
【摘要】 HarmonyOS开发:迁移实战——完整Android应用迁移鸿蒙📌 核心要点:完整应用迁移不是逐文件翻译,而是"先分析→再拆解→逐层迁移→集成验证"的系统工程,这篇文章带你走一遍全流程。 背景与动机前面9篇文章,我们讲了架构对比、组件映射、状态管理、网络层、数据层……每一层都单独讲清楚了。但真正的迁移不是一层一层孤立进行的——你要把一个完整的Android应用搬到鸿蒙上,UI、状态、网...

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对完整应用迁移有几个关键改进:

  1. @ObservedV2深度观测:AppStore中的嵌套对象变化能自动触发UI更新,不需要手动展开。

  2. List性能优化:List组件的懒加载和回收机制更成熟,配合@Reusable,万级列表也能流畅滚动。

  3. Navigation增强:NavPathStack支持路由拦截和转场动画,导航体验更接近原生。

  4. 分布式能力:文章详情页可以无缝流转到平板/智慧屏,这是Android完全没有的能力。

  5. DevEco Studio 6迁移助手:支持一键分析Android项目结构,生成迁移计划,自动转换简单代码。

总结

完整应用迁移是一项系统工程,核心是"自底向上、逐层验证"。

维度 迁移难度 耗时占比 重要程度
数据模型 5% ⭐⭐⭐⭐
数据库层 ⭐⭐⭐ 15% ⭐⭐⭐⭐
网络层 ⭐⭐⭐ 15% ⭐⭐⭐⭐⭐
状态管理 ⭐⭐⭐ 20% ⭐⭐⭐⭐⭐
UI层 ⭐⭐⭐ 35% ⭐⭐⭐⭐⭐
导航 ⭐⭐⭐⭐ 5% ⭐⭐⭐⭐
测试与优化 ⭐⭐ 5% ⭐⭐⭐⭐⭐

迁移一个完整App,UI层占了35%的工作量,但大部分是机械性映射。真正需要动脑子的是状态管理和网络层——它们决定了App的架构质量。

最后一句忠告:别追求一次迁移到位。先让核心功能跑起来,再逐步迁移次要功能,最后打磨细节。敏捷迁移,小步快跑。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。