HarmonyOS NEXT应用开发实战:玩鸿蒙App客户端开发 原创

特立独行的猫a
发布于 2025-3-28 15:32
浏览
0收藏

之前学习android时候,有一个玩android的客户端项目很火,不但能够学习知识,还能够动手实践,激发学习兴趣。这里作者通过一个完整的实战项目—玩鸿蒙客户端App,一块儿深入学习如何在HarmonyOS平台上开发一个功能丰富且完整的移动应用。玩鸿蒙客户端App旨在为用户提供一个方便的鸿蒙社区交流学习平台,用户可以通过该APP浏览社区的精彩文章、查看热门文章、以及阅读文章详情等。通过此实战,将能够掌握在HarmonyOS平台上使用@nutpi/axios网络库进行网络请求的基本方法,了解如何设计和实现简洁且美观的UI界面,以及如何遵循设计原则和最佳实践来开发一个功能完整的社区客户端App。

为什么选择玩鸿蒙App作为学习项目?

学习一项新技能,最快最直接的办法就是亲手去做一个项目。这样不仅能激发兴趣,还能让你从中不断总结经验,这种学习方式是最高效的。鸿蒙社区客户端App是一个简单小巧的应用,非常适合用来练手。在开发过程中,不仅能帮助你提升编程技能,还能让你在真实的开发环境中感受到项目开发的乐趣,看到自己的开发成果,形成一种正反馈,不断激励你学习进步。

此外,这样的项目非常适合入门,能够逐步培养你的开发能力,增强对技术的理解和运用。通过直接参与项目实战,可以提升学习效率和质量,培养解决实际问题的能力,促进知识体系的构建。不要等到每个细节都掌握后再去做项目,也不要等到完美了再开始,通过项目驱动的形式系统化学习,把前面学到的知识串联起来,避免知识点的碎片化。

玩鸿蒙客户端App的主要功能

玩鸿蒙客户端App是一个基于HarmonyOS平台的移动应用。该应用集成了多种功能,包括文章浏览、热门文章展示、文章详情阅读和个人中心页面。通过鸿蒙社区客户端App,用户可以轻松地使用网络接口获取数据,掌握常用组件的使用,享受丰富的内容和服务。本项目旨在为用户提供一个高效便捷的鸿蒙社区交流学习平台,促进信息的共享与互动。

项目中使用的主要网络接口

以下是本项目中使用的主要网络接口:

  • 查询文章列表GET https://api.nutpi.net/queryTopicList?page=1&pageSize=10,用于获取指定页码和每页数量的文章列表。
  • 查询热门文章GET https://api.nutpi.net/queryHotTopic,用于获取当前热门的文章列表。
  • 查询文章详情GET https://api.nutpi.net/thread?topicId=712,用于获取指定文章的详细信息。

使用@nutpi/axios进行网络请求

导入和配置

我们首先需要导入@nutpi/axios库,并进行相应的配置。配置信息包括基础URL、超时时间、是否显示加载对话框等。

import {AxiosHttpRequest,HttpPromise} from '@nutpi/axios'
import {AxiosHeaders,AxiosRequestHeaders,AxiosError } from '@nutpi/axios';
import { Log } from './logutil';
import { promptAction } from "@kit.ArkUI";

function showToast(msg:string){
  promptAction.showToast({ message: msg })
}

function showLoadingDialog(msg:string){
  promptAction.showToast({ message: msg })
}

function hideLoadingDialog() {}

export const BaseURL = "http://120.27.146.247:7000"
const axiosClient = new AxiosHttpRequest({
  baseURL: BaseURL+'/api/v1',
  timeout: 10 * 1000,
  showLoading:true,
  headers: new AxiosHeaders({
    'Content-Type': 'application/json'
  }) as AxiosRequestHeaders,
  interceptorHooks: {
    requestInterceptor: async (config) => {
      Log.debug('网络请求Request 请求方法:', `${config.method}`);
      Log.debug('网络请求Request 请求链接:', `${config.url}`);
      Log.debug('网络请求Request Params:', `\n${JSON.stringify(config.params)}`);
      Log.debug('网络请求Request Data:', `${JSON.stringify(config.data)}`);
      axiosClient.config.showLoading = config.showLoading
      if (config.showLoading) {
        showLoadingDialog("加载中...")
      }
      return config;
    },
    requestInterceptorCatch: (err) => {
      Log.error("网络请求RequestError", err.toString())
      if (axiosClient.config.showLoading) {
        hideLoadingDialog()
      }
      return err;
    },
    responseInterceptor: (response) => {
      if (axiosClient.config.showLoading) {
        hideLoadingDialog()
      }
      Log.debug('网络请求响应Response:', `\n${JSON.stringify(response.data)}`);
      if (response.status === 200) {
        const checkResultCode = response.config.checkResultCode
        if (checkResultCode && response.data.errorCode != 0) {
          showToast(response.data.errorMsg)
          return Promise.reject(response)
        }
        return Promise.resolve(response);
      } else {
        return Promise.reject(response);
      }
    },
    responseInterceptorCatch: (error) => {
      if (axiosClient.config.showLoading) {
        hideLoadingDialog()
      }
      Log.error("网络请求响应异常", error.toString());
      errorHandler(error);
      return Promise.reject(error);
    },
  }
});

function errorHandler(error: any) {
  if (error instanceof AxiosError) {
  } else if (error != undefined && error.response != undefined && error.response.status) {
    switch (error.response.status) {
      case 401:
        break;
      case 403:
        break;
      case 404:
        break;
      default:
    }
  }
}

export  {axiosClient,HttpPromise};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.

使用接口

接下来,我们定义了获取热门文章、话题列表和话题详情的接口函数。这些函数都返回一个HttpPromise对象,用于处理异步请求的结果。

import {axiosClient,HttpPromise} from '../../utils/axiosClient';
import { HotTopicResp } from '../bean/HotTopicResp';
import { TopicDetailResp } from '../bean/TopicDetailResp';
import { TopicListResp } from '../bean/TopicListResp';

export const getHotTopics = (): HttpPromise<HotTopicResp> => axiosClient.get({url:'/hottopic'});

export const getTopicList = (page:number): HttpPromise<TopicListResp> => axiosClient.get({url:'/topiclist',params:{page:page}});

export const getTopicDetail = (id:number): HttpPromise<TopicDetailResp> => axiosClient.get({url:'/topicdetail',params:{id:id}});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

设计UI界面

设计UI界面时,我们需要遵循简洁美观的原则,并结合鸿蒙平台的设计规范。这包括合理的布局、清晰的视觉层次、以及合适的颜色搭配等。通过设计UI界面,我们可以提升用户的体验,同时也能更好地展示应用程序的功能。
HarmonyOS NEXT应用开发实战:玩鸿蒙App客户端开发-鸿蒙开发者社区

import { Log } from '../../utils/logutil';
import { BusinessError } from '@kit.BasicServicesKit';
import { promptAction } from '@kit.ArkUI';
import { HotTopicItem } from '../../common/bean/HotTopicResp';
import { getHotTopics, getTopicList } from '../../common/api/nutpiApi';
import { TopicListItem } from '../../common/bean/TopicListResp';
import { BaseURL } from '../../utils/axiosClient';


@Component
export default struct Home {
  @Consume pageStack: NavPathStack
  private swiperController: SwiperController = new SwiperController()
  @State swiperData: HotTopicItem[] = []

  @State topicList:TopicListItem[] = []

  private curPage:number = 1;
  private curCount:number = 0;
  private total:number = 0;
  // 判断滚动条是否触底
  @State
  private isEnd: boolean = false;

  // 触底之后触发下一页
  getMoreRecList():void {
    console.log('getMoreRecList:')
    if(this.curCount >= this.total){
      promptAction.showToast({ message: '已经到底啦' })
      return
    }
    promptAction.showToast({ message: '数据加载中' })
    getTopicList(this.curPage).then((res) => {
      Log.debug(res.data.code)
      this.total= res.data.total
      this.topicList = this.topicList.concat(res.data.data);
      this.curCount += res.data.data.length
      this.curPage += 1
    }).catch((err:BusinessError) => {
      Log.debug("request","err.code:%d",err.code)
      Log.debug("request",err.message)
    });
  }

  // 组件生命周期
  aboutToAppear() {
    Log.info('Home aboutToAppear');
    getHotTopics().then((res) => {
      Log.debug(res.status)
      for (const itm of res.data.data) {
        this.swiperData.push(itm)
      }
    }).catch((err:BusinessError) => {
      Log.debug("request","err.code:%d",err.code)
      Log.debug("request",err.message)
    });

    getTopicList(1).then((res) => {
      Log.debug(res.status)
      this.topicList = res.data.data
      this.total= res.data.total
      this.curCount =this.topicList.length
      this.curPage += 1
    }).catch((err:BusinessError) => {
      Log.debug("request","err.code:%d",err.code)
      Log.debug("request",err.message)
    });

  }

  // 组件生命周期
  aboutToDisappear() {
    Log.info('Home aboutToDisappear');
  }

  build() {
      Scroll() {
        Column({ space: 0 }) {
          //title
          Text('鸿蒙社区').fontSize(26).fontWeight(FontWeight.Bold).textAlign(TextAlign.Center)
            .width('100%').height(50)
          // 轮播图
          Swiper(this.swiperController) {
            ForEach(this.swiperData, (item: HotTopicItem) => {
              Stack({ alignContent: Alignment.Center }) {
                Image(BaseURL+'/static/img'+item.cover)
                  .width('100%')
                  .height(180)
                  .backgroundColor(0xAFEEEE)
                  .zIndex(1)
                  .onClick(() => {
                    this.pageStack.pushDestinationByName("PageDetail", { id:item.id }).catch((e:Error)=>{
                      // 跳转失败,会返回错误码及错误信息
                      console.log(`catch exception: ${JSON.stringify(e)}`)
                    }).then(()=>{
                      // 跳转成功
                    });
                  })

                // 显示轮播图标题
                Text(item.title)
                  .padding(5)
                  .margin({ top: 110 })
                  .width('100%')
                  .height(60)
                  .textAlign(TextAlign.Center)
                  .maxLines(2)
                  .textOverflow({ overflow: TextOverflow.Clip })
                  .fontSize(18)
                  .fontColor(Color.White)
                  .opacity(100)// 设置标题的透明度 不透明度设为100%,表示完全不透明
                  .backgroundColor('#808080AA')// 背景颜色设为透明
                  .zIndex(2)
                  .onClick(() => {
                    this.pageStack.pushDestinationByName("PageDetail", { id:item.id }).catch((e:Error)=>{
                      // 跳转失败,会返回错误码及错误信息
                      console.log(`catch exception: ${JSON.stringify(e)}`)
                    }).then(()=>{
                      // 跳转成功
                    });
                  })
              }
            }, (item: HotTopicItem) => item.id)
          }
          .cachedCount(2)
          .index(1)
          .autoPlay(true)
          .interval(4000)
          .loop(true)
          .indicatorInteractive(true)
          .duration(1000)
          .itemSpace(0)
          .curve(Curve.Linear)
          .onChange((index: number) => {
            console.info(index.toString())
          })
          .onGestureSwipe((index: number, extraInfo: SwiperAnimationEvent) => {
            console.info("index: " + index)
            console.info("current offset: " + extraInfo.currentOffset)
          })
          .height(180) // 设置高度

          Text('文章列表').fontWeight(FontWeight.Bold).padding(10).alignSelf(ItemAlign.Start)

          List({ space: 5 }) {
            ForEach(this.topicList, (item:TopicListItem) => {
              ListItem() {
                Column(){
                  Row() {
                    Image(item.avatar).objectFit(ImageFit.Cover)
                      .width(100).height(85).borderRadius(5)

                    Column({ space: 10 }) {
                      Text(item.title)
                        .maxLines(2)
                        .textOverflow({overflow:TextOverflow.Ellipsis })
                        .fontSize(16).width('100%')
                        .fontWeight(FontWeight.Bold).textAlign(TextAlign.Start)
                      Text(item.summary)
                        .maxLines(2)
                        .textOverflow({overflow:TextOverflow.Ellipsis })
                        .fontSize(14).width('100%').textAlign(TextAlign.Start)
                    }.height(85).layoutWeight(1).align(Alignment.Start).padding({left:5})

                  }.size({ width: '100%' })

                  Row() {
                    Text(item.postTime).fontSize(14).padding(5)
                    Text("浏览:"+item.viewCnt).width(80).fontSize(12).padding(5)
                    Text(item.nickname).fontSize(12).padding(5)
                  }.width('100%').justifyContent(FlexAlign.Start).padding({ top:10 })
                }.padding({ left:10,right:10 })

              }.onClick(() => {
                this.pageStack.pushDestinationByName("PageDetail", { id:item.id }).catch((e:Error)=>{
                  // 跳转失败,会返回错误码及错误信息
                  console.log(`catch exception: ${JSON.stringify(e)}`)
                }).then(()=>{
                  // 跳转成功
                });
              })
            }, (itm:TopicListItem) => itm.id)
          }

          .divider({strokeWidth:2,color:'#F1F3F5'})
          .listDirection(Axis.Vertical)
          .edgeEffect(EdgeEffect.Spring, {alwaysEnabled:true})
          // 当画面能滚动说明没有触底
          .onScrollStart(() => {
            this.isEnd = false
          })
          // 判断当前是否停止滚动
          .onScrollStop(() => {
            // 如果停止滚动并且满足滚动条已经在底部进行数据的加载
            if (this.isEnd) {
              // 加载数据
              this.getMoreRecList();
            }
          })
          // 当滚动条触底把 flag 设置成 true
          .onReachEnd(() => {
            this.isEnd = true
            console.log("onReachEnd")
          })
        }
        .width('100%')
        .height('100%')
      }
      .width('100%')
      .height('100%')
      .scrollable(ScrollDirection.Vertical)
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.

总结

通过玩鸿蒙客户端App,我们可以系统地学习HarmonyOS平台的应用开发知识。从网络请求到UI设计,每一个环节都是一个宝贵的学习机会,能够帮助我们提升开发技能,更好地理解和运用技术。不要害怕从零开始,也不要追求完美,只要动手去做,就能不断进步。希望这篇实战教程能够帮助你在HarmonyOS平台上开发出优秀的应用。

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
收藏
回复
举报


回复
    相关推荐