一个flutter开发者的创作小结

为啥选择flutter

flutter自北京时间2018年12月5日在中国发布以来,引起了多方关注。我在2019年6月接触这一新兴事物,主要熟悉了dart语法和flutter开发框架。2020年初,谷歌开启fuchsia系统dogfood测试,标志着dart+flutter+fuchsia的技术栈体系正式形成。如果有志于在移动开发大前端领域砥砺前行,那么势必要对这一技术栈有所了解。

flutter开发app效果图

一个flutter开发者的创作小结_第1张图片


一个flutter开发者的创作小结_第2张图片

概念性问题的思考

首先,记住两句话:
everything is object in Dart, everything is widget in Flutter.

1.MVP开发模式

model是自定义的数据模型,考虑到和json格式的文件或信息流对接,必须要有fromJson和toJson两个基本映射方法。针对一些不需要进行全局共享的数据,我们可以建立单例模式的仓库repository,一个实例对象,独占一份内存,对一个页面负责。
view是视图层,在flutter框架里被定义为stateless和stateful两种,前者是亘古不变的,后者则会接收来自用户操作、网络数据流和其他一切可能的变动信息。后者会绑定一个State类,我们成为状态类,通过provider这一类的包进行状态管理。
按我的浅见,p在这里不是presenter,而是理解为provider,它可以负责后台数据抓取、本地数据增删改查和通知view层进行页面刷新等一系列逻辑操作。它不像view层一样放在台面上,但是确实app制作中十分重要的一环,优雅的provider逻辑设计可以创造出流畅度非凡的app。

2.provider状态管理与widget树

一个flutter开发者的创作小结_第3张图片
在一个页面开发过程中,通过两次push入路有栈,页面来到了UserStatistics,但是这时候我用Provider.of(context)取不到绑在UserPage上的UserProvider实例,报错cant get the ancestor of UserStatistics with the type of UserProvider。
这里主要是widget树的架构理解错误,provider绑在了兄弟节点上,冒泡上寻父节点根本不可能找到UserProvider的实例对象。
一个flutter开发者的创作小结_第4张图片
所以这里就需要将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,
            )
    );
  }
}

3.provider中巧用Consumer

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开发者的创作小结_第5张图片

好用的插件包简介

包名 用途
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

你可能感兴趣的:(flutter开发)