断断续续学习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), ); } }, ); }}