1. 程式人生 > >Flutter 完整開發實戰詳解(一、Dart 語言和 Flutter 基礎)

Flutter 完整開發實戰詳解(一、Dart 語言和 Flutter 基礎)

前言

在如今的 Fultter 大潮下,本系列是讓你看完會安心的文章。本系列將完整講述:如何快速從0開發一個完整的 Flutter APP,配套高完成度 Flutter 開源專案 GSYGithubAppFlutter。同時也會提供一些 Flutter 的開發細節技巧,並針對開發過程中可能遇到的問題進行填坑。

系列文章分為三篇,第一部分是基礎篇(針對 Dart 語言和 Flutter 基礎),第二部分是App快速開發實戰篇,第三部分是細節填坑篇。

筆者相繼開發過 Flutter、React Native 、Weex 等主流跨平臺框架專案,其中 Flutter 的跨平臺相容性無疑最好。前期開發除錯完全在 Android 端進行的情況下,第一次在 iOS 平臺執行居然沒有任何錯誤,並且還沒出現UI相容問題,相信對於經歷過跨平臺開發的猿們而言,這是多麼的不可思議畫面。並且 Fluuter 的 HotLoad 相比較其他兩個平臺,也是絲滑的讓人無法相信。吹爆了!

這些特點其實這得益於Flutter Engine 和 Skia ,如果有興趣的可以看看筆者之前的《移動端跨平臺開發的深度解析》。好了,感慨那麼多,讓我們進入正題吧。

一、基礎篇

本篇主要涉及:環境搭建、Dart 語言、Flutter 的基礎。

1、環境搭建

Flutter 的環境搭建十分省心,特別對應 Android 開發者而言,只是在 Android Stuido
上安裝外掛,並下載flutter Sdk到本地,配置在環境變數即可。其實中文網的搭建Futter開發環境 已經很貼心詳細,從平臺指引開始安裝基本都不會遇到問題。

這裡主要是需要注意,因為某些不可抗力的原因,國內的使用者需要配置 Flutter 的代理,並且國內使用者在搜尋 Flutter 第三方包時,也是在

https://pub.flutter-io.cn 內查詢,下方是需要配置到環境變數的地址。(ps Android Studio下執行 iOS 也是蠻有意思的(◐‿◑))

///win直接配置到環境編輯即可,mac 配置到 bash_profile
export PUB_HOSTED_URL=https://pub.flutter-io.cn //國內使用者需要設定
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn //國內使用者需要設定

2、Dart語言下的Flutter

在跨平臺開領域被 JS 一統天下的今天,Dart 語言的出現無疑是一股清流。作為後來者,Dart語言有著不少Java、kotlin 和 JS 的影子,所以對於 Android 原生開發者、前端開發者而言無疑是非常友好的。

官方也提供了包括 iOS開發者,React Native 等開發者遷移到 Flutter 上的文件,所以請不要擔心,Dart語言不會是你掌握 Flutter 的門檻。甚至作為開發者,就算你不懂 Dart 也可以看著程式碼摸索。

Come on,下面主要通過對比,簡單講述下 Dart 的一些特性,主要涉及的是 Flutter 下使用。

基本型別

var 可以定義變數,如 var tag = "666" ,這和 JS 、 Kotlin 等語言類似,同時 Dart 屬於動態型別語言,支援閉包。

Dart 中 number 型別分為 intdouble ,其中 java 中的 long 對應的也是 Dart 中的 int 型別。Dart 中沒有 float 型別。

Dart 下只有 bool 型可以用於 if 等判斷,不同於 JS 這種使用方式是不合法的 var g = "null"; if(g){}

DART中,switch 支援 String 型別。

變數

Dart 不需要給變數設定 setter getter 方法, 這和 kotlin 等類似。Dart 中所有的基礎型別、類等都繼承 Object ,預設值是 NULL, 自帶 getter 和 setter ,而如果是 final 或者 const 的話,那麼它只有一個 getter 方法。

Dart 中 final 和 const 表示常量,比如 final name = 'GSY'; const value= 1000000; 同時 static const 組合代表了靜態常量。其中 const 的值在編譯期確定,final 的值要到編譯時才確定。(ps Flutter 在 Release 下是 AOT 模式。)

Dart 下的數值,在作為字串使用時,是需要顯式指定的。比如:int i = 0; print("aaaa" + i); 這樣並不支援,需要 print("aaaa" + i.toString()); 這樣使用。這和 Java 與 JS 存在差異。所以在使用動態型別時,需要注意不要把 number 型別當做 String 使用。

DART 中陣列等於列表,所以 var list = [];List list = new List() 可以簡單看做一樣。

方法

Dart 下 ????= 屬於操作符,如: AA ?? "999" 表示如果 AA 為空,返回999;AA ??= "999" 表示如果 AA 為空,給 AA 設定成 999。

Dart 方法可以設定 引數預設值指定名稱 。比如: getDetail(Sting userName, reposName, {branch = "master"}){} 方法,這裡 branch 不設定的話,預設是 “master” 。引數型別 可以指定或者不指定。呼叫效果: getRepositoryDetailDao(“aaa", "bbbb", branch: "dev");

Dart 不像 Java ,沒有關鍵詞 public 、private 等修飾符,_下橫向直接代表 private ,但是有 @protected 註解。

Dart 中多建構函式,可以通過如下程式碼實現的。預設構造方法只能有一個,而通過Model.empty() 方法可以建立一個空引數的類,其實方法名稱隨你喜歡。而變數初始化值時,只需要通過 this.name 在構造方法中指定即可:

class ModelA {
  String name;
  String tag;
  
  //預設構造方法,賦值給name和tag
  ModelA(this.name, this.tag);

  //返回一個空的ModelA
  ModelA.empty();
  
  //返回一個設定了name的ModelA
  ModelA.forName(this.name);
}

Flutter

Flutter 中支援 async/await 。這一點和 ES7 很像,如下程式碼所示,只是定義的位置不同。同時非同步操作也和 ES6 中的Promise 很像,只是 Flutter 中返回的是 Future 物件,通過 then 可以執行下一步。如果返回的還是 Future 便可以 then().then.() 的流式操作了 。

  ///模擬等待兩秒,返回OK
  request() async {
    await Future.delayed(Duration(seconds: 1));
    return "ok!";
  }

  ///得到"ok!"後,將"ok!"修改為"ok from request"
  doSomeThing() async {
    String data = await request();
    data = "ok from request";
    return data;
  }

  ///列印結果
  renderSome() {
    doSomeThing().then((value) {
      print(value);
      ///輸出ok from request
    });
  }

Flutter 中 setState 很有 React Native 的既視感,Flutter 中也是通過 state 跨幀實現管理資料狀態的,這個後面會詳細講到。

Flutter 中一切皆 Widget 呈現,通過 build方法返回 Widget,這也是和 React Native 中,通過 render 函式返回需要渲染的 component 一樣的模式。

3、Flutter Widget

在 Flutter 中,一切的顯示都是 Widget 。Widget 是一切的基礎,作為響應式的渲染,屬於 MVVM 的實現機制。我們可以通過修改資料,再用setState 設定資料,Flutter 會自動通過繫結的資料更新 Widget 。 所以你需要做的就是實現 Widget 介面,並且和資料繫結起來

Widget 分為 有狀態無狀態 兩種,在 Flutter 中每個頁面都是一幀。無狀態就是保持在那一幀。而有狀態的 Widget 當資料更新時,其實是繪製了新的 Widget,只是 State 實現了跨幀的資料同步儲存。

這裡有個小 Tip ,當代碼框裡輸入 stl 的時候,可以自動彈出建立無狀態控制元件的模板選項,而輸入 stf 的時,就會彈出建立有狀態 Widget 的模板選項。

程式碼格式化的時候,括號內外的逗號都會影響格式化時換行的位置。

如果覺得預設換行的線太短,可以在設定-Editor-Code Style-Dart-Wrapping and Braces-Hard wrap at 設定你接受的數值。

3.1、無狀態StatelessWidget

直接進入主題,下方程式碼是無狀態 Widget 的簡單實現。

繼承 StatelessWidget,通過 build 方法返回一個佈局好的控制元件。可能現在你還對 Flutter 的內建控制元件不熟悉,but Don’t worry , take is easy ,後面我們就會詳細介紹。這裡你只需要知道,一個無狀態的 Widget 就是這麼簡單。

Widget 和 Widget 之間通過 child: 進行巢狀。其中有的 Widget 只能有一個 child,比如下方的 Container ;有的 Widget 可以多個 child ,也就是children:,比如` Colum 佈局。下方程式碼便是 Container Widget 嵌套了 Text Widget。

import 'package:flutter/material.dart';

class DEMOWidget extends StatelessWidget {
  final String text;

  //資料可以通過構造方法傳遞進來
  DEMOWidget(this.text);

  @override
  Widget build(BuildContext context) {
    //這裡返回你需要的控制元件
    //這裡末尾有沒有的逗號,對於格式化程式碼而已是不一樣的。
    return Container(
      //白色背景
      color: Colors.white,
      //Dart語法中,?? 表示如果text為空,就返回尾號後的內容。
      child: Text(text ?? "這就是無狀態DMEO"),
    );
  }
}

3.2、有狀態StatefulWidget

繼續直插主題,如下程式碼,是有狀態的widget的簡單實現。

你需要建立管理的是主要是 State , 通過 State 的 build 方法去構建控制元件。在 State 中,你可以動態改變資料,這類似 MVVM 實現,在 setState 之後,改變的資料會觸發 Widget 重新構建重新整理。而下方程式碼中,是通過延兩秒之後,讓文字顯示為 “這就變了數值”

如下程式碼還可以看出,State 中主要的宣告週期有 :

  • initState :初始化,理論上只有初始化一次,第二篇中會說特殊情況下。
  • didChangeDependencies:在 initState 之後呼叫,此時可以獲取其他 State 。
  • dispose :銷燬,只會呼叫一次。

看到沒,Flutter 其實就是這麼簡單!你的關注點只要在:建立你的 StatelessWidget 或者 StatefulWidget 而已。你需要的就是在 build 中堆積你的佈局,然後把資料新增到 Widget 中,最後通過 setState 改變資料,從而實現畫面變化。

import 'dart:async';
import 'package:flutter/material.dart';

class DemoStateWidget extends StatefulWidget {

  final String text;

  ////通過構造方法傳值
  DemoStateWidget(this.text);

  ///主要是負責建立state
  @override
  _DemoStateWidgetState createState() => _DemoStateWidgetState(text);
}

class _DemoStateWidgetState extends State<DemoStateWidget> {

  String text;

  _DemoStateWidgetState(this.text);
  
  @override
  void initState() {
    ///初始化,這個函式在生命週期中只調用一次
    super.initState();
    ///定時2秒
    new Future.delayed(const Duration(seconds: 1), () {
      setState(() {
        text = "這就變了數值";
      });
    });
  }

  @override
  void dispose() {
    ///銷燬
    super.dispose();
  }

  @override
  void didChangeDependencies() {
    ///在initState之後調 Called when a dependency of this [State] object changes.
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Text(text ?? "這就是有狀態DMEO"),
    );
  }
}

4、Flutter 佈局

Flutter 中擁有需要將近30種內建的 佈局Widget,其中常用有 Container、Padding、Center、Flex、Stack、Row、Colum、ListView 等,下面簡單講解它們的特性和使用。

型別 作用特點
Container 只有一個子 Widget。預設充滿,包含了padding、margin、color、寬高、decoration 等配置。
Padding 只有一個子 Widget。只用於設定Padding,常用於巢狀child,給child設定padding。
Center 只有一個子 Widget。只用於居中顯示,常用於巢狀child,給child設定居中。
Stack 可以有多個子 Widget。 子Widget堆疊在一起。
Colum 可以有多個子 Widget。垂直佈局。
Row 可以有多個子 Widget。水平佈局。
Expanded 只有一個子 Widget。在 Colum 和 Row 中充滿。
ListView 可以有多個子 Widget。自己意會吧。
  • Container :最常用的預設佈局!只能包含一個child:,支援配置 padding,margin,color,寬高,decoration(一般配置邊框和陰影)等配置,在 Flutter 中,不是所有的控制元件都有 寬高、padding、margin、color 等屬性,所以才會有 Padding、Center 等 Widget 的存在。
    new Container(
        ///四周10大小的maring
        margin: EdgeInsets.all(10.0),
        height: 120.0,
        width: 500.0,
        ///透明黑色遮罩
        decoration: new BoxDecoration(
            ///弧度為4.0
            borderRadius: BorderRadius.all(Radius.circular(4.0)),
            ///設定了decoration的color,就不能設定Container的color。
            color: Colors.black,
            ///邊框
            border: new Border.all(color: Color(GSYColors.subTextColor), width: 0.3)),
        child:new Text("666666"));
  • Colum、Row 絕對是必備佈局, 橫豎佈局也是日常中最常見的場景。如下方所示,它們常用的有這些屬性配置:主軸方向是 start 或 center 等;副軸方向方向是 start 或 center 等;mainAxisSize 是充滿最大尺寸,或者只根據子 Widget 顯示最小尺寸。
//主軸方向,Colum的豎向、Row我的橫向
mainAxisAlignment: MainAxisAlignment.start, 
//預設是最大充滿、還是根據child顯示最小大小
mainAxisSize: MainAxisSize.max,
//副軸方向,Colum的橫向、Row我的豎向
crossAxisAlignment :CrossAxisAlignment.center,

  • Expanded 在 Colum 和 Row 中代表著平均充滿,當有兩個存在的時候預設均分充滿。同時頁可以設定 flex 屬性決定比例。
    new Column(
     ///主軸居中,即是豎直向居中
     mainAxisAlignment: MainAxisAlignment.center,
     ///大小按照最小顯示
     mainAxisSize : MainAxisSize.min,
     ///橫向也居中
      crossAxisAlignment : CrossAxisAlignment.center,
      children: <Widget>[
        ///flex預設為1
        new Expanded(child: new Text("1111"), flex: 2,),
        new Expanded(child: new Text("2222")),
      ],
    );

接下來我們來寫一個複雜一些的控制元件。首先我們建立一個私有方法_getBottomItem,返回一個 Expanded Widget,因為後面我們需要將這個方法返回的 Widget 在 Row 下平均充滿。

如程式碼中註釋,佈局內主要是現實一個居中的Icon圖示和文字,中間間隔5.0的 padding:

  ///返回一個居中帶圖示和文字的Item
  _getBottomItem(IconData icon, String text) {
    ///充滿 Row 橫向的佈局
    return new Expanded(
      flex: 1,
      ///居中顯示
      child: new Center(
        ///橫向佈局
        child: new Row(
          ///主軸居中,即是橫向居中
          mainAxisAlignment: MainAxisAlignment.center,
          ///大小按照最大充滿
          mainAxisSize : MainAxisSize.max,
          ///豎向也居中
          crossAxisAlignment : CrossAxisAlignment.center,
          children: <Widget>[
            ///一個圖示,大小16.0,灰色
            new Icon(
              icon,
              size: 16.0,
              color: Colors.grey,
            ),
            ///間隔
            new Padding(padding: new EdgeInsets.only(left:5.0)),
            ///顯示文字
            new Text(
              text,
              //設定字型樣式:顏色灰色,字型大小14.0
              style: new TextStyle(color: Colors.grey, fontSize: 14.0),
              //超過的省略為...顯示
              overflow: TextOverflow.ellipsis,
              //最長一行
              maxLines: 1,
            ),
          ],
        ),
      ),
    );
  }

item效果

接著我們把上方的方法,放到新的佈局裡。如下流程和程式碼:

  • 首先是 Container包含了Card,用於快速簡單的實現圓角和陰影。
  • 然後接下來包含了FlatButton實現了點選,通過Padding實現了邊距。
  • 接著通過Column垂直包含了兩個子Widget,一個是Container、一個是Row
  • Row 內使用的就是_getBottomItem方法返回的 Widget ,效果如下圖。
  @override
  Widget build(BuildContext context) {
    return new Container(
      ///卡片包裝
      child: new Card(
           ///增加點選效果
          child: new FlatButton(
              onPressed: (){print("點選了哦");},
              child: new Padding(
                padding: new EdgeInsets.only(left: 0.0, top: 10.0, right: 10.0, bottom: 10.0),
                child: new Column(
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    ///文字描述
                    new Container(
                        child: new Text(
                          "這是一點描述",
                          style: TextStyle(
                            color: Color(GSYColors.subTextColor),
                            fontSize: 14.0,
                          ),
                          ///最長三行,超過 ... 顯示
                          maxLines: 3,
                          overflow: TextOverflow.ellipsis,
                        ),
                        margin: new EdgeInsets.only(top: 6.0, bottom: 2.0),
                        alignment: Alignment.topLeft),
                    new Padding(padding: EdgeInsets.all(10.0)),

                    ///三個平均分配的橫向圖示文字
                    new Row(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        _getBottomItem(Icons.star, "1000"),
                        _getBottomItem(Icons.link, "1000"),
                        _getBottomItem(Icons.subject, "1000"),
                      ],
                    ),
                  ],
                ),
              ))),
    );
  }

完整Item

Flutter 中,你的佈局很多時候就是這麼一層一層嵌套出來的,當然還有其他更高階的佈局方式,這裡就先不展開了。

5、Flutter 頁面

Flutter 中除了佈局的 Widget,還有互動顯示的 Widget 和完整頁面呈現的Widget。其中常見的有 MaterialApp、Scaffold、Appbar、Text、Image、FlatButton等。下面簡單介紹這些 Wdiget,並完成一個頁面。

型別 作用特點
MaterialApp 一般作為APP頂層的主頁入口,可配置主題,多語言,路由等
Scaffold 一般使用者頁面的承載Widget,包含appbar、snackbar、drawer等material design的設定。
Appbar 一般用於Scaffold的appbar ,內有標題,二級頁面返回按鍵等,當然不止這些,tabbar等也會需要它 。
Text 顯示文字,幾乎都會用到,主要是通過style設定TextStyle來設定字型樣式等。
RichText 富文字,通過設定TextSpan,可以拼接出富文字場景。
TextField 文字輸入框 :new TextField(controller: //文字控制器, obscureText: "hint文字");
Image 圖片載入: new FadeInImage.assetNetwork( placeholder: "預覽圖", fit: BoxFit.fitWidth, image: "url");
FlatButton 按鍵點選: new FlatButton(onPressed: () {},child: new Container());

那麼再次直插主題實現一個簡單完整的頁面試試。如下方程式碼:

  • 首先我們建立一個StatefulWidget:DemoPage
  • 然後在 _DemoPageState中,通過build建立了一個Scaffold
  • Scaffold內包含了一個AppBar和一個ListView
  • AppBar類似標題了區域,其中設定了 title為 Text Widget。
  • body是ListView,返回了20個之前我們建立過的 DemoItem Widget。
import 'package:flutter/material.dart';
import 'package:gsy_github_app_flutter/test/DemoItem.dart';

class DemoPage extends StatefulWidget {
  @override
  _DemoPageState createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  @override
  Widget build(BuildContext context) {
    ///一個頁面的開始
    ///如果是新頁面,會自帶返回按鍵
    return new Scaffold(
      ///背景樣式
      backgroundColor: Colors.blue,
      ///標題欄,當然不僅僅是標題欄
      appBar: new AppBar(
        ///這個title是一個Widget
        title: new Text("Title"),
      ),
      ///正式的頁面開始
      ///一個ListView,20個Item
      body: new ListView.builder(
        itemBuilder: (context, index) {
          return new DemoItem();
        },
        itemCount: 20,
      ),
    );
  }
}

最後我們建立一個StatelessWidget作為入口檔案,實現一個MaterialApp將上方的DemoPage設定為home頁面,通過main入口執行頁面。

import 'package:flutter/material.dart';
import 'package:gsy_github_app_flutter/test/DemoPage.dart';

void main() {
  runApp(new DemoApp());
}

class DemoApp extends StatelessWidget {
  DemoApp({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(home: DemoPage());
  }
}

最終顯示

好吧,第一部分終於完了,這裡主要講解都是一些簡單基礎的東西,適合安利入坑,後續還有兩篇主要實戰,敬請期待喲!( ̄^ ̄)ゞ

資源推薦

完整開源專案推薦:
文章