Flutter - 鹰眼视频 鹰眼视频 App 相关技术知识点
目录结构
页面拆封
封装 dio < Flutter http 插件 >
使用懒加载图片特效
json自动序列化 model 映射
ListView 组件
GestureDetector 手势
路由导航
视频播放
目录结构 1 2 3 4 5 6 7 lib comments // 组件封装 model // 序列号model page // 页面 serves // http 相关的 目录 view // 存放一些 组件组合的 控件 main.dart // Flutter入口dart文件
页面拆封 main.dart 入口文件内容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import 'package:flutter/material.dart' ;import 'package:yysp/page/homePage.dart' ;void main()=>runApp(new RootPage());class RootPage extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: '鹰眼视频' , home: new HomePage() ); } }
RootPage组件类是根入口,用于汇集所有的 page页面。同时页面的路由配置就可以放到这个RootPage类中去管理。
导入其他文件时的2中方法,如下。
import ‘package:yysp/page/homePage.dart’; import ‘./page/homePage.dart’;
homePage.dart 首页 该文件存放在 ** /page/ ** 目录下, ** page ** 目录用于管理所有的页面资源。
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 import 'package:flutter/material.dart' ;class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State <HomePage > { @override Widget build(BuildContext context) { return new MaterialApp( home: new Scaffold( appBar: AppBar( title: Text( "鹰眼视频" , style: new TextStyle(fontSize: 20.0 , letterSpacing: 3.0 ), ), ), body: new Text('我是首页' ), ), ); } @override void initState() { super .initState(); } }
这是一个最基本的有状态组件,有状态组件可以做交互、让组件可控。如react中的 this.setState()后,组件就会重新绘制。不过Flutter中的修改状态值的方法是 ** setState((){ }) ** ,与React 一样。
封装自己的dio Api.dart、httpUtil.dart 文件都存放在 ** /servers/**目录下。
Api.dart 文件内容
1 2 3 4 5 6 class Api { static const String BaseUrl = 'https://436086407.dmi.net.cn/weapp/' ; static const String VideList = '$BaseUrl /videolist' ; }
httpUtil.dart 文件内容
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 import 'package:dio/dio.dart' ;import 'package:yysp/servers/Api.dart' ;class HttpUtil { static HttpUtil instance; Dio dio; Options options; static HttpUtil getInstance() { print ('getInstance' ); if (instance == null ) { instance = new HttpUtil(); } return instance; } HttpUtil() { print ('dio赋值' ); options = Options( baseUrl: Api.BaseUrl, connectTimeout: 10000 , receiveTimeout: 3000 , headers: {}, ); dio = new Dio(options); } get (url, {data, options, cancelToken}) async { print ('get请求启动! url:$url ,body: $data ' ); Response response; try { response = await dio.get ( url, data: data, cancelToken: cancelToken, ); print ('get请求成功!response.data:${response.data} ' ); } on DioError catch (e) { if (CancelToken.isCancel(e)) { print ('get请求取消! ' + e.message); } print ('get请求发生错误:$e ' ); } return response.data; } post(url, {data, options, cancelToken}) async { print ('post请求启动! url:$url ,body: $data ' ); Response response; try { response = await dio.post( url, data: data, cancelToken: cancelToken, ); print ('post请求成功!response.data:${response.data} ' ); } on DioError catch (e) { if (CancelToken.isCancel(e)) { print ('post请求取消! ' + e.message); } print ('post请求发生错误:$e ' ); } return response.data; } }
这里httpUtil类,采用了单列模式。可以想一下 ** java ** 中连接数据库JDBC的情景。
使用懒加载图片特效 实现这个效果,需要使用第三方的插件。 在 ** pubspec.yaml ** 中添加。
Flutter与node 使用第三方库的区别。
node 可以 npm i xxx ,也可以在package 中直接写库名,然后执行 npm i。
flutter 需要手动写到 pubspec.yaml中,在执行 flutter packages get 。
加载图片特效插件:transparent_image
封装图片组件,代码示意图
1 2 3 4 5 6 7 8 9 10 11 Widget _buildImage(String imgUrl) { return new FadeInImage.memoryNetwork( placeholder: kTransparentImage, image: imgUrl, height: 200.0 , fit: BoxFit.fitWidth, fadeInDuration: const Duration (milliseconds: 300 ), fadeOutDuration: const Duration (milliseconds: 300 ), ); }
json自动序列化 model 映射 我们在向服务器请求数据后,服务器往往会返回一段json字符串。而我们要想更加灵活的使用数据的话需要把json字符串转化成对象。由于flutter只提供了json to Map。而手写反序列化在大型项目中极不稳定,很容易导致解析失败。所以flutter团队推荐使用json_serializable 自动反序列化。
使用第三方库: json_annotation: ^2.0.0 \ build_runner \ json_serializable
1 2 3 4 5 6 7 8 9 10 11 dependencies: flutter: sdk: flutter json_annotation: ^2.0 .0 dev_dependencies: flutter_test: sdk: flutter build_runner: ^1.1 .0 json_serializable: ^2.0 .0
flutter中如何解析json对象 假如我们的mock数据是如下形式。
1 2 3 4 5 6 { "id" : 8863 , "list" : [ 8952 , 9224 , 8917 ], "score" : 111 , }
那么我们需要根据相应的json数据去创建对应的model实体类
1 2 3 4 5 6 7 8 9 10 import 'package:json_annotation/json_annotation.dart' ;class Data { final int id; @JsonKey (name: 'list' ) final List <int > listData; final int score; Data({this .id, this .listData, this .score}); }
在这里使用了 ** @JsonKey(name: ‘list) ** ,原因是,因为我们的json数据中使用了list关键字,所以我们给起个别名listData。并且与Json的list字断映射!
生成Json解析文件 在这里,我们需要使用 ** build_runner ** 去生成dart代码,** build_runner **是dart团队提供的一个生成dart代码文件的外部包。
当前项目的目录下运行
flutter packages pub run build_runner build
运行成功后,就可以在我们的model实体类下面看见一个叫 ** Data.g.dart ** 的文件,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 part of 'data.dart' ;Data _$DataFromJson(Map <String , dynamic > json) { return Data( id: json['id' ] as int , kids: (json['kids' ] as List )?.map((e) => e as int )?.toList(), score: json['score' ] as int ); } Map <String , dynamic > _$DataToJson(Data instance) => <String , dynamic >{ 'id' : instance.id, 'kids' : instance.kids, 'score' : instance.score };
注释 ** GENERATED CODE - DO NOT MODIFY BY HAND” **, 意思是不要手写生成这个文件,使用根据去解决。
关联实体类文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import 'package:json_annotation/json_annotation.dart' ;part 'data.g.dart' ;@JsonSerializable ()class Data { final int id; @JsonKey (name: 'list' ) final List <int > listData; final int score; Data({this .id, this .listData, this .score}); factory Data.fromJson(Map <String , dynamic > json) => _$DataFromJson(json); Map <String , dynamic > toJson() => _$DataToJson(this ); }
ListView 组件 在Flutter中,用ListView来显示列表项,支持垂直和水平方向展示,通过一个属性我们就可以控制其方向,在这里只做大数据的渲染介绍。
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 import 'package:flutter/material.dart' ;import 'package:yysp/servers/httpUtil.dart' ;import 'package:yysp/servers/Api.dart' ;import 'package:yysp/model/videos.dart' ;class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State <HomePage > { HttpUtil httpUtil = new HttpUtil(); List <dynamic > _videosList = new List (); int _pageNo = 0 ; @override Widget build(BuildContext context) { return new MaterialApp( home: new Scaffold( appBar: AppBar( title: Text( "鹰眼视频" , style: new TextStyle(fontSize: 20.0 , letterSpacing: 3.0 ), ), ), body: new ListView.builder( itemCount: _videosList.length, itemBuilder: this ._buildRow, ), ), ); } @override void initState() { super .initState(); } Widget _buildRow(BuildContext context, int position) { print (_videosList[position].toString()); return new Card( margin: EdgeInsets.only(bottom: 38.0 ), child: new Container( height: 256.0 , child: this ._buildImage(_videosList[position]['img' ]) ) ); } @override void initState() { super .initState(); _getContent(_pageNo); } void _getContent(pageNo) async { String url = Api.VideList; var data = {'page' : pageNo}; var response = await HttpUtil().get (url, data: data); Videos videos = Videos.fromJson(response); if (videos.code == 0 ) { setState(() { _videosList.addAll(videos.data.listData); _pageNo = pageNo + 1 ; }); } } }
上面介绍了 如何http 获取数据,并且把json数据映射到model实体类。其实listView最主要的就是,做大数据渲染。
1 2 3 4 5 6 7 8 new ListView.builder( itemCount: _videosList.length, itemBuilder: this ._buildRow, ), itemCount: 表示列表项的总数。 itemBuilder: 渲染子项的方法,使其list列表中显示不同的组件界面。
GestureDetector 手势 GestureDetector 手势控件没有图像展示,只是检测用户输入的手势。当用户点击Container时,GestureDetector会调用onTap回调。也可以使用GestureDetector检测各种输入手势,包括点击、拖动和缩放。不过也有许多的控件使用GestureDetector为其他控件提供回调,比如IconButton、RaisedButton和FloatingActionButton控件有onPressed回调,当用户点击控件时触发回调。
手势事件
属性/回调
描述
onTapDown
每次用户与屏幕联系时都会触发 OnTapDown
onTapUp
当用户停止触摸屏幕时,onTapUp 被调用
onTap
当短暂触摸屏幕时,onTap 被触发
onTapCancel
当用户触摸屏幕但未完成 Tap 时,将触发此事件
onDoubleTap
当屏幕被快速连续触摸两次时调用 onDoubleTap
onLongPress
用户触摸屏幕超过 500毫秒 时,onLongPress 被触发
onVerticalDragDown
当指针与屏幕接触并开始沿垂直方向移动时,onVerticalDown 被调用
onVerticalDragStart
当指针 开始 沿垂直方向移动时调用 onVerticalDragStart
onVerticalDragUpdate
每次指针在屏幕上的位置发生变化时都会调用此方法
onVerticalDragEnd
当用户停止移动时,拖动被认为是完成的,将调用此事件
onVerticalDragCancel
当用户突然停止拖动时调用
onHorizontalDragDown
当用户/指针与屏幕接触并开始水平移动时调用
onHorizontalDragStart
用户/指针已与屏幕接触并 开始 沿水平方向移动
onHorizontalDragUpdate
每次指针在水平方向/x轴上的位置发生变化时调用
onHorizontalDragEnd
在水平拖动结束时,将调用此事件
onHorizontalDragCancel
当指针未成功触发 onHorizontalDragDown 时调用
onPanDown
当指针与屏幕接触时调用
onPanStart
指针事件开始移动时,onPanStart 触发
onPanUpdate
每次指针改变位置时,调用 onPanUpdate
onPanEnd
平移完成后,将调用此事件
onScaleStart
当指针与屏幕接触并建立 1.0 的焦点时,将调用此事件
onScaleUpdate
与屏幕接触的指针指示了新的焦点
onScaleEnd
当指针不再与指示手势结束的屏幕接触时调用
onTap 事咧 这里以onTap手势事件,为例。
1 2 3 4 5 6 7 8 9 10 11 12 13 new GestureDetector( child:Container( width: double .infinity, height: 200.0 , child: this ._buildImage(_videosList[position]['img' ]), ), onTap: this ._videoPlayToggle(_videosList[position]['id' ]), ), Function _videoPlayToggle(id) { return () => parint('我是onTab事件,参数 $id ' ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 new GestureDetector( child:Container( width: double .infinity, height: 200.0 , child: Text('无参数' ), ), onTap: this ._videoPlayToggle(), ), void _videoPlayToggle() { parint('我是onTab事件,无参数' ); }
路由导航 在上节手势介绍文章中介绍了onTab事件,那么现在我们就看看app应用的多页面切换吧。看看Flutter的页面切换与 ** React-router ** 有什么不一样。
在Flutter中有着两种路由跳转的方式,一种是静态路由,在创建时就已经明确知道了要跳转的页面和值。另一种是动态路由,跳转传入的目标地址和要传入的值都可以是动态的。
静态路由 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import 'package:flutter/material.dart' ;import 'package:yysp/page/homePage.dart' ;import 'package:yysp/page/aboutPage.dart' ;void main()=>runApp(new RootPage());class RootPage extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: '鹰眼视频' , home: new HomePage(), routes: <String , WidgetBuilder>{ '/aboutPage' : (BuildContext context) => new AboutPage() } ); } }
在MaterialApp中,是存在一个叫routers的参数的,用于配置静态路由。
动态路由 在手势的小节中讲解了,如何去使用手势事件。那么这节继续扩充。点击事件后,让其携带参数切换页面。
1 2 3 4 5 6 7 8 9 10 11 Function _videoPlayToggle(id) { return () => Navigator.of(context).push( new PageRouteBuilder( pageBuilder: (BuildContext context, Animation<double > animation, Animation<double > secondaryAnimation) { return new DetailsPage(id.toString()); } ) ); }
原则就是:“简单的学习,快速的掌握”。至于动画效果,后续优化。先跑下框架结构!
下一个页面的,参数接收。
其实就是,通过构造方法实例话对象时传进去。就是这么简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 class DetailsPage extends StatefulWidget { final String videoId; DetailsPage(this .videoId); @override _DetailsPageState createState() => _DetailsPageState(this .videoId); } class _DetailsPageState extends State <DetailsPage > { final String _videoId; _DetailsPageState(this ._videoId);
记住,类对象必须构造方法传参数。
视频播放 视频播放使用了这2个插件,video_player、chewie
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 import 'package:flutter/material.dart' ;import 'package:video_player/video_player.dart' ;import 'package:chewie/chewie.dart' ;import 'package:yysp/servers/httpUtil.dart' ;import 'package:yysp/servers/Api.dart' ;import 'package:yysp/model/video_detail.dart' ;class DetailsPage extends StatefulWidget { final String videoId; DetailsPage(this .videoId); @override _DetailsPageState createState() => _DetailsPageState(this .videoId); } class _DetailsPageState extends State <DetailsPage > { final String _videoId; Video _videoInfo = new Video(0 , '' , '' , '' , '' , '' , '' , '' , '' , '' , 0 , 0 , '' ); VideoPlayerController _videoPlayController = new VideoPlayerController.network('' ); HttpUtil httpUtil = new HttpUtil(); _DetailsPageState(this ._videoId); @override Widget build(BuildContext context) { return new MaterialApp( home: new Scaffold( appBar: AppBar( title: Text( "鹰眼视频" , style: new TextStyle(fontSize: 20.0 , letterSpacing: 3.0 ), ), ), body: Column( children: <Widget>[ Container( child: this ._buildVideo() ), Text(_videoInfo.title) ], ) ) ); } @override void initState() { super .initState(); this ._getVideoInfo(); } @override void dispose() { super .dispose(); _videoPlayController.pause(); } void _getVideoInfo() async { String url = Api.VideoInfo; var data = {'id' : this ._videoId}; var response = await HttpUtil().get (url, data: data); VideoDetail videos = VideoDetail.fromJson(response); if (videos.code == 0 ) { _videoPlayController = VideoPlayerController.network( videos.data.video.src ); setState(() { _videoInfo = new Video(videos.data.video.id, videos.data.video.title, videos.data.video.url, videos.data.video.auther, videos.data.video.pubtime, videos.data.video.img, videos.data.video.src, videos.data.video.desc1, videos.data.video.cate, videos.data.video.pt, videos.data.video.collect, videos.data.video.count, videos.data.video.insertTime); }); } } Widget _buildVideo() { _videoPlayController.play(); return new Chewie( _videoPlayController, aspectRatio: 3 / 2 , autoPlay: true , looping: false , ); } }
#### 总结:说了这么多,其实Flutter学的是框架,最主要的是学会dart语言。