基础入门
从一个Flutter项目APP分析技术栈
- 技术栈
携程网App需求分析
Android快速上手
-
组件对比
-
三方库 pub.dev
iOS 快速上手
- 组件对比
入门实战
环境搭建(具体去看官网)
- 配置Flutter镜像
- 配置FlutterSDK
创建项目以及目录分析
jiayuanfa@B-L1B6LVDL-2301 Desktop % flutter create my_app
Signing iOS app for device deployment using developer identity: "Apple Development: jiayuanfa@yeah.net (MHE7BBZRB3)"
Creating project my_app...
Running "flutter pub get" in my_app... 2,001ms
Wrote 127 files.
All done!
In order to run your application, type:
$ cd my_app
$ flutter run
Your application code is in my_app/lib/main.dart.
jiayuanfa@B-L1B6LVDL-2301 Desktop % cd my_app
jiayuanfa@B-L1B6LVDL-2301 my_app % ls
README.md
android
lib
macos
pubspec.lock
test
windows
analysis_options.yaml
ios
linux
my_app.iml
pubspec.yaml
web
jiayuanfa@B-L1B6LVDL-2301 my_app %
运行项目
- 连接设备
flutter run
- 热重载
r
- 自动按照设备列表优先级运行在设备上
安装Flutter 和 Dart 插件
- 前者是为了让Android Studio 识别Flutter项目
- 后者是为了让Android Studio 识别Dart文件代码
- 总结:方便开发、调试
环境问题、工具问题、版本问题
- 使用Flutter Doctor进行问题诊断
- Google 搜索 英文关键词
- VPN与镜像问题:八仙过海,各显神通
空安全(Null Safely)
- 从Flutter 2开始,Flutter便在配置中默认启用了空安全,通过将空检查合并到类型系统中,可以在开发过程中捕获这些错误,从而防止再生产环境导致的崩溃
- 主流语言如Java、Kotlin、Swift都有了这个空安全区保护措施
空安全的好处
- 通过空安全开发人员可以有效避免null错误崩溃
- 可以将原本运行时的空值引用错误将变为编辑时的分析错误
- 增强程序的健壮性,有效避免由Null而导致的崩溃
- 跟随Dart和Flutter的发展趋势,为程序的后续迭代不留坑
空安全代码使用
- 可空(?)类型的使用:可空类型声明
- 延迟初始化(late)的使用:延迟初始化
- 空值断言操作符(!)的使用:知道不为空的情况下处理
空安全适配
- 空安全报错如下
type ‘Null’ is not a subtype of type ‘xxx’
- 导致此问题的主要原因将一个null值传递给了一个不能为null的参数,常见在使用model时,如:
type 'Null' is not a subtype of type 'String'.
type 'Null' is not a subtype of type 'bool'.
late bool isWaterMarked;
//改成
bool? isWaterMarked;
- 三方库运行报错
Error: Cannot run with sound null safety, because the following dependencies
don't support null safety:
- package:flutter_swiper
- package:flutter_page_indicator
- package:transformer_page_view
- 解决方案 First, you should read through the guide to understand unsound null safety. If you are sure that you want to run your application with unsound null safety, you can use the following command:
flutter run --no-sound-null-safety
The --no-sound-null-safety option is not documented in the article, however, I have not experienced any problems with it for the last few months (and especially not since the whole Flutter framework has been migrated to null safety).
The documentation has now been updated to include this. See Testing or running mixed-version programs.
IDE run arguments/configuration
To set this up in your IDE of choice, you can use:
- In IntelliJ/Android Studio: "Edit Configurations" (in your run configurations) → "Additional run args".
- In Visual Studio Code: search for "Flutter run additional args" in your user settings.
In both cases, add --no-sound-null-safety.
Dart
Dart概述
-
区别于强类型语言、静态语言 Java、C#
-
Dart是面向对象语言
JIT&AOT
- JIT:即时编译、开发期间、更快编译、更快重载
- AOT:事前编译、release期间、更快更流畅
常用数据类型
面向对象(OOP)
抽象类
- 不能有自己的实例
函数/方法
泛型
import 'package:flutter/animation.dart';
import 'package:my_app/oop_learn.dart';
class TestGeneric {
void start() {
Cache<String> cache = Cache();
cache.setItem("cache", "111");
String result = cache.getItem("cache") as String;
print(result);
Cache<int> cache2 = Cache();
cache2.setItem("cache2", 222);
int cache2Result = cache2.getItem("cache2") as int;
print(cache2Result);
}
}
/// 泛型
/// 通俗理解:解决类、接口、方法的复用性、以及对不特性数据类型的支持
/// 泛型类
/// 作用:提高代码的复用程度
class Cache<T> {
static final Map<String, Object?> _cached = Map();
/// 泛型方法
void setItem(String key, T value) {
_cached[key] = value as Object;
}
Object? getItem(String key) {
return _cached[key];
}
}
/// 泛型约束
class Member<T extends Person> {
T _person;
Member(this._person);
String fixedName() {
print('fixed:${_person.name}');
return "";
}
}
异步
编程技巧
/// 面向对象的编程技巧
/// 一:封装、继承、多态
/// 模块封装、代码封装、类封装、方法封装、目的:易用、扩展
/// 方法代码<100行
/// 二:点点点的习惯
/// 万物皆对象
/// 一点查看属性、方法
/// 二点看源码
/// 三点探究竟
void main() {
List? list;
print("fage");
/// 安全调用
print(list?.length);
/// 为表达式设置默认值
print(list?.length??-1);
/// 简化判断
list = [];
list.add(0);
list.add('');
list.add(null);
if (list[0] == 0 || list[0] == '' || list[0] == null) {
print('list[0] is empty');
}
if ([null, '', 0].contains(list[0])) {
print('list[0] is empty');
}
}
Flutter基础
Flutter项目创建
- 通过Android Studio创建
- 通过命令行创建
flutter create flutter_name
Flutter项目的运行
- Android Studio运行
- 命令行运行
flutter run
- 如果有多个设备,需要运行到指定设备上,则
flutter run -d '设备名称'
Flutter的包和插件
- 网站搜索插件 pub.dev/packages/fl…
- 项目中依赖
- 下载插件
- 使用
- 导入头文件
import 'package:flutter_color_plugin/flutter_color_plugin.dart';
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children:<Widget>[
Text(
'Flutter插件的使用',
style: TextStyle(color: ColorUtil.color('#899900')),
),
],
),
Flutter组件
StatelessWidget(无状态组件)
StatefulWidget(有状态组件)
- 生命周期
Flutter声明周期
- 重写生命周期方法
- 重写方法的快捷键
command + N
I/flutter (29949): -------initState--------
I/flutter (29949): --------didChangeDependencies-------
I/flutter (29949): -----build------
I/flutter (29949): -----build------
I/flutter (29949): -----build------
I/flutter (29949): -----build------
I/flutter (29949): -----build------
I/flutter (29949): -----build------
I/flutter (29949): -----build------
I/flutter (29949): ---------deactivate---------
I/flutter (29949): ---------dispose---------
Flutter布局
Flutter手势检测
Flutter资源文件的引用
Flutter打开第三方APP
- 使用 url_launch插件
- 注意点:插件代码编写好之后,不起作用,并报错
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: MissingPluginException(No implementation found for method launch on channel plugins.flutter.io/url_launcher)
- 此问题的原因是插件要么没有同步好,要么需要重新启动应用,同步更新插件的命令为
flutter pub outdated
flutter pub upgrade --major-versions
Flutter应用的生命周期
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// 如何获取Flutter应用维度的生命周期
/// WidgetBindingObserver: 是一个Widget绑定观察器,通过它我们可以监听应用的生命周期
class AppLifecycle extends StatefulWidget {
const AppLifecycle({Key? key}) : super(key: key);
@override
State<AppLifecycle> createState() => _AppLifecycleState();
}
class _AppLifecycleState extends State<AppLifecycle> with WidgetsBindingObserver{
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('App 的生命周期'),
leading: BackButton(),
),
body: Container(
child: Text('App 的生命周期'),
),
);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (kDebugMode) {
print('state = $state');
}
switch (state) {
case AppLifecycleState.resumed:
if (kDebugMode) {
print('App进入前台');
}
break;
case AppLifecycleState.inactive:
// 不常用,比如来了个电话
if (kDebugMode) {
print('App变得不活跃');
}
break;
case AppLifecycleState.paused:
if (kDebugMode) {
print('App进入后台');
}
break;
case AppLifecycleState.detached:
if (kDebugMode) {
print('App detached');
}
break;
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
Flutter 主题的修改
Flutter 字体的更换
- 字体下载地址:fonts.google.com/specimen/Ru…
- 更多参考:flutter.dev/docs/cookbo…
- 下载之后拖入项目
- 全局更换字体
theme: ThemeData(
// fontFamily: 'RubikMonoOne', // 全局更换字体
brightness: _brightness,
primarySwatch: Colors.blue,
),
- 局部更换字体
child: const Text(
'切换模式ABC',
style: TextStyle(fontFamily: 'RubikMonoOne')
),
拍照Or相册选择插件
image_picker
- AndroidX的兼容
- iOS info.plist文件相册读取权限的添加,具体字段可以在插件的example demo中对应的文件中获取
图片控件Image分析
-
加载手机内存里边绝对路径的图片
-
加载手机内存中相对路径的图片(通过插件通过相对路径获取绝对路径)
如何加载不同分辨率的图片
Flutter动画
-
Hero动画 github.com/jiayuanfa/F…
-
让动画循环播放的技巧
animation = CurvedAnimation(parent: controller, curve: Curves.easeIn)
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
- 如何解决动画播放的时候动画本身布局的变化影响到其他控件的布局问题
- 添加占位Container
child: Column(
children: [
/// 顶部文字
Padding(
padding: const EdgeInsets.all(10),
child: Text(
speakTips,
style: const TextStyle(color: Colors.blue, fontSize: 12),
),
),
/// 下方按钮动画执行区域
Stack(
children: [
const SizedBox(
//占坑,避免动画执行过程中导致父布局大小变得
height: MIC_SIZE,
width: MIC_SIZE,
),
Center(
child: AnimationMic(
animation,
)),
],
)
],
),
Flutter调试技巧
断点调试
断点查看变量的值的三种方式
- 将光标放在变量上面即可显示
- 通过变量Variables视窗查看
- 通过Watches视窗+号,输入变量的名称查看
变量Variables视窗与Watches视窗
通过Frames回退(调试中的后悔药)
善用控制台(Console)
项目必备问题问答
如何自定义AppBar
Scaffold都有哪些常见的用法?
- 一个页面的子节点
- appBar
- body
- bottomBar
Scaffold+PageView如何跳转到指定tab?
- 使用PageController传入PageView即可实现对PageView内部的StatefulWidgetPage进行控制
NotificationListener除了可以用来监听列表滚动之外,还可以用来做什么?
列表滚动除了实现导航栏的渐变效果之外,还可以实现哪些有意思的效果?
HTTP
常用的有哪些类型的请求?
如何解决请求中的中文乱码问题?
Future与ES6中的Promise有哪些异同?
FutureBuild都可以用来做什么?
JSON解析都有哪些实用方式?
shared_preference在Android与iOS中都是怎么实现的?
在复杂的JSON转模型上你都有哪些心得体会?
ListView
如何自定义一个 LoadMore Widget ?
使用ListView实现水平滚动需要注意哪些点?
如何自定义ExpansionTile的Title?
如何设置GridView每行显示多少列?
- crossAxisCount属性设置
如何为列表添加上拉加载和下拉刷新功能?
- 下拉刷新使用RefreshIndicator组件
- 上拉加载通过给ListView添加一个ScrollController监听ListView的最大滑动距离是否超过屏幕底部
Flutter混合开发
步骤
创建一个Flutter Moudle都有哪些方式?
- 命令行创建
- cd到项目文件夹下面,如果是原生项目,则是原生项目的上一级目录下
- 执行命令 flutter_module 为模块名称
flutter create -t module flutter_module
简述为现在的Android 项目集成Flutter都有哪些步骤?
为现在的iOS项目集成Flutter都有哪些步骤?
- iOS和Flutter项目在同一文件夹
- iOS项目Podfile配置
- iOS项目flutterSDK与flutter_module路径配置
- pod install
纯Flutter项目的调试和混合项目的Flutter调试都有哪些异同?
运行集成了Flutter的Android项目是在Android Studio的Android模式下运行还是Flutter模式下运行?
- Android模式
如何打包一个集成了Flutter的Android项目?有哪些步骤?
简述Flutter与Native通信都有哪几种方式?分别适用于哪些场景?
- BasicChannelPlugin: 基础通信带回调
- MethodChannelPlugin:Flutter调用Native方法
- EventChannelPlugin:不带回调
描述Channel是如何工作的?
- Flutter中初始化插件,监听Native发来的消息
- Native中初始化插件,监听Flutter发来的消息
- Android通过接口
- iOS通过Block回调
Flutter如何调试Native代码?
Native如何调试Flutter代码?
flutter attach -d '设备名字' r R
如何将Flutter作为页面一部分集成到现有页面?
- Android使用FrameLayout,提交其FlutterFragment即可
- iOS部分使用Flutter,创建一个UIView,把FlutterViewController的View设置给它即可
面试题
说一下你对Flutter布局风格的理解
你了解MediaQuery么?说一下它的作用
简述自定义组件的步骤
简述Flutter_webview_plugin的实现原理
如何获取H5URL中的参数
Expanded与FractionallySizedBox的作用?
关于提高实时搜索的效率你有什么见解?
进阶提升
Flutter断点调试技巧
- 第一步:关掉try catch
- 第二步:打开两个红圈里面的,全部勾选
- 这样,代码运行过程中就会在出现问题的地方停住,以方便查看问题
- 定位到以后,去观察者面板查看值
自定义组件的步骤
- 继承StatelessWidget或者StatefulWidget
- 声明构造参数与入参
- 通过@required声明必传参数
- 设置参数默认值
- 不可变的用final修饰
- 重写WidgetBuild
实战开发技巧
ListView
- 没高度处理
Expanded(
flex: 1,
child: xxx
)
- padding处理
MediaQuery.removePadding(
removeTop: true,
context: context,
child: Expanded(
flex: 1,
child: ListView()
)
)
封装
获取屏幕参数
child: Image.network(
model.icon ?? '',
/// 获取屏幕高度的技巧
width: MediaQuery.of(context).size.width / 2 - 10,
height: isBig ? 129 : 80,
),
输入框与键盘处理
- 禁用输入
- 收起软键盘
- 富文本
Flutter插件
Flutter的插件的开发都有哪些流程?
- 插件各平台适配代码
- 此文件声明包的依赖以及说明
命令创建包或者插件
flutter create --org org.devio --template=plugin 项目名字
flutter create --template=package 项目名字
- 创建好的插件可以直接运行
如果让你去选择一个Flutter第三方插件,你会从哪几个方面考虑?
简述Flutter包和插件的异同?
- 包:只包含Dart代码
- 插件:包含Android、iOS、Web等平台插件
开发Or发布一个Flutter包都需要哪些步骤?
- 创建插件Or包项目
- 编写代码 lib文件夹下
- 修改版本号 pubspec.yaml
- 编写文档 readme.md
- 编写版本变更记录 changelog.md
- 处理包的依赖
- 解决包版本冲突
- 发布
- 检查包是否OK
flutter packages pub publish --dry-run
- 发布
flutter packages pub publish
如何甄选插件包?
- 查看一下文档是否健全?
- 发布是否频繁?(社区够不够活跃)
- Github仓库Star多不多,决定了优不优秀
- issues解决率高不高,解决率高说明插件足够优秀
搜索模块、自定义搜索SearchBar设计细节
进阶实战
组件化
- 将项目中独立的模块拆分在单独的package然后通过依赖的方式集成进主项目来实现组件化
瀑布流实现
多切换Tab
- 如何修复Tab空白问题? 拿到数据之后,初始化TabController即可
- Tab页和首页左右滑动冲突问题解决
集成NativeSDK
Android 集成
- SDK混淆文件
- 权限配置
- SDK key secret 配置
-
识别Service等
-
让app主工程依赖该module
等同于:
-
如果以上步骤Sync now报错,就需要App和Module的compileSdk等配置同步,以防止发生编译冲突
-
如何在Android的任意一个Module里边依赖FlutterSDK?
- 参照app中依赖FlutterSDK的方式,给module添加了相同的依赖,但是直接报错
- 解决方案如下:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
// 第一步 配置flutterRoot目录
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
android {
compileSdk 32
defaultConfig {
minSdk 21
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
//flutter {
// source '../..'
//}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'com.google.android.material:material:1.7.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
// 第二步 直接引入jar包
//fix Failed to apply plugin class 'FlutterPlugin'
compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar")
}
主App依赖FlutterSDK与module依赖FlutterSDK会产生冲突,怎么解决呢?
iOS集成SDK
- 首先cd到iOS项目目录,执行pod install 安装三方库
- 然后打开iOS项目,保证能正常运行到真机
- 然后创建Plugin文件夹,在Plugin文件夹下引入包
- 这样,我们就能在Framework目录中看到它了
- 引入资源文件夹,必须使用 create folder references ,完了是一个蓝色的文件夹
- 添加Framework
- 添加语音权限
- 编写SDKManager,也就是SDK的调用代码
- 编写PluginManager,也就是打通Flutter和SDK的代码
- 通过PluginManager注册到AppDelegate即可
create groups 和 create folder references的区别是什么?
- 区别应该就是一个是黄色文件夹,一个是蓝色文件夹。黄色文件夹中的会被编译,使用的使用不用标明其完整路径,蓝色不会被编译,使用之前,则需要引入其完整路径。这么设计的目的难道是为了节省内存,提高编译速度。
Flutter与Native通信除了MethodChannel还有其他什么方式?
在Android的一个Module中引入Flutter模块需要哪些步骤?
Flutter提供了Android哪些平台的so包?
如何解决Android平台打包过程中的so merge冲突
如何注册在项目中添加的Flutter Android插件?
如何注册在项目中添加的Flutter iOS插件?
集成支付或推送等SDK的需求
进阶拓展
启动白屏问题出现的原因?
- Flutter应用在启动的时候会加载FlutterSDK,在背后完成渲染,在这个过程中它是没有内容可以显示的,所以在不同的手机上会出现或长或短的白屏
如何解决Android启动白屏问题?
- 添加一个启动页,也叫欢迎页面
- github.com/crazycodebo…
- 注意安卓透明主题的设置
如何给iOS设置启动屏?
全面屏怎么适配?
- 第一种方法,外面嵌套SafaArea
import 'package:flutter/material.dart';
import 'package:flutter_splash_screen/flutter_splash_screen.dart';
void main() {
runApp(const FullScreenFit());
}
class FullScreenFit extends StatefulWidget {
const FullScreenFit({Key? key}) : super(key: key);
@override
State<FullScreenFit> createState() => _FullScreenFitState();
}
class _FullScreenFitState extends State<FullScreenFit> {
@override
void initState() {
super.initState();
hideScreen();
}
///hide your splash screen
Future<void> hideScreen() async {
Future.delayed(const Duration(milliseconds: 3600), () {
FlutterSplashScreen.hide();
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.white
),
home: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text('顶部', style: TextStyle(fontSize: 25, color: Colors.red),),
Text('底部', style: TextStyle(fontSize: 25, color: Colors.red),)
],
),
),
);
}
}
- 第二种方法,使用Mequery获取padding,并使用
import 'package:flutter/material.dart';
import 'package:flutter_splash_screen/flutter_splash_screen.dart';
void main() {
runApp(const FullScreenFit());
}
class FullScreenFit extends StatefulWidget {
const FullScreenFit({Key? key}) : super(key: key);
@override
State<FullScreenFit> createState() => _FullScreenFitState();
}
class _FullScreenFitState extends State<FullScreenFit> {
@override
void initState() {
super.initState();
hideScreen();
}
///hide your splash screen
Future<void> hideScreen() async {
Future.delayed(const Duration(milliseconds: 3600), () {
FlutterSplashScreen.hide();
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.white
),
home: const ContentPage(),
);
}
}
/// MediaQuery不能直接在MaterialApp使用,否则报错
/// 所以我们创建一个ContentPage来实现对padding的获取
class ContentPage extends StatelessWidget {
const ContentPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final EdgeInsets padding = MediaQuery.of(context).padding;
return Container(
padding: EdgeInsets.fromLTRB(0, padding.top, 0, padding.bottom),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text('顶部', style: TextStyle(fontSize: 25, color: Colors.red),),
Text('底部', style: TextStyle(fontSize: 25, color: Colors.red),)
],
),
);
}
}
折叠屏怎么适配?
浏览器上跨域问题解决
-
转到flutter\bin\cache并删除名为: flutter_tools.*的文件
-
转到flutter\packages\flutter_tools\lib\src\web并打开文件chrome.dart。
-
查找'--disable-extensions‘
-
添加'--disable-web-security‘
Flutter项目优化
代码优化
- 删除冗余代码
- 封装
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class NavigatorUtil {
/// 跳转页面的方法封装
static push(BuildContext context, Widget page) {
Navigator.push(context, MaterialPageRoute(builder: (context) => page));
}
}