记录学习flutter开发仿网易云音乐(1)

767 阅读5分钟

断断续续学习flutter有一段时间后,就想着做一个demo应用来练练手,看到之前已经有大佬做了一个flutter网易云,所以也跟着做了一个。

放一下大佬的链接github.com/boyan01/flu…

另外api方面,用的是github.com/Binaryify/N…

全局状态管理方面用的是bloc+provider的模式,也是参考外网大佬的实现

本地存储使用的是shared_preferences,不知道是不是js写多了老是感觉像js-cookie

效果截图

登录界面

登录界面的话很容易,其实就是两个输入框,用组件TextField还有控制器TextEditingController就可以完成输入了,登录按钮的话用container实现,圆角幅度在属性decoration设置borderRadius,然后最外层还要用GestureDetector监控手势动作就可以了。输入账号密码后点击登录,若登录方法回调成功即调用**Navigator.pushNamed(context, '/home')**跳转进入主页。

登录按钮代码如下

        GestureDetector(          child: Container(            width: double.infinity,            height: ScreenUtil().setWidth(120),            decoration: BoxDecoration(              color: Colors.red,              borderRadius: BorderRadius.circular(                ScreenUtil().setWidth(60),              ),            ),            child: Center(              child: Text(                'Login',                style: TextStyle(                  color: Colors.white,                  fontSize: ScreenUtil().setSp(48),                ),              ),            ),          ),          onTap: () async {            var phone = _phoneController.text;            var pwd = _pwdController.text;            bloc.login(phone: phone, password: pwd).then((_) {              Fluttertoast.showToast(                msg: '登录成功',                toastLength: Toast.LENGTH_SHORT,                gravity: ToastGravity.CENTER,                timeInSecForIosWeb: 1,                backgroundColor: Colors.black45,                textColor: Colors.white,                fontSize: 16.0,              );              Navigator.pushNamed(context, '/home');            });          },        )

主页

主页的话,仔细看可以分三个区块出来,分别是上部的轮播图,中部的跳转按钮列,底部的推荐歌单,可以单独实现这3个组件然后再放到主页面,方便维护。

1.轮播图

轮播图组件我封装成了SwiperBanner,使用的是flutter_swiper这个包,推荐使用,文档写得很详细,配置项也多,方便自己去修改,Swiper组件最外层需要用一层container组件包裹,设置好宽高即可,另外我使用了FutureBuilder,方便显示loading。重要的一点是需要对轮播图的item绑定点击事件,因为需要根据不同的item类型(调用网易云接口/banner有返回)触发不同的行为,例如跳转到播放页播放歌曲,或者是跳转到专辑详情页,mv播放页,还有webview页。

调用轮播图数据接口返回结果

swiper插件使用

          Swiper(            itemCount: list.length,            autoplay: true,            duration: 500,            outer: false,            itemBuilder: (context, i) {              return Container(                width: MediaQuery.of(context).size.width,                height: MediaQuery.of(context).size.width / 2.5,                child: Stack(                  children: <Widget>[                    Container(                      height: MediaQuery.of(context).size.width / 3,                      color: Colors.red,                    ),                    Positioned(                      top: 10,                      left: 10,                      child: CacheImage(                        width: MediaQuery.of(context).size.width - 20,                        height: (MediaQuery.of(context).size.width / 2.5) - 20,                        url: list[i].pic,                        radius: 4,                      ),                    )                  ],                ),              );            },            pagination: SwiperPagination(              margin: EdgeInsets.fromLTRB(5, 0, 5, 15),              builder: DotSwiperPaginationBuilder(                color: Colors.grey.withOpacity(0.58),                size: 6,                activeSize: 6,              ),            ),          );

轮播图点击行为

            onTap: (int index) async {              Banners banner = list[index]; // 获取当前点击的轮播图item              int targetType = banner.targetType;              if (targetType == 0 || targetType == 3000) { // 需要跳转到webview                String url = banner.url;                bool can = await canLaunch(url);  // 判断当前url是否可以加载                if (can) {                  Navigator.pushNamed(context, '/webview', arguments: url);                }              } else if (targetType == 1) { // 需要播放歌曲                MusicPlayerBloc bloc = Provider.of<MusicPlayerBloc>(                  context,                  listen: false,                );                Map res = await api.getSongDetail(id: banner.id);                Song song = Song.fromJson(res);                bloc.playSongList([song], 0);                Future.delayed(Duration(microseconds: 500), () {                  Navigator.pushNamed(context, '/playPage');                });              } else if (targetType == 10) { // 需要跳转到专辑详情                Navigator.pushNamed(                  context,                  '/albumDetail',                  arguments: banner.targetId,                );              } else if (targetType == 1000) {                Navigator.pushNamed(   // 需要跳转到歌单详情                  context,                  '/songSheetList',                  arguments: banner.targetId,                );              } else if (targetType == 1004) {                // 此处应该是mv播放页,传入mvid后调用获取mv播放源跟获取mv详情              }            },

2.跳转按钮列表

这块地方使用row封装一下就好了,很容易,无非就是给各个按钮添加不同的点击行为。可以封装按钮,传入标题跟图片返回一个widget,减少重复代码。

3.推荐歌单

推荐歌单组件分上下两块,上面是标题以及跳转按钮,下面才是推荐歌单列表。

推荐歌单的数据调的是接口/personalized,获取数据后,再使用map方法渲染各个item,布局的话使用了Wrap,自动换行,然后给每个item包裹一层InkWell(其实跟GestureDetector这个很像,只是InkWell在点击后会有波纹动画),监控点击事件,触发后跳转到对应的歌单详情页即可。

对于每个歌单item的生成,为了使歌单缩略图上面有一个播放按钮,可以用Stack和Positioned这两个组件,感觉就是css里面的absolute的实现,下面是歌单item的实现代码

 Widget _buildSheetItem(RecommendSongSheet sheet) {    return InkWell(      child: Container(        width: (MediaQuery.of(context).size.width - 16 * 4) / 3,        child: Column(          children: <Widget>[            Stack(              children: <Widget>[                CacheImage(                  url: sheet.picUrl,                  width: (MediaQuery.of(context).size.width - 64) / 3,                  height: 150,                  radius: 4,                ),                Positioned(                  top: 5,                  right: 10,                  child: Icon(                    Icons.play_circle_outline,                    color: Colors.white,                    size: 18,                  ),                )              ],            ),            SizedBox(height: 6),            Container(              child: Text(                sheet.name,                style: TextStyle(                  fontSize: ScreenUtil().setSp(28),                ),                maxLines: 2,                overflow: TextOverflow.ellipsis,              ),              width: (MediaQuery.of(context).size.width - 16 * 4) / 3,            )          ],        ),      ),      onTap: () {        Navigator.pushNamed(context, '/songSheetList', arguments: sheet.id);      },    );  }

歌单详情&专辑详情

歌单详情跟专辑详情页内容基本一致,所以放到一起写了。路由跳转进详情页的时候,需要带歌单id或者专辑id,例如

Navigator.pushNamed(context, '/songSheetList', arguments: sheet.id);

进入页面,根据携带的id获取数据详情,生成详情,这里使用FutureBuilder,在数据加载过程中,显示loading动画,加载完成后才进行展示,根据上面的效果可以看出使用的是CustomScrollView,内容方面分成两块组件,

一块是Top,Top组件需要传入歌单或专辑的详细数据,展示缩略图,标题,作者,以及其他相关数据,点击缩略图还可以弹出歌单专辑描述。

另一块是SliverSongList,需要传入歌单或专辑中的关联歌曲列表,点击对应的歌曲item后可以跳转到播放页进行播放。

下面是详情页的代码

class _SongSheetListState extends State<SongSheetList> {  Future futureDetail;  @override  void initState() {    // TODO: implement initState    super.initState();    futureDetail = getDetail();  }  Future<SongSheetDetail> getDetail() async {    Map res = await api.getPlayList(id: widget.id);    return SongSheetDetail.fromJson(res);  }  @override  Widget build(BuildContext context) {    return FutureBuilder<SongSheetDetail>(      future: futureDetail,      builder: (BuildContext context, AsyncSnapshot<SongSheetDetail> async) {        if (async.connectionState == ConnectionState.active ||            async.connectionState == ConnectionState.waiting) {          return Center(            child: CircularProgressIndicator(),          );        } else if (async.connectionState == ConnectionState.done) {          return Scaffold(            backgroundColor: Colors.white,            body: CustomScrollView(              slivers: <Widget>[                Top(detail: async.data),                SliverSongList(                  songList: async.data.songList,                )              ],            ),          );        } else {          return Center(            child: Icon(Icons.error),          );        }      },    );  }}