flutter自北京时间2018年12月5日在中国发布以来,引起了多方关注。我在2019年6月接触这一新兴事物,主要熟悉了dart语法和flutter开发框架。2020年初,谷歌开启fuchsia系统dogfood测试,标志着dart+flutter+fuchsia的技术栈体系正式形成。如果有志于在移动开发大前端领域砥砺前行,那么势必要对这一技术栈有所了解。
首先,记住两句话:
everything is object in Dart, everything is widget in Flutter.
model是自定义的数据模型,考虑到和json格式的文件或信息流对接,必须要有fromJson和toJson两个基本映射方法。针对一些不需要进行全局共享的数据,我们可以建立单例模式的仓库repository,一个实例对象,独占一份内存,对一个页面负责。
view是视图层,在flutter框架里被定义为stateless和stateful两种,前者是亘古不变的,后者则会接收来自用户操作、网络数据流和其他一切可能的变动信息。后者会绑定一个State类,我们成为状态类,通过provider这一类的包进行状态管理。
按我的浅见,p在这里不是presenter,而是理解为provider,它可以负责后台数据抓取、本地数据增删改查和通知view层进行页面刷新等一系列逻辑操作。它不像view层一样放在台面上,但是确实app制作中十分重要的一环,优雅的provider逻辑设计可以创造出流畅度非凡的app。
在一个页面开发过程中,通过两次push入路有栈,页面来到了UserStatistics,但是这时候我用Provider.of(context)取不到绑在UserPage上的UserProvider实例,报错cant get the ancestor of UserStatistics with the type of UserProvider。
这里主要是widget树的架构理解错误,provider绑在了兄弟节点上,冒泡上寻父节点根本不可能找到UserProvider的实例对象。
所以这里就需要将UserProvider在widget的元素树里上移,这样UserStatistics在冒泡寻求最近父节点UserProvider实例时才能找到。例如用户信息这种全局都可能用到的状态管理类尽量绑在MaterialApp上。
class MyApp extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return MultiProvider(
providers: [
ChangeNotifierProvider.value(value: UserInfoProvider(userID: "default_user_id")),
],
child:
MaterialApp(
debugShowCheckedModeBanner: false,
title: "灵兔",
home: WelcomePage(),
routes: Routes.routeMap,
)
);
}
}
provider状态管理包consumer的妥善利用,减少不必要的rebuild。当页面使用Provider.of方法获取状态管理数据时,每一次notifyListeners被触发,这个页面都会重新构建一次。这就导致在一个路有栈中,当多个页面都涉及provider数据的使用时,一个页面的数据修改会触发多个页面rebuild,可谓牵一发而动全身。这样的重构浪费了时间和内存,严重影响app运行时的流畅度。这里我们就可以使用Consumer将页面中需要用到状态管理数据的子控件包裹起来,在页面构建时只重构Consumer包含的子控件。这就是经典的供应商-消费者模式,惊不惊喜,意不意外?!
根据flutter官方文献,Consumer主要有两个使用场景:
1.找不到context,provider下面子控件就要用到当前这个provider
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Foo(),
child: Text(Provider.of(context).value),
);
}
上面这段代码会抛出ProviderNotFoundException的异常,因为Provider.of方法冒泡查询时的上下文环境是这个provider的祖先环境,差一层完美错过。
这里可以用Consumer控件来进行替换,代码如下:
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Foo(),
child: Consumer(
builder: (_, foo, __) => Text(foo.value),
},
);
}
2.避免整个页面重构,只对部分子控件进行重构
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black)
),
//当notifyListeners的信号传递过来时,
//只更新这个Consumer包含的TextFormField控件
child: Consumer(
builder: (_, userInfo,__)=>TextFormField(
initialValue: userInfo.user.userID,
),
),
),
),
1.Flutter 项目 Your app isn’t using AndroidX错误
在gradle.properties中添加如下代码即可
android.enableJetifier=true
android.useAndroidX=true
2.BottomNavigationBar超过三个元素,背景颜色出现问题
添加 type: BottomNavigationBarType.fixed, 这个属性设置
3.ListView放在column里面无法展示
需要用Expanded控件包含ListView
4.border属性设置
border:Border.fromBorderSide(
BorderSide(
width: 0.1,
color: Colors.grey,
style: BorderStyle.solid
)
),
5.当scaffold的body用bottomTabBar切换页面时,一个scrollController控制两个页面回到顶部
方法是在scaffold页面初始化一个scrollController,然后绑到body对应的list页面元素里
body: _pageList[_tabIndex],
floatingActionButton:
_tabIndex==1 || _tabIndex==2?
FloatingActionButton(
onPressed: (){
scrollCtrl.animateTo(0.0,
duration: Duration(milliseconds: 500),
curve: Curves.decelerate);
},
child: Text("回顶部"),
): null,
class FindProjectsPage extends StatefulWidget{
ScrollController scrollCtrl;
FindProjectsPage({this.scrollCtrl}){}
//...
}
6.stateful widget的状态类如何调用 stateful 实例的成员
用 widget.member,e.g. controller: widget.scrollCtrl
7.本地数据持久化的方法
可以存放在本地sqlite数据库里;
可以存放在sharedPreferences键值对存储器里,sp类似redis。
e.g. 比如用户信息本地保存,sharedPreferences中保存userID : userInfoLocalSavedPath,取数据时根据userInfoLocalSavedPath找到本地保存的json文件(例如Jack/userInfo.json),进行正常的crud操作。
8.json序列化过程中无法生成*.g.dart文件
使用json_annotation和json_serializable包时,model类的part第一部分必须与类型完全一致。比如类名叫TalentModel,则part ‘TalentModel.g.dart’。
9.生成json转dart model类的命令行
flutter packages pub run build_runner build
10.Text文本加下划线
Text(‘给Ta评价’, style: TextStyle(decoration: TextDecoration.underline)),
11.如何隐藏控件
//隐藏控件
new Offstage(
offstage: true, //这里控制
child: Container(color: Colors.blue,height: 100.0,),
),
12.flutter中provider的异步操作在initState中执行
//异步方法入 微任务循环队列
initState() {
super.initState();
Future.microtask(() =>
Provider.of(context).fetchSomething(someValue);
);
}
13.十分不建议图片存文件存sqlite,把文件索引存sqlite里就好。图片存sdcard或者data/
14.页面A有个list,滑动到一定位置,切到页面B,再切回页面A,list原先的滑动位置保留,解决方法如下
想法是在PageState中保持listview的偏移量,当我们滚动listview时,我们只是从notifier获得偏移并通过setter设置它。
然后,当我们重建listview时,我们只需要让我们的主窗口小部件给我们保存的偏移量,并通过ScrollController我们用该偏移量初始化列表。
class StatefulListView extends StatefulWidget {
StatefulListView({Key key, this.getOffsetMethod, this.setOffsetMethod}) : super(key: key);
final GetOffsetMethod getOffsetMethod;
final SetOffsetMethod setOffsetMethod;
@override
_StatefulListViewState createState() => new _StatefulListViewState();
}
class _StatefulListViewState extends State {
ScrollController scrollController;
@override
void initState() {
super.initState();
scrollController = new ScrollController(
initialScrollOffset: widget.getOffsetMethod()
);
}
@override
Widget build(BuildContext context) {
return new NotificationListener(
child: new ListView.builder(
controller: scrollController,
itemCount: 50,
itemBuilder: (BuildContext context, int index) {
return new Text("Data "+index.toString());
},
),
onNotification: (notification) {
if (notification is ScrollNotification) {
widget.setOffsetMethod(notification.metrics.pixels);
}
},
);
}
}
15.flutter开发中为了系统兼容性,文件路径分隔符使用 Platform.pathSeparator
16.flutter中文件特别是json文件的读写操作
//根据路径获取文件指针
Future _getLocalFile({@required String path}) async{
Directory appDocDir=await getApplicationDocumentsDirectory();
String dirPath=appDocDir.path;
File file=new File(dirPath+Platform.pathSeparator+path);
return file;
}
//异步拿到文件指针后读取json文本转map,再转User对象
_getLocalFile(path: userListLocalPath).then((userFile) {
if (userFile == null) return;
//contents是用户所有的列表信息
String contents = userFile.readAsStringSync();
Map userData = Convert.jsonDecode(contents);
user = UserModel.fromJson(userData);
});
/**********************************************************************************
写文件的注意事项
* By default [writeAsStringSync] creates the file for writing and
* truncates the file if it already exists. In order to append the bytes
* to an existing file, pass [FileMode.append] as the optional mode
* parameter.
********************************************************************************/
_getLocalFile(path: userListLocalPath).then((userFile) {
String userInfoFromServer = Convert.jsonEncode(user.toJson());
userFile.writeAsString(userInfoFromServer);
});
17.flutter判断文件是否存在
File txt=File('/data/data/sms.com.smsexample/files/2.txt');
var dir_bool=await txt.exists(); //返回真假
18.FutureBuilder使用方法以及防止重绘
引自 _卓原 大神的总结,memoizer确实惊艳
FutureBuilder使用方法以及防止重绘
19.widget渲染完成后跑的生命周期函数
//元素渲染完成后最后一帧调用且只调用一次
WidgetsBinding.instance.addPostFrameCallback(function);
20.flutter获取插件包的镜像源问题
在国内开发flutter如果不科学上网,需要使用交大的镜像源拉取插件包。
//Shanghai Jiaotong University Linux User Group
FLUTTER_STORAGE_BASE_URL: https://mirrors.sjtug.sjtu.edu.cn/
PUB_HOSTED_URL: https://dart-pub.mirrors.sjtug.sjtu.edu.cn/
上面两个路径添加到环境变量中,然后取flutter sdk的bin/cache/文件夹中删除flutter.bat.lock和lockfile文件,后台干掉正在运行的dart.exe,并重启IDE。
包名 | 用途 |
---|---|
flutter_screenutil: ^1.0.2 | 全款型适配包,多个机型适配就用它 |
flutter_swiper: ^1.1.6 | 轮播图 |
provider: ^4.0.4 | 状态管理包,官方钦定 |
shared_preferences: ^0.5.6 | 键值对存储库,token、用户的json文件地址 |
json_annotation: ^3.0.1 | json序列化,配json_serializable和build_runner使用 |
sqflite: ^1.2.2 | sqlite数据的crud操作包,原生也好用的 |
city_picker: ^0.1.4 | 城市选择器,弹出框大小待调整 |
dio: ^3.0.9 | 对http的restful api再封装,网络层操作包 |
path_provider: ^1.6.5 | 数据本地化操作时必配包 |
web_socket_channel: ^1.1.0 | 即时通讯包,可以做广播、单播和组播 |
flutter_webrtc: ^0.2.6 | 音视频通讯包 |
最后的最后,我想说,我是一个flutter开发爱好者。真心找份工作,如果大家有开发需求的话,请加我(注明来源flutter)微信 cheersAndCherish