学习Flutter也有一阵子了。闲着没事,用了公司一个已经凉凉的App设计图来练手。当然了接口不可能用的了,所以都是些死数据,实现效果可以说是很完美了(得到了设计的认可。。。)。当然自己也是边查边写,也借鉴了许多Github上优秀的Flutter项目。现在开源出来(附带设计图),供大家交流学习。希望多多Star、Fork支持,有问题可以Issue。附上链接:https://github.com/simplezhli/flutter_deer
本篇主要分享一下自己在此项目中遇到的问题及心得,希望对你有所帮助!
1.部件溢出
异常大致如下:
A RenderFlex overflowed by 22 pixels on the bottom.
1
导致的原因就是在水平或者垂直方向上的内容超过了父部件的大小。一般来说我们的页面不存在这样的问题,因为根据页面的设计,事先可以预料到是否超出。不过要注意到有输入法弹出的页面。比如我下面的这个例子:
可以看到底部溢出了22个像素,可能在18:9的手机以上不太会出现这种问题,因为屏幕的高度足够。但是这种16:9的手机可能会暴露出来。解决的方法有两种:
包一层SingleChildScrollView,让你的页面可以滑动起来。
在Scaffold中设置resizeToAvoidBottomInset为false。默认为ture,防止部件被遮挡。如果使用了这个方法,如果底部有输入框,则会造成遮挡。
大家可以根据实际需求选择。
2.输入框的遮挡
页面如下:
底部有输入框,同时“提交”的按钮固定在底部。一开始觉得既然固定在底部,那就使用Stack配合Positioned来实现,然而就导致输入法弹出时,发生遮挡。
上图中,我选中了最后一个输入框,但因为输入法默认都是在输入框的下方弹出,然而上面盖着这个“提交”按钮,发生了遮挡。
最终我的解决方法就是使用Column配合Expanded来实现。修复后如下:
3.SafeArea
一旦有部件固定在顶部或者底部(严谨点的话可以说是在屏幕的四边)。那我我们最好使用SafeArea来包一下。因为Android 和 IOS都有状态栏,甚至IOS还有叫做“HomeIndicator”的横条。所以一不留神就会出现适配问题。
我们在Flutter中常使用的BottomNavigationBar 和 AppBar 其实就在内部处理了此类问题。以 AppBar源码为例:
class _AppBarState extends State<AppBar> {
@override
Widget build(BuildContext context) {
if (widget.primary) {
appBar = SafeArea( // <--- 1
top: true,
child: appBar,
);
}
return Semantics(
container: true,
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: overlayStyle,
child: Material( // <--- 2
color: widget.backgroundColor
?? appBarTheme.color
?? themeData.primaryColor,
child: Semantics(
explicitChildNodes: true,
child: appBar,
),
),
),
);
}
}
所以使用方法为:
Material( // 需要颜色填充到边界区域可以使用
color: Colors.white,
child: SafeArea(
child: Container(),
),
)
还是上面的页面,我们对比一下处理前后的效果:
4.善用Theme
Flutter 在开发中,让人诟病的就是大量的嵌套,而我们只能尽量避免。比如将一些部件、属性进行封装,避免重复的书写。不过封装也讲究使用场景。如果这种样式的部件仅仅只是某一两处使用,封装显得有点小题大做。并且封装的大而全也会增加使用的复杂度。那么这时就可以使用Theme这种办法。
举一个例子,在下图中圈起来的部分有三个按钮,它们的高度相同,文字、圆角大小也相同。如果每一个都去设定这些属性,未免太过麻烦。
这时我们使用Theme去统一修改它们的样式,就会很方便了。
Theme(
data: Theme.of(context).copyWith(
buttonTheme: ButtonThemeData(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
minWidth: 64.0,
height: 30.0,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape:RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.0),
)
),
textTheme: TextTheme(
button: TextStyle(
fontSize: 14.0,
)
)
),
child: Row(
children: <Widget>[
FlatButton(
color: Color(0xFFF6F6F6),
onPressed: (){},
child: Text("联系客户"),
),
......
FlatButton(
color: Color(0xFFF6F6F6),
onPressed: (){},
child: Text("拒单"),
)
],
),
)
同时使用Theme还可以修改许多默认的设置,比如FlatButton的默认宽度为88,高度为36,但是FlatButton中没有直接修改的属性,网上好多的方法都是通过包一层Container去修改,不仅增加的嵌套,有些需求还不能达到。所以善用Theme可以让你省时省力,不过缺点就是你需要去翻翻源码,寻找使用这些Theme的地方。
5.注意平台差异
注意部分组件在Android与IOS平台之间的差异。
Scaffold的 AppBar,AppBar中默认的title在Android中靠左显示,IOS中居中显示。如果需要两个平台效果统一,需要设置在AppBar中主动设置centerTitle属性。同时AppBar的返回箭头图标也不相同,统一的话需要自定义leading。
页面跳转如果使用MaterialPageRoute来做过渡效果,注意Android中新的页面会从屏幕底部滑动到屏幕顶部,IOS中新的页面会从屏幕右侧滑动到屏幕左侧。
如果需要两个平台效果统一,我们不使用自带效果,可以自定义一个。
Navigator.push(context, PageRouteBuilder(transitionDuration: Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation){
return new FadeTransition( //使用渐隐渐入过渡,
opacity: animation,
child: TestPage(),
);
})
);
要么修改Theme,统一两平台的实现。:
class MyApp extends StatelessWidget {
static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(),
};
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: _defaultBuilders
)
),
...
);
}
}
ScrollPhysics效果,可以滑动的部件都有一个physics属性。滑动到边界时,Android平台为边缘阴影的效果ClampingScrollPhysics,IOS为回弹效果BouncingScrollPhysics。如果需要统一,可以指定physics属性。
状态栏方面,Android平台默认是半透明的效果,IOS则是透明效果。比如Android要实现IOS的效果,可以设置状态栏为透明。不过IOS要实现Android的效果则不行。。。,难道只能自定义?有知道方法的可以分享一下。
void main(){
runApp(MyApp());
// 透明状态栏
if (Platform.isAndroid) {
SystemUiOverlayStyle systemUiOverlayStyle =
SystemUiOverlayStyle(statusBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
}
}
输入键盘
当TextField的keyboardType属性设置为TextInputType.phone 或TextInputType.number时,IOS系统弹出的数字输入键盘没有”完成”按钮,导致输入法无法关闭。当然了Android不存在这个问题。
比较成熟有效的方案是在键盘弹出的上方悬浮一个按钮,点击可以关闭键盘。当然了,这种问题也有对应的库可以解决,我使用的是flutter_keyboard_actions来解决了这个问题。因为在Android端我发现了部分输入法的兼容问题,所以只针对IOS做了处理。大家可以看一下前后对比图,具体实现代码可以参考flutter_keyboard_actions的文档和我的项目代码:
当然平台差异不仅仅是这么多,比如IOS自带侧滑返回等。具体我们可以去查看调用TargetPlatform枚举类的代码。
如果你觉得这样真麻烦,我给你支个大招,修改ThemeData的platform,指定一个平台。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
platform: TargetPlatform.android
),
...
);
}
}
其次就是使用TextInputType.number在IOS中弹起的键盘没有小数点符号。在输入金额类型数据时,需要将keyboardType属性设置为TextInputType.numberWithOptions(decimal: true)。
6.keyboardType
keyboardType属性主要含义为弹起的键盘类型,并不代表输入数据的类型。
而在Android开发中,在EditText中设置android:inputType不仅可以指定弹起的键盘类型,同时也确定了输入数据的类型,也就是内置了数据的格式校验。Flutter中并没有后者,所以可能一开始你是TextInputType.number,但是在输入法中切换成中文键盘,一样可以输入中文字符。所以数据的校验需要我们使用inputFormatters自己处理。
比如TextInputType.phone时可以使用WhitelistingTextInputFormatter 白名单校验,只允许输入0~9:
TextField(
keyboardType: TextInputType.phone,
inputFormatters: [WhitelistingTextInputFormatter(RegExp("[0-9]"))]
)
输入密码时可以使用BlacklistingTextInputFormatter 黑名单校验,除去中文字符:
TextField(
keyboardType: TextInputType.text,
inputFormatters: [BlacklistingTextInputFormatter(RegExp("[\u4e00-\u9fa5]"))]
)
输入小数时,可以自定义TextInputFormatter来限制输入小数格式:
TextField(
keyboardType: TextInputType.numberWithOptions(decimal: true),
inputFormatters: [UsNumberTextInputFormatter()]
)
//来源:https://www.cnblogs.com/yangyxd/p/9639588.html
class UsNumberTextInputFormatter extends TextInputFormatter {
static const defaultDouble = 0.001;
static double strToFloat(String str, [double defaultValue = defaultDouble]) {
try {
return double.parse(str);
} catch (e) {
return defaultValue;
}
}
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
String value = newValue.text;
int selectionIndex = newValue.selection.end;
if (value == ".") {
value = "0.";
selectionIndex++;
} else if (value != "" && value != defaultDouble.toString() && strToFloat(value, defaultDouble) == defaultDouble) {
value = oldValue.text;
selectionIndex = oldValue.selection.end;
}
return new TextEditingValue(
text: value,
selection: new TextSelection.collapsed(offset: selectionIndex),
);
}
}
7.InkWell
InkWell有的叫溅墨效果,有的叫水波纹效果。使用场景是给一些无点击事件的部件添加点击事件时使用(也支持长按、双击等事件),同时你也可以去修改它的颜色和形状。
InkWell(
borderRadius: BorderRadius.circular(8.0), // 圆角
splashColor: Colors.transparent, // 溅墨色(波纹色)
highlightColor: Colors.transparent, // 点击时的背景色(高亮色)
onTap: () {},// 点击事件
child: Container(),
);
不过有时你会发现并不是包一层InkWell就一定会有溅墨效果。主要原因是溅墨效果是在一个背景效果,并不是覆盖的前景效果。所以InkWell中的child一旦有设置背景图或背景色,那么就会遮住这个溅墨效果。如果你需要这个溅墨效果,有两种方式实现。
包一层 Material,将背景色设置在 Material中的color里。
Material(
color: Colors.white,
child: InkWell(),
)
使用Stack布局,将InkWell放置在上层。这种适用于给图片添加点击效果,比如Banner图的点击。
Stack(
children: <Widget>[
Positioned.fill(
child: Image(),
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
splashColor: Color(0X40FFFFFF),
highlightColor: Colors.transparent,
onTap: () {},
),
),
)
],
)
8.保持页面状态
比如点击导航栏来回切换页面,默认情况下会丢失原页面状态,也就是每次切换都会重新初始化页面。这种情况解决方法就是PageView与BottomNavigationBar结合使用,同时子页面State中继承AutomaticKeepAliveClientMixin并重写wantKeepAlive为true。代码大致如下:
class _TestState extends State<Test> with AutomaticKeepAliveClientMixin{
@override
Widget build(BuildContext context) {
super.build(context);
return Container();
}
@override
bool get wantKeepAlive => true;
}
详细的可以看这篇文章:Flutter 三种方式实现页面切换后保持原页面状态
9.依赖版本问题
首先这里建议凡是Flutter的插件在填写版本号时不要使用^符号。
^符号意味着你可以使用此插件的最新版本(大于等于当前版本)。这会导致什么问题呢?可能你前一天代码还能跑起来,今天就编译出错了。因为这些插件中包括Android、IOS的所用依赖环境配置,常见的就是新版本使用了AndroidX的依赖,但是还有些插件并没有使用AndroidX,导致了两者的冲突。
我之前在看flutter-go的代码时,就是因为webview的插件突然升级了,导致了安装失败。具体问题可以看这里。所以在代码稳定的情况下不建议使用^符号。
发生了这种问题,有以下几个解决方法:
使用非AndroidX的版本插件。(优点就是见效快。缺点就是此插件后续的更新无法使用)
手动修改插件的冲突,因为Flutter插件的代码是可以直接修改的,所以你可以手动修改掉这些冲突,统一插件的版本(优点就是可以使用最新的版本。缺点就是这种方法首先麻烦,其次不利于团队开发使用)
我偏好使用第二种,只要做好修改的相关记录就行,算是一劳永逸。
10.Flutter Android 打包
打包本身流程没有问题,配置好签名文件,执行flutter build apk命令。但是发现打包后没有将插件中的AndroidManifest.xml文件合并。比如我有使用image_picker插件,它的AndroidManifest.xml文件如下:
可以看到有权限的及Android 7.0FileProvider的声明。诸如此类的信息没有打包进去(但是引用xml中的flutter_image_picker_file_paths文件却在),导致我实际使用这些功能时没有反应,但是在平时的调试过程中却是好的。
中间我发现打包后的App名称也是之前的,怀疑是缓存问题,所以我手动删除了项目根目录的build与.gradle文件夹,重新打包就好了。所以打包后最好检查一下AndroidManifest.xml文件,避免此类缓存造成的问题。
11.其他
Container 功能强大,设置宽高、padding、margin、背景色、背景图、圆角、阴影等都可以使用它。
有些widget 自带padding 属性,所以不必多套一层Padding部件。(比如ListView、GridView、Container、ScrollView、Button )
尽量使用const来定义常量。比如padding、color、style 这些地方:
class Colours {
static const Color text_dark = Color(0xFF333333);
}
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Test",
style: TextStyle(
fontSize: 26.0,
color: Colours.text_dark
)
)
)
Dart2中的new 关键字可选,所以就不要选了,哈哈!!
紧接下一篇:Flutter开发中的一些Tips(二)
其实我在这中间遇到的小问题还有很多,有的暂时还没有找到好的方法去解决。不过这才刚刚开始,希望Flutter越来越好。
Flutter开发中的一些Tips(二)
1. setState() called after dispose()
这个是我偶然在控制台发现的,完整的错误信息如下:
Unhandled Exception: setState() called after dispose(): _AboutState#9c33a(lifecycle state: defunct, not mounted)
当然flutter在错误信息之后还有给出问题原因及解决方法:
This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the “mounted” property of this object before calling setState() to ensure the object is still in the tree.
This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose().
大致的意思是,widget已经在dispose方法时销毁了,但在这之后却调用了setState方法,那么会发生此错误。比如定时器或动画回调调用setState(),但此时页面已关闭时,就会发生此错误。这个错误一般并不会程序崩溃,只是会造成内存的泄露。
那么解决的办法分为两部分:
及时停止或者销毁监听,例如一个定时器:
Timer _countdownTimer;
@override
void dispose() {
_countdownTimer?.cancel();
_countdownTimer = null;
super.dispose();
}
为了保险我们还要在调用setState()前判断当前页面是否存在:
_countdownTimer = Timer.periodic(Duration(seconds: 2), (timer) {
if (mounted){
setState(() {
});
}
});
我们可以看看 mounted在源码中是什么
BuildContext get context => _element;
StatefulElement _element;
/// Whether this [State] object is currently in a tree.
///
/// After creating a [State] object and before calling [initState], the
/// framework “mounts” the [State] object by associating it with a
/// [BuildContext]. The [State] object remains mounted until the framework
/// calls [dispose], after which time the framework will never ask the [State]
/// object to [build] again.
///
/// It is an error to call [setState] unless [mounted] is true.
bool get mounted => _element != null;
BuildContext是Element的抽象类,你可以认为mounted 就是 context 是否存在。那么同样在回调中用到 context时,也需要判断一下mounted。比如我们要弹出一个 Dialog 时,或者在请求接口成功时退出当前页面。BuildContext的概念是比较重要,需要掌握它,错误使用一般虽不会崩溃,但是会使得代码无效。
本问题详细的代码见:点击查看
2.监听Dialog的关闭
问题描述:我在每次的接口请求前都会弹出一个Dialog 做loading提示,当接口请求成功或者失败时关闭它。可是如果在请求中,我们点击了返回键人为的关闭了它,那么当真正请求成功或者失败关闭它时,由于我们调用了Navigator.pop(context) 导致我们错误的关闭了当前页面。
那么解决问题的突破口就是知道何时Dialog的关闭,那么就可以使用 WillPopScope 拦截到返回键的输入,同时记录到Dialog的关闭。
bool _isShowDialog = false;
void closeDialog() {
if (mounted && _isShowDialog){
_isShowDialog = false;
Navigator.pop(context);
}
}
void showDialog() {
/// 避免重复弹出
if (mounted && !_isShowDialog){
_isShowDialog = true;
showDialog(
context: context,
barrierDismissible: false,
builder:(_) {
return WillPopScope(
onWillPop: () async {
// 拦截到返回键,证明dialog被手动关闭
_isShowDialog = false;
return Future.value(true);
},
child: ProgressDialog(hintText: "正在加载..."),
);
}
);
}
}
本问题详细的代码见:点击查看
3.addPostFrameCallback
addPostFrameCallback回调方法在Widget渲染完成时触发,所以一般我们在获取页面中的Widget大小、位置时使用到。
前面第二点我有说到我会在接口请求前弹出loading。如果我将请求方法放在了initState方法中,异常如下:
inheritFromWidgetOfExactType(_InheritedTheme) or inheritFromElement() was called before initState() completed.
When an inherited widget changes, for example if the value of Theme.of() changes, its dependent widgets are rebuilt. If the dependent widget’s reference to the inherited widget is in a constructor or an initState() method, then the rebuilt dependent widget will not reflect the changes in the inherited widget.
Typically references to inherited widgets should occur in widget build() methods. Alternatively, initialization based on inherited widgets can be placed in the didChangeDependencies method, which is called after initState and whenever the dependencies change thereafter.
原因:弹出一个DIalog的showDialog方法会调用Theme.of(context, shadowThemeOnly: true),而这个方法会通过inheritFromWidgetOfExactType来跨组件获取Theme对象。
inheritFromWidgetOfExactType方法调用inheritFromElement:
但是在_StateLifecycle为created 和 defunct 时是无法跨组件拿到数据的,也就是initState()时和dispose()后。所以错误信息提示我们在 didChangeDependencies 调用。
然而放在didChangeDependencies后,新的异常:
setState() or markNeedsBuild() called during build.
This Overlay widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
提示我们必须在页面build时,才可以去创建这个新的组件(这里就是Dialog)。
所以解决方法就是使用addPostFrameCallback回调方法,等待页面build完成后在请求数据:
@override
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_){
/// 接口请求
});
}
导致这类问题的场景很多,但是大体解决思路就是上述的办法。
本问题详细的代码见:点击查看
4.删除emoji
不多哔哔,直接看图:
简单说就是删除一个emoji表情,一般需要点击删除两次。碰到个别的emoji,需要删除11次!!其实这问题,也别吐槽Flutter,基本emoji在各个平台上都或多或少有点问题。
原因就是:
这个问题我发现在Flutter 的1.5.4+hotfix.2版本,解决方法可以参考:https://github.com/flutter/engine/pull/7555 虽然只适用于长度为2位的emoji。
幸运的是在最新的稳定版1.7.8+hotfix.3中修复了这个问题。不幸的是我发现了其他的问题,比如在我小米MIX 2s上删除文字时,有时会程序崩溃,其他一些机型正常。异常如下图:
我也在Flutter上发现了同样的问题Issue,具体情况可以关注这个Issue :https://github.com/flutter/flutter/issues/33642 ,据Flutter团队的人员的回复,这个问题修复后不太可能进入1.7的稳定版本。。
所以建议大家谨慎升级,尤其是用于生产环境。那么这个问题暂时只能搁置下来了,等待更稳定的版本。。。
19.07.20更新,官方发布了1.7.8+hotfix.4,修复了此问题。经过测试问题修复,大家可以放心使用了。
5.键盘
1.是否弹起
MediaQuery.of(context).viewInsets.bottom > 0
viewInsets.bottom就是键盘的顶部距离底部的高度,也就是弹起的键盘高度。如果你想实时过去键盘的弹出状态,配合使用didChangeMetrics。完整如下:
import 'package:flutter/material.dart';
typedef KeyboardShowCallback = void Function(bool isKeyboardShowing);
class KeyboardDetector extends StatefulWidget {
KeyboardShowCallback keyboardShowCallback;
Widget content;
KeyboardDetector({this.keyboardShowCallback, @required this.content});
@override
_KeyboardDetectorState createState() => _KeyboardDetectorState();
}
class _KeyboardDetectorState extends State<KeyboardDetector>
with WidgetsBindingObserver {
@override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
@override
void didChangeMetrics() {
super.didChangeMetrics();
WidgetsBinding.instance.addPostFrameCallback((_) {
print(MediaQuery.of(context).viewInsets.bottom);
setState(() {
widget.keyboardShowCallback
?.call(MediaQuery.of(context).viewInsets.bottom > 0);
});
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.content;
}
}
代码来自项目GSYFlutterDemo:https://github.com/CarGuo/GSYFlutterDemo
2.弹出键盘
if (MediaQuery.of(context).viewInsets.bottom == 0){
final focusScope = FocusScope.of(context);
focusScope.requestFocus(FocusNode());
Future.delayed(Duration.zero, () => focusScope.requestFocus(_focusNode));
}
其中_focusNode是对应的TextField的focusNode属性。
3.关闭键盘
FocusScope.of(context).requestFocus(FocusNode());
这里提一下关闭,一般来说即使键盘弹出,点击返回页面关闭,键盘就会自动收起。但是顺序是:
页面关闭 –> 键盘关闭
这样会导致键盘短暂的出现在你的上一页面,也就会出现短暂的部件溢出(关于溢出可见上篇)。
所以这时你就需要在页面关闭前手动调用关闭键盘的代码了。按道理是要放到deactivate或者dispose中处理的,可谁让context已经为null了,所以,老办法,拦截返回键:
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// 拦截返回键
FocusScope.of(context).requestFocus(FocusNode());
return Future.value(true);
},
child: Container()
);
}
当然如果你是自己代码调用的返回,可以在返回页面之前关闭键盘:
FocusScope.of(context).requestFocus(new FocusNode());
Navigator.pop(context);
本问题详细的代码见:点击查看
6.Android 9.0适配
话说现在新建的Flutter项目,Android的 targetSdkVersion 默认都是28。所以不可避免的就是Android 9.0的适配甚至6,7,8的适配,那我碰到的一个问题是接入的高德2D地图在9.0的机子上显示不出来。
问题的主要原因是Android 9.0 要求默认使用加密连接,简单地说就是不允许使用http请求,要求使用https。高德的2D地图sdk怀疑是使用了http请求,所以会加载不出。
解决方法两个:
targetSdkVersion 改为28以下(长远看来不推荐)
android -> app – > src -> main -> res 目录下新建xml,添加network_security_config.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
AndroidManifest.xml中的application添加:
android:networkSecurityConfig="@xml/network_security_config"
这个问题只是Android适配中的一小部分,相应的iOS中也有适配问题。比如常用的权限适配等。
不得不说做Flutter的开发需要对原生开发有一定了解。尤其是之前在写Flutter的地图插件时感受深刻,那么我本来就是做Android开发的,所以Android端的部分很快就完成了。iOS部分就很吃力,首先OC的语法就不会,其次说实话写完了心里也没底,还是需要向iOS的同事请教确保一下。所以跨平台方案的出现并不会对原生开发造成冲击,反而是对原生开发提出了更高的要求。
本问题详细的代码见:点击查看
7.其他
Flutter开发中的json解析确实很麻烦,当然有许多的插件来解决我们的问题。我个人推荐使用FlutterJsonBeanFactory。关于它的一系列使用可以参看:https://www.jianshu.com/nb/33360539
UI层面的功能最好还是使用Flutter来解决。比如Toast功能,很多人都会选择fluttertoast这个插件,而我推荐oktoast这类使用Flutter的解决方案 。因为fluttertoast是调用了Android原生的Toast,首先在各个系统上的样式就不统一,同时部分系统机型上受限通知权限,会导致Toast无法弹出。
篇幅有限,那么先分享以上几条Tips,如果本篇对你有所帮助,可以点赞支持!其实收藏起来不是以后遇到问题时查找更方便吗?。
最后再次奉上Github地址:https://github.com/simplezhli/flutter_deer
———————
作者:唯鹿
来源:CSDN
原文:https://blog.csdn.net/qq_17766199/article/details/94849020
版权声明:本文为博主原创文章,转载请附上博文链接!
热文推荐:
2、面试官问我:一个 TCP 连接可以发多少个 HTTP 请求?我竟然回答不上来…
3、程序员疑似出bug被吊打!菲律宾的高薪工作机会了解一下?
4、“一键脱衣”的DeepNude下架后,我在GitHub上找到它涉及的技术
8、腾讯新开源一吊炸天神器—零反射全动态Android插件框架正式开源
喜欢 就关注吧,欢迎投稿!
本网站文章均为原创内容,并可随意转载,但请标明本文链接
如有任何疑问可在文章底部留言。为了防止恶意评论,本博客现已开启留言审核功能。但是博主会在后台第一时间看到您的留言,并会在第一时间对您的留言进行回复!欢迎交流!
本文链接: https://leetcode.jp/flutter开发中的一些tips/