Flutter入门——开发过程中常用功能实现
本篇文章主要介绍Flutter实际开发过程中主要功能的实现,例如网络和Http请求、
Json和序列化、推送工具使用和Provider之类的状态管理的使用。
一、网络与Http请求
作为一个网络应用程序进行网络请求是必不可少的功能,这里主要介绍Flutter中Get与Post的使用。Flutter的Http网络请求的实现主要分为三种:io.dart里的HttpClient实现、Dart原生http请求库实现、第三方库实现。
-
HttpClient
代码示例:
import 'dart:io'; get() async { var httpClient = new HttpClient(); var uri = new Uri.http( 'example.com', '/path1/path2', {'param1': '42', 'param2': 'foo'}); var request = await httpClient.getUrl(uri); var response = await request.close(); var responseBody = await response.transform(UTF8.decoder).join(); } post() async { var httpClient = new HttpClient(); var uri = new Uri.http( 'example.com', '/path1/path2', {'param1': '42', 'param2': 'foo'}); var request = await httpClient.postUrl(uri); var response = await request.close(); var responseBody = await response.transform(UTF8.decoder).join(); }
httpClient只是实现了基本的网络请求,一些复杂的功能还没有实现,例如不支持multipart/form-data类型的请求。
-
Dart原生http请求库实现
仓库地址:https://github.com/dart-lang/http
使用时需要在yaml文件中添加依赖,最新版为http 0.12.2
代码示例:
import 'package:http/http.dart' as http; var url = 'https://example.com/whatsit/create'; var response = await http.post(url, body: {'name': 'doodle', 'color': 'blue'}); print('Response status: ${response.statusCode}'); print('Response body: ${response.body}'); print(await http.read('https://example.com/foobar.txt')); var client = http.Client(); try { var uriResponse = await client.post('https://example.com/whatsit/create', body: {'name': 'doodle', 'color': 'blue'}); print(await client.get(uriResponse.bodyFields['uri'])); } finally { client.close(); }
-
第三方库
这里推荐大家使用第三方库DIO,这是一个国人开发的Dart Http请求库。
dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等…、
仓库地址:https://github.com/flutterchina/dio
最新版本:v 3.0.10
Dio实现功能相当丰富,仓库中Readme.md中描述的非常详细,并且有相关文档,这里就不再赘述。
其中有一点需要注意的是,如果要实现Cookie管理功能,需要添加dio_cookie_manager库(https://github.com/flutterchina/dio/tree/master/plugins/cookie_manager),此插件基于原来的cookie_jar插件。
使用:
import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:cookie_jar/cookie_jar.dart'; main() async { var dio = Dio(); var cookieJar=CookieJar(); dio.interceptors.add(CookieManager(cookieJar)); // Print cookies print(cookieJar.loadForRequest(Uri.parse("https://baidu.com/"))); // second request with the cookie await dio.get("https://baidu.com/"); ... } //默认是将cookie保存到内存中,如果想要讲cookie保存到文件中需要以下方法来实现 import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:cookie_jar/cookie_jar.dart'; main() async { var dio = Dio(); Directory appDocDir = await getApplicationDocumentsDirectory(); String dir = appDocDir.path + "/cookies/"; cookieJar = PersistCookieJar(dir: dir); dio.interceptors.add(CookieManager(cookieJar)); dio.options.connectTimeout = 10000; // second request with the cookie await dio.get("https://baidu.com/"); ... }
-
举例(某项目post请求通用方法)
import 'dart:async'; import 'dart:io'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:zsb/models/response_model.dart'; import 'package:zsb/utils/notice_util.dart'; import 'package:zsb/utils/toast.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; Future<SharedPreferences> _prefs = SharedPreferences.getInstance(); PersistCookieJar cookieJar; ///网络请求公共方法 ///url:请求地址 ///formdata:参数 Future post({String url,FormData formData})async{ try { Response response; Dio dio=new Dio(); //使用cookieJar实现cookie管理并持久化到文件 Directory appDocDir = await getApplicationDocumentsDirectory(); String dir = appDocDir.path + "/cookies/"; cookieJar = PersistCookieJar(dir: dir); dio.interceptors.add(CookieManager(cookieJar)); dio.options.connectTimeout = 10000; response=await dio.post(url,data: formData); if(response.statusCode==200){ ResponseModel responseModel = ResponseModel.fromJson(response.data); if(!responseModel.success&&responseModel.tip=='10001'){ //清空已存储token SharedPreferences prefs = await _prefs; prefs.remove("token"); cookieJar.deleteAll(); //通知订阅者页面跳转至登录页面 showToast2("登录状态失效请重新登陆", Colors.black54, Colors.white); NoticeUtil.ctrl.sink.add('toLogin'); } if(!responseModel.success&&responseModel.tip=='100026'){ showToast2("账号在别处登录", Colors.black54, Colors.white); NoticeUtil.ctrl.sink.add('toLogin'); } if(responseModel.success){ SharedPreferences prefs = await _prefs; String session = "session"; prefs.setString("token", session); } return response.data; }else{ return "error"; } } catch (e) { return "error"; } }
二、JSON和序列化
在实际项目开发过程中,对Json的操作是不可避免的,下面介绍两种主要的Json序列化方法,需要强调的是Flutter是没有类似Jackson和Fastjson类似工具的,因为他们需要使用反射机制进行实现,但是Flutter禁用反射。
-
手动序列化
手动JSON序列化是指使使用
dart:convert
中内置的JSON解码器。它将原始JSON字符串传递给JSON.decode() 方法,然后在返回的Map
中查找所需的值。 它没有外部依赖或其它的设置,对于小项目很方便。例子:
var encoded = JSON.encode([1, 2, { "a": null }]); var decoded = JSON.decode('["foo", { "bar": 499 }]');
在Model中使用,以UserModel为例:
class UserModel { bool success; String tip; String failReason; List<User> users; UserModel( {this.success,this.tip, this.failReason, this.users}); UserModel.fromJson(Map<String, dynamic> json) { success = json['success']; tip = json['tip']; failReason = json['failReason']; users = new List<User>(); if (json['root'] != null) { json['root'].forEach((v) { users.add(new User.fromJson(v)); }); } } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['success'] = this.success; data['tip'] = this.tip; data['failReason'] = this.failReason; if (this.users != null) { data['root'] = this.users.map((v) => v.toJson()).toList(); } return data; } } class User{ var stuId; var stuName; var stuPassword; var stuIdNumber; var stuPhone; var stuSex; var stuLanguage; var stuImgUrl; var stuCompareStatus; User( {this.stuId, this.stuName, this.stuIdNumber, this.stuPhone, this.stuPassword, this.stuSex, this.stuLanguage, this.stuImgUrl, this.stuCompareStatus }); User.fromJson(Map<String, dynamic> json) { stuId = json['stuId']; stuName = json['stuName']; stuIdNumber = json['stuIdNumber']; stuPhone = json['stuPhone']; stuSex = json['stuSex']; stuLanguage = json['stuLanguage']; stuImgUrl = json['stuImgUrl']; stuCompareStatus = json['stuCompareStatus']; } Map<String, dynamic> toJson() { final Map<String, dynamic> data = new Map<String, dynamic>(); data['stuId'] = this.stuId; data['stuName'] = this.stuName; data['stuIdNumber'] = this.stuIdNumber; data['stuPhone'] = this.stuPhone; data['stuSex'] = this.stuSex; data['stuLanguage'] = this.stuLanguage; data['stuImgUrl'] = this.stuImgUrl; data['stuCompareStatus'] = this.stuCompareStatus; return data; } }
这是根据后台返回的Json字符串生成的Model,其中使用JSON.decode将json字符串转换为Map,调用fromJson方法实现从Map到实体对象的转换,调用toJson方法实现从Model到Map的转换,使用JSON.encode将Map转换为json字符串。
-
使用json_serializable模型
我们需要因为三个依赖项:最新地址:https://github.com/google/json_serializable.dart/blob/master/example/pubspec.yaml
dependencies: # Your other regular dependencies here json_annotation: ^2.0.0 dev_dependencies: # Your other dev_dependencies here build_runner: ^1.0.0 json_serializable: ^2.0.0
具体查看官方文档。
-
总结
官方文档中介绍说建议在小项目中使用第一种方式,而在大项目中使用第二种方式,主要是因为第一种方式手动编写Model不需要其他依赖项,不是要做繁杂配置,而第二种方式避免了大量手动编写Model的工作。其实在实际使用过程中第一种方式并不需要手动编写代码,有人提供了Model的自动化生成工具https://javiercbk.github.io/json_to_dart/
三、推送工具使用
使用过的推送插件时极光推送,这也是国内最早发布的Flutter版插件。
仓库地址:https://github.com/jpush/jpush-flutter-plugin
使用之前需要注册极光账号并在控制台建立应用,极光地址:https://www.jiguang.cn/
注意在应用设置中存在两个key,一个是AppKey,它是极光平台应用的唯一标识,另一个是Master Secret ,用于服务器端 API 调用时与 AppKey 配合使用达到鉴权的目的。
完成之后就要添加极光推送的依赖,jpush_flutter: 0.5.6
还需要做些配置,设置相关参数并适配各种架构的平台,配置文件路径:android/app/build.gradle
android: {
....
defaultConfig {
applicationId "替换成自己应用 ID"
...
ndk {
//选择要添加的对应 cpu 类型的 .so 库。
abiFilters 'armeabi', 'armeabi-v7a', 'x86', 'x86_64', 'mips', 'mips64', 'arm64-v8a',
}
manifestPlaceholders = [
JPUSH_PKGNAME : applicationId,
JPUSH_APPKEY : "appkey", // NOTE: JPush 上注册的包名对应的 Appkey.
JPUSH_CHANNEL : "developer-default", //暂时填写默认值即可.
]
}
}
下面就是使用:
首先要初始化
// 推送消息是异步的,因此我们在异步方法中初始化。
Future<void> initPlatformState() async {
//获得RegistrationID
jpush.getRegistrationID().then((rid) {
print('getRegistrationID$rid');
});
//初始化SDK
jpush.setup(
appKey: "*************************",
channel: "theChannel",
production: false,
debug: true,
);
//设置ios
jpush.applyPushAuthority(new NotificationSettingsIOS(
sound: true,
alert: true,
badge: true));
}
何时进行推送呢?那就需要对推送事件进行监听
///添加推送事件监听方法
Future addPushEventHandler(BuildContext context){
try {
NoticeUtil.jpush.addEventHandler(
//接受推送事件
onReceiveNotification: (Map<String, dynamic> message) async {
assert(true,'flutter onReceiveNotification: $message');
},
//在通知栏打开推送事件
onOpenNotification: (Map<String, dynamic> message) async {
assert(true,'flutter onOpenNotification: $message');
//实现自己的业务逻辑
},
//接收自定义消息回调方法
onReceiveMessage: (Map<String, dynamic> message) async {
assert(true,'flutter onReceiveMessage: $message');
},
);
} on PlatformException {
print('Failed to get platform version.');
}
}
上面介绍的是后台的网路推送,我们的实际使用只使用了本地推送功能,具体逻辑是建立一个socket链接,对socket进行监听,如果收到服务端发送的信息就推动消息
///接收到推送后的响应
static void dataHandler(data){
isTimer = false;//关闭定时任务
var fireDate = DateTime.fromMillisecondsSinceEpoch(DateTime.now().millisecondsSinceEpoch + 1000);
int n = Random().nextInt(1000000);
var localNotification = LocalNotification(//设置推送
id: n,
title: '推送',
buildId: i++,
content: '${utf8.decode(data)}',
//content: data,
fireTime: fireDate,
subtitle: '一个测试',
);
jpush.sendLocalNotification(localNotification);//发送本地推送
isHave = true;
ctrl.sink.add(utf8.decode(data));
}
到此就集成了极光推送。
四、屏幕适配
不同的设备其屏幕大小都有多不同,我们不可能针对不同的设备去单独开发不同的应用,这里介绍一个Flutter屏幕适配工具:flutter_ScreenUtil,它的作用就是可以让设计稿的尺寸通过计算转化为不同设备的对应大小,实现原理也很简单,就是通过比例来进行缩放。
仓库地址:https://github.com/OpenFlutter/flutter_screenutil/
在使用之前请设置好设计稿的宽度和高度,传入设计稿的宽度和高度(单位px) 一定在MaterialApp的home中的页面设置(即入口文件,只需设置一次),以保证在每次使用之前设置好了适配尺寸:
//填入设计稿中设备的屏幕尺寸
//默认 width : 1080px , height:1920px , allowFontScaling:false
ScreenUtil.init(context);
//假如设计稿是按iPhone6的尺寸设计的(iPhone6 750*1334)
ScreenUtil.init(context, width: 750, height: 1334);
//设置字体大小根据系统的“字体大小”辅助选项来进行缩放,默认为false
ScreenUtil.init(context, width: 750, height: 1334, allowFontScaling: true);
API
ScreenUtil().setWidth(540) (sdk>=2.6 : 540.w) //根据屏幕宽度适配尺寸
ScreenUtil().setHeight(200) (sdk>=2.6 : 200.h) //根据屏幕高度适配尺寸
ScreenUtil().setSp(24) (sdk>=2.6 : 24.sp) //适配字体
ScreenUtil().setSp(24, allowFontScalingSelf: true) (sdk>=2.6 : 24.ssp) //适配字体(根据系统的“字体大小”辅助选项来进行缩放)
ScreenUtil().setSp(24, allowFontScalingSelf: false) (sdk>=2.6 : 24.nsp) //适配字体(不会根据系统的“字体大小”辅助选项来进行缩放)
ScreenUtil.pixelRatio //设备的像素密度
ScreenUtil.screenWidth (sdk>=2.6 : 1.wp) //设备宽度
ScreenUtil.screenHeight (sdk>=2.6 : 1.hp) //设备高度
ScreenUtil.bottomBarHeight //底部安全区距离,适用于全面屏下面有按键的
ScreenUtil.statusBarHeight //状态栏高度 刘海屏会更高 单位px
ScreenUtil.textScaleFactor //系统字体缩放比例
ScreenUtil().scaleWidth // 实际宽度的dp与设计稿px的比例
ScreenUtil().scaleHeight // 实际高度的dp与设计稿px的比例
0.2.wp //屏幕宽度的0.2倍
0.5.hp //屏幕宽度的50%
五、Provide状态管理
我的理解(可能不准确):状态管理的目的其实就是为了让界面与业务逻辑进行分离,并且根据逻辑来控制组件状态的改变。
首先添加依赖,仓库地址:https://pub.dev/packages/provider
创建ProvideModel,其实就是一个混入了ChangeNotifier的类,下面是登录页面使用实例
class LoginProvider with ChangeNotifier{
Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
bool loginSuccess = true;
LoginModel loginModel;
///用户登录
Future<ResponseModel> login(User user,bool first)async{
ResponseModel responseModel;
if(user==null){
//传入为空
return null;
}else{
//封装参数
FormData formData=new FormData.fromMap(
{
'idNumber':'${user.stuIdNumber}',
'password':'${user.stuPassword}',
'appVersion':'${Base.appVersion}'
}
);
await post(url: ServiceUrl.servicePath['login'],formData:formData ).then((value){
//将json字符串转换为Model
if(value=="error"){
responseModel = new ResponseModel(success: false,failReason: "网络拥塞请重试");
}else{
responseModel = ResponseModel.fromJson(value);
}
});
if(NetworkStatus.connected&&!responseModel.success){
//登录失败
showToast2("${responseModel.failReason}", Colors.black, Colors.white);
loginSuccess = false;
//通知状态刷新
notifyListeners();
}
}
return responseModel;
}
}
顶层依赖管理
获取状态,provide具有两种获取状态的方式
-
第一种
Provide<LoginProvider>( builder: (context, child, counter) { return Text( '${counter.value}' ); }, )
这种方式将一直监听状态通知,当执行notifyListeners()方法通知刷新时就会改变return 的Widget的状态,然后调用build方法刷新界面。
builder方法接收三个参数,这里主要介绍第二个和第三个。
-
第二个参数child:假如这个小部件足够复杂,内部有一些小部件是不会改变的,那么我们可以将这部分小部件写在Provide的child属性中,让builder不再重复创建这些小部件,以提升性能。
-
第三个参数counter:这个参数代表了我们获取的顶层providers中的状态。
-
-
第二种
var value = Provide.value<LoginProvider>(context);
这种方式可以直接访问其内部方法和变量。
虽然Provide已经能够基本满足需要,但是后来官方已经不再更新,而是合并到了新的仓库Provider中,使用方式有所改变。
-
顶层依赖方式不同
provide
void main() { //顶层依赖 var counter = Counter(); var providers = Provider(); providers ..(Provider<Counter>.value(counter)) runApp(ProviderNode(child: MyApp(), providers: providers)); }
provider
class MyApp extends StatelessWidget { var counter = Counter(); @override Widget build(BuildContext context) { return MultiProvider( providers: [ //这里是关键注册通知吧 ChangeNotifierProvider(builder: (_) => counter), ], child: Container( child: MaterialApp( title: 'Test', onGenerateRoute: Application.router.generator, //去掉DEBUG字样 debugShowCheckedModeBanner: false, //设置主题 theme: ThemeData(primaryColor: Colors.pink), home: IndexPage(), ), ), ); } }
-
使用
provide
Provide.value<Counter>(context).increment();
provider
Provider.of<Counter>(context, listen: false).increment(); //这里也可以传参数
其他区别查看官方文档:https://github.com/rrousselGit/provider
六、学习资源推荐
flutterGo:https://github.com/alibaba/flutter-go
实现了大部分组件的示例,我认为他的最大作用就是可以让我们预览不同组件的效果,为我们的选用和学习提供参考
仿微博客户端:https://github.com/huangruiLearn/flutter_hrlweibo
flutter学习免费教程:技术胖课程https://jspang.com/detailed?id=58
此教程包括基本组件的学习,还有一个完整的电商项目,最重要的是这些内容全部一行一行手敲!!!