Flutter飞起Flutter面试Flutter

[Flutter] Flutter 快速上手指南

2021-06-05  本文已影响0人  沉江小鱼

前言:学习 Flutter 有一段时间了,本篇文章主要是记录下 Flutter 学习历程的一些心得和开发体验,罗列出一些重要的点,需要结合官方文档食用,希望能够帮助到大家,共同学习进步。

目录:
0.学习资料汇总
1.对 Flutter 的初步认识
2.环境搭建&项目结构
3.Dart语言
4.直接撸一个登录页面?or 先学一些基础的控件?
5.页面绘制 - 熟能生巧
6.项目中功能点实现(网络请求、数据共享、数据存储、路由跳转)
7.总结

0. 学习资料汇总

网站 介绍
Flutter 中文网 资料很全,对于不同平台的开发者了解入门 Flutter 很有帮助
Flutter 实战 系统化的 Flutter 开发教程,由浅入深
[Flutter SDK源码] 项目中 command + 单击就可以查看,注释很清楚,组件都带有使用示例
Dart 语言学习 Dart官方教程
咸鱼技术 咸鱼团队掘金地址,Flutter的进阶使用以及原理探索
[Gallery源码] Flutter 官方示例 APP
flutter-go Flutter开发者最强辅助 App(阿里开源),包含常用Widget 的介绍和使用示例(目前不再维护,但仍可作为参考)

1. 对 Flutter 的初步认识

提起Flutter,不得不说一下跨平台开发,先来了解一下跨平台开发的一些知识。

1.1 跨平台技术

根据其原理,大致分为三类:

  1. H5 + 原生混合开发(Cordova、 lonic、微信小程序)
  1. JavaScript 开发 + 原生渲染(React Native、Weex)
  1. 自绘 UI + 原生(Flutter、QT for mobile)
截屏2021-06-05 下午9.23.51.png

补充知识:AOT & JIT
程序主要有两种运行方式:静态编译动态解释

上面👆内容详细可见:Flutter实现:跨平台技术简介

1.2 Flutter 框架介绍

官方提供的Flutter框架的分层结构图如下:


image.png
1.2.1 Flutter Framework

一般来说,我们开发接触最多的也就是最上面两层了。

1.2.2 Flutter Engine

Flutter 的底层图像渲染引擎是 Skia,Skia 是 Android 官方的图像渲染引擎,支持跨平台,使用同一个渲染引擎保证了在不同平台上的渲染效果, 但是Flutter iOS SDK则需要嵌入 Skia,导致 Flutter iOS SDK 打包的包体积要比 Android 大一些。

1.2.3 Embedder

从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。

1.3 为什么选择 Flutter?

因为 Boss 直聘上看到很多岗位要求中,Flutter 都作为了加分项,另外,作为一名称职码农,多一门手艺也能多混一碗饭吃。😺

但是实际上我是被 Flutter 的优点所吸引的,主要是以下两点:

  1. 跨平台
    保持两个端(Web 端的开发还没有体验过,暂且不提)的视觉效果一致,另一方面方便资本主义更好的剥削我们,之前需要 Android&iOS两个端的开发成本,现在减半了;

  2. 开发效率高
    Debug 模式基于 JIT,热重载(Hot Reload)多🐂🍺,不用重新编译就能看到改动后的效果;
    Release 模式基于AOT,发布时通过 AOT 生成高效的本地代码,保证应用性能;

  3. 响应式框架
    相比于 Vue 这个框架来说,略有不及,但比原生好的太多了。

这些概念在Flutter官网以及一些初识Flutter 博客上都有介绍(毕竟要吹嘘一番),对这些概念有了解之后,才能更好的向身边其他人推广Flutter。

2. 环境搭建&项目结构

2.1 环境搭建

Flutter 开发最好使用 Mac book,因为需要支持 iOS 开发。

环境搭建基本流程:

具体操作可见Flutter实战:安装 Flutter

2.2 项目结构认识

以 VS Code 为例,如下图:


截屏2021-06-05 下午10.31.09.png
2.2.1 项目依赖库管理

可以在 pub.dev 上面搜索到支持使用的依赖包,然后在 pubspec.yaml 文件中进行配置,如下所示:
pubspec.yaml 文件如下:

image.png

当有添加或者删除文件配置时,需要执行(flutter pub get),在 VS Code中 Command +S 保存时,默认去执行了,如果使用 Android Studio 的话,右上角会有pub get的按钮,点击即可。

2.2.2 图片资源配置

在根目录创建一个文件夹,将所需要的图片资源放到该文件夹下,然后将图片路径写到 pubspec.yaml 文件中,在 Image 控件中填写路径即可。具体可以查看:Flutter 中文网:在 Flutter 中添加资源和图片

这里有一个注意的点,可以不用把每个图片的详细路径写到pubspec.yaml 文件中,直接写文件夹就可以,如下图:

image.png
2.2.3 程序的启动入口:lib/main.dart

Flutter中 main()函数也是应用程序的起点,在"lib/main.dart"文件中,实现很简单:

void main() {
  runApp(MyApp());
}

MyApp()可以理解为一个控件或者说一个页面视图,runApp 方法就是将这个控件显示到屏幕上,这个控件就是视图树的根节点。

应用的入口介绍详细可以看:Flutter实战:计数器应用示例

3. Dart 语言

大概了解了项目接口之后,我们先别急着去写一些布局,在此之前我们需要先学习 Dart 语言,这非常重要。

Dart 语言和Java & JavaScript 在某些方面很相似,集百家之长吧,变量的声明&类型&使用,函数的使用,类的使用都很简单,需要重点学习的有:

Dart 语言的学习,推荐Dart中文网中学习,很全,推荐先过一遍,太过复杂的可以先略过,用到的时候再去学习。

4. 直接撸一个登录页面?or 先学一些基础的控件?

相信你肯定忍不住去先学习了一些基础的控件,这是对的,希望你能够继续认真了解一些基础控件,这样能够帮助你顺利的撸一个登录页面出来。

在 Flutter 开发中,万物皆 Widget,比如加个视图(页面)、点击事件、加个背景色、改个位置等,都得需要相应的Widget,实例如下:

// 可点击的文本
Widget testWidget() {
    return GestureDetector( // 给非 button 组件增加点击事件
      onTap: () {
        print("点击了按钮");
      },
      child: Padding( // 设置 padding
        padding: EdgeInsets.all(10),
        child: Text("文本显示"),
      ),
    );
  }

如果没有前端开发经验的小伙伴们最好先在网上了解一下Margin(组件之间的距离)和Padding(组件内部内容的边距)的作用,网上搜了这篇文章:CSS 彻底理解margin与padding,理解Margin 和 Padding的意思即可。有意思的是 Flutter 提供了 Padding 组件,并没有提供 Margin 组件,我都是使用 Container(相当于多个组件的结合体)。

控件(Widget) 是一个抽象类,我们可以简单把它理解为 视图(View)来使用,在实际开发中我们自定义控件并不直接继承 Widget ,一般继承StatelessWidget(无状态组件)或者StatefulWidget(有状态组件),这里的状态可以理解为组件内的数据,它们也是抽象类,继承 Widget。

StatelessWidget(无状态组件) 和 StatefulWidget(有状态组件)最大的区别在于:

这里的build 方法是上面两个组件的共同点,我们自定义控件的时候,也是重写build 方法来构建控件,示例代码:

# StatelessWidget:
class Echo extends StatelessWidget {
  const Echo({
    Key key,  
    @required this.text,
    this.backgroundColor:Colors.grey,
  }):super(key:key);
    
  final String text;
  final Color backgroundColor;

  @override
// 重写 build 方法构建控件
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

# StatefulWidget
class CounterWidget extends StatefulWidget {
  const CounterWidget({
    Key key,
    this.initValue: 0
  });

  final int initValue;

  @override
  // createState 方法中,返回了对应的 State 类的实例
  _CounterWidgetState createState() => new _CounterWidgetState();
}

// 和 StatefulWidget 对应的 State 类
class _CounterWidgetState extends State<CounterWidget> {  
  int _counter;

  @override
  void initState() {
    super.initState();
    //初始化状态  
    _counter=widget.initValue;
    print("initState");
  }

  @override
  // 重写 build 方法,StatefulWidget的 build 方法在 State 类中
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: FlatButton(
          child: Text('$_counter'),
          //点击后计数器自增,相当于状态改变了,这里调用 setState方法,之后会调用 build 方法,根据当前的_counter去显示
          onPressed:()=>setState(()=> ++_counter,
          ),
        ),
      ),
    );
  }
}

也就是说,在开发中,如果我们想单纯显示数据的话就继承 StatelessWidget 去自定义控件,如果控件中的数据可能会变化,进而引起视图的变化,就需要继承StatefulWidget 了。
StatefulWidget 相比于 StatelessWidget 更复杂,因为它多了状态管理这个功能,我们需要着重了解 State 的生命周期了,如下图:

image.png

理解StatefulWidget 的生命周期很重要,开发中经常需要用到,上面的知识来自于Flutter实战:Widget简介

下面列出一些常用的基础组件布局组件,大概先了解一下就行,明白什么样的布局使用什么样的控件就行,开发时可以直接点进去查看文档以及附带的使用示例:

基础组件名字 介绍
Image 图片显示,支持本地图片资源显示和网络图片显示
Text 文字显示,相当于 Label,其中 style 可以设置各种样式
Text.rich + TestSpan 富文本显示,比如登录页面一般都有是否同意<xxx 用户协议>
RaisedButton 、FlatButton、OutlineButton等等 按钮,提供点击事件
Icon 图标显示,内置了一些图标,估计实际开发中也不太能用得上
TextField 文本输入,设置多行输入就成了 TextView 了哦
Wrap 流水式布局
ListView 滚动视图
布局组件名字 介绍
Flex(Row、Column) 相当于前端的 Flex 布局,Row&Column分别是在横向和纵向对于 Flex 布局的封装
Expanded 在 Flex 的 children 中使用,内部flex属性,决定对于剩余空间的占有比例
Stack + Positioned 可以实现绝对布局
Align (Center) 控制自身在父控件中的位置
Padding 控制自身内边距
Container 使用的比较多,可以设置边框、背景色等
Scaffold 页面布局脚手架,AppBar(导航栏) + BottomNavigationBar(底部选项卡) + Drawer(抽屉) + FloatButton(右下角浮动按钮)
功能类组件 介绍
Inkwell 给控件增加点击事件,有涟漪效果
GestureDetector 给控件增加点击事件,没有涟漪效果,手势很多
SafeArea 页面安全区域适配

上面👆的组件使用在Flutter 实战
中都可以找到详细的介绍,当然也可以在 B站上可以搜索一下 Flutter教学视频,上面有很多哦。

5. 页面绘制 - 熟能生巧

5.1 登录页面的绘制

终于到了这一步,开始上手实操了,先截个登录页面的效果图吧:


image.png

UI 布局大概如下:

布局方式并不是唯一的,只要能够实现就可以。

另外,实现一个完整的登录页面需要注意以下几点:

5.2 首页底部选项卡功能

先看一下整体的页面效果,先忽略首页的页面布局:


image.png

上面提到过的 Scaffold 就可以实现这种布局,实现方式如下:

实现代码如下:

class TabbarPage extends StatefulWidget {
  @override
  _TabbarPageState createState() => _TabbarPageState();
}

class _TabbarPageState extends State<TabbarPage> {
  // 四个子页面
  var _pageList = [HomePage(), FoundPage(), StudyPage(), MyPage()];
  // 记录当前选择的Index
  var _selectInex = 0;
  // 控制pageView滑动
  PageController _pageController = PageController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // 这里不需要 AppBar,子页面中带有就行
      // PageView 控制页面切换
      body: PageView(
        children: _pageList,
        controller: _pageController,
        physics: NeverScrollableScrollPhysics(), // 禁止滑动
      ),
      // 使用 BottomNavigationBar 类即可,这里我是自定义的,为了去除点击涟漪效果
      bottomNavigationBar: BottomAppBar(
          child: Container(
        height: 50,
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            tabbarItem(0, "首页", "Image/tabbar/icon_home_normal.png",
                "Image/tabbar/icon_home_selected.png"),
            tabbarItem(1, "发现", "Image/tabbar/icon_found_normal.png",
                "Image/tabbar/icon_found_selected.png"),
            tabbarItem(2, "学习", "Image/tabbar/icon_study_normal.png",
                "Image/tabbar/icon_study_selected.png"),
            tabbarItem(3, "我的", "Image/tabbar/icon_my_normal.png",
                "Image/tabbar/icon_my_selected.png")
          ],
        ),
      )),
    );
  }

  // 底部item
  Widget tabbarItem(
      int index, String title, String normalImgName, String selectImgName) {
    var imgName = index == _selectInex ? selectImgName : normalImgName;
    var titleColor = index == _selectInex
        ? ColorUtil.hexColor(0x007AFF)
        : ColorUtil.hexColor(0x666666);
    return GestureDetector(
      onTap: () {
        // 底部 item 点击时的事件
        if (_selectInex == index) {
          return;
        }
        // 调用 setState 方法,重新 build
        setState(() {
          // 控制 PageView 显示第一个子页面
          _pageController.jumpToPage(index);
          _selectInex = index;
        });
      },
      child: Container(
        padding: EdgeInsets.symmetric(vertical: 5, horizontal: 20),
        child: Column(
          children: [
            Image.asset(
              imgName,
              width: 20,
              height: 20,
            ),
            Text(
              title,
              style: TextStyle(color: titleColor),
            )
          ],
        ),
      ),
    );
  }
}

这种布局需要注意的是子页面状态的保存,可以在每个子页面的 initState 方法中输出一下,每一次切换都会重新调用 initState 方法,说明页面重新创建了,这时需要子页面的 State 类混入(mixin)AutomaticKeepAliveClientMixin,有三步:

代码如下:


image.png
5.3 首页布局挑战 & 用户信息页面实现

可以找一个稍微复杂点的App 首页进行模仿,先不要急着去封装控件,直接写下来就行,这一步主要练习的是基础控件的使用和基础的布局方式。


image.png

基本上写完这一个首页之后,对于基础控件&布局就可以熟练使用了。

然后,实现一个我的页面,显示登录用户的信息,示例如下:


image.png

要求:

登录页面 + 主页选项卡 + 我的信息页面为下面的功能实现提供了基础,下面会讲到网络请求、数据共享、数据存储、路由跳转等功能实现。

6. 功能实现

6.1 网络请求管理

Dart IO库中提供了用于发起Http请求的一些类,可以直接使用HttpClient来发起请求。使用HttpClient发起请求的示例:

try {
    //创建一个HttpClient
    HttpClient httpClient = new HttpClient();
    //打开Http连接
    HttpClientRequest request = await httpClient.getUrl(
    Uri.parse("https://www.baidu.com"));
    //使用iPhone的UA
    request.headers.add("user-agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1");
    //等待连接服务器(会将请求信息发送给服务器)
    HttpClientResponse response = await request.close();
    //读取响应内容
    _text = await response.transform(utf8.decoder).join();
    //输出响应头
    print(response.headers);
    //关闭client后,通过该client发起的所有请求都会中止。
    httpClient.close();

} catch (e) {
    _text = "请求失败:$e";
 }

使用HttpClient发起网络请求比较麻烦,目前使用最多第三方网络请求库就是dio, 支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等,使用很简单,文档非常详细,简单的get 请求示例:

Response response;
var dio = Dio();
response = await dio.get('/test?id=12&name=wendu');
print(response.data.toString());
// Optionally the request above could also be done as
response = await dio.get('/test', queryParameters: {'id': 12, 'name': 'wendu'});
print(response.data.toString());

使用时只需要按需对其进行简单封装即可,可以参考这篇文章:强大的 dio 封装,可以满足你的一切需要

6.2 数据转Model

这个真的是比较坑的一个地方,Flutter中并没有像Java开发中的Gson/Jackson一样的Json序列化类库。因为这样的库需要使用运行时反射,这在Flutter中是禁用的。因为运行时反射会干扰Dart的_tree shaking_(核心思想:一个程序所有可能的执行流程都可以用函数调用的树来表示,这样就可以消除那些从未被调用的函数。),使用_tree shaking_,可以在release版中“去除”未使用的代码,优化应用程序的大小。由于反射会默认应用到所有代码,因此_tree shaking_会很难工作,在启用反射时很难知道哪些代码未被使用,因此冗余代码很难剥离,所以Flutter中禁用了Dart的反射功能,而正因如此也就无法实现动态转化Model的功能。

所以一般定义 model 的时候,最终都是使用命名构造函数,参数为 Map<String, dynamic>类型:

class Student {
  late String name;
  late int age;
  late String sex;

  Student(this.name, this.age, this.sex);

  // 使用命名构造函数
  Student.fromJson(Map<String, dynamic> map) {
    this.name = map["name"];
    this.age = map["age"];
    this.sex = map["sex"];
  }
}

针对于有大量成员变量的类,下面几种方式可以稍微提高下效率:

6.3 数据存储

数据存储方式一般由:写入本地文件、数据库、使用三方库shared_preferences

使用最多的也就是shared_preferences了,比如保存用户登录信息、用户配置信息等。它保存数据的形式为 Key-Value,支持 Android 和 iOS。在 Android 中使用 SharedPreferences,在 iOS中使用 NSUserDefaults

使用方式很简单,如下:

// 保存数据
_saveData() async {
  var prefs = await SharedPreferences.getInstance();
  prefs.setInt('Key_Int', 12);
}

// 读取数据
Future<int> _readData() async {
  var prefs = await SharedPreferences.getInstance();
  var result = prefs.getInt('Key_Int');
  return result ?? 0;
}

但是需要注意的是获取 SharedPreferences单例对象 是一个异步操作,一般在 app 启动时会先获取到 SharedPreferences 的单例对象,保存起来,再继续往下操作。

6.4 路由管理
6.4.1 普通路由使用

系统提供了 Navigator 类来进行路由管理,简单的使用如下:

// 跳转一个新页面
Navigator.push( context,
           MaterialPageRoute(builder: (context) {
              return NewRoute();
           }));
          
6.4.2 路由传值操作

Navigator 的push 操作会返回一个Future 对象,用于接收二级页面的返回值:

# 一级页面中
// 打开`TipRoute`,并等待返回结果
var result = await Navigator.push(
   context,
   MaterialPageRoute(
    builder: (context) {
     return TipRoute(
      // 路由参数
      text: "我是提示xxxx",
     );
    },
  ),
);
//输出`TipRoute`路由返回结果
print("路由返回值: $result");

# 二级页面 pop 时回传值
Navigator.pop(context, "我是返回值"),
6.4.3 命名路由

开发中一般使用命名路由,就是建立一个路由表,路由跳转时根据对应的名字进行跳转:

MaterialApp(
  title: 'Flutter Demo',
  initialRoute:"/", //名为"/"的路由作为应用的home(首页)
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  //注册路由表
  routes:{
   "new_page":(context) => NewRoute(),
   "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注册首页路由
  },
  # home: MyHomePage(title: 'Flutter Demo Home Page'), // initialRoute 和 home 不能同时存在,因为都是指定首页
);
6.4.4 路由钩子

MaterialApp有一个onGenerateRoute属性,当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由,可以不使用routes属性,直接使用onGenerateRoute属性,这样实现控制页面权限的功能就非常容易:

  // 自定义路由
  MaterialPageRoute routeWithSetting(RouteSettings setting) {
    print("********************************");
    print("routeName:${setting.name}");
    print("routeArguments:${setting.arguments}");
    print("********************************");

    Object? arguments = setting.arguments;
    // 判断如果需要登录,返回登录页面路由
    if (arguments != null &&
        (arguments as Map)["needLogin"] == true &&
        !UserUtil().isLogin()) {
      return loginRoute();
    }

    // _routeMap 同样为维护的一个路由表
    WidgetBuilder? builder = _routeMap[setting.name];
    if (builder != null) {
      return MaterialPageRoute(builder: builder, settings: setting);
    }
    // 如果未找到,则返回指定的 404 页面
    return unknowRouteWithSetting(setting);
  }

# main.dart 中
MaterialApp(
...
  debugShowCheckedModeBanner: false,
  initialRoute: "/",
  onGenerateRoute: (settings) {
    // 自定义去处理
    return RouteManager().routeWithSetting(settings);
  },
...
}));
6.5 全局状态管理

Flutter 也是一个响应式的框架,就避免不了状态管理这个概念了,同一个页面多个子组件的状态可以由这个页面(父组件)去管理,但是跨页面的状态管理,就需要使用一个全局状态管理器了。

Provider 是官方推出的一个状态管理框架,也有一些其他开源的状态管理框架:Redux、BloC 等等。

Provider 是基于InheritedWidget的特性实现的,所以需要先了解一下 InheritedWidget:

这里推荐:inherited_widget介绍provider实现两篇文章,能深入理解 provider 的实现。

简单的使用示例,以用户信息为例:

class UserInfoState with ChangeNotifier {
  Userinfo? userinfo;
  bool isLogin = false;

  UserInfoState() {
    _configUserInfo();
  }

  // 更新用户信息
  updateUserInfo() {
    _configUserInfo();
  }

  // 获取用户数据,并通知观察者
  _configUserInfo() {
    userinfo = UserUtil().userinfo;
    isLogin = userinfo != null;
    notifyListeners();
  }
}
void main() {
  // 设置默认的请求环境,如果有多个,使用MultiProvider
  runApp(MultiProvider(providers: [
    ChangeNotifierProvider(
      create: (context) => UserInfoState(),
    ),
    // ChangeNotifierProvider(create: (context) => CommonState())
  ], child: MyApp()));
}
Consumer<UserInfoState>(builder: (context, data, child) {
    return Scaffold(
        appBar: AppBar(
        title: Text(
           "我的学习",
           style: TextStyle(fontSize: 18),
        ),
        elevation: 0,
        bottom: data.isLogin ? _topTabbar() : null,
    ),
    body: data.isLogin ? _bodyWidget() : _placeHolderWidget());
});

在需要改变状态的位置使用 Provider.of<UserInfoState>(context,listen: false).updateUserInfo() 这种形式更改状态,listen代表不会依赖(监听)这个状态,只是获取。

7. 总结

到了这一步之后,可以自己尝试撸一个完整的项目,同时去看一些Flutter SDK 的源码和进阶知识点了。

去年学习了Uni 开发框架( Vue.js 开发跨平台应用的前端框架),今年又重新学了一下 Flutter 框架,两者间布局方式异步处理方式(Future&Promise、await、async)都非常相似。
另外最重要的一点是:学习路线很像。了解框架 -> 学习框架使用的语言 -> 简单布局学习 -> 项目开发最常用的功能使用(网络请求、路由、状态)-> 进阶学习。

同时不得不感叹,前端 or 移动端的框架层出不穷,学好基础知识很有必要,这样可以让你在学习一个新的框架的时候能够快速上手。

目前来看,Flutter还是比较火的,因为其相对于原生开发而言:节省效率、开发成本低,相对于H5、React Native来说,性能更高。
但是,Flutter也是有局限性的:

感觉不支持动态更新是个致命的问题,开发成本、开发效率可以日夜加班来补足,性能最高 <= 原生,所以之后可能还是几种跨平台方案共存,Flutter大一统还是不太可能。

上一篇下一篇

猜你喜欢

热点阅读