Flutter 进阶系列篇

文章目录

    • 1、http get请求数据、post提交数据、以及渲染动态数据
    • 2、Dio库实现网络请求以及动态渲染数据
    • 3、下拉刷新 上拉分页加载更多
    • 4、实现简单的新闻系统渲染新闻详情数据以及用flutter_html解析html
    • 5、使用WebView组件flutter_inappbrowser加载远程web页面渲染新闻详情数据
    • 6、获取设备信息 以及 使用高德Api获取地理位置
    • 7、调用原生硬件Api实现照相机拍照和相册选择 以及拍照上传到服务器
    • 8、实现视频播放
    • 9、检测网络连接,监听网络变化
    • 10、 本地存储,封装本地存储类,实现最简单的状态管理
    • 11、调用原生硬件Api实现扫码 扫描条形码 扫描二维码
    • 12、检测应用版本号、服务器下载文件以及实现App自动升级、安装
    • 13、打开外部浏览器、打开外部应用、拨打电话、发送短信
    • 14、支付宝支付【上】
    • 15、支付宝支付【下】
    • 16、ListView嵌套GridView、不同终端屏幕适配方案
    • 17、JSON的序列化和反序列化、创建模型类转换Json数据
    • 18、底部 Tab 切换保持页面状态的几种方法
    • 19、inappbrowser、StatefulBuilder 更新 Flutter showDialog、showModalBottomSheet中的状态
    • 20、官方推荐的状态管理库 provider 的使用
    • 21、事件广播 、事件监听
    • 22、点击穿透问题、页面禁止左右滑动

喜欢记得点个赞哟,我是王睿,很高兴认识大家!

1、http get请求数据、post提交数据、以及渲染动态数据

效果图:

在这里插入图片描述

在这里插入图片描述

导入: http: ^0.12.0+2库

Http_Back.dart

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';class HttpDemo extends StatefulWidget {HttpDemo({Key key}) : super(key: key);_HttpDemoState createState() => _HttpDemoState();
}class _HttpDemoState extends State<HttpDemo> {List _list=[];@overridevoid initState() {// TODO: implement initStatesuper.initState();this._getData();}_getData() async{var apiUrl="http://a.itying.com/api/productlist";var result=await http.get(apiUrl);if(result.statusCode==200){print(result.body);setState(() {this._list=json.decode(result.body)["result"];/*{"result": [{"_id": "5ac0896ca880f20358495508","title": "精选热菜","pid": "0",		}, {"_id": "5ac089e4a880f20358495509","title": "特色菜","pid": "0",}]}*/});}else{print("失败${result.statusCode}");}}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("请求数据Demo"),),body: this._list.length>0?ListView(children: this._list.map((value){return ListTile(title: Text(value["title"]),);}).toList(),):Text("加载中..."));}
}

请求数据

  //请求数据_getData() async{var apiUrl="http://192.168.0.5:3000/news";var result=await http.get(apiUrl);if(result.statusCode==200){// print(json.decode(result.body));setState(() {this._news=json.decode(result.body)["msg"]; });}else{print(result.statusCode);}}

提交数据

  _postData() async{var apiUrl="http://192.168.0.5:3000/dologin";var result=await http.post(apiUrl, body: {'username': '张三', 'age': '20'});if(result.statusCode==200){print(json.decode(result.body));      }else{print(result.statusCode);}}

2、Dio库实现网络请求以及动态渲染数据

导入第三方库:Dio库

请求数据

  _getData() async{var apiUrl="http://192.168.0.5:3000/news";    Response response = await Dio().get(apiUrl);print(response.data);}

提交数据

 _postData() async{Map jsonData={"username":"哈哈哈","age":20};var apiUrl="http://192.168.0.5:3000/dologin";Response response = await Dio().post(apiUrl,data:jsonData);print(response.data);}

Dio Get请求数据、渲染数据

import 'dart:convert';import 'package:flutter/material.dart';import 'package:dio/dio.dart';class HttpDemo extends StatefulWidget {HttpDemo({Key key}) : super(key: key);_HttpDemoState createState() => _HttpDemoState();
}class _HttpDemoState extends State<HttpDemo> {List _list=[];@overridevoid initState() { super.initState();this._getData();}_getData() async{var apiUrl="http://www.phonegap100.com/appapi.php?a=getPortalList&catid=20&page=1";Response result=await Dio().get(apiUrl);// print(json.decode(result.data)["result"]);setState(() {this._list=json.decode(result.data)["result"]; });  }@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("请求数据Dio Demo"),),body: this._list.length>0?ListView(children: this._list.map((value){return ListTile(title: Text(value["title"]),);}).toList(),):Text("加载中..."));}
}

3、下拉刷新 上拉分页加载更多

效果图:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

学习之前需要导入Dio库,具体用法与导入连接请查看我的博客
Flutter进阶第2篇:Dio库实现网络请求以及动态渲染数据

News.dart

import 'package:flutter/material.dart';import 'dart:convert';
import 'package:dio/dio.dart';class NewsPage extends StatefulWidget {NewsPage({Key key}) : super(key: key);_NewsPageState createState() => _NewsPageState();
}class _NewsPageState extends State<NewsPage> {List _list = [];int _page = 1;bool hasMore = true; //判断有没有数据ScrollController _scrollController = new ScrollController();@overridevoid initState() {// TODO: implement initStatesuper.initState();this._getData();//监听滚动条事件_scrollController.addListener(() {print(_scrollController.position.pixels); //获取滚动条下拉的距离print(_scrollController.position.maxScrollExtent); //获取整个页面的高度if (_scrollController.position.pixels >_scrollController.position.maxScrollExtent - 40) {this._getData();}});}void _getData() async {if (this.hasMore) {var apiUrl ="http://www.phonegap100.com/appapi.php?a=getPortalList&catid=20&page=${_page}";var response = await Dio().get(apiUrl);var res = json.decode(response.data)["result"];   setState(() {this._list.addAll(res);  //拼接this._page++;});//判断是否是最后一页if (res.length < 20) {setState(() {this.hasMore = false;});}}}//下拉刷新Future<void> _onRefresh() async {await Future.delayed(Duration(milliseconds: 2000), () {print('请求数据完成');_getData();});}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("新闻列表"),),body: this._list.length > 0? RefreshIndicator(onRefresh: _onRefresh,child: ListView.builder(controller: _scrollController,itemCount: this._list.length, //20itemBuilder: (context, index) {//19                  if (index == this._list.length-1) {   //列表渲染到最后一条的时候加一个圈圈//拉到底return Column(children: <Widget>[ListTile(title: Text("${this._list[index]["title"]}",maxLines: 1),),Divider(),_getMoreWidget()],);} else {return Column(children: <Widget>[ListTile(title: Text("${this._list[index]["title"]}",maxLines: 1),),Divider()],);}},)): _getMoreWidget(),);}//加载中的圈圈Widget _getMoreWidget() {if(hasMore){return Center(child: Padding(padding: EdgeInsets.all(10.0),child: Row(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.center,children: <Widget>[Text('加载中...',style: TextStyle(fontSize: 16.0),),CircularProgressIndicator(strokeWidth: 1.0,)],),),);}else{return Center(child: Text("--我是有底线的--"),);}}
}

4、实现简单的新闻系统渲染新闻详情数据以及用flutter_html解析html

效果图:

点击这三个新闻列表的内容,即可进入新闻详情
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

学习之前需要导入Dio库,具体用法与导入连接请查看我的博客
Flutter进阶第2篇:Dio库实现网络请求以及动态渲染数据

还需要导入解析HTML代码的第三方库:
flutter_html

新闻列表代码:

import 'package:flutter/material.dart';import 'dart:convert';
import 'package:dio/dio.dart';class NewsPage extends StatefulWidget {NewsPage({Key key}) : super(key: key);_NewsPageState createState() => _NewsPageState();
}class _NewsPageState extends State<NewsPage> {List _list = [];int _page = 1;bool hasMore = true; //判断有没有数据ScrollController _scrollController = new ScrollController();@overridevoid initState() {    super.initState();this._getData();//监听滚动条事件_scrollController.addListener(() {print(_scrollController.position.pixels); //获取滚动条下拉的距离print(_scrollController.position.maxScrollExtent); //获取整个页面的高度if (_scrollController.position.pixels >_scrollController.position.maxScrollExtent - 40) {this._getData();}});}void _getData() async {if (this.hasMore) {var apiUrl ="http://www.phonegap100.com/appapi.php?a=getPortalList&catid=20&page=${_page}";var response = await Dio().get(apiUrl);var res = json.decode(response.data)["result"];   setState(() {this._list.addAll(res);  //拼接this._page++;});//判断是否是最后一页if (res.length < 20) {setState(() {this.hasMore = false;});}}}//下拉刷新Future<void> _onRefresh() async {await Future.delayed(Duration(milliseconds: 2000), () {print('请求数据完成');_getData();});}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("新闻列表"),),body: this._list.length > 0? RefreshIndicator(onRefresh: _onRefresh,child: ListView.builder(controller: _scrollController,itemCount: this._list.length, //20itemBuilder: (context, index) {//19                  if (index == this._list.length-1) {   //列表渲染到最后一条的时候加一个圈圈//拉到底return Column(children: <Widget>[ListTile(title: Text("${this._list[index]["title"]}",maxLines: 1),onTap: (){Navigator.pushNamed(context, '/newscontent',arguments:{"aid":this._list[index]["aid"]});},),Divider(),_getMoreWidget()],);} else {return Column(children: <Widget>[ListTile(title: Text("${this._list[index]["title"]}",maxLines: 1),onTap: (){Navigator.pushNamed(context, '/newscontent',arguments:{"aid":this._list[index]["aid"]});},),Divider()],);}},)): _getMoreWidget(),);}//加载中的圈圈Widget _getMoreWidget() {if(hasMore){return Center(child: Padding(padding: EdgeInsets.all(10.0),child: Row(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.center,children: <Widget>[Text('加载中...',style: TextStyle(fontSize: 16.0),),CircularProgressIndicator(strokeWidth: 1.0,)],),),);}else{return Center(child: Text("--我是有底线的--"),);}}
}

新闻详情

import 'package:flutter/material.dart';import 'dart:convert';
import 'package:dio/dio.dart';import 'package:flutter_html/flutter_html.dart';class NewsContent extends StatefulWidget {Map arguments;NewsContent({Key key,this.arguments}) : super(key: key);_NewsContentState createState() => _NewsContentState(this.arguments);
}class _NewsContentState extends State<NewsContent> {Map arguments;List _list=[];_NewsContentState(this.arguments);@overridevoid initState() {// TODO: implement initStatesuper.initState();print(this.arguments);this._getData();}_getData() async{var apiUrl="http://www.phonegap100.com/appapi.php?a=getPortalArticle&aid=${this.arguments["aid"]}";var response=await Dio().get(apiUrl);     setState(() {this._list=json.decode(response.data)["result"];});}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("新闻详情")),body:ListView(children: <Widget>[// Text("${this._list.length>0?this._list[0]["title"]:''}"),// Text("${this._list.length>0?this._list[0]["content"]:''}")Html(data: """${this._list.length>0?this._list[0]["content"]:''}                            """,//Optional parameters:padding: EdgeInsets.all(8.0),backgroundColor: Colors.white70,defaultTextStyle: TextStyle(fontFamily: 'serif'),linkStyle: const TextStyle(color: Colors.redAccent,),onLinkTap: (url) {// open url in a webview})],));}
}

记得配置路由时,要传值:
在这里插入图片描述

5、使用WebView组件flutter_inappbrowser加载远程web页面渲染新闻详情数据

效果图

点击这三个新闻列表的内容,即可进入新闻详情
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

导入第三方库:flutter_inappbrowser

News.dart 新闻列表

import 'package:flutter/material.dart';import 'dart:convert';
import 'package:dio/dio.dart';class NewsPage extends StatefulWidget {NewsPage({Key key}) : super(key: key);_NewsPageState createState() => _NewsPageState();
}class _NewsPageState extends State<NewsPage> {List _list = [];int _page = 1;bool hasMore = true; //判断有没有数据ScrollController _scrollController = new ScrollController();@overridevoid initState() {    super.initState();this._getData();//监听滚动条事件_scrollController.addListener(() {print(_scrollController.position.pixels); //获取滚动条下拉的距离print(_scrollController.position.maxScrollExtent); //获取整个页面的高度if (_scrollController.position.pixels >_scrollController.position.maxScrollExtent - 40) {this._getData();}});}void _getData() async {if (this.hasMore) {var apiUrl ="http://www.phonegap100.com/appapi.php?a=getPortalList&catid=20&page=${_page}";var response = await Dio().get(apiUrl);var res = json.decode(response.data)["result"];   setState(() {this._list.addAll(res);  //拼接this._page++;});//判断是否是最后一页if (res.length < 20) {setState(() {this.hasMore = false;});}}}//下拉刷新Future<void> _onRefresh() async {await Future.delayed(Duration(milliseconds: 2000), () {print('请求数据完成');_getData();});}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("新闻列表"),),body: this._list.length > 0? RefreshIndicator(onRefresh: _onRefresh,child: ListView.builder(controller: _scrollController,itemCount: this._list.length, //20itemBuilder: (context, index) {//19                  if (index == this._list.length-1) {   //列表渲染到最后一条的时候加一个圈圈//拉到底return Column(children: <Widget>[ListTile(title: Text("${this._list[index]["title"]}",maxLines: 1),onTap: (){Navigator.pushNamed(context, '/newscontent',arguments:{"aid":this._list[index]["aid"]});},),Divider(),_getMoreWidget()],);} else {return Column(children: <Widget>[ListTile(title: Text("${this._list[index]["title"]}",maxLines: 1),onTap: (){Navigator.pushNamed(context, '/newscontent',arguments:{"aid":this._list[index]["aid"]});},),Divider()],);}},)): _getMoreWidget(),);}//加载中的圈圈Widget _getMoreWidget() {if(hasMore){return Center(child: Padding(padding: EdgeInsets.all(10.0),child: Row(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.center,children: <Widget>[Text('加载中...',style: TextStyle(fontSize: 16.0),),CircularProgressIndicator(strokeWidth: 1.0,)],),),);}else{return Center(child: Text("--我是有底线的--"),);}}
}

NewsContent.dart 新闻详情


import 'package:flutter/material.dart';import 'package:flutter_inappbrowser/flutter_inappbrowser.dart';class NewsContent extends StatefulWidget {Map arguments;NewsContent({Key key,this.arguments}) : super(key: key);_NewsContentState createState() => _NewsContentState(this.arguments);
}class _NewsContentState extends State<NewsContent> {Map arguments;bool _flag=true;_NewsContentState(this.arguments);@overridevoid initState() {// TODO: implement initStatesuper.initState();print(this.arguments);}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("新闻详情")),body:Column(children: <Widget>[this._flag?_getMoreWidget():Text(""),Expanded(child: InAppWebView(initialUrl: "http://www.phonegap100.com/newscontent.php?aid=${this.arguments["aid"]}",                   onProgressChanged: (InAppWebViewController controller, int progress) {print(progress/100);if((progress/100)>0.999){setState(() {this._flag=false;});}},),     )],));}//加载中的圈圈Widget _getMoreWidget() {return Center(child: Padding(padding: EdgeInsets.all(10.0),child: Row(mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.center,children: <Widget>[Text('加载中...',style: TextStyle(fontSize: 16.0),),CircularProgressIndicator(strokeWidth: 1.0,)],),),);    }
}

记得配置路由时,要传值:
在这里插入图片描述

注意事项:
SDK版本一定要大于等于17
在这里插入图片描述

6、获取设备信息 以及 使用高德Api获取地理位置

效果图:

在这里插入图片描述

在这里插入图片描述
引入第三方库:device_info

Device.dart

import 'package:flutter/material.dart';
import 'package:device_info/device_info.dart';class DevicePage extends StatefulWidget {DevicePage({Key key}) : super(key: key);_DevicePageState createState() => _DevicePageState();
}class _DevicePageState extends State<DevicePage> {@overridevoid initState() {// TODO: implement initStatesuper.initState();this._getDevice();}_getDevice() async{DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;print('设备号 ${androidInfo.androidId}');  // e.g. "Moto G (4)"}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("Flutter Native Device演示"),),body: Text("看控制台 信息已经打印到控制台了"),);}
}

第一步:使用高德定位准备工作获取 key

1、申请成为开发者
2、创建应用配置获取Key

点击查看教程

第二步:引入第三方库

amap_location

第三步:修改 你的项目目录e /app/build.gradle 在 在 g android/defaultConfig 节点修 改 改 manifestPlaceholders, 新增高德地图 y key 配置

android {
.... 你的代码
defaultConfig {
.....
manifestPlaceholders = [
AMAP_KEY : "aa9f0cf8574400f2af0078392c556e25", // 高德
地图 key
]
}
...你的代码
dependencies {
/// 注意这里需要在主项目增加一条依赖,否则可能发生编译不通过的
情况
implementation 'com.amap.api:location:latest.integration'
...你的代码
}

在这里插入图片描述

7、调用原生硬件Api实现照相机拍照和相册选择 以及拍照上传到服务器

效果图:
在这里插入图片描述
相册

在这里插入图片描述
拍照

在这里插入图片描述

拍照后的照片显示在界面上

在这里插入图片描述

7.1丶 调用原生硬件Api实现照相机拍照和相册选择

导入第三方库:image_picker

拍照

_takePhoto() async {
var image = await ImagePicker.pickImage(source: ImageSource.camera);
setState(() {
_imgPath = image;
});
}

相册

_openGallery() async {
var image = await ImagePicker.pickImage(source: ImageSource.gallery);
setState(() {
_imgPath = image;
});
}

7.2丶 拍照上传到服务器

导入第三方库:上传图片到服务器

上传图片代码

_uploadData(imageFile) async{
FormData formData = new FormData.from({
"name": "wendux",
"age": 25,
'file': new UploadFileInfo(imageFile, "imageFileName.jpg")
});
var response = await Dio().post("http://jd.itying.com/imgupload", data: formData);
print(response);
}

完整代码:

import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:dio/dio.dart';
class ImagePickerPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ImagePickerState();
}
}
class _ImagePickerState extends State<ImagePickerPage> {
var _imgPath;@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ImagePicker"),
),
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
_ImageView(_imgPath),
RaisedButton(
onPressed: _takePhoto,
child: Text("拍照"),
),
RaisedButton(
onPressed: _openGallery,
child: Text("选择照片"),
),
],
),
));
}
/*图片控件*/
Widget _ImageView(imgPath) {
if (imgPath == null) {
return Center(
child: Text("请选择图片或拍照"),
);
} else {
return Image.file(
imgPath,
);
}
}
/*拍照*/
_takePhoto() async {
var image = await ImagePicker.pickImage(source: ImageSource.camera,maxWidth: 400);
_uploadData(image); //上传
setState(() {
_imgPath = image;
});
}
/*相册*/
_openGallery() async {
var image = await ImagePicker.pickImage(source: ImageSource.gallery);
setState(() {
_imgPath = image;
});
}
_uploadData(imageFile) async{
FormData formData = new FormData.from({
"name": "wendux",
"age": 25,
'file': new UploadFileInfo(imageFile, "imageFileName.jpg")
});
var response = await Dio().post("http://jd.itying.com/imgupload", data: formData);
print(response);
}
}

8、实现视频播放

效果图:

在这里插入图片描述

引入第三方库:chewie

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';class ChewieVideoDemo extends StatefulWidget {ChewieVideoDemo({Key key}) : super(key: key);_ChewieVideoDemoState createState() => _ChewieVideoDemoState();
}class _ChewieVideoDemoState extends State<ChewieVideoDemo> {VideoPlayerController videoPlayerController;ChewieController chewieController;@overridevoid initState() {// TODO: implement initStatesuper.initState();videoPlayerController = VideoPlayerController.network('http://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4');chewieController = ChewieController(videoPlayerController: videoPlayerController,aspectRatio: 3 / 2,autoPlay: true,looping: true,);}/*销毁*/@overridevoid dispose() {videoPlayerController.dispose();chewieController.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('在线视频播放'),),body: Center(child: Chewie(controller: chewieController,)),);}
}

9、检测网络连接,监听网络变化

效果图:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
引入第三方库:connectivity

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:chewie/chewie.dart';class ChewieVideoDemo extends StatefulWidget {ChewieVideoDemo({Key key}) : super(key: key);_ChewieVideoDemoState createState() => _ChewieVideoDemoState();
}class _ChewieVideoDemoState extends State<ChewieVideoDemo> {VideoPlayerController videoPlayerController;ChewieController chewieController;@overridevoid initState() {// TODO: implement initStatesuper.initState();videoPlayerController = VideoPlayerController.network('http://vfx.mtime.cn/Video/2019/02/04/mp4/190204084208765161.mp4');chewieController = ChewieController(videoPlayerController: videoPlayerController,aspectRatio: 3 / 2,autoPlay: true,looping: true,);}/*销毁*/@overridevoid dispose() {videoPlayerController.dispose();chewieController.dispose();super.dispose();}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('在线视频播放'),),body: Center(child: Chewie(controller: chewieController,)),);}
}

10、 本地存储,封装本地存储类,实现最简单的状态管理

引入第三方库:shared_preferences

设置值:

SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString(key, value);
prefs.setBool(key, value)
prefs.setDouble(key, value)
prefs.setInt(key, value)
prefs.setStringList(key, value)

获取值:

SharedPreferences prefs = await SharedPreferences.getInstance();
var data=prefs.getString("name");

删除值:

SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.remove(key); //删除指定键
prefs.clear();//清空键值对

完整代码:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';class StoragePage extends StatefulWidget {StoragePage({Key key}) : super(key: key);_StoragePageState createState() => _StoragePageState();
}class _StoragePageState extends State<StoragePage> {_saveData() async{SharedPreferences sp=await SharedPreferences.getInstance();sp.setString("username", "张三111");sp.setString("age", "26");}_getData() async{SharedPreferences sp=await SharedPreferences.getInstance();print(sp.getString("username"));print(sp.getString("age"));}_removeData() async{SharedPreferences sp=await SharedPreferences.getInstance();print(sp.remove("age"));}@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("本地存储"),),body: Center(child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [RaisedButton(child: Text('保存数据'),onPressed: _saveData,),SizedBox(height: 10),RaisedButton(child: Text('获取数据'),onPressed:_getData,),SizedBox(height: 10),RaisedButton(child: Text('清除数据'),onPressed:_removeData,)         ]),),);}
}

简单封装:

import 'package:shared_preferences/shared_preferences.dart';class Storage{static Future<void> setString(key,value) async{SharedPreferences sp=await SharedPreferences.getInstance();sp.setString(key, value);}static Future<String> getString(key) async{SharedPreferences sp=await SharedPreferences.getInstance();return sp.getString(key);}static Future<void> remove(key) async{SharedPreferences sp=await SharedPreferences.getInstance();sp.remove(key);}
}

11、调用原生硬件Api实现扫码 扫描条形码 扫描二维码

效果图:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

第一步:

导入第三方库:barcode_scan

第二步:

<uses-permission android:name="android.permission.CAMERA" />

第三步:

3.1 的 编 辑 你 的 d android 的 目 录 下 面 的 e build.gradle ( Edit your project-level
build.gradle file to look like this)
注意: : 官方文档配置的 kotlin_version 的版本是 1.2.31,但是实际发现 1.2.31
会报错。所以本项目使用 1.3.0。

buildscript {
ext.kotlin_version = '1.3.0'
...
dependencies {
...
classpath
"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

第四步:编辑你 的 p android/app 的 目 录下 面 的 e build.gradle ( Edit your app-levelbuild.gradle file to look like this)

apply plugin: 'kotlin-android'
...
dependencies {
implementation
"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
...
}
import 'package:flutter/material.dart';import 'package:barcode_scan/barcode_scan.dart';
import 'package:flutter/services.dart';class ScanPage extends StatefulWidget {ScanPage({Key key}) : super(key: key);_ScanPageState createState() => _ScanPageState();
}class _ScanPageState extends State<ScanPage> {String barcode;Future _scan() async {try {String barcode = await BarcodeScanner.scan();setState(() {return this.barcode = barcode;});} on PlatformException catch (e) {if (e.code == BarcodeScanner.CameraAccessDenied) {setState(() {return this.barcode = 'The user did not grant the camera permission!';});} else {setState(() {return this.barcode = 'Unknown error: $e';});}} on FormatException {setState(() => this.barcode ='null (User returned using the "back"-button before scanning anything. Result)');} catch (e) {setState(() => this.barcode = 'Unknown error: $e');}}@overrideWidget build(BuildContext context) {return Scaffold(floatingActionButton: FloatingActionButton(child: Icon(Icons.photo_camera),onPressed: _scan,),appBar: AppBar(title: Text("扫码"),),body: Text("${barcode}"));}
}

12、检测应用版本号、服务器下载文件以及实现App自动升级、安装

12.1丶 配置版本号:

<manifest android:hardwareAccelerated="true" android:versionCode="1" android:versionName="0.0.1"
package="io.jdshop.demo" xmlns:android="http://schemas.android.com/apk/res/android">

12.2丶 升级 app 之前的准备工作配置权限
配置 AndroidMenifest.xml

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

12.3丶 Android 升级 app 涉及的 API 库

在这里插入图片描述

12.4、获取版本信息

https://pub.dev/packages/package_info

PackageInfo packageInfo = await PackageInfo.fromPlatform();
String appName = packageInfo.appName;
String packageName = packageInfo.packageName;
String version = packageInfo.version;
String buildNumber = packageInfo.buildNumber;print("appName:${appName}");
print("packageName:${packageName}");
print("version:${version}");
print("buildNumber:${buildNumber}");

12.5、获取文件存储路径.

https://pub.dev/packages/path_provider

Directory tempDir = await getTemporaryDirectory();
String tempPath = tempDir.path;Directory appDocDir = await getApplicationDocumentsDirectory();
String appDocPath = appDocDir.path;var directory = await getExternalStorageDirectory();String storageDirectory=directory.path;print("tempPath:${tempPath}");print("appDocDir:${appDocPath}");print("StorageDirectory:${storageDirectory}");

12.6、下载文件

https://pub.dev/packages/flutter_downloader

final directory = await getExternalStorageDirectory();
String _localPath = directory.path;
final taskId = await FlutterDownloader.enqueue(url: "http://www.ionic.wang/jdshop.apk",
savedDir: _localPath,
showNotification:
true, // show download progress in status bar (for Android)
openFileFromNotification:
true, // click on notification to open downloaded file (for Android)
);

12.7、打开文件

https://pub.dev/packages/open_file

OpenFile.open("${_localPath}/jdshop.apk");

12.8、注意事项

1、服务器的 App 版本必须大于本地 App 版本
2、本地 App 和服务器 App 的包名称 签名必须一致,这样的话服务器的包才可以替换本地
的包。

13、打开外部浏览器、打开外部应用、拨打电话、发送短信

效果图:

在这里插入图片描述

打开外部浏览器
在这里插入图片描述
发送短信
在这里插入图片描述
拨打电话
在这里插入图片描述
打开外部应用
在这里插入图片描述

导入第三方库:url_launcher

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';class UrlLauncher extends StatefulWidget {UrlLauncher({Key key}) : super(key: key);_UrlLauncherState createState() => _UrlLauncherState();
}class _UrlLauncherState extends State<UrlLauncher> {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('UrlLauncher'),),body: Center(child: Padding(padding: EdgeInsets.all(20),child: ListView(children: [RaisedButton(child: Text('打开外部浏览器'),onPressed: () async{                                 const url = 'https://cflutter.com';if (await canLaunch(url)) {await launch(url);} else {throw 'Could not launch $url';}},),SizedBox(height: 10),RaisedButton(child: Text('拨打电话'),onPressed: () async{var tel = 'tel:10086';if (await canLaunch(tel)) {await launch(tel);} else {throw 'Could not launch $tel';}},),SizedBox(height: 10),RaisedButton(child: Text('发送短信'),onPressed: () async{var tel = 'sms:10086';if (await canLaunch(tel)) {await launch(tel);} else {throw 'Could not launch $tel';}},),SizedBox(height: 10),RaisedButton(child: Text('打开外部应用'),onPressed: () async{/*weixin://alipays://*/var url = 'alipays://';	//支付宝的 scheme码if (await canLaunch(url)) {await launch(url);} else {throw 'Could not launch $url';}},)       ]),)));}
}

打开其他APP的scheme码:
https://www.cflutter.com/topic/5d0853733b57e317a4d0af01

14、支付宝支付【上】

一丶 准备工作、接入支付宝:

1. 必须注册企业支付宝账户,如果已有企业支付宝账户忽略此步骤
2. 百度搜索《支付宝开放平台》,或者点击下面链接进入支付宝开发接入页面:
https://open.alipay.com/developmentAccess/developmentAccess.htm

在这里插入图片描述
3. 点击支付应用

在这里插入图片描述

4. 填写对应应用名称 图标 点击创建:

在这里插入图片描述

官方答复:应用类型分为两大类:第三方应用、自用型应用 第三方应用:适用于服务商,
为商户开发应用,拓展商户使用,详见供他人使用;目前仅支持小程序的三方接入,接入小
程序前,必须先申请小程序的公测; 自用型应用:使用开放的功能,为自己或自己公司开
发应用,详见自己使用;自研型应用分为网页/移动,AR(仅企业支付宝),小程序(仅企
业支付宝),生活号
网页&移动应用 和 第三方应用的区别是一个是自己使用,一个是帮助第三方去签约相关产

5.设置应用公钥,然后提交审核。 如何创建公钥继续往后看…
在这里插入图片描述

二丶 接口加密签名

  1. 下载签名工具。
    https://docs.open.alipay.com/291/106097/

2.签名。并保存好私钥、公钥

在这里插入图片描述
在这里插入图片描述

三丶 配置签名、提交审核

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
设置完成以后支付宝后台也会给我们生成一个公钥,注意两个不一样

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四丶 安装支付流程

官方支付流程文档:https://docs.open.alipay.com/59/103658/

15、支付宝支付【下】

一丶 支付宝客户端支付流程

官方支付流程文档:https://docs.open.alipay.com/59/103658/

二丶 准备已有的 Flutter 项目安装插件

https://pub.dev/packages/sy_flutter_alipay

三丶服务器端调用支付宝 sdk 生成订单信息

  1. 服务端 sdk 下载地址:https://docs.open.alipay.com/54/103419/
  2. 本教程采用的 php 的 sdk,看演示

四丶 客户端调用服务器端接口生成订单签名信息,调用支付插件完成支付

import 'package:flutter/material.dart';
import 'package:sy_flutter_alipay/sy_flutter_alipay.dart';
import 'package:dio/dio.dart';
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
_doPay() async{
var apiUrl='http://agent.itying.com/alipay/index.php';
var myPayInfo =await Dio().get(apiUrl);
final payInfo =myPayInfo.data;
print(payInfo);var result = await SyFlutterAlipay.pay(
payInfo,
// urlScheme: '你的 ios urlScheme', //前面配置的 urlScheme
// isSandbox: true //是否是沙箱环境,只对 android 有效
);
print(result);
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(height: 20),
RaisedButton(
child: Text('支付宝支付'),
onPressed: _doPay,
),
SizedBox(height: 20),
],
),
);
}
}

五丶 服务器端异步回调更新订单信息

当支付成功后支付宝会异步给服务器 post 数据,服务器更新订单信息
在这里插入图片描述
服务端 sdk 下载地址:https://docs.open.alipay.com/54/103419/
本教程采用的 php 的 sdk,看演示

六丶 Flutter 在 Xcode 上编译提示:Target ‘Runner’: script phase “[CP] Embed Pods Frameworks” 的解决办法
在这里插入图片描述
解决方法:https://www.cflutter.com/topic/5d09a1c73b57e317a4d0af08

16、ListView嵌套GridView、不同终端屏幕适配方案

一丶 竖向 ListView 嵌套横向 ListView ,以 及ListView 嵌套 GridView

1 、 竖向 ListView 嵌套横向 ListView 注意事项:
在竖向 ListView 中嵌套横向 ListView 的时候要注意给横向 ListView 外层加一个容器,然后外
层这个容器要设置高度,外层这个容器可以是 SizedBox ,也可以是 Container。

2 、ListView 嵌套 GridView 注意事项:
由于 GridView 和 ListView 都是可以滚动的组件,所以嵌套的时候要注意把里面的组件改为不可滚动组件。
重要属性:
shrinkWrap: true, //解决无限高度问题
physics:NeverScrollableScrollPhysics(), //禁用滑动事件

二丶 不同终端屏幕适配方案

导入第三方库: flutter_screenutil

import 'package:flutter/material.dart';class ScreenUtil {static ScreenUtil instance = new ScreenUtil();//设计稿的设备尺寸修改double width;double height;bool allowFontScaling;static MediaQueryData _mediaQueryData;static double _screenWidth;static double _screenHeight;static double _pixelRatio;static double _statusBarHeight;static double _bottomBarHeight;static double _textScaleFactor;ScreenUtil({this.width = 1080,this.height = 1920,this.allowFontScaling = false,});static ScreenUtil getInstance() {return instance;}void init(BuildContext context) {MediaQueryData mediaQuery = MediaQuery.of(context);_mediaQueryData = mediaQuery;_pixelRatio = mediaQuery.devicePixelRatio;_screenWidth = mediaQuery.size.width;_screenHeight = mediaQuery.size.height;_statusBarHeight = mediaQuery.padding.top;_bottomBarHeight = _mediaQueryData.padding.bottom;_textScaleFactor = mediaQuery.textScaleFactor;}static MediaQueryData get mediaQueryData => _mediaQueryData;///每个逻辑像素的字体像素数,字体的缩放比例static double get textScaleFactory => _textScaleFactor;///设备的像素密度static double get pixelRatio => _pixelRatio;///当前设备宽度 dpstatic double get screenWidthDp => _screenWidth;///当前设备高度 dpstatic double get screenHeightDp => _screenHeight;///当前设备宽度 pxstatic double get screenWidth => _screenWidth * _pixelRatio;///当前设备高度 pxstatic double get screenHeight => _screenHeight * _pixelRatio;///状态栏高度 dp 刘海屏会更高static double get statusBarHeight => _statusBarHeight;///底部安全区距离 dpstatic double get bottomBarHeight => _bottomBarHeight;///实际的 dp 与设计稿 px 的比例get scaleWidth => _screenWidth / instance.width;get scaleHeight => _screenHeight / instance.height;///根据设计稿的设备宽度适配///高度也根据这个来做适配可以保证不变形setWidth(double width) => width * scaleWidth;/// 根据设计稿的设备高度适配/// 当发现设计稿中的一屏显示的与当前样式效果不符合时,/// 或者形状有差异时,高度适配建议使用此方法/// 高度适配主要针对想根据设计稿的一屏展示一样的效果setHeight(double height) => height * scaleHeight;///字体大小适配方法///@param fontSize 传入设计稿上字体的 px ,///@param allowFontScaling 控制字体是否要根据系统的“字体大小”辅助选项来进行缩放。默认值为 false///@param allowFontScaling Specifies whether fonts should scale to respect Text Sizeaccessibility settings. The default is false.setSp(double fontSize) => allowFontScaling? setWidth(fontSize): setWidth(fontSize) / _textScaleFactor;
}

17、JSON的序列化和反序列化、创建模型类转换Json数据

一、Flutter JSON 序列化反序列化

1、使用 dart:convert 手动序列化 JSON
2、模型类中序列化 JSON
小项目中使用 dart:convert 手动序列化 JSON 非常好,也非常快速。但是随着项目的增大,
dart:convert 手动序列化 JSON 的话失去了大部分静态类型语言特性:类型安全、自动补全和
最重要的编译时异常。这样一来,我们的代码可能会变得非常容易出错。
当我们访问 name 或 email 字段时,我们输入的很快,导致字段名打错了。但由于这个 JSON
在 map 结构中,所以编译器不知道这个错误的字段名。
为了解决上面的问题在大型项目中使用的更多的是在模型类中序列化 JSON。

二、 Flutter JSON 字符串和 Map 换 类型的转换 dart:convert手动序列化 JSON

import 'dart:convert'
var mapData={"name":"张三","age":"20"};
var strData='{"name":"张三","age":"20"}';
print(json.encode(mapData)); //Map 转换成 Json 字符串
print(json.decode(strData)); //Json 字符串转化成 Map 类型

三丶 Flutter 在模型类中序列化 JSON

class FocusModel {String sId;String title;String status;String pic;String url;FocusModel ({this.sId, this.title, this.status, this.pic, this.url});FocusModel .fromJson(Map<String, dynamic> json) {sId = json['_id'];title = json['title'];status = json['status'];pic = json['pic'];url = json['url'];}Map<String, dynamic> toJson() {final Map<String, dynamic> data = new Map<String, dynamic>();data['_id'] = this.sId;data['title'] = this.title;data['status'] = this.status;data['pic'] = this.pic;data['url'] = this.url;return data;}
}
var strData='{"_id":"59f6ef443ce1fb0fb02c7a43","title":"笔记本电脑
","status":"1","pic":"public\upload\UObZahqPYzFvx_C9CQjU8KiX.png","
url":"12"
}';
var data= FocusModel.fromJson( strData )

可参考: https://flutterchina.club/json/

四丶 json_to_dart 自动生成模型类

https://javiercbk.github.io/json_to_dart/

18、底部 Tab 切换保持页面状态的几种方法

一、IndexedStack 保持页面状态

IndexedStack 和 Stack 一样,都是层布局控件, 可以在一个控件上面放置另一
个控件,但唯一不同的是 IndexedStack 在同一时刻只能显示子控件中的一个控
件,通过 Index 属性来设置显示的控件。
IndexedStack 来保持页面状态的优点就是配置简单。IndexedStack 保持页面状
态的缺点就是不方便单独控制每个页面的状态。
k IndexedStack 用法:

Container(
width: double.infinity,
height: double.infinity,
child: new IndexedStack(
index: 0,
alignment: Alignment.center,
children: <Widget>[
Image.network("https://www.itying.com/images/flutter/list1.jpg",fit:
BoxFit.cover,),
Image.network("https://www.itying.com/images/flutter/list2.jpg",fit:
BoxFit.cover)
],
),
)

结合底部 tab 切换

body:IndexedStack(
children: this._pageList,
index: _currentIndex,
),

二丶 AutomaticKeepAliveClientMixin 保持页面状态
AutomaticKeepAliveClientMixin 结合 tab 切换保持页面状态相比 IndexedStack 而言配置起来稍
微有些复杂。它结合底部 BottomNavigationBar 保持页面状态的时候需要进行如下配置。

import 'package:flutter/material.dart';
import 'Home.dart';
import 'Category.dart';
import 'Cart.dart';
import 'User.dart';
class Tabs extends StatefulWidget {Tabs({Key key}) : super(key: key);_TabsState createState() => _TabsState();
}
class _TabsState extends State<Tabs> {int _currentIndex=1;
//创建页面控制器var _pageController;@overridevoid initState(){
//页面控制器初始化_pageController = new PageController(initialPage : _currentIndex);super.initState();}List<Widget> _pageList=[HomePage(),CategoryPage(),CartPage(),UserPage()];@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text("jdshop"),),
//必须用 PageView 加载不同的页面body: PageView(controller: _pageController,children: this._pageList,onPageChanged: (index){_currentIndex = index;},),bottomNavigationBar: BottomNavigationBar(currentIndex:this._currentIndex ,onTap: (index){setState(() {
//this._currentIndex=index;
//页面控制器进行跳转_pageController.jumpToPage(this._currentIndex);});},type:BottomNavigationBarType.fixed ,fixedColor:Colors.red,items: [BottomNavigationBarItem(icon: Icon(Icons.home),title: Text("首页")),BottomNavigationBarItem(icon: Icon(Icons.category),title: Text("分类")),BottomNavigationBarItem(icon: Icon(Icons.shopping_cart),title: Text("购物车")),BottomNavigationBarItem(icon: Icon(Icons.people),title: Text("我的"))],),);}
}

需要持久化的页面加入如下代码:

class HomePage extends StatefulWidget {HomePage({Key key}) : super(key: key);_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin{@overridebool get wantKeepAlive => true;
}

19、inappbrowser、StatefulBuilder 更新 Flutter showDialog、showModalBottomSheet中的状态

一丶 Flutter WebView 组件 inappbrowser的使用
https://pub.dev/packages/flutter_inappbrowser

注意事项:

在这里插入图片描述

二丶 StatefulBuilder 更新 Flutter showDialog、showModalBottomSheet中的状态

参考:https://www.cflutter.com/topic/5d202202403aa10564178c65

20、官方推荐的状态管理库 provider 的使用

一丶 状态管理

通俗的讲:当我们想在多个页面(组件/Widget)之间共享状态(数据),或者一个页面(组
件/Widget)中的多个子组件之间共享状态(数据),这个时候我们就可以用 Flutter 中的状
态管理来管理统一的状态(数据),实现不同组件直接的传值和数据共享。
现在 Flutter 的状态管理方案很多,redux、bloc、state、provide、provider。
目前我们推荐使用 provider,这个是官方提供的状态管理解决方案。相比其他状态管理库使
用起来比较方便。

二、 关于 于flutter provider 库和 和 flutter provide 库

provider 是 Flutter 团队推出的状态管理模式。
官方地址为: https://pub.dev/packages/provider
注意:p rovider 和 provide 是两个库哦。Flutter 官方推荐使用的是 provider 哦,provider 是
flutter 官方出的。provide 不是 Flutter 官方写的哦

三丶 flutter provider 的使用

1、配置依赖 provider: ^3.0.0+1
2、新建一个文件夹叫 provider,在 provider 文件夹里面放我们对于的状态管理类
3、在 provider 里面新建 Counter.dart
4、Counter.dart 里面新建一个类继承 minxins 的 ChangeNotifier 代码如下

import 'package:flutter/material.dart';
class Counter with ChangeNotifier {int _count = 0;int get count => _count;void increment() {_count++;notifyListeners();}
}

5、找到 main.dart 修改代码如下

import 'package:flutter/material.dart';
import 'routers/router.dart';
import 'package:provider/provider.dart';
import 'provider/Counter.dart';
void main() =>runApp(MyApp());
// void main() => runApp(MyApp());
class MyApp extends StatefulWidget {MyApp({Key key}) : super(key: key);_MyAppState createState() => _MyAppState();
}class _MyAppState extends State<MyApp> {@overrideWidget build(BuildContext context) {return MultiProvider(providers: [
// Provider<Counter>.value(value: foo),ChangeNotifierProvider(builder: (_) => Counter()),],child: MaterialApp(
// home: Tabs(),debugShowCheckedModeBanner: false,initialRoute: '/productContent',onGenerateRoute: onGenerateRoute,theme: ThemeData(
// primaryColor: Colors.yellowprimaryColor: Colors.white),),);}
}

6、获取值、以及设置值

import 'package:provider/provider.dart';
import '../../provider/Counter.dart';
Widget build(BuildContext context) {final counter = Provider.of<Counter>(context);
// counter.init();return Scaffold(floatingActionButton: FloatingActionButton(child: Icon(Icons.add),onPressed: (){counter.increment();},),body: Text("counter 的值:${counter.count}"));
}

21、事件广播 、事件监听

一丶 event_bus 介绍

在前面的课程我们给大家讲过状态管理 Provider 的使用。
通俗的讲状态管理就是:当我们想在多个页面(组件/Widget)之间共享状态(数据),或
者一个页面(组件/Widget)中的多个子组件之间共享状态(数据),这个时候我们就可以
用 Flutter 中的状态管理来管理统一的状态(数据),实现不同组件直接的传值和数据共享。
那么这一讲给大家讲的 event_bus 主要是实现不同组件之间的数据传值,以及在一个组件中
执行另一个组件的方法。

二丶 event_bus 使用事件广播 、事件监听

https://pub.dev/packages/event_bus

1 、配置安装依赖
event_bus: ^1.1.0

2 、 新建一文件 EventBus.dart 配置如下代码

import 'package:event_bus/event_bus.dart';
//Bus 初始化
EventBus eventBus = EventBus();
class ProductContentEvent {String text;ProductContentEvent(String text){this.text = text;}
}

3 、 在需要广播事件的页面引入上面的 EventBus.dart类 然后配置如下代码

eventBus.fire(new ProductContentEvent('购物车'));

4 、 在需要监听广播的地方引入上面的 EventBus.dart 类,然后配置如下代码

void initState() {super.initState();
//监听广播eventBus.on<ProductContentEvent>().listen((event){print(event);this._attrBottomSheet();});
}

三丶 event_bus 取消事件监听

var actionSubscription =eventBus.on<ProductContentEvent>().listen((event){print(event);this._attrBottomSheet();
});
actionSubscription.cancel();

22、点击穿透问题、页面禁止左右滑动

一丶 点击穿透问题

HitTestBehavior.deferToChild 只有有子 Widget 通过了 Hit-Test,才接收一系列的事件,接收
区域也会被限制在该子 Widget 区域中。
HitTestBehavior.opaque 能够通过 Hit-Test,接收事件,且能阻止在它之前的 Widget(直观
来看就是被它挡住的 Widget)接收事件。简单来说就是事件 不能透传。
HitTestBehavior.translucent 能够通过 Hit-Test,接收事件,且不会阻止它之前的 Widget(直
观来看就是被它挡住的 Widget)接收事件。简单来说就是事件 能透传。

GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
return false;
}
)

二丶 页面禁止左右滑动

部分真机详情左右滑动太灵敏 导致详情上下滑动不好滑动。这个时候也可禁用详情Tab滑动

physics: NeverScrollableScrollPhysics(), //禁止 pageView 滑动

喜欢记得点个赞哟,我是王睿,很高兴认识大家!

Published by

风君子

独自遨游何稽首 揭天掀地慰生平