在繁忙的都市生活中,我喜欢找一个安静的角落,拿起手机沉浸在《凡人修仙传》的世界里跟随道友一同修仙。然而,这本该是享受的过程,却常常被各种广告打断,甚至有些视频需要付费才能观看。我意识到如果能开发一款既无广告又免费的观影App,不仅能满足自己,还能帮助更多和我一样的鸿蒙用户作为有趣的学习素材。于是,我决定将这个想法付诸行动。”

鸿蒙HarmonyOS操作系统以其独特的优势吸引了我。它不仅界面简洁流畅、开发效率高,还具有强大的生态系统支持。我开始学习鸿蒙开发,不断从社区里汲取知识,最终决定开发一个名为“鸿蒙观影”的App。从零开始,我充满了期待,但也伴随着不少挑战。

第一次跑通功能的狂喜

初入鸿蒙开发圈,我就像一个初入修仙界的凡人。韩老魔在乱星海修仙,我则是在鸿蒙的世界里修练,充满了好奇和兴奋。我开始学习鸿蒙的基本开发框架,阅读官方文档,参加各类线上课程,逐渐掌握了鸿蒙开发的基础知识。随着知识的积累,我开始动手编写代码,尝试搭建观影App的基本框架。

在这里插入图片描述

经过数日的努力,我终于实现了一个完整的观影app功能。当在鸿蒙手机上看到视频流畅播放时,我激动不已。那一刻,所有的辛劳都化为了喜悦,我知道自己已经迈出了第一步,鸿蒙观影的第一步已经成功了。

在这里插入图片描述


在这里插入图片描述

项目开源地址: HarmonyOS版github :https://github.com/yangyongzhen/hmmovie

深夜改bug的倔强

开发过程并非一帆风顺,遇到bug是常有的事情。尤其是当我们开发的应用涉及到网络请求和视频播放时,bug的出现更是难以避免。为了实现更稳定的应用体验,我经常熬夜排查问题,查找资料,反复测试,直到问题解决为止。

有一次,我在测试视频播放功能时发现,视频播放时会出现卡顿现象。起初我以为是网络问题,但经过多次测试,我发现无论是在高速网络还是4G网络下,卡顿现象依然存在。我迅速意识到,这可能与应用的代码逻辑优化有关。于是,我开始阅读更多关于鸿蒙开发优化的资料,尝试各种方法解决这个问题。经过数日的努力,我终于找到了问题的根源,并成功优化了代码,使视频播放更加流畅。

原始网络请求繁琐,简化网络接口使用

我在项目中引入了@nutpi/axios库,配置了网络请求的基础URL和拦截器。原始的写法太繁琐,给我造成了不少困难。结果我封装成了三方库的形式,nutpi/axios库就是我封装并发布到开源鸿蒙中心仓的。通过这个库,写网络接口彻底变成一种享受,一分钟写完一个网络接口。

// 引入@nutpi/axios库
import axios from '@nutpi/axios';

// 配置网络请求的基础URL
axios.defaults.baseURL = 'https://api.example.com';

// 添加请求拦截器
axios.interceptors.request.use(config => {
    // 添加请求拦截器
    return config;
}, error => {
    return Promise.reject(error);
});

实现影视首页功能

在影视首页中,我实现了轮播图、热映电影、即将上映电影和热门电视剧集的功能。通过API获取数据并在前端展示,整个过程充满了学习和实践的乐趣。以下是网络后台接口封装。在HarmonyOS NEXT开发环境中,可以使用@nutpi/axios库来简化网络请求的操作。本项目使用HarmonyOS NEXT框架和@nutpi/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 }});

电影详情页的设计

在电影详情页中,将使用BadgeSymbolSpanButtonRating等组件来展示电影的详细信息。整个设计过程让我深刻体会到了鸿蒙组件的强大和灵活。

// 引入相关组件和方法
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 { promptAction } from "@kit.ArkUI";

@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) + '...';
    }
});

开发心路历程

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

选择@nutpi/axios的原因

为什么选择@nutpi/axiosnutpi/axios是坚果派对axios封装过的鸿蒙HTTP客户端库,用于简化axios库的使用和以最简单的形式写代码。使用nutpi/axios库可以大大简化代码,使网络接口变得简单直观。

结语

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

通过这个项目,还感受到了鸿蒙开发的魅力和乐趣。未来,我将继续探索鸿蒙开发的更多可能性,为鸿蒙社区贡献一份力量。同时,我也希望更多的开发者能够加入到鸿蒙开发的行列中来,成为我的道友,咱们共同打造一个更加美好的鸿蒙生态系统。道友,不知你意下如何?

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐