2019年8月27日 星期二

理解 Flutter 的專有名詞 Widget 與 State

Flutter 讓人很容易短時間就能開發出 app, 關鍵在於他開發了很多種小部件稱為 Widget, 將Widget 組合起來就是一個 app. 所謂件(Widget)就是一個可以呈現在螢幕上的視覺系件,幾乎可以說 Flutter 每個物件都是件.  開發者透過件類型的建構式產生件(instance), Flutter 內的狀態機再呼叫 build 專屬方法產生件, 物件類型可以透過繼承(extends)兩種部件類型分別是靜態件類型(StatelessWidget)與動態件類型(StatefulWidget), 在專屬的方法 build 裏面客製出靜態部件動態部件, 動態件類型與靜態件類型差異在於前者產生 build 件之前先呼叫 createState 產生動態物件的狀態件(instance 簡稱物件狀態), 接著再呼叫物件專屬方法 build 產生一個動態件:
           class 靜態類型 extends  StatelessWidget { // 靜態物件 = 靜態類型( )
                    // ... 特性常數在此宣告 propertyName
                    @override  build(BuildContext _) { // Flutter 動態機呼叫後產生靜態部件
                           // 用靜態或動態件(instance)當養份
                           //... 在此客製化下層靜態物件
                          // 最後 return 靜態部件
                     }
           }
           class 動態類型 extends  StatefulWidget {  // 動態物件 = 動態類型( )
                    // ... 特性常數在此宣告 propertyName
                    @override  createState( ) => 件狀態類型( )
           }
           class  件狀態類型 extends  State<動態件類型> { //件狀態 = 件狀態類型( )
                    // ...動態變數在此宣告 stateName                   
                    @initState ( ) {
                       //一次性作動在此執行,一旦更新時可呼叫setState註冊,執行build產生新
                       // 可以透過 widget.propertyName 讀取動態物件裡面的特性 propertyName
                    }
                   @override  build(BuildContext _)  { // Flutter 動態機呼叫後產生動態部件
                       // 用靜態或動態件 instance 當養份
                       // ... 在此客製化下層 動態物件
                       // ...   一旦更新可以呼叫setState註冊, 再一次執行 build 產生新
                       // 可以透過 widget.propertyName 讀取動態物件裡面的特性 propertyName
                       // 最後 return 動態部件
                   }
          }
          class    根類型 extends StatelessWidget {  // 根物件 = 根類型 ( )
                       @override  build(BuildContext _) => MaterialApp( // 客制化根部件
                               theme : ThemeData.dark( ),  // Flutter 內部物件
                               home  :  動態物件 // 用動態物件當養份客制化其他枝葉部件 ...
                           // home  :  靜態物件 // 或用 靜態物件當養份客制化其他枝葉部件 ...
                       )
          }
          main( ) => runApp(根物件);
備註:
關於部件樹(Widget tree)的理解:
Fluter 內部所維護的一棵件樹, 是從物件(instance)中的方法(method) build 根據上下文(BuildContex)開始長出來的, 打從第一顆種子(通常是一個靜態類型的物件), 交給程序 runApp 把他發芽, 長出(build)第一個樹根, 例如可以用 MaterialApp 產生根(部件), 只要在樹根部件(方法 build 裏面)供應養份給根(部件)就能繼續客製化下層部件, 進而開枝散葉, 長出(build)新樹枝(部件), 新樹枝茁壯長出(build)葉子(部件)等等(全都比擬成部件成長事件),最後佈建出一整顆樹(佈建樹). 部件因為生長點位置(BuildContext), 自然而然形成一個有上下階層的關係(樹根 -> ...樹枝 -> ...樹葉), 至於動態部件會隨時間流逝, 狀態的改變, 隨之新陳代謝. Flutter 內部可能是以 1/60 秒執行一次狀態機(state machine)方式, 長出(build)部件,呈現(render)一個螢幕畫面.

關於狀態的理解:
螢幕以 x-y 為座標軸的平面來呈現(render)畫面,以z 軸(垂直螢幕軸)代表時間軸變化的狀態軸, 當每個畫面不斷更新時就能產生動畫, 動態物件隨著不斷的更新變數的狀態就能呈現動態畫面, 而靜態物件因為永遠不作更新,自然看起來就是靜止畫面!

還有一種部件類型是  SingleChildRenderObjectWidget, 稱為獨顯類型, 是一種可以在 Canvas 直接作畫的類型, 此類類型要先繼承自作畫類型 CustomPainter, 透過參數 painter 或是 foregroundPainter (前景作家), 傳給建構式 CustomPaint 產生部件, 後續就會呼叫裡面的方法 paint 直接作畫, CustomPaint 建構式用 size 參數(包含長與寬)設定 canvas 的長跟寬, 或是繼承自 child 參數所設定的 size(包含長與寬).

一些常用部件建構式:
Text 部件用來包裝文字串成為單一部件, 甚至加上參數還能做一些特效, 例如:
              Text("這是文字串",  style:  TextStyle(fontSize: 48) )
Row 部件用來包裝部件陣列 <Widget>[ ] 成單一部件, 參數是 children, 將會佔據螢幕一整列(螢幕橫軸 width 拉至最大), 佔據長度(蹤軸 height)依據部件列表裡面長度取最大值,  陣列裏面每個部件依序放在不同行顯示, 若是文字, 預設是擺在該行的中間. 例如:
              Row( children: [
                     Text("這是第 1 行"),
                     Text("第 2 行"),
                     Text("行")
              ])
Column 部件用來包裝部件陣列 <Widget>[ ] 成單一部件, 參數是 children, 將會來佔據螢幕一整行(螢幕蹤軸 height 拉至最大), 佔據寬度(橫軸 width)依據部件列表裡面寬度取最大值,  陣列裏面每個部件依序放在不同列顯示, 若是文字, 預設是擺在當列的中間. 例如:
               Column( children: [
                    Text("這是第 1 列"),    Text("第 2 列"),    Text("列")
               ])
Center  部件用來包裝單一部件, 參數是 child, 會將子部件擺至自由度不受限的中間, 當 child 指定用 Center 部件去包裝文字部件時, 因為子部件只是單純文字部件, 文字的 height 及 width 自由度並不設限, Center 就會儘可能往外擴展, 於是就佔據了整個螢幕(width * height)空間, 而文字部件就放在正中間, 因此要將整個螢幕當畫布可以這樣用:
         CustomPaint(
               painter: Logo( ),
               child    : Center(child: Text("Logo"))
         )
上述是先作畫, 接著貼上子部件, 換句話說就是背景作畫. 若要用前景作畫, 則需改用參數 foregroundPainter 先畫子部件, 接著在作畫蓋掉子部件(如果作畫的座標一樣的話):
         CustomPaint(child: Center(child: Text("Logo")), foregroundPainter: Logo( ))
要注意的是:如果要將 Column 部件包在  Center 裏面時, 意味是將 Column 部件擺中間而已並不會去調整裡面子部件的位置, Column 的寬度是不受限的, 因此整行看起來是放在螢幕橫軸中間, 但若將 Row 部件包在  Center 裏面時, 意味是將 Row 部件擺至中間而已,也不會去調整裡面子部件的位置, Row  的長度是不受限的, 因此整列看起來是放在螢幕蹤軸中間. 底下是完整範例:
main( ) => runApp(MyApp( ));
class MyApp extends StatelessWidget { // 靜態部件類型,  將產生根物件
      @override build(BuildContext _)  => MaterialApp(  // 根部件
           theme : ThemeData.dark( ),
           home  :  HomePage( )
     );
}
class HomePage extends StatelessWidget  { // 靜態部件類型
      @override build(BuildContext  _)  => Scaffold (  // 主幹部件
           appBar: AppBar(title: Text("關於")),
           body    : CustomPaint(painter: Logo( ),  child: Center(child: Text("這是 Logo")))
       );
}
class   Logo  extends CustomPainter { // 作畫類型
     final _pen = Paint( ) .. color  = Colors.red;
     @override shouldRepaint(Logo _) => _pen != _._pen;
     @override paint (Canvas canvas, Size size)  { // 直接用 paint 方法在 Canvas 作畫
         final center = Offset(size.width/2, size.height/2); // 畫筆至中心點
         final radius = size.width/2; // 圓半徑
         canvas.drawCircle(center,  radius, _pen); // 畫圓
     }
}
因為建構根部件時用不到 context , MaterialApp 本身是一個根部件建構式, 根物件也是物件之一, 因此可以簡單這樣用, 省去一個 StatelessWidget:
main( ) => runApp(MaterialApp(theme : ThemeData.dark(),  home  :  HomePage( ));
class HomePage extends StatelessWidget  { // 靜態部件類型
      @override build(BuildContext  _)  => Scaffold (  // 主幹部件
           appBar: AppBar(title: Text("關於")),
           body    : CustomPaint(painter: Logo( ),  child: Center(child: Text("這是 Logo")))
       );
}
class   Logo  extends CustomPainter { // 作畫類型
      final _pen = Paint( ) .. color  = Colors.red;
     @override shouldRepaint(Logo _) => _pen != _._pen;
     @override paint (Canvas canvas, Size size)  { // 直接用 paint 方法在 Canvas 作畫
         final center = Offset(size.width/2, size.height/2); // 畫筆至中心點
         final radius = size.width/2; // 圓半徑
         canvas.drawCircle(center,  radius, _pen); // 畫圓
     }
}
或者乾脆刪除 StatelessWidget,  作畫程式變得更簡單:
void main() =>  runApp(
       MaterialApp( theme : ThemeData.dark(),  home: Scaffold (      
           appBar : AppBar(title: Text("關於")),
           body     : CustomPaint(painter: Logo( ),  child: Center(child: Text("這是 Logo")))
       ))
);
class   Logo  extends CustomPainter { // 作畫類型
      final _pen = Paint( ) .. color  = Colors.red;
     @override shouldRepaint(Logo _) => _pen != _._pen;
     @override paint (Canvas canvas, Size size)  { // 直接用 paint 方法在 Canvas 作畫
         final center = Offset(size.width/2, size.height/2); // 畫筆至中心點
         final radius = size.width/2; // 圓半徑
         canvas.drawCircle(center,  radius, _pen); // 畫圓
     }
}

沒有留言:

張貼留言

使用 pcie 轉接器連接 nvme SSD

之前 AM4 主機板使用 pcie ssd, 但主機板故障了沒辦法上網, 只好翻出以前買的 FM2 舊主機板, 想辦法讓老主機復活, 但舊主機板沒有 nvme 的界面, 因此上網買了 pcie 轉接器用來連接 nvme ssd, 遺憾的是 grub2 bootloader 無法識...