Flutter 笔记
学习路线
Flutter越来越火,学习Flutter的人越来越多,对于刚接触Flutter的人来说最重要的是如何学习Flutter,重点学习Flutter的哪些内容。下面是Flutter的学习路线。
一、学习Dart语法
Flutter是用Dart语言开发的,所以我们需要Dart语言的基础知识,如果你有其他高级语言的基础,这一部分基本可以略过,只需了解如下内容:
如何导入包。
异步编程(Future、async、await)。
注释。
命名规范
如何定义变量作用域
二、学习Flutter
基础组件、路由、动画、手势事件、网络请求、本地存储、混合开发
三、开发一个简易项目
学习完以上内容,你就可以上手开发一个简易项目了,这个时候开发项目的目的是为了将你之前所学的知识整合。
项目完成后如果你想让自己的编码水平以及项目达到更高的一个水平,那么你需要学习一下互联网上优质开源项目,一般来说学习两到三个项目就可以了,下面我会列出我觉得不错的开源项目。
四、学习开源项目
关注点:
目录文件结构
是否符合官方的包管理规范,
组件拆分
页面、组件、通用、业务
工具类设计
通讯、持久化、安全、字符、数字、浮点
第三方组件
流媒体、播放器、编辑器、图片、Web 视图、原生扩展
状态管理
bloc、provider
云服务
firebase、google cloud、AWS、serverless
业务完整性
可运行、业务全
五、整合之前所学知识开发一个项目
将之前所学的基础知识和优质开源项目经验整合后,你的编码水平和项目设计将提升到一个新的高度。
入门书籍《flutter实战》
https://book.flutterchina.club/
组件介绍
优质开源项目
Prism
https://github.com/Hash-Studios/Prism
Best-Flutter-UI-Templates
https://github.com/mitesh77/Best-Flutter-UI-Templates
flutter_vignettes
https://github.com/gskinnerTeam/flutter_vignettes
flutter_wanandroid
https://github.com/Sky24n/flutter_wanandroid
APP名称
Android: 在 android ▸ app ▸ src ▸ main ▸ AndroidManifest.xml 中修改package="xxx.xxx.xxx";
以及在 android ▸ app ▸ src ▸ build.gradle中修改applicationId "xxx.xxx.xxx";
并且需要修改android ▸ app ▸ src ▸ main ▸ ...... ▸ MainActivity.java对应的包路径
iOS :在ios ▸ Runner ▸ Info.plist 中修改CFBundleIdentifier对应的Value
APP图标(Android)
1、准备一个图标
可以从网上下载一个图标
2、制作图标
https://icon.wuruihong.com/#/ios
图片制作完成后下载到本地,然后将下载的图片复制到项目目录下\android\app\src\main\res,替换原有图标。重启项目即可。
屏幕锁定
void main() {
runApp(new MyApp());
// 强制竖屏SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp,DeviceOrientation.portraitDown
]);
// 强制横屏SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft,DeviceOrientation.landscapeRight
]);
}
启动页
Android
1、准备好启动页所需要的图片,将图片复制到项目目录下\android\app\src\main\res 以mipmap开头的文件夹中
2、android\app\src\main\res\drawable\launch_background.xml
<item>
<bitmap
android:gravity="fill" // 这句是全屏显示的关键
android:src="@mipmap/itest_splash" // 启动图按照分辨率放在mipmap-**hdpi目录中
/>
</item>
3、android\app\src\main\res\values\styles.xml
<?xml version="1.0" encoding="utf-8"?>
<resources><!-- Theme applied to the Android Window while the process is starting --><style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar"><!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame --><item name="android:navigationBarColor">@android:color/transparent</item><item name="android:statusBarColor">@android:color/transparent</item><item name="android:windowFullscreen">true</item>
// 以上三行是新增的
<item name="android:windowBackground">@drawable/launch_background</item></style>
</resources>
IOS
准备好启动页所需要的图片,将对应的图片拖动到对应的位置
勾选 use safe area layout guide(使用安全区域布局)
IOS 启动页设置时间
找到项目下的 ios/Runner/AppDelegate.swift
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
Thread.sleep(forTimeInterval: 2) // 加入此行代码,便可延迟2秒关闭启动页
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
启动页/广告
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_jd/widgets/layout/TabNavigator.dart';
import 'package:get/route_manager.dart';
// 启动页/广告页
class LaunchPage extends StatefulWidget {
@override
_MyState createState() => _MyState();
}
class _MyState extends State<LaunchPage> {
Timer _timer;
int count = 3;
@overridevoid initState() {
super.initState();
setStatus();
startTime();
}
@overrideWidget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.light,
child: Scaffold(
body: Stack(
alignment: Alignment(1.0, -1.0), // 右上角对齐
children: [
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: Image.asset(
'assets/images/launch/ad.jpg',
fit: BoxFit.fill,
),
),
Positioned(
bottom: 40,
right: 20,
// ignore: deprecated_member_use
child: FlatButton(
minWidth: 80,
height: 34,
padding: EdgeInsets.only(bottom: 2),
color: Color.fromRGBO(0, 0, 0, 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Text(
"$count 跳过",
style: TextStyle(color: Colors.white, fontSize: 13.0),
),
onPressed: () {
toHomePage();
},
),
),
],
),
),
);
}
// 隐藏状态栏,保留底部按钮栏void setStatus() {SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
}
// 设置启动图时间void startTime() async {Timer(Duration(milliseconds: 0), () {
_timer = Timer.periodic(const Duration(milliseconds: 1000), (v) {
setState(() {
--count;
});
if (count == 0) {
Timer(Duration(milliseconds: 500), () {
toHomePage();
});
}
});
return _timer;
});
}
// 跳转到主页void toHomePage() {
print(_timer);
if (_timer != null) {
_timer.cancel();
SystemChrome.setEnabledSystemUIOverlays(
SystemUiOverlay.values,
);
Get.off(
() => new TabNavigator(),
duration: Duration(milliseconds: 600),
transition: Transition.noTransition,
);
}
}
}
使用:
main.js
home: LaunchPage(),
2、修改配置文件
项目目录下\android\app\src\main\res\drawable\launch_background.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"><item><bitmap android:src="@mipmap/splash_bg" /></item>
</layer-list>
重启生效
沉浸式状态栏
修改前
修改后
main.dart
// void main() => runApp(MyApp());
void main() {
runApp(new MyApp());
if (Platform.isAndroid) {
// 以下两行 设置android状态栏为透明的沉浸。写在组件渲染之后,是为了在渲染后进行set赋值,覆盖状态栏,写在渲染之前MaterialApp组件会覆盖掉这个值。
SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
}
}
设置AppBar的高度
在AppBar外面包裹PreferredSize
appBar: PreferredSize(
child: new AppBar(
title: new Text('搜索'),
),
preferredSize: Size(double.infinity, 60)
),
去除AppBar底部阴影
appBar: AppBar(
elevation: 0, //默认是4, 设置成0 就是没有阴影了
),
自定义水波纹颜色
color.dart
import 'dart:ui';
import 'package:flutter/material.dart';
MaterialColor createMaterialColor(Color color) {
List strengths = <double>[.05];
Map swatch = <int, Color>{};
final int r = color.red, g = color.green, b = color.blue;
for (int i = 1; i < 10; i++) {
strengths.add(0.1 * i);
}
strengths.forEach((strength) {
final double ds = 0.5 - strength;
swatch[(strength * 1000).round()] = Color.fromRGBO(
r + ((ds < 0 ? r : (255 - r)) * ds).round(),
g + ((ds < 0 ? g : (255 - g)) * ds).round(),
b + ((ds < 0 ? b : (255 - b)) * ds).round(),
1,
);
});
return MaterialColor(color.value, swatch);
}
使用:
theme: ThemeData(
primarySwatch: createMaterialColor(Color(0xFFeeeeee)),
primaryColor: Colors.blue,
),
全局去除水波纹效果
在main.dart中设置下面两个属性透明
theme: ThemeData.light().copyWith(
splashColor: Colors.transparent, // 水波纹样式
highlightColor: Colors.transparent, // 点击后的颜色
),
局部去除水波纹效果
Theme(
data: ThemeData(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
),
child: Scaffold(
backgroundColor: Color(0xFF297CFF),
appBar: TabBar(
tabs: <Widget>[
Tab(
text: "a",
),
Tab(
text: "b",
),
Tab(
text: "c",
)
],
)
),
)
去除ListView 滑动波纹
ScrollConfiguration(
behavior: MyBehavior(),
child: new ListView(),
),
class MyBehavior extends ScrollBehavior {
@overrideWidget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
if (Platform.isAndroid || Platform.isFuchsia) {
return child;
} else {
return super.buildViewportChrome(context, child, axisDirection);
}
}
}
AppBar
常用属性:
- leading → Widget - 在标题前面显示的一个控件,在首页通常显示应用的 logo;在其他界面通常显示为返回按钮。
- title → Widget - Toolbar 中主要内容,通常显示为当前界面的标题文字。
- actions → List - 一个 Widget 列表,代表 Toolbar 中所显示的菜单,对于常用的菜单,通常使用 IconButton 来表示;对于不常用的菜单通常使用 PopupMenuButton 来显示为三个点,点击后弹出二级菜单。
- bottom → PreferredSizeWidget - 一个 AppBarBottomWidget 对象,通常是 TabBar。用来在 Toolbar 标题下面显示一个 Tab 导航栏。
- elevation → double - 控件的 z 坐标顺序,默认值为 4,对于可滚动的 SliverAppBar,当 SliverAppBar 和内容同级的时候,该值为 0, 当内容滚动 SliverAppBar 变为 Toolbar 的时候,修改 elevation 的值。
- flexibleSpace → Widget - 一个显示在 AppBar 下方的控件,高度和 AppBar 高度一样,可以实现一些特殊的效果,该属性通常在 SliverAppBar 中使用。
- backgroundColor → Color - Appbar 的颜色,默认值为 ThemeData.primaryColor。改值通常和下面的三个属性一起使用。
- brightness → Brightness - Appbar 的亮度,有白色和黑色两种主题,默认值为 ThemeData.primaryColorBrightness。
- iconTheme → IconThemeData - Appbar 上图标的颜色、透明度、和尺寸信息。默认值为 ThemeData.primaryIconTheme。
- textTheme → TextTheme - Appbar 上的文字样式。
- centerTitle → bool - 标题是否居中显示,默认值根据不同的操作系统,显示方式不一样。
- toolbarOpacity → double
封装AppBar
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class TopBar extends StatefulWidget implements PreferredSizeWidget {
final PreferredSizeWidget bottom;
final String title;
final List<Widget> actions;
final TextStyle titleStyle;
final Color backgroundColor;
final String backImgName;
final bool isBack;
final Brightness brightness;
final double height;
TopBar({
this.bottom,
this.title,
this.actions,
this.titleStyle,
this.backgroundColor,
this.backImgName,
this.isBack: false,
this.brightness,
this.height: 45
});
@override
_TopBarState createState() => _TopBarState();
@overrideSize get preferredSize => Size.fromHeight(this.bottom != null ? 91 : this.height);
}
class _TopBarState extends State<TopBar> {
@overrideWidget build(BuildContext context) {
return AppBar(
centerTitle: true, // 标题居中
brightness: widget.brightness,
title: new Text(
widget.title ?? 'title',
style: widget.titleStyle ?? new TextStyle(
color: Colors.white,
fontSize: 17.0,
),
),
leading: widget.isBack ? FlatButton(
// 返回按钮
child: Icon(Icons.arrow_back_ios),
onPressed: () {
print("返回");
},
) : null,
backgroundColor: widget.backgroundColor,
elevation: 0,
// bottom: new AppBarBottom(// child: widget.bottom,// ),
actions: widget.actions,
);
}
}
class AppBarBottom extends StatelessWidget implements PreferredSizeWidget {
final Widget child;
AppBarBottom({
this.child,
});
@overrideWidget build(BuildContext context) {
return Container(
child: Column(
children: <Widget>[
Container(
width: double.infinity,
height: 1,
color: Color(0xFFE5E5E5),
),
child ??
SizedBox(
height: 0,
),
],
),
);
}
@overrideSize get preferredSize => Size.fromHeight(this.child != null ? 47 : 1);
}
使用:
appBar: TopBar(
title: '搜索',
brightness: Brightness.light,
),
TabBar
效果图
return DefaultTabController(
length: 3,
initialIndex: 0,
child: Scaffold(
appBar: AppBar(
backgroundColor: Utils.hexToColor('#297CFF'),
bottom: PreferredSize(
preferredSize: Size(double.infinity, 0),
child: Stack(
children: <Widget>[
InkWell(
onTap: () {
Navigator.pop(context);
},
child: Padding(
padding: EdgeInsets.fromLTRB(25.w, 29.w, 35.w, 25.w),
child: Image.asset('assets/arrow_return.png', width: 17.w,)
),
),
Align(
child: TabBar(
indicatorColor: Colors.white, //指示器颜色indicatorWeight: 4.w, // 指示器高度indicatorPadding: EdgeInsets.only(left: 50.w, right: 50.w), //指示器内边距,修改之后不会立即生效,需要刷新labelPadding: EdgeInsets.symmetric(horizontal: 15.w), // label内边距isScrollable: true,
indicatorSize: TabBarIndicatorSize.label,
labelColor: Colors.white, //选中label的颜色unselectedLabelColor: Colors.white,
labelStyle: TextStyle( //选中文字样式fontSize: 36.w,
fontWeight: FontWeight.bold
),
unselectedLabelStyle: TextStyle( //未选中文字样式fontSize: 28.w, fontWeight: FontWeight.w500
),
tabs: <Widget>[
Tab(text: '入库信息'),
Tab(text: '入库物资'),
Tab(text: '工单进度'),
],
),
),
Container(
width: 110.w,
height: 56.w,
)
],
)
)
),
body: TabBarView(
children: <Widget>[
Align(
child: Text('入库信息'),
),
Align(
child: Text('入库物资'),
),
Align(
child: Text('工单进度'),
),
],
),
)
);
去除右上角Debug文字
return MaterialApp(
debugShowCheckedModeBanner: false,
);
使用 iconfont
1、将下载好的 iconfont.ttf 放到项目目录下,我这边选择的是 /assets/iconfont/
2、在 pubspec.yaml 中 引入 iconfont.ttf 文件
flutter:
uses-material-design: true
fonts:- family: Iconfontfonts:- asset: assets/iconfont/iconfont.ttf
3、封装 iconfont 组件
class Utils {
// iconFont 组件static Icon iconFont(int icon, [
Color color = Colors.black45,
double size = 15,
]) {
return Icon(
IconData(icon, fontFamily: 'iconfont'),
color: color,
size: size,
);
}
}
4、使用组件
Utils.iconFont(0xe671, Color(0xFF333333), 18)
MaterialApp
MaterialApp 是我们app开发中常用的符合MaterialApp Design设计理念的入口Widget,从源码可以看出该widget的构造方法中有多个参数,但是基本上大多数参数是可以省略的。
MaterialApp({
Key key,
this.title = '', // 设备用于为用户识别应用程序的单行描述this.home, // 应用程序默认路由的小部件,用来定义当前应用打开的时候,所显示的界面this.color, // 在操作系统界面中应用程序使用的主色。this.theme, // 应用程序小部件使用的颜色。this.routes = const <String, WidgetBuilder>{}, // 应用程序的顶级路由表this.navigatorKey, // 在构建导航器时使用的键。this.initialRoute, // 如果构建了导航器,则显示的第一个路由的名称this.onGenerateRoute, // 应用程序导航到指定路由时使用的路由生成器回调this.onUnknownRoute, // 当 onGenerateRoute 无法生成路由(initialRoute除外)时调用this.navigatorObservers = const <NavigatorObserver>[], // 为该应用程序创建的导航器的观察者列表this.builder, // 用于在导航器上面插入小部件,但在由WidgetsApp小部件创建的其他小部件下面插入小部件,或用于完全替换导航器this.onGenerateTitle, // 如果非空,则调用此回调函数来生成应用程序的标题字符串,否则使用标题。this.locale, // 此应用程序本地化小部件的初始区域设置基于此值。this.localizationsDelegates, // 这个应用程序本地化小部件的委托。this.localeListResolutionCallback, // 这个回调负责在应用程序启动时以及用户更改设备的区域设置时选择应用程序的区域设置。this.localeResolutionCallback, // this.supportedLocales = const <Locale>[Locale('en', 'US')], // 此应用程序已本地化的地区列表 this.debugShowMaterialGrid = false, // 打开绘制基线网格材质应用程序的网格纸覆盖this.showPerformanceOverlay = false, // 打开性能叠加this.checkerboardRasterCacheImages = false, // 打开栅格缓存图像的棋盘格this.checkerboardOffscreenLayers = false, // 打开渲染到屏幕外位图的图层的棋盘格this.showSemanticsDebugger = false, // 打开显示框架报告的可访问性信息的覆盖this.debugShowCheckedModeBanner = true, // 在选中模式下打开一个小的“DEBUG”横幅,表示应用程序处于选中模式
})
ThemeData主题
ThemeData({
Brightness brightness, //深色还是浅色
MaterialColor primarySwatch, //主题颜色样本,见下面介绍
Color primaryColor, //主色,决定导航栏颜色
Color accentColor, //次级色,决定大多数Widget的颜色,如进度条、开关等。
Color cardColor, //卡片颜色
Color dividerColor, //分割线颜色
ButtonThemeData buttonTheme, //按钮主题
Color cursorColor, //输入框光标颜色
Color dialogBackgroundColor,//对话框背景颜色
String fontFamily, //文字字体
TextTheme textTheme,// 字体主题,包括标题、body等文字样式
IconThemeData iconTheme, // Icon的默认样式
TargetPlatform platform, //指定平台,应用特定平台控件风格
...
})
屏幕适配
在flutter中我们写width: 750,宽度并不是100%,也对不上设计稿的尺寸(750x1334),为了与设计稿的尺寸对应,我们可以使用第三方库来解决这个问题。
屏幕适配插件:flutter_screenutil
地址:https://pub.flutter-io.cn/packages/flutter_screenutil#-readme-tab-
1、安装包
dependencies:
flutter:
sdk: flutter
# add flutter_screenutil
flutter_screenutil: ^1.0.2
2、导入包
import 'package:flutter_screenutil/flutter_screenutil.dart';
3、在build方法中初始化配置,width、height就是设计稿的尺寸
ScreenUtil.init(context, width: 750, height: 1334, allowFontScaling: false);
4、使用时在尺寸后面加上.w就可以了
width: 250.w,
height: 250.w,
自适应宽度高度方法
外层宽度高度已知
width: double.infinity,
height: double.infinity,
外层宽度高度未知
Expanded(
child: child
)
颜色转换
import 'package:flutter/material.dart';
class Utils {
// 颜色转换
static Color hexToColor(String s) {
return Color(
int.parse(s.substring(1, 7), radix: 16) + 0xFF000000
);
}
}
使用:
Utils.hexToColor('#333333')
入场动画
import 'package:flutter/material.dart';
class RouteAnimation extends PageRouteBuilder{
final Widget widget;
RouteAnimation(this.widget)
:super(
// 设置过度时间
transitionDuration: Duration(milliseconds: 350),
// 构造器
pageBuilder:(
// 上下文和动画
BuildContext context,
Animation<double> animaton1,
Animation<double> animaton2,
){
return widget;
},
transitionsBuilder:(
BuildContext context,
Animation<double> animaton1,
Animation<double> animaton2,
Widget child,
){
// 左右滑动动画效果return SlideTransition(
position: Tween<Offset>(
// 设置滑动的 X , Y 轴
begin: Offset(1.0, 0),
end: Offset(0.0,0.0)
).animate(CurvedAnimation(
parent: animaton1,
curve: Curves.fastOutSlowIn
)),
child: child,
);
}
);
}
使用:
Navigator.of(context).push(RouteAnimation(RoutePage()));
自适应宽度高度方法
外层宽度高度已知
width: double.infinity,
height: double.infinity,
外层宽度高度未知
Expanded(
child: child
)
路由
普通路由跳转
Navigator.push( context,MaterialPageRoute(builder: (context) {
return HomePage();
}));
无返回的路由跳转
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => new HomePage()), (route) => route == null
);
StatelessWidget & StatefullWidget 如何选择
其实刚接触flutter的读者应该都会有类似的想法,StatelessWidget & StatefullWidget 我们怎么区别二者,如何选用二者中的某一个呢?
代码分析
场景一:我要在UI上显示一串文字,这串文字从始至终都不需要改变,也不可能会改变,这种场景下就需要选用StatelessWidget
效果图
import 'package:flutter/material.dart';
class TextPage extends StatelessWidget {
@overrideWidget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Hello Flutter"),
),
body: new Center(
child: new Text(
"我从UI被渲染完成之后就这个状态,不可能发生改变",
style: new TextStyle(fontSize: 18.0),
textAlign: TextAlign.center,
),
));
}
}
场景二:UI页上有一个按钮,我每次点击按钮UI页上的Text显示内容加1
这种情况下,我们很清楚的知道当前的UI页是不固定的,换句话说,UI页上的控件可能会在某一个时刻或者某种逻辑状态下改变自身的状态,那这个时候StatelessWidget显然是不能完成这一要求的,我们来用StatefullWidget模拟上场景二的具体实现。
效果图
示例代码
import 'package:flutter/material.dart';
class TextPage extends StatefulWidget {
@overrideState<StatefulWidget> createState() => MyState();
}
class MyState extends State<TextPage> {
var _count = 0;
@overrideWidget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text("Hello StatefulWidget"),
),
body: new Stack(
children: <Widget>[
new Align(
child: new Text(
"当前count值:$_count",
style: new TextStyle(fontSize: 18.0),
textAlign: TextAlign.center,
),
),
new Align(
alignment: new FractionalOffset(0.5, 0.0),
child: new MaterialButton(
color: Colors.blueAccent,
textColor: Colors.white,
onPressed: () {
//重新渲染当前UI页的状态
setState(() {
_count++;
});
},
child: new Text('点我加下方文字自动加1'),
),
),
],
)
);
}
}
总结
通过今天的学习我们从原先死板的UI静态页过渡到了状态可改变的UI绘制,了解到了StatelessWidget和StatefullWidget的区别,并且能根据不同的UI绘制场景合理的选用不同的根Widget,比如我们所要绘制的UI页的状态包括被渲染的内容都是始终不变的,那我们会选用StatelessWidget来完成,如果所绘制的UI可能在未来的某个场景下发生变化我们会选用StatefullWidget来实现。
GridView
GridView一共有5个构造函数:GridView,GridView.builder,GridView.count,GridView.extent和GridView.custom。
来看下GridView构造函数
GridView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required this.gridDelegate,
double cacheExtent,
List<Widget> children = const <Widget>[],
})
常用参数:
crossAxisCount:列数,即一行有几个子元素;
mainAxisSpacing:主轴方向上的空隙间距;
crossAxisSpacing:次轴方向上的空隙间距;
childAspectRatio:子元素的宽高比例。
GridView.count(crossAxisCount: 3,crossAxisSpacing: 10.0,mainAxisSpacing: 20,padding: EdgeInsets.all(5.0),children: <Widget>[Container(color: Colors.green,),Container(color: Colors.green,),Container(color: Colors.green,),Container(color: Colors.green,),Container(color: Colors.green,),Container(color: Colors.green,)],
),
AspectRatio
将子widget的大小指定为某个特定的长宽比
例子:
new AspectRatio(spectRatio: 16.0 / 9.0,child: Swiper(),
)
将子元素宽高比设置为16比9
CachedNetworkImage
缓存图片、图片占位符、淡入淡出图片
例子:
new CachedNetworkImage(
fit: BoxFit.fill, // 图片显示方式
placeholder: new CircularProgressIndicator(), // 图片占位符
imageUrl:
'https://github.com/flutter/website/blob/master/_includes/code/layout/lakes/images/lake.jpg?raw=true',
errorWidget: (context, url, error) => new Icon(Icons.error), // 发生错误时要显示的Widget
),
InkWell
在flutter 开发中用InkWell或者GestureDetector将某个组件包起来,可添加点击事件。
例子:
new Material(
child: new Ink(//INK可以实现装饰容器,实现矩形 设置背景色color: Colors.red,child:new InkWell(
onTap: (){
},
child: new Container(
width: 230.0,
height: 80.0,
),
),
),
),
new Material(
//INK可以实现装饰容器child: new Ink(//用ink圆角矩形// color: Colors.red,decoration: new BoxDecoration(//不能同时”使用Ink的变量color属性以及decoration属性,两个只能存在一个color: Colors.purple,//设置圆角borderRadius: new BorderRadius.all(new Radius.circular(25.0)),
),
child: new InkWell(
//圆角设置,给水波纹也设置同样的圆角//如果这里不设置就会出现矩形的水波纹效果borderRadius: new BorderRadius.circular(25.0), //设置点击事件回调onTap: () {
},
child: new Container(
width: 300.0,
height: 50.0,
//设置child 居中alignment: Alignment(0, 0),child: Text("登录",style: TextStyle(color: Colors.white,fontSize: 16.0),),
),
),
),
),
GestureDetector
负责处理跟用户的简单手势交互,GestureDetector控件没有图像展示,只是检测用户输入的手势,并作出相应的处理,包括点击、双击、长按...
例子:
GestureDetector(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: 200.0,
height: 100.0,
child: Text(_operation,
style: TextStyle(color: Colors.white),
),
),
onTap: () => updateText("Tap"),//点击onDoubleTap: () => updateText("DoubleTap"), //双击onLongPress: () => updateText("LongPress"), //长按
),
StreamBuilder
我们知道,在Dart中 Stream
也是用于接收异步事件数据,和 Future
不同的是,它可以接收多个异步操作的结果,它常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。StreamBuilde
正是用于配合Stream
来展示流上事件(数据)变化的UI组件。
VerticalDivider
圆角
设置所有圆角
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Container(
),
)
顶部圆角
borderRadius: BorderRadius.vertical(top: Radius.circular(20.w)),
borderRadius: BorderRadius.vertical(top: Radius.elliptical(60.w, 60.w)),
底部圆角
borderRadius: BorderRadius.vertical(bottom: Radius.elliptical(60.w, 60.w)),
Column
Column(
crossAxisAlignment: CrossAxisAlignment.start, // 左对齐children: <Widget>[
Text('电子产品走私', style: TextStyle(
color: Color(0xFF333333),
fontSize: 30.w,
fontWeight: FontWeight.bold
)),
Text('申请时间:2019-02-03 12:03:28', style: TextStyle(
color: Color(0xFF666666),
fontSize: 24.w,
)),
Text('申请人:张三', style: TextStyle(
color: Color(0xFF666666),
fontSize: 24.w,
)),
],
),
点击空白处隐藏键盘
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: ()async {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Container(
width: 100.w,
height: 100.w,
color: Colors.red,
),
)
Utils
颜色转换
import 'package:flutter/material.dart';
class Utils {
// 颜色转换
static Color hexToColor(String s) {
return Color(
int.parse(s.substring(1, 7), radix: 16) + 0xFF000000
);
}
}
使用:
Utils.hexToColor('#297CFF')
网络请求
Dio封装
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:material_collection_app/pages/login_page.dart';
import 'dart:async';
import 'dart:convert';
import 'package:material_collection_app/utils/utils.dart';
import 'package:connectivity/connectivity.dart';
/*
* 封装 restful 请求
*
* GET、POST、DELETE、PATCH
* 主要作用为统一处理相关事务:
* - 统一处理请求前缀;
* - 统一打印请求信息;
* - 统一打印响应信息;
* - 统一打印报错信息;
*/
class DioUtils {
static Dio dio;
static const String API_PREFIX = 'http://47.106.35.143:8001';
static const int CONNECT_TIMEOUT = 5000;
static const int RECEIVE_TIMEOUT = 3000;
/// http request methodsstatic const String GET = 'get';static const String POST = 'post';static const String PUT = 'put';static const String PATCH = 'patch';static const String DELETE = 'delete';
/*
* url 请求链接
* params 请求参数
* metthod 请求方式
* onSuccess 成功回调
* onError 失败回调
*/static Future request<T>(String url,{
context,
params,
method,
Function(T t) onSuccess,
Function(String error) onError
}) async {
params = params ?? {};
method = method ?? 'GET';
// 判断网络状态var connectivityResult = await (Connectivity().checkConnectivity());
if (connectivityResult == ConnectivityResult.none) {
Utils.showText(text: '网络连接不可用', align: Alignment(0, 0.5));
}
if (connectivityResult != ConnectivityResult.none) {
/// 请求处理
params.forEach((key, value) {
if (url.indexOf(key) != -1) {
url = url.replaceAll(':$key', value.toString());
}
});
if(Utils.isDebugMode()) {
print('请求地址:【' + method + ' ' + url + '】');
print('请求参数:' + params.toString());
}
Dio dio = createInstance();
//请求结果var result;try {
Response response = await dio.request(url, data: params, options: new Options(method: method));
if(Utils.isDebugMode()) {
print(response.data.toString());
}
result = json.decode(response.data); //对数据进行Json转化
if(result['code'] == 101) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => new LoginPage()), (route) => route == null
);
return;
}
if (response.statusCode == 200) {
if (onSuccess != null) {
onSuccess(result);
}
} else {
throw Exception('statusCode:${response.statusCode}');
}
if(Utils.isDebugMode()) {
print('响应数据:' + response.toString());
}
} on DioError catch (e) {
if(Utils.isDebugMode()) {
print('请求出错:' + e.toString());
}
// 网络连接超时if (e.type == DioErrorType.CONNECT_TIMEOUT) {
Utils.showText(text: '网络连接超时');
}
onError(e.toString());
}
return result;
}
}
/// 创建 dio 实例对象static Dio createInstance() {
if (dio == null) {
/// 全局属性:请求前缀、连接超时时间、响应超时时间var options = BaseOptions(
connectTimeout: CONNECT_TIMEOUT,
receiveTimeout: RECEIVE_TIMEOUT,
responseType: ResponseType.plain,
validateStatus: (status) {
// 不使用http状态码判断状态,使用AdapterInterceptor来处理(适用于标准REST风格)return true;
},
baseUrl: API_PREFIX + '/collect/',
);
dio = new Dio(options);
//拦截器
dio.interceptors.add(InterceptorsWrapper(onRequest: (RequestOptions options) async {
String token = await Utils.getToken();
options.headers.addAll({"Authorization":"Bearer $token"});
// print("开始请求");return options; //continue
}, onResponse: (Response response) {
// print("成功之前");return response; // continue
}, onError: (DioError e) {
// print("错误之前");return e; //continue
}));
}
return dio;
}
/// 清空 dio 对象static clear() {
dio = null;
}
}
缓存封装shared_preferences
import 'package:shared_preferences/shared_preferences.dart';
class StorageUtil {
// 设置布尔的值
static setBoolItem(String key, bool value) async {
final prefs = await SharedPreferences.getInstance();
prefs.setBool(key, value);
}
// 设置double的值
static setDoubleItem(String key, double value) async {
final prefs = await SharedPreferences.getInstance();
prefs.setDouble(key, value);
}
// 设置int的值
static setIntItem(String key, int value) async {
final prefs = await SharedPreferences.getInstance();
prefs.setInt(key, value);
}
// 设置Sting的值
static setStringItem(String key, String value) async {
final prefs = await SharedPreferences.getInstance();
prefs.setString(key, value);
}
// 设置StringList
static setStringListItem(String key, List<String> value) async {
final prefs = await SharedPreferences.getInstance();
prefs.setStringList(key, value);
}
// 获取返回为bool的内容
static getBoolItem(String key) async {
final prefs = await SharedPreferences.getInstance();
bool value = prefs.getBool(key);
return value;
}
// 获取返回为double的内容
static getDoubleItem(String key) async {
final prefs = await SharedPreferences.getInstance();
double value = prefs.getDouble(key);
return value;
}
// 获取返回为int的内容
static getIntItem(String key) async {
final prefs = await SharedPreferences.getInstance();
int value = prefs.getInt(key);
return value;
}
// 获取返回为String的内容
static getStringItem(String key) async {
final prefs = await SharedPreferences.getInstance();
String value = prefs.getString(key);
return value;
}
// 获取返回为StringList的内容
static getStringListItem(String key) async {
final prefs = await SharedPreferences.getInstance();
List<String> value = prefs.getStringList(key);
return value;
}
// 移除单个
static remove(String key) async {
final prefs = await SharedPreferences.getInstance();
prefs.remove(key);
}
// 清空所有的
static clear() async {
final prefs = await SharedPreferences.getInstance();
prefs.clear();
}
}
封装eventBus
在Flutter中实现跨页面事件,我们可以自定义一个事件总线。
步骤
- 定义一个单例类
- 添加一个Map<String ,CallBack>变量,保存监听的事件名和对应的回调方法。
- 添加on方法,用于向map中添加事件。
- 添加off方法,用于从map中移除事件监听。
- 定义emit方法,用于事件源发射事件。
//订阅者回调签名
typedef void EventCallback(arg);
class EventBus {
//私有构造函数
EventBus._internal();
//保存单例static EventBus _singleton = new EventBus._internal();
//工厂构造函数factory EventBus()=> _singleton;
//保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者var _emap = new Map<String, EventCallback>();
//添加订阅者void on(eventName, EventCallback f) {if (eventName == null || f == null) return;
_emap[eventName]=f;
}
//移除订阅者void off(eventName) {
_emap[eventName] = null;
}
//触发事件,事件触发后该事件所有订阅者会被调用void emit(eventName, [arg]) {
var f = _emap[eventName];
f(arg);
}
}
使用:
1、获取事件总线
var eventBus = new EventBus();
2、监听事件
@override
void initState() {
super.initState();
eventBus.on("out_return", (parmas){
setState(() {
list.removeAt(parmas['index']);
});
});
}
3、发送事件
eventBus.emit('out_return', {'index': 1});
4、销毁
@override
void dispose() {
super.dispose();
eventBus.off("out_return");
}
打开生产环境网络访问
android > app > src > main > AndroidManifest.xml
manifest下添加如下代码
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
上传图片
import 'package:image_picker/image_picker.dart';
import 'dart:io';
List uploadList = []; // 上传到线上的图片路径
List locatUploadList = []; // 上传到本地的图片路径
// 拍照
Future getPhoto() async {
Navigator.of(context).pop();final pickedFile = await picker.getImage(source: ImageSource.camera);_upLoadImage(pickedFile, locatUploadList.length-1);
}
// 相册照片
Future getImage() async {
Navigator.of(context).pop();final pickedFile = await picker.getImage(source: ImageSource.gallery);_upLoadImage(pickedFile, locatUploadList.length-1);
}
//上传图片
// index 如果图片上传失败,从本地列表删除图片
_upLoadImage(PickedFile image, int index) async {
if(image != null) {
setState(() {
locatUploadList.add({'url': image.path, 'success': false});
});
String path = image.path;var name = path.substring(path.lastIndexOf("/") + 1, path.length);int index = locatUploadList.length - 1;
FormData formdata = FormData.fromMap({
"file": await MultipartFile.fromFile(path, filename: name)
});
DioUtils.request("upload/upload",
context: context,
method: "POST",
params: formdata,
onSuccess: (res) {
if(res['code'] == 1) {
int locatLength = locatUploadList.length;int successIndex = index == locatLength ? locatLength-1 : index;
setState(() {
locatUploadList[successIndex]['success'] = true;uploadList.add(res['data']['path']);
});
}else {
setState(() {
locatUploadList.removeAt(index);
});Utils.showText(text: res['msg']);
}
},
onError: (error) {}
);
}
}
// 删除图片
_removeImage(int index) {
Utils.openAlert('确定要删除吗?', context).then((confirm) => {
if(confirm) {
setState(() {
uploadList.removeAt(index);locatUploadList.removeAt(index);
})
}
});
}
// 图片列表
Widget _buildImageList() {
List<Widget> widget = [];
for(int i=0; i<locatUploadList.length; i++) {
var item = locatUploadList[i];
widget.add(Container(decoration: BoxDecoration(color: Utils.hexToColor('#F1F3F4'),
borderRadius: BorderRadius.all(Radius.circular(8.w)),
),
child: Stack(children: <Widget>[
ClipRRect(borderRadius: BorderRadius.all(Radius.circular(8.w)),
child: Image.file(File(item['url']), fit: BoxFit.cover, width: 136.w, height: 136.w),
),
Positioned(top: 96.w,
child: GestureDetector(onTap: () {
if(item['success'] == true) {
_removeImage(i);
}
},
child: Container(width: 136.w,
height: 40.w,
alignment: Alignment.center,
decoration: BoxDecoration(color: Color.fromRGBO(0, 0, 0, 0.4),
borderRadius: BorderRadius.vertical(bottom: Radius.elliptical(8.w, 8.w)),
),
child: Text(item['success'] == false ? '上传中...' : '删除图片', style: TextStyle(color: Colors.white,
fontSize: 10)))))
]
)));
}
widget.add(Container(child: InkWell(onTap: () {
selectImage();
},
child: Image.asset('assets/add_img.png', width: 136.w,height: 136.w),
)));
return Wrap(spacing: 25.w,
runSpacing: 25.w,
children: widget);
}
监听键盘事件、物理按键
按照目前的了解flutter中监听物理必须要让键盘获取到焦点,然后才能监听物理按键,这种方式不好的地方就是键盘需要弹起,后面找到好的方法后会更新。
RawKeyboardListener(
focusNode: FocusNode(),// 焦点
onKey: (RawKeyEvent event){
if(event.runtimeType.toString() == 'RawKeyDownEvent'){
RawKeyEventDataAndroid data = event.data as RawKeyEventDataAndroid;
String _keyCode;
_keyCode = data.keyCode.toString(); //keycode of key event (66 is return)
if(_keyCode == '280' || _keyCode == '293' || _keyCode == '139') {
readTagFromBuffer();
}
}
},
child: Column(
children: <Widget>[
ConstrainedBox(
constraints: BoxConstraints(
maxHeight: 0,
),
child: new TextField(
showCursor: false,
autofocus: true,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide.none
),
),
),
),
],
)
)
模仿Android开发者写的插件
这段时间在弄一个手持机RFID扫描,但是厂家提供的sdk只有android可以使用,flutter要想使用的话必须通过android来进行一次转换,对于一个前端开发者来说要使用android sdk是不太可能的,于是我们公司请了一个会android 和 flutter的开发者来帮助我们开发可以符合flutter调用的接口。在最后对接要结束的时候还需要一个转换函数,于是我想能不能自己模仿一下跟我对接的那位大佬写的代码自己完成这个函数,由于之前的几次对接已经知道了要修改的文件,找到文件后看了看代码,搜索一些熟悉的函数,看完代码后就模仿他写的代码,没想到经过多次尝试竟然写好了这个函数,到这里还是挺开心的。
最后总结一下,在没有开始尝试之前都不要说不行,只有在尝试了之后才能知道自己行不行。
点击两次返回键退出APP
在主页面最外层套一个WillPopScope,然后在onWillPop中设置一下时间
DateTime lastPopTime;
// 点击两次返回才退出APP
Future<bool> _onWillPop() async{if(lastPopTime == null || DateTime.now().difference(lastPopTime) > Duration(seconds: 2)){lastPopTime = DateTime.now();Utils.showText(text: ' 再次点击退出 ', radius: 40, align: Alignment(0, 0.8));return false;}else{lastPopTime = DateTime.now();// 退出appawait SystemChannels.platform.invokeMethod('SystemNavigator.pop');return true;}
}
Widget build(BuildContext context) {
return WillPopScope(child: Scaffold(body: Container(height: double.infinity,
color: Utils.hexToColor('#F5F6FA'),
child: SingleChildScrollView(child: Stack(children: <Widget>[
HomeBackground(),
HomeUserInfo(username: username),
HomeNotice(),
HomeDataCard(),
HomeGridCard()
],
),
))),
onWillPop: _onWillPop);
}
申请权限
引入插件
https://pub.flutter-io.cn/packages/permission_handler
dependencies:
permission_handler: ^8.0.0+2
IOS
1、在项目下ios/Runner/Info.plist文件里面加入如下代码
<!-- 相册 -->
<key>NSPhotoLibraryUsageDescription</key>
<string>App需要您的同意,才能访问相册</string>
<!-- 相机 -->
<key>NSCameraUsageDescription</key>
<string>App需要您的同意,才能访问相机</string>
<!-- 麦克风 -->
<key>NSMicrophoneUsageDescription</key>
<string>App需要您的同意,才能访问麦克风</string>
<!-- 位置 -->
<key>NSLocationUsageDescription</key>
<string>App需要您的同意,才能访问位置</string>
<!-- 在使用期间访问位置 -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>App需要您的同意,才能在使用期间访问位置</string>
<!-- 始终访问位置 -->
<key>NSLocationAlwaysUsageDescription</key>
<string>App需要您的同意,才能始终访问位置</string>
<!-- 日历 -->
<key>NSCalendarsUsageDescription</key>
<string>App需要您的同意,才能访问日历</string>
<!-- 提醒事项 -->
<key>NSRemindersUsageDescription</key>
<string>App需要您的同意,才能访问提醒事项</string>
<!-- 运动与健身 -->
<key>NSMotionUsageDescription</key>
<string>App需要您的同意,才能访问运动与健身</string>
<!-- 健康更新 -->
<key>NSHealthUpdateUsageDescription</key>
<string>App需要您的同意,才能访问健康更新 </string>
<!-- 健康分享 -->
<key>NSHealthShareUsageDescription</key>
<string>App需要您的同意,才能访问健康分享</string>
<!-- 蓝牙 -->
<key>NSBluetoothPeripheralUsageDescription</key>
<string>App需要您的同意,才能访问蓝牙</string>
<!-- 媒体资料库 -->
<key>NSAppleMusicUsageDescription</key>
<string>App需要您的同意,才能访问媒体资料库</string>
2、找到项目下ios/Podfile文件添加如下代码
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
# 您可以在此处删除未使用的权限
# 例如,当您不需要相机权限时,只需添加'PERMISSION_CAMERA=0'
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.calendar
'PERMISSION_EVENTS=1',
## dart: PermissionGroup.reminders
'PERMISSION_REMINDERS=1',
## dart: PermissionGroup.contacts
'PERMISSION_CONTACTS=1',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
## dart: PermissionGroup.microphone
'PERMISSION_MICROPHONE=1',
## dart: PermissionGroup.speech
'PERMISSION_SPEECH_RECOGNIZER=1',
## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=1',
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
'PERMISSION_LOCATION=1',
## dart: PermissionGroup.notification
'PERMISSION_NOTIFICATIONS=1',
## dart: PermissionGroup.mediaLibrary
'PERMISSION_MEDIA_LIBRARY=1',
## dart: PermissionGroup.sensors
'PERMISSION_SENSORS=1',
## dart: PermissionGroup.bluetooth
'PERMISSION_BLUETOOTH=1',
## dart: PermissionGroup.appTrackingTransparency
'PERMISSION_APP_TRACKING_TRANSPARENCY=1'
]
end
end
end
3、开启权限
// 请求开启权限(单个)
void requestPermiss() async {
PermissionStatus status = await Permission.notification.request();
// 通知被永久关闭if (status.isPermanentlyDenied) {openAppSettings();
}
}
// 请求开启权限(多个)
void requestPermiss() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.notification,
Permission.photos,
Permission.camera,
Permission.location,
Permission.locationAlways,
Permission.locationWhenInUse,
].request();
// 相机被永久关闭if (statuses[Permission.camera].isPermanentlyDenied) {openAppSettings();
}
// 通知被永久关闭if (statuses[Permission.notification].isPermanentlyDenied) {openAppSettings();
}
// 定位服务被永久关闭if (statuses[Permission.location].isPermanentlyDenied) {openAppSettings();
}
}
自定义组件
class HomeCard extends StatelessWidget {
const HomeCard({
Key key,
this.title,
this.subText: "参数默认值"
}) : super(key: key);
final String title;
final String subText;
@overrideWidget build(BuildContext context) {
return new Container(
width: 100,
height: 100,
color: const Color(0xFF2DBD3A),
child: Column(
children: <Widget>[
Text(title),
Text(subText)
],
),
);
}
}
使用:
HomeCard(title: '参数')
打包APK
1、生成签名文件
keytool -genkey -v -keystore G:\sign.jks -keyalg RSA -keysize 2048 -validity 10000 -alias sign
G:\sign.jks(这个是签名存储的位置以及文件名)
然后把文件放到Flutter工程中/android/app/key/目录下
2、创建key.properties
在Flutter工程中/android/key.properties创建该文件。里面内容如下:
storePassword=123456
keyPassword=123456
keyAlias=sign
storeFile=key/sign.jks
3、配置/android/app/build.gradle文件
def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {signingConfigs {release {keyAlias 'sign'keyPassword 'android'storeFile file('key/sign.jks')storePassword 'android'}}
buildTypes {release {signingConfig signingConfigs.release}}
}
4、打包
flutter build apk
运行到真机
IOS
1、用 Xcode 打开项目目录下的 ios 文件夹
2、配置团队、Bundle Identifier (包标识符,如果是个人账户七天最多设置10个标识符)
3、连接手机,选择设备,然后点击左上角的启动按钮即可
上面的这种运行方式,在断开数据线后应用程序就不能使用了,如果想要断开数据线还可用使用,运行下面的代码
flutter run --release
常见报错
Could not determine the dependencies of task ':app:lintVitalRelease'.
解决方案:
在/android/app/build.gradle文件中添加如下代码
android {
compileSdkVersion 28
lintOptions {
checkReleaseBuilds false
abortOnError false
}
}