美文网首页
Flutter开发初体验

Flutter开发初体验

作者: Imkata | 来源:发表于2023-02-28 10:40 被阅读0次

    一. 创建Flutter项目

    创建Flutter项目有两种方式:通过命令行创建通过开发工具创建

    1.1. 通过命令行创建

    通过命令行创建非常简单,在终端输入以下命令即可:

    • 注意:Flutter的名称不要包含特殊的字符,另外不可以使用驼峰标识
    • 创建完之后使用自己喜欢的开发工具打开即可
    flutter create learn_flutter
    

    1.2. 通过开发工具创建

    我这里也可以直接通过Android Studio来进行创建:

    • 选择Start a new Flutter project,之后填写相关的信息即可,这里不再赘述

    1.3. 默认程序分析

    我们将创建的应用起来跑在模拟器上(我这里选择iPhone模拟器,Android也可以),会看到如下效果:

    项目目录分析:

    1. .dart_tool:dart语法依赖的第三方库,不用管,不用动。
    2. .idea:Android Studio是谷歌基于IDEA开发的,所以有这个文件。
    3. android:安卓工程。
    4. ios:ios工程。
    5. lib:里面会存放我们编写的Flutter代码,打开发现里面有一个main.dart,它是我们Flutter启动的入口文件,里面有main函数
    6. test:一些测试的东西。
    7. .gitignore:gith忽略。
    8. .metadata:flutter版本记录,不用手动改。
    9. .packagespubspec.lockpubspec.yaml:三方库依赖相关。

    默认代码分析:

    • 这是一个计数器的案例程序,点击右下角的 + 符号,上面显示的数字会递增;
    • 但是我们第一次接触main.dart中的代码,可能会发现很多不认识的代码,不知道这个内容是如何编写出来的;

    作为初学者,我的建议是将其中所有的代码全部删除掉,从零去创建里面的代码,这样我们才能对Flutter应用程序的结构非常清晰。

    二. 开始Flutter代码

    2.1. Hello World

    2.1.1. Hello World的需求

    做任何的开发,我们都是从祖传的Hello World开始,那么现在我们的需求来了:

    • 在界面中心位置,显示一个Hello World;

    2.1.2. Hello World的实现

    下面,我们就动手开始编写Hello World:

    // 导入库文件
    import 'package:flutter/material.dart';
    
    main(List<String> args) {
      // runApp方法要求传入一个widget,Text也是继承于widget
      // 第一个参数是文字,必传,第二个参数是命名可选参数,表示方向
      runApp(Text("Hello World", textDirection: TextDirection.ltr));
    }
    

    当然,上面的代码我们已经实现了在界面上显示Hello World:

    • 但是没有居中字体也有点小
    • 这些问题,我们放到后面再来解决,先搞懂目前的几行代码;

    上面的代码我们有一些比较熟悉,有一些并不清楚是什么:

    • 比如我们知道Dart程序的入口都是main函数,而Flutter是Dart编写的,所以入口也是main函数;
    • 但是我们导入的Material是什么呢
    • 另外,我们在main函数中调用了一个runApp()函数又是什么呢?

    下面,我们对不认识的代码进行一些分析。

    2.2. 代码分析

    2.2.1. runApp和Widget

    runApp是Flutter内部提供的一个函数,当我们启动一个Flutter应用程序时就是从调用这个函数开始的。

    • 我们可以点到runApp的源码,查看到该函数
    • 我们暂时不分析具体的源码(因为我发现过多的理论,对于初学者来说并不友好)
    void runApp(Widget app) {
      ...省略代码
    }
    

    该函数让我们传入一个东西:Widget

    我们先说Widget的翻译

    • Widget在国内有很多的翻译;
    • 做过Android、iOS等开发的人群,喜欢将它翻译成控件
    • 做过Vue、React等开发的人群,喜欢将它翻译成组件
    • 如果我们使用Google,Widget翻译过来应该是小部件
    • 没有说哪种翻译一定是对的,或者一定是错的,但是我个人更倾向于小部件或者组件

    Widget到底什么东西呢?

    • 我们学习Flutter,从一开始就可以有一个基本的认识:Flutter中万物皆Widget(万物皆可盘)
    • 在我们iOS或者Android开发中,我们的界面有很多种类的划分:应用(Application)、视图控制器(View Controller)、活动(Activity)、View(视图)、Button(按钮)等等;
    • 但是在Flutter中,这些东西都是不同的Widget而已
    • 也就是我们整个应用程序中所看到的内容几乎都是Widget,甚至是内边距的设置,我们也需要使用一个叫Padding的Widget来做;

    runApp函数让我们传入的就是一个Widget:

    • 但是我们现在没有Widget,怎么办呢?
    • 我们可以导入Flutter默认已经给我们提供的Material库,来使用其中的很多内置Widget;

    2.2.2. Material设计风格

    material是什么呢?

    • material是Google公司推行的一套设计风格,或者叫设计语言设计规范等;
    • 里面有非常多的设计规范,比如颜色文字的排版响应动画与过度填充等等;
    • 在Flutter中高度集成了Material风格的Widget
    • 在我们的应用中,我们可以直接使用这些Widget来创建我们的应用(后面会用到很多);

    Text小部件分析

    • 我们可以使用Text小部件来完成文字的显示;
    • 我们发现Text小部件继承自StatelessWidget,StatelessWidget继承自Widget;
    • 所以我们可以将Text小部件传入到runApp函数中
    • 属性非常多,但是我们已经学习了Dart语法,所以你会发现只有this.data属性是必须传入的。
    class Text extends StatelessWidget {
      const Text(
        this.data, {
        Key key,
        this.style,
        this.strutStyle,
        this.textAlign,
        this.textDirection,
        this.locale,
        this.softWrap,
        this.overflow,
        this.textScaleFactor,
        this.maxLines,
        this.semanticsLabel,
        this.textWidthBasis,
      });
    }
    

    StatelessWidget简单介绍:

    • StatelessWidget继承自Widget;
    • 后面我会更加详细的介绍它的用法;
    abstract class StatelessWidget extends Widget {
        // ...省略代码
    }
    

    2.3. 代码改进

    2.3.1. 改进界面样式

    我们发现现在的代码并不是我们想要的最终结果:

    • 我们可能希望文字居中显示,并且可以大一些;
    • 居中显示: 需要使用另外一个Widget,Center
    • 文字大一些: 需要给Text文本设置一些样式;

    我们修改代码如下:

    • 我们在Text小部件外层包装了一个Center部件,让Text作为其child;
    • 并且,我们给Text组件设置了一个属性:style,对应的值是TextStyle类型;
    import 'package:flutter/material.dart';
    
    main(List<String> args) {
      runApp(
        // 居中组件
        Center(
          child: Text(
            "Hello World",
            textDirection: TextDirection.ltr,
            style: TextStyle(
                fontSize: 36,
                color: Colors.white
            ),
          ),
        )
      );
    }
    

    2.3.2. 改进界面结构

    目前我们虽然可以显示HelloWorld,但是我们发现最底部的背景是黑色,并且我们的页面并不够结构化。

    • 正常的App页面应该有一定的结构,比如通常都会有导航栏,会有一些背景颜色

    在开发当中,我们并不需要从零去搭建这种结构化的界面,我们可以使用Material库,直接使用其中的一些封装好的组件来完成一些结构的搭建。

    我们通过下面的代码来实现:

    import 'package:flutter/material.dart';
    
    main(List<String> args) {
      runApp(
        // 使用Material设计风格
        MaterialApp(
          // Scaffold脚手架,帮助我们快速搭建界面
          home: Scaffold(
            // 导航条
            appBar: AppBar(
              title: Text("CODERWHY"),
            ),
            // 内容
            body: Center(
              child: Text(
                "Hello World",
                textDirection: TextDirection.ltr,
                style: TextStyle(fontSize: 36),
              ),
            ),
          ),
        )
      );
    }
    

    在最外层包裹一个MaterialApp

    • 这意味着整个应用我们都会采用MaterialApp风格的一些东西,方便我们对应用的设计,并且目前我们使用了其中两个属性;
    • home:是该应用启动时显示的页面,我们传入了一个Scaffold;
    • title:这个是定义在Android系统中打开多任务切换窗口时显示的标题;(暂时可以不写)

    Scaffold是什么呢?

    • 翻译过来是脚手架,脚手架的作用就是搭建页面的基本结构;
    • 所以我们给MaterialApp的home属性传入了一个Scaffold对象,作为启动显示的Widget;
    • Scaffold也有一些属性,比如appBarbody
    • appBar是用于设计导航栏的,我们传入了一个title属性
    • body是页面的内容部分,我们传入了之前已经创建好的Center中包裹的一个Text的Widget;

    注意

    // 使用material的系统风格
    import 'package:flutter/material.dart';
    // 使用cupertino系统风格,这种风格和苹果更相似
    // 但是我们都使用material的系统风格也没事,因为material更丰富
    import 'package:flutter/cupertino.dart';
    

    2.3.3. 进阶案例实现

    我们可以让界面中存在更多的元素:

    • 写到这里的时候,你可能已经发现嵌套太多了,不要着急,我们后面会对代码重构的
    import 'package:flutter/material.dart';
    
    main(List<String> args) {
      runApp(
        MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              title: Text("CODERWHY"),
            ),
            body: Center(
              // 使用row,可以一行放多个元素 
              child: Row(
                // 主轴对齐方式:居中对齐
                mainAxisAlignment: MainAxisAlignment.center,
                // children是个数组
                children: <Widget>[
                  Checkbox(
                    value: true,
                    onChanged: (value) => print("Hello World")),
                  Text(
                    "同意协议",
                    textDirection: TextDirection.ltr,
                    style: TextStyle(fontSize: 20),
                  )
                ],
              ),
            ),
          ),
        )
      );
    }
    

    2.4. 代码重构

    2.4.1. 创建自己的Widget

    很多学习Flutter的人,都会被Flutter的嵌套劝退,当代码嵌套过多时,结构很容易看不清晰。

    这里有两点我先说明一下:

    1. Flutter整个开发过程中就是形成一个Widget树,所以形成嵌套是很正常的。
    2. 关于Flutter的代码缩进,更多开发中我们使用的是2个空格(前端开发2个空格居多,你喜欢4个也没问题)。

    但是,我们开发一个这么简单的程序就出现如此多的嵌套,如果应用程序更复杂呢?

    • 我们可以对我们的代码进行封装,将它们封装到自己的Widget中,创建自己的Widget;

    如何创建自己的Widget呢?

    • 在Flutter开发中,我们可以继承自StatelessWidget或者StatefulWidget来创建自己的Widget类;
    • StatelessWidget: 没有状态改变的Widget,通常这种Widget仅仅是做一些展示工作而已;
    • StatefulWidget: 需要保存状态,并且可能出现状态改变的Widget;

    在上面的案例中对代码的重构,我们使用StatelessWidget即可,所以我们接下来学习一下如果利用StatelessWidget来对我们的代码进行重构;

    StatefulWidget我们放到后面的一个案例中来学习;

    2.4.2. StatelessWidget

    StatelessWidget通常是一些没有状态(State,也可以理解成data)需要维护的Widget:

    • 它们的数据通常是直接写死(放在Widget中的数据,必须被定义为final,为什么呢?我在下一个章节讲解StatefulWidget会讲到);
    • 从parent widget中传入的而且一旦传入就不可以修改;
    • 从InheritedWidget获取来使用的数据(这个放到后面会讲解);

    我们来看一下创建一个StatelessWidget的格式:

    1. 让自己创建的Widget继承自StatelessWidget;
    2. StatelessWidget包含一个必须重写的方法:build方法;
    class MyStatelessWidget extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return <返回我们的Widget要渲染的Widget,比如一个Text Widget>;
      }
    }
    

    build方法的解析:

    • Flutter在拿到我们自己创建的StatelessWidget时,就会执行它的build方法;
    • 我们需要在build方法中告诉Flutter,我们的Widget希望渲染什么元素,比如一个Text Widget;
    • StatelessWidget没办法主动去执行build方法,当我们使用的数据发生改变时,build方法会被重新执行;

    build方法什么情况下被执行呢?:

    1. 当我们的StatelessWidget第一次被插入到Widget树中时(也就是第一次被创建时);
    2. 当我们的父Widget(parent widget)发生改变时,子Widget会被重新构建;
    3. 如果我们的Widget依赖InheritedWidget的一些数据,InheritedWidget数据发生改变时;

    2.4.3. 重构案例代码

    现在我们就可以通过StatelessWidget来对我们的代码进行重构了

    • 因为我们的整个代码都是一些数据展示,没有数据的改变,使用StatelessWidget即可;
    • 另外,为了体现更好的封装性,我对代码进行了两层的拆分,让代码结构看起来更加清晰;(具体的拆分方式,我会在后面的案例中不断的体现出来,目前我们先拆分两层)

    重构后的代码如下:

    import 'package:flutter/material.dart';
    
    main(List<String> args) {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              title: Text("CODERWHY"),
            ),
            body: HomeContent(),
          ),
        )
      }
    }
    
    class HomeContent extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Checkbox(
                  value: true,
                  onChanged: (value) => print("Hello World")),
              Text(
                "同意协议",
                textDirection: TextDirection.ltr,
                style: TextStyle(fontSize: 20),
              )
            ],
          ),
        );
      }
    }
    

    上面的StatelessWidget是没有数据交互的,如果想要有状态,我们需要使用StatefulWidget。

    只要是widget都是带有@immutable注解的,都是不能定义状态的,包括StatefulWidget,既然StatefulWidget不能定义状态,所以创建一个单独的类,这个类负责维护状态,如下:

    import 'package:flutter/material.dart';
    
    /**
     * Widget:
     * 有状态的Widget: StatefulWidget 在运行过程中有一些状态(data)需要改变
     * 无状态的Widget: StatelessWidget 内容是确定没有状态(data)的改变
     */
    class MyApp extends StatelessWidget {
      // build
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
            //去掉右上角的debug标识
            debugShowCheckedModeBanner: false,
            home: HYHomePage()
        );
      }
    }
    
    class HYHomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        print("HYHomePage Build");
        return Scaffold(
            appBar: AppBar(
              title: Text("第一个Flutter程序"),
            ),
            body: HYContentBody()
        );
      }
    }
    
    // 只要是widget都是带有@immutable注解的,都是不能定义状态的,包括StatefulWidget
    // StatefulWidget不能定义状态,所以创建一个单独的类, 这个类负责维护状态
    class HYContentBody extends StatefulWidget {
      @override
      //必须实现createState方法,返回这个状态类
      State<StatefulWidget> createState() {
        return HYContentBodyState();
      }
    }
    
    //这个状态类里面可以定义状态,然后需要实现build方法
    class HYContentBodyState extends State<HYContentBody> {
      //定义状态
      var flag = true;
    
      @override
      //实现build方法
      Widget build(BuildContext context) {
        print("HYContentBodyState Build");
        return Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Checkbox(
                value: flag,
                onChanged: (value) {
                  //需要刷新数据的地方,必须通过调用setState来改变
                  this.setState(() {
                    flag = value;
                  });
                },
              ),
              Text("同意协议", style: TextStyle(fontSize: 20))
            ],
          ),
        );
      }
    }
    

    这时候点击Checkbox就会在选中和未选中之间切换了。

    三. 案例练习

    3.1. 案例最终效果

    我们先来看一下案例的最终展示效果:

    • 这个效果中我们会使用很多没有接触的Widget;
    • 没有关系,后面这些常用的Widget我会一个个讲解;
    • 这个案例最主要的目的还是让大家更加熟悉Flutter的开发模式以及自定义Widget的封装过程;

    3.2. 自定义Widget

    在我们的案例中,很明显一个产品的展示就是一个大的Widget,这个Widget包含如下Widget:

    • 标题的Widget:使用一个Text Widget完成;
    • 描述的Widget:使用一个Text Widget完成;
    • 图片的Widget:使用一个Image Widget完成;
    • 上面三个Widget要垂直排列,我们可以使用一个Column的Widget(上一个章节中我们使用了一次Row是水平排列的)

    另外,三个展示的标题、描述、图片都是不一样的,所以我们可以让Parent Widget来决定内容:

    • 创建三个成员变量保存父Widget传入的数据
    // 在Widget里面定义的所有的成员变量都要是final的
    class ProductItem extends StatelessWidget {
      final String title;
      final String desc;
      final String imageURL;
    
      // 构造方法
      ProductItem(this.title, this.desc, this.imageURL);
    
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            Text(title, style: TextStyle(fontSize: 24)),
            Text(desc, style: TextStyle(fontSize: 18)),
            Image.network(imageURL)
          ],
        );
      }
    }
    

    3.3. 列表数据展示

    现在我们就可以创建三个ProductItem来让他们展示了:

    • MyApp和上一个章节是一致的,没有任何改变;
    • HomeContent中,我们使用了一个Column,因为我们创建的三个ProductItem是垂直排列的
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData(
            primaryColor: Colors.blueAccent
          ),
          home: Scaffold(
            appBar: AppBar(
              title: Text("CODERWHY"),
            ),
            body: HomeContent(),
          ),
        );
      }
    }
    
    class HomeContent extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Column(
          children: <Widget>[
            ProductItem("Apple1", "Macbook Product1", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg"),
            ProductItem("Apple2", "Macbook Product2", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72imm9u5zj30u00k0adf.jpg"),
            ProductItem("Apple3", "Macbook Product3", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72imqlouhj30u00k00v0.jpg"),
          ],
        );
      }
    }
    

    运行效果如下:

    • 错误信息:下面出现了黄色的斑马线;
    • 这是因为在Flutter的布局中,内容是不能超出屏幕范围的,当超出时不会自动变成滚动效果,而是会报下面的错误;

    如何可以解决这个问题呢?

    • 我们将Column换成ListView即可;
    • ListView可以让自己的子Widget变成滚动的效果;

    3.4. 案例细节调整

    3.4.1. 界面整体边距

    如果我们希望整个内容距离屏幕的边缘有一定的间距,怎么做呢?

    • 我们需要使用另外一个Widget:Padding,它有一个padding属性用于设置边距大小;
    • 没错,设置内边距也是使用Widget,这个Widget就是Padding;

    3.4.2. 商品内边距和边框

    我们现在希望给所有的商品也添加一个内边距,并且还有边框,怎么做呢?

    • 我们可以使用一个Container的Widget,它里面有padding属性,并且可以通过decoration来设置边框;
    • Container我们也会在后面详细来讲,我们先用起来;

    3.4.3. 文字图片的间距

    我们希望给图片和文字之间添加一些间距,怎么做呢?

    • 方式一:给图片或者文字添加一个向上的内边距或者向下的内边距;
    • 方式二:使用SizedBox的Widget,设置一个height属性,可以增加一些距离(使用的比较多);

    3.5. 最终实现代码

    最后,我给出最终实现代码:

    import 'package:flutter/material.dart';
    
    main(List<String> args) {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          theme: ThemeData(
            primaryColor: Colors.blueAccent
          ),
          home: Scaffold(
            appBar: AppBar(
              title: Text("CODERWHY"),
            ),
            body: HomeContent(),
          ),
        );
      }
    }
    
    class HomeContent extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Padding(
          padding: const EdgeInsets.all(8.0),
          child: ListView(
            children: <Widget>[
              ProductItem("Apple1", "Macbook Product1", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72j6nk1d4j30u00k0n0j.jpg"),
              ProductItem("Apple2", "Macbook Product2", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72imm9u5zj30u00k0adf.jpg"),
              ProductItem("Apple3", "Macbook Product3", "https://tva1.sinaimg.cn/large/006y8mN6gy1g72imqlouhj30u00k00v0.jpg"),
            ],
          ),
        );
      }
    }
    
    class ProductItem extends StatelessWidget {
      final String title;
      final String desc;
      final String imageURL;
    
      ProductItem(this.title, this.desc, this.imageURL);
    
      @override
      Widget build(BuildContext context) {
        // 这里补充一个快捷键:alt+enter可以选择包裹一个容器
        return Container(
          // 设置内边距
          padding: EdgeInsets.all(20),
          // 设置边框
          decoration: BoxDecoration(
              border: Border.all(
                  width: 5, // 设置边框的宽度
                  color: Colors.pink// 设置边框的颜色
              )
          ),
          child: Column(
            children: <Widget>[
              Text(title, style: TextStyle(fontSize: 24)),
              Text(desc, style: TextStyle(fontSize: 18)),
              // 垂直方向添加间距 
              SizedBox(height: 10,),
              Image.network(imageURL)
            ],
          ),
        );
      }
    }
    

    相关文章

      网友评论

          本文标题:Flutter开发初体验

          本文链接:https://www.haomeiwen.com/subject/xyqskdtx.html