HarmonyOS NEXT 免费无广告看电影app:从想法到实现的经验总结

news2025/5/10 16:17:21

学习一项新技能,最好也是最快的方法就是动手实战。学习鸿蒙也一样,给自己定一个小目标,直接找项目练,这样进步是最快的。最近,我在网上看到360周董的一句话:“想干什么就去干,干得烂总比不干强!”这对我来说,就像一盏明灯,照亮了我心中的迷雾。于是,我决定将自己心中的想法付诸行动,开发一款既无广告又免费的鸿蒙观影App——“爱影家”。

开发背景

一直以来,手机上的观影体验总是被各种广告打扰,让人不胜其烦。我想,如果能有一个干净、免费的观影平台,对于像我一样的普通用户来说,那该有多好!于是,“爱影家”这个项目就应运而生了。即便因某些原因你懂的,肯定无法上架,但是自己装自己手机上用着美,且达到了依靠兴趣来快速上手的目的,足够啦。
image.png
image.png

从想法到行动

兴趣是最好的老师,学习鸿蒙开发也同样如此。给自己定一个小目标,去上手实战,这比什么都重要。我给自己定了一个目标——开发一款基于HarmonyOS NEXT的影视客户端APP。

项目概述

“爱影家”是一个基于HarmonyOS NEXT的开源影视客户端APP项目,主要分为三个页面:影视首页、知乎日报页和个人中心页。通过这个项目,我不仅学习了如何使用HarmonyOS NEXT进行应用开发,还了解了如何进行API数据交互、前端展示和后端处理等基本功能。

开源仓库地址1:https://gitee.com/yyz116/hmmovie
开源仓库地址2: https://atomgit.com/csdn-qq8864/hmmovie

好的作品是需要不断打磨,在你的学习和体验过程中有任何问题,欢迎到我的开源项目代码仓下面提交issue,持续优化。

开发步骤
  1. 搭建开发环境:首先,我确保安装了HarmonyOS NEXT的开发环境。根据官方文档进行安装和配置,一切准备就绪。

  2. 创建项目:使用DevEco Studio创建一个新的HarmonyOS NEXT项目,并选择ArkTS作为开发语言。一切从零开始,充满了未知和挑战。

  3. 配置网络请求:项目中引入了@nutpi/axios库,配置了网络请求的基础URL和拦截器。

    import axios from '@nutpi/axios';
    
    axios.defaults.baseURL = 'https://api.example.com';
    axios.interceptors.request.use(config => {
        // 添加请求拦截器
        return config;
    }, error => {
        return Promise.reject(error);
    });
    
  4. 实现影视首页功能:在影视首页中,我实现了轮播图、热映电影、即将上映电影和热门电视剧集的功能。通过API获取数据并在前端展示,整个过程充满了学习和实践的乐趣。以下是网络后台接口封装。在HarmonyOS NEXT开发环境中,可以使用@nutpi/axios库来简化网络请求的操作。本项目使用HarmonyOS NEXT框架和@nutpi/axios库实现一行代码写接口。大幅简化了网络接口的实现。
    为什么选择@nutpi/axios?
    nutpi/axios是坚果派对axios封装过的鸿蒙HTTP客户端库,用于简化axios库的使用和以最简单的形式写代码。使用nutpi/axios库可以大大简化代码,使网络接口变得简单直观。

    import {axiosClient, HttpPromise} from '../../utils/axiosClient';
    import { HotMovieReq, MovieRespData, SwiperData } from '../bean/ApiTypes';
    
    // 1.获取轮播图接口
    export const getSwiperData = (): HttpPromise<SwiperData> => axiosClient.get({url:'/swiperdata'});
    
    // 2.获取即将上映影视接口
    export const getSoonMovie = (start:number, count:number): HttpPromise<MovieRespData> => axiosClient.post({url:'/soonmovie', data: { start:start, count:count }});
    
    // 3.获取热门影视接口
    export const getHotMovie = (req:HotMovieReq): HttpPromise<MovieRespData> => axiosClient.post({url:'/hotmovie', data:req});
    
    // 4.获取最新上演影视接口
    export const getNewMovie = (start:number, count:number): HttpPromise<MovieRespData> => axiosClient.post({url:'/newmovie', data: { start:start, count:count }});
    
    // 5.获取最热门剧集接口
    export const getHotTv = (start:number, count:number): HttpPromise<MovieRespData> => axiosClient.post({url:'/tvhot', data: { start:start, count:count }});
    
首页电影海报轮播图懒加载
// 轮播图
Swiper(this.swiperController) {
  LazyForEach(this.swiperData, (item: SwiperItem) => {
    Stack({ alignContent: Alignment.Center }) {
      Image(item.imageUrl)
        .width('100%')
        .height(180)
        .zIndex(1)
        .onClick(() => {
          this.pageStack.pushDestinationByName("MovieDetailPage", { id:item.id }).catch((e:Error)=>{
            // 跳转失败,会返回错误码及错误信息
            console.log(`catch exception: ${JSON.stringify(e)}`)
          }).then(()=>{
            // 跳转成功
          });
        })

      // 显示轮播图标题
      Text(item.title)
        .padding(5)
        .margin({ top: 135 })
        .width('100%')
        .height(60)
        .textAlign(TextAlign.Center)
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Clip })
        .fontSize(22)
        .fontColor(Color.White)
        .opacity(100)// 设置标题的透明度 不透明度设为100%,表示完全不透明
        .backgroundColor('#808080AA')// 背景颜色设为透明
        .zIndex(2)
        .onClick(() => {
          this.pageStack.pushDestinationByName("MovieDetailPage", { id:item.id }).catch((e:Error)=>{
            // 跳转失败,会返回错误码及错误信息
            console.log(`catch exception: ${JSON.stringify(e)}`)
          }).then(()=>{
            // 跳转成功
          });
        })
    }
  }, (item: SwiperItem) => 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) // 设置高度

电影详情页的设计

在电影详情页中,将使用 Badge、SymbolSpan、Button、Rating 等组件来展示电影的详细信息。

import { getDetailMv, getMovieSrc } from "../../common/api/movie"
import { Log } from "../../utils/logutil"
import { BusinessError } from "@kit.BasicServicesKit"
import { DetailMvResp, DetailMvRespCast } from "../../common/bean/DetailMvResp"
import { LengthMetrics, promptAction } from "@kit.ArkUI"
import { MvSourceResp } from "../../common/bean/MvSourceResp"

@Builder
export function MovieDetailPageBuilder() {
  Detail()
}

@Component
struct Detail {
  pageStack: NavPathStack = new NavPathStack()
  private uid = ''
  @State detailData: DetailMvResp | null = null;
  private srcData: MvSourceResp | null = null;

  private description: string = ''
  private isToggle = false
  @State toggleText: string = ''
  @State toggleBtn: string = '展开'

  build() {
    NavDestination() {
      Column({ space: 0 }) {
        Row() {
          Image(this.detailData?.images).objectFit(ImageFit.Auto).width(120).borderRadius(5)
          Column({ space: 8 }) {
            Text(this.detailData?.title).fontSize(18)
            Text(this.detailData?.year + " " + this.detailData?.genre).fontSize(14)

            Row() {
              Badge({
                count: this.detailData?.wish_count,
                maxCount: 10000,
                position: BadgePosition.RightTop,
                style: { badgeSize: 22, badgeColor: '#fffab52a' }
              }) {
                Row() {
                  Text() {
                    SymbolSpan($r('sys.symbol.heart'))
                      .fontWeight(FontWeight.Lighter)
                      .fontSize(32)
                      .fontColor(['#fffab52a'])
                  }
                  Text('想看')
                }.backgroundColor('#f8f4f5').borderRadius(5).padding(5)
              }.padding(8)

              Blank(10).width(40)
              Badge({
                count: this.detailData?.reviews_count,
                maxCount: 10000,
                position: BadgePosition.RightTop,
                style: { badgeSize: 22, badgeColor: '#fffab52a' }
              }) {
                Row() {
                  Text() {
                    SymbolSpan($r('sys.symbol.star'))
                      .fontWeight(FontWeight.Lighter)
                      .fontSize(32)
                      .fontColor(['#fffab52a'])
                  }
                  Text('看过')
                }.backgroundColor('#f8f4f5').borderRadius(5).padding(5)
              }.padding(8)
            }

            Button('播放', { buttonStyle: ButtonStyleMode.NORMAL, role: ButtonRole.NORMAL })
              .borderRadius(8)
              .borderColor('#fffab52a')
              .fontColor('#fffab52a')
              .width(100)
              .height(35)
              .onClick(() => {
                console.info('Button onClick')
                if (this.srcData != null) {
                  this.pageStack.pushDestinationByName("VideoPlayerPage", { item: { video: this.srcData.urls[0], tvurls: this.srcData.tvurls, title: this.srcData.title, desc: this.detailData?.summary } }).catch((e: Error) => {
                    // 跳转失败,会返回错误码及错误信息
                    console.log(`catch exception: ${JSON.stringify(e)}`)
                  }).then(() => {
                    // 跳转成功

                  });
                } else {
                  promptAction.showToast({ message: '暂无资源' })
                }
              })
          }.alignItems(HorizontalAlign.Start) // 水平方向靠左对齐
            .justifyContent(FlexAlign.Start)   // 垂直方向靠上对齐
            .padding(10)
        }.height(160).width('100%')

        Row() {
          Text('豆瓣评分').fontSize(16).padding(5)
          Rating({ rating: (this.detailData?.rate ?? 0) / 2, indicator: true })
            .stars(5)
            .stepSize(0.5).height(28)

          Text(this.detailData?.rate.toString()).fontColor('#fffab52a').fontWeight(FontWeight.Bold).fontSize(36).padding(5)
        }.width('100%').height(80).borderRadius(5).backgroundColor('#f8f4f5').margin(20)

        Text('简介').fontSize(18).padding({ bottom: 10 }).fontWeight(FontWeight.Bold).alignSelf(ItemAlign.Start)

        Text(this.toggleText).fontSize(14).lineHeight(20).alignSelf(ItemAlign.Start)
        Text(this.toggleBtn).fontSize(14).fontColor(Color.Gray).padding(10).alignSelf(ItemAlign.End).onClick(() => {
          this.isToggle = !this.isToggle
          if (this.isToggle) {
            this.toggleBtn = '收起'
            this.toggleText = this.description
          } else {
            this.toggleBtn = '展开'
            this.toggleText = this.description.substring(0, 100) + '...'
          }
        })

        Text('影人').fontSize(18).padding({ bottom: 10 }).fontWeight(FontWeight.Bold).alignSelf(ItemAlign.Start)
        Scroll() {
          Row({ space: 5 }) {
            ForEach(this.detailData?.cast, (item: DetailMvRespCast) => {
              Column({ space: 0 }) {
                Image(item.cover).objectFit(ImageFit.Auto).height(120).borderRadius(5)
                  .onClick(() => {

                  })

                Text(item.name)
                  .alignSelf(ItemAlign.Center)
                  .maxLines(1)
                  .textOverflow({ overflow: TextOverflow.Ellipsis })
                  .fontSize(14).padding(10)
              }.justifyContent(FlexAlign.Center)

            }, (itm: DetailMvRespCast, idx) => itm.id)
          }
        }.scrollable(ScrollDirection.Horizontal)

      }.padding({ left: 10, right: 10 })
    }.title("电影详情")
      .width('100%')
      .height('100%')
      .onReady(ctx => {
        this.pageStack = ctx.pathStack
        //从上个页面拿参数
        this.pageStack.getParamByName("MovieDetailPage")
        interface params {
          id: string;
        }
        let par = ctx.pathInfo.param as params
        Log.debug("par:%s", par.id)
        this.uid = par.id
      })
      .onShown(() => {
        console.info('Detail onShown');
        getDetailMv(this.uid).then((res) => {
          Log.debug(res.data.message)
          Log.debug("request", "res.data.code:%{public}d", res.data.code)
          this.detailData = res.data
          this.description = this.detailData.summary
          this.toggleText = this.description.substring(0, 100) + '...'
        }).catch((err: BusinessError) => {
          Log.debug("request", "err.data.code:%d", err.code)
          Log.debug("request", err.message)
        });

        getMovieSrc(this.uid).then((res) => {
          Log.debug(res.data.message)
          Log.debug("request", "res.data.code:%{public}d", res.data.code)
          if (res.data.code == 0) {
            this.srcData = res.data
          }
        }).catch((err: BusinessError) => {
          Log.debug("request", "err.data.code:%d", err.code)
          Log.debug("request", err.message)
        });
      })
  }
}

折叠效果的实现

在电影详情页中,对于电影的简介,使用了折叠效果,即默认只显示部分简介内容,用户点击“展开”按钮后可以查看完整简介。这个效果的实现主要通过控制 Text 组件的显示内容来实现。具体代码如下:

Text(this.toggleText).fontSize(14).lineHeight(20).alignSelf(ItemAlign.Start)
Text(this.toggleBtn).fontSize(14).fontColor(Color.Gray).padding(10).alignSelf(ItemAlign.End).onClick(() => {
  this.isToggle = !this.isToggle
  if (this.isToggle) {
    this.toggleBtn = '收起'
    this.toggleText = this.description
  } else {
    this.toggleBtn = '展开'
    this.toggleText = this.description.substring(0, 100) + '...'
  }
})

image.png

心路历程

从一开始对鸿蒙开发的陌生,到如今能够熟练地完成项目,这背后是无数次的尝试、失败和总结。遇到问题时,我会查阅官方文档,甚至会寻求社区的帮助。每当解决一个问题,都会有一种成就感。通过这个项目,我不仅提升了编程技能,也学会了如何进行项目管理和时间规划。最重要的是,我体验到了项目开发的乐趣,每一次的进步都让我更加自信。

技术突破与职业发展

开发“爱影家”让我在技术上有了显著的突破,尤其是在API数据交互和前端展示方面。更让我高兴的是,这个项目也让我在职业发展上获得了宝贵的经验。现在,我已经能够独立承担一些小项目,甚至帮助一些朋友解决他们遇到的技术难题。开发“爱影家”的过程,无疑是我在鸿蒙开发旅程中最宝贵的一段经历。

结语

“想干什么就去干,干得烂总比不干强!”这句话对我来说意义非凡。也许一开始的作品并不完美,但只要迈出了第一步,未来就会越来越熟练,也就会有成绩有起色。做事情不要想太多,尤其是别太去计较什么意义和得失,开心就好。希望我的开发手记能够激励到更多的鸿蒙开发者,让我们一起踏上鸿蒙之旅,鸿蒙开发相比android太简单了,让鸿蒙生态因你而更加繁荣!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2372432.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

代码随想论图论part06冗余连接

图论part06 冗余连接 代码随想录 冗余边就是已经边已经在并查集里了&#xff0c;从图的角度来说构成了环&#xff08;冗余连接2要用到这个概念&#xff09; 代码其他部分为&#xff1a;并查集初始化&#xff0c;查根&#xff0c;判断是否在集合里&#xff0c;加入集合 冗余…

企业高性能WEB服务器—Nginx

Nginx介绍 Nginx是一款轻量级的网页服务器、反向代理服务器以及电子邮件代理服务器。 具有高并发&#xff08;特别是静态资源&#xff09;、占用系统资源少的特性。它不仅是Web服务软件&#xff0c;还具有反向代理负载均衡功能和缓存服务功能 具备如下基本特性 可针对静态资…

Python MNE-Python 脑功能磁共振数据分析

一、什​​么​​是​​Python MNE-Python 脑​​功​​能​​磁​​共​​振​​数​​据​​分​​析 为大脑功能磁共振成像数据分析工具&#xff0c;致力于为神经科学研究提供便捷、高效的数据分析处理工具。MNE-Python提供了处理和分析脑电图&#xff08;EEG&#xff09;、…

DevExpressWinForms-AlertControl-使用教程

文章目录 AlertControl-使用教程一、将 AlertControl 添加到 Form二、编辑 AlertControl 的 HtmlTemplateHTML Template Editor介绍编辑HTML Template 三、使用AlertControl弹出AlertAlert中的按钮事件获取 Alert 标题等信息向Alert传递参数 总结源码 AlertControl-使用教程 一…

【PostgreSQL数据分析实战:从数据清洗到可视化全流程】电商数据分析案例-9.4 可视化报告输出

&#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 文章大纲 电商数据分析实战&#xff1a;基于PostgreSQL的可视化报告生成全流程9.4 可视化报告输出9.4.1 可视化报告设计框架9.4.1.1 报告目标与受众9.4.1.2 数据准备与指标体系 9.4.2…

屎上雕花系列-2nd

以下为“屎上雕花”的尝试2nd 使用Deepseek扩容而来&#xff0c;我竟然没有找到明显的错误&#xff0c;太强大了&#xff0c;工作改变生活了 LeCroy 以太网与 SAN 网络测试解决方案 硬件平台一&#xff1a;Xena 以太网流量生成器 Xena 以太网流量生成器是一款高性能的网络测…

MCP专题| 突破LLM三大瓶颈!模型上下文协议(MCP)如何重塑AI交互体验?

最近引爆了整个AI圈的Model Context Protocol&#xff08;MCP&#xff09;到底是什么&#xff1f;你是否也和小编一样一头雾水&#xff0c;不是说好的LLM风潮呢&#xff0c;怎么现在变成通信协议啦&#xff1f;最近小编也是找到一篇神仙综述&#xff0c;带你一遍搞清什么是MCP的…

我的AD快捷键方案【留存】

留存我的快捷键方案文件&#xff0c;以便换电脑的时候能够快速导入快捷键。 我的快捷键文件&#xff1a; 通过网盘分享的文件&#xff1a;JB20250509.DXPPrf 链接: https://pan.baidu.com/s/1t6V0GjdGFPNSFydP5Z_tfg?pwde4xs 提取码: e4xs 复制这段内容后打开百度网盘手机Ap…

Edwards爱德华STP泵软件用于操作和监控涡轮分子泵

Edwards爱德华STP泵软件用于操作和监控涡轮分子泵

QT6(35)4.8定时器QTimer 与QElapsedTimer:理论,例题的界面搭建,与功能的代码实现。

&#xff08;112&#xff09; &#xff08;113&#xff09;模仿随书老师给的源代码搭建的&#xff0c; LCD 显示的部分不一样 &#xff1a; &#xff08;114&#xff09;以下开始代码完善&#xff1a; 关联定时器的信号与槽函数 &#xff1a; &#xff08;115&#xff09;…

02 mysql 管理(Windows版)

一、启动及关闭 MySQL 服务器 1.1 通过 “服务” 管理工具 winr打开运行&#xff0c;输入services.msc 找到MySQL80&#xff0c;这个是我们在安装mysql的时候给的服务的名称&#xff0c;具体见文章mysql 安装 右键选择启动或者停止。 1.2 通过命令提示符 1.2.1 关闭命令…

不同渲染任务,用CPU还是GPU?

一、CPU与GPU渲染的核心差异与选型建议 CPU渲染的核心优势与适用场景 复杂场景处理能力&#xff1a;CPU凭借强大的多核性能&#xff08;如AMD Threadripper 3990x的64核&#xff09;和高内存容量&#xff08;最高支持512GB&#xff09;&#xff0c;擅长处理影视级光线追踪、全…

硅基计划 学习总结 拾贰

一、二级指针 难道指针也有分等级的吗&#xff0c;我们学过的指针要存放变量的地址的&#xff0c;那二级指针是干嘛的呢&#xff1f; 一级指针&#xff1a;int a 10; int *pa &a; 指针变量&#xff0c;它终究是个变量&#xff0c;也有自己的地址 那我们以后是不是可以通…

【C语言指针超详解(三)】--数组名的理解,一维数组传参的本质,冒泡排序,二级指针,指针数组

目录 一.数组名的理解 二.使用指针访问数组 三.一维数组传参的本质 四.冒泡排序 五.二级指针 六.指针数组 6.1--指针数组的定义 6.2--指针数组模拟二维数组 &#x1f525;个人主页&#xff1a;草莓熊Lotso的个人主页 &#x1f3ac;作者简介&#xff1a;C方向学习者 &…

QT聊天项目DAY10

1.封装redis操作类 头文件 #ifndef REDISMANAGE_H #define REDISMANAGE_H#include "Singletion.h" #include "GlobalHead.h"class RedisManage : public Singletion<RedisManage> {friend class Singletion<RedisManage>; public:~RedisMana…

养生:开启健康生活的钥匙

养生&#xff0c;是对生活的精心呵护&#xff0c;是通往健康之路的秘诀。以下从饮食、运动、睡眠和心态四个方面&#xff0c;为你呈现科学养生之道。 饮食养生&#xff1a;营养均衡的智慧 合理的饮食是养生的基础。遵循 “食物多样&#xff0c;谷类为主” 的原则&#xff0c;…

基于springboot的海洋环保知识分享系统的设计与实现

博主介绍&#xff1a;java高级开发&#xff0c;从事互联网行业六年&#xff0c;熟悉各种主流语言&#xff0c;精通java、python、php、爬虫、web开发&#xff0c;已经做了六年的毕业设计程序开发&#xff0c;开发过上千套毕业设计程序&#xff0c;没有什么华丽的语言&#xff0…

操作系统 第2章节 进程,线程和作业

一:多道程序设计 1-多道程设计的目的 for:提高吞吐量(作业道数/处理时间),我们可以从提高资源的利用率出发 2-单道程序设计缺点: 设备的利用率低,内存的利用率低,处理机的利用率低 比如CPU去访问内存,CPU空转.内存等待CPU访问也是没有任何操作的.要是有多个东西要去访问不冲…

RT-Thread 深入系列 Part 2:RT-Thread 内核核心机制深度剖析

摘要&#xff1a; 本文从线程管理、调度器原理、中断处理与上下文切换、IPC 同步机制、内存管理五大核心模块出发&#xff0c;深入剖析 RT-Thread 内核实现细节&#xff0c;并辅以源码解读、流程图、时序图与性能数据。 目录 线程管理与调度器原理 1.1 线程控制块&#xff08;T…

在线caj转换word

CAJ格式是中国知网特有的一种文献格式&#xff0c;在学术研究等领域广泛使用&#xff0c;但有时我们需要将其转换为Word格式&#xff0c;方便编辑、引用文献。本文分享如何轻松将CAJ转换为word的转换工具&#xff0c;提高阅读和办公效率。 如何将CAJ转换WORD? 1、使用CAJ转换…