0%

Flutter 筆記(三) - 動畫與頁面路由

前言

本篇記錄了 Flutter 動畫機制的介紹以及有別其他 UI 針對動畫所發展出的一套狀態管理機制。最後也會探討到頁面路由的各項管理機制的差別。

Animation In Flutter

第一個動畫

  • 讓箱子的兩邊可以拍打
  • 停止拍打時, 貓咪垂直往上跑出

動畫的各種Widget

Animation AnimationController Tween AnimatedBuilder 是四個不同的組件分別執行不同的功能:

  1. AnimatedBuilder

    • 最常被用到的 widget
    • 需要一個 Animation widget 實體和一個 builder function
    • 當每一次動畫發生改變時就調用 builder function 來更新在我們裝置上的 widget
    • 該 widget 非常類似 StreamBuilder
  2. Animation

    • 記錄”要動畫”的屬性的當前
    • 記錄動畫的狀態(正在跑, 已停止, 等等)
  3. AnimationController

    • 主要負責則控制動畫的狀態 (開始, 暫停, 重新…)
    • 記錄動畫要執行多久 duration of animation
    • 通知 Animation需要改變 (TickerProvider)
  4. Tween - beTween

    • 描述動畫的所會跨越的範圍

Why StatefulWidget For Animation ?


為什麼用StatefulWidget而不是用Bloc來管理widget的狀態和數據呢?

  1. Bloc 就之前的學習經驗上我們可以得知他的用途 :

    • 讓不同的 widget 共享相同的資訊
    • 集體管理各個 widget 所需用到的數據和狀態以及業務邏輯使其與介面分離
  2. Animation 當然可以使用 Bloc 來操作但為啥沒有 :

    • 動畫基本上他所需要的狀態和數據並不會影響其 widget
    • Bloc 需要大量的模板程式碼會過於複雜, 對於不需要使用到它上述功能的 widget 來說反而更不方便
    • StatefulWidget 在操作動畫上會更為靈活和彈性好多 (他只在乎當下該 widget 的 state)


重新渲染的盲點

  • 動畫

    • State 來儲存不想隨著 widget 更新而丟棄的數據. 在整個 widget 生命中期間持續存在被使用著
    • 不用 setState() 來重新渲染 widget , 而是用 AnimatedBuilder 來重新渲染畫面
  • 一般使用

    • Bloc 來儲存各個 widget 所可能交互使用的數據, 以 StreamController 的概念加以維護控制
    • 因為不用 State 來做, 所以 widget 都用 StatelessWidget 就行了. 至於重新渲染方面就交給 StreamBuilder 來達成
  • 結論

    • 重新渲染方面都交由 XxBuilder 來完成就行了

Animation Development Details

動畫實例開發:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import 'package:flutter/material.dart';
import '../widgets/cat.dart';

class Home extends StatefulWidget {
@override
_HomeState createState() => _HomeState();
}

// TickerProvider 給widget外面的世界一個識別證可以進來該widget
//, 為了通知AnimationController更新Animation
class _HomeState extends State<Home>
with TickerProviderStateMixin {
// 這兩個變數會在widget整個生命中持續存在被持有
Animation<double> catAnimation;
AnimationController catController;

// 1. 當State第一次被創建時會調用這個方法
// 2. 只有繼承State的widget才有該方法
// 3. 用來初始化實體變數
@override
void initState() {
// TODO: implement initState
super.initState();

catController = AnimationController(
duration: Duration(seconds: 1), // 該動畫會持續多久
vsync: this, // 代表嵌入TickerProvider到當前運行的widget實體
);

catAnimation = Tween(begin: 0.0, end: 200.0)
.animate(CurvedAnimation(
// 該widget用來描述, 動畫數值從begin到end的比率有多快
parent: catController,
curve: Curves.easeIn,
));

}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Animation!'),
),
body: GestureDetector( // 手勢偵測器
child: buildAnimation(), // 偵測該子widget上使用者的手勢
onTap: onTap,
),
);
}

Widget buildAnimation() {
return AnimatedBuilder(
animation: catAnimation,
child: Cat(), // a really expensive widget to create
builder: (context, child) {
return Container(
// inexpensive widget to create
child: child,
margin: EdgeInsets.only(top: catAnimation.value),
);
},
);
}

void onTap() {
catController.forward();
}
}


AnimatedBuilder

  • 兩個問題的探討 :

    1. XxBuilder 的概念是什麼? - 就 StreamBuilder 經驗來看

      • 監控隨時會改變的東西 Stream/Animation
      • 一有改變就呼叫 builder function , 裡面返回新的要重新渲染的 Widget
    2. 為什麼需要 child 屬性??

      • child 負責持有重新渲染會造成很大負擔的 widget 的參考
      • 在調用 builder function 時將 child 參考傳入參數.
      • builder function 去更新重建不昂貴的 widget

AnimationController

  • vsync: this 的意義?? - 動畫運行的機制
    1. StreamController 設定完動畫的持續時間並不會無中生有的去驅動提醒動畫要改變數值或是要更新了
    2. vsync: this 在這裡的用途就是讓當前已經mixin TickerProviderStateMixin 的 widget 擁有TickerProvider, 再用 this 傳入 AnimationController.
    3. TickerProvider 會提供 Ticker , 可以想像成一個 鉤子hook, 他用來提供該 Widget(這裡是Home widget)以外的世界,一個可以進入該 widget 來通知需要更新 frame 的管道

Tween & Animation

  • Tween : 可以看成是製作動畫的前置作業
    • 設定動畫數值的範圍 - begin & end
  • Aniamtion : 描述動畫的主體
    • begin —–> end 變動的比率有多快

Flutter Animation 的小小觀察

  1. 從頭至尾, 所有的動畫程式碼都圍繞在處理數值變動
  2. Flutter 動畫並不關心也不知道動畫最後會應用到哪一個 Widget
  3. 動畫並不會去綁定特定 Widget , 反之亦然.
  4. 動畫和 widget 沒有任何關係

API Performing Strategy

Fetching data

  • Repository
    1. 當作 App 與資料來源中間的媒介
    2. UI 不能也不需要跟資料來源溝通

Architecture of the App

Testing with dart

Testing 的用途

  • 如果檔案後面沒有後綴 _test 測試時會找不到

  • 每一個 test file 都有一個 main()

  • 都是獨立的程式

  • 方便進行單元測試, 不用啟動整個 app


對於網路的測試

有兩個理由為什麼不直接真實的 http request :

  1. 做單元測試本來就是耗時的工作, 如果加上網路的延遲會造成極大的負擔
  2. 真實 API 的異動是很大的. 往往會造成測試的不方便

解法 :

  • 使用 http 套件裡面的 testing.dart, 裡面有模仿 http request 的功能

Offline Data Storage In Flutter

(my project structure)

  1. init

    • 該方法用來對 DB 進行初始會以及連結的設定
    • 通常初始化都在建構子
    • 跟 DB 有關的都是異步 asynchronous
    • 建構子不能有異步
  2. fetchItem

    • query 返回的類型一定是 List<Map<String, dynamic>>

Refactor Repository By Abstract Class


重構概述

利用abstract class來重構現有App的架構


為什麼要用抽象類別在 Dart 中

  1. 抽象出不同類之間共同的特性變成另一個類
  2. 增加程式碼的重用率

Navigation In Flutter


Navigation 概述

  • 主要負責決定要在裝置的螢幕上顯示甚麼頁面
  • route 就是 screen

  • route 是由 Navigator 所控制的

  • Navigator 提供管理 route 物件堆疊的方法

  • new 一個 MaterialApp 時, Navigation 就被實例化供我們使用了


設定初始頁面

設定初始頁面的方式

  1. home 屬性: 物件(Widget)

    1
    2
    3
    4
    5
    6
    7
    8
    void main() {
    runApp(MaterialApp(
    home: MyHomePage(),
    .
    .
    .
    ));
    }
    • 不一定要設定 routesonGenerateRoute 屬性(通常會需要,因為隨著 App 複雜性的增高,統一管理頁面是比較恰當的) ; 反之,initialRoute 就必須要設定
  2. initialRoute 屬性: 路徑

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void main() {
    runApp(MaterialApp(
    initialRoute: '/',
    routes: <String, WidgetBuilder>{
    '/': (context) => HomePage(),
    '/second': (context) => SecondHome()
    }
    .
    .
    .
    ));
    }
    • 如果使用 initialRoute 就必須要設定 routes 或是 onGenerateRoute 屬性
    • Flutter 預設路徑是 **’/‘**,因此如果是用預設路徑可以不需要加 initialRoute: '/'
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      void main() {
      runApp(MaterialApp(
      routes: <String, WidgetBuilder>{
      '/': (context) => HomePage(),
      '/second': (context) => SecondHome()
      }
      .
      .
      .
      ));
      }
      • 也可以不用預設路徑自己命名

事實上,homeinitialRiute 是一樣的,不過是一些設定上的差異而已

MaterialApp會依序檢查三件事情

  1. 有沒有一個 Widget 被指派到 home 屬性

  2. 有沒有一個 map 被指派到 routes 屬性

    • Routes Table : 用 map 來記錄相對應的頁面
      Routes Name PageBuilder That Produces
      / NewsList
      /details NewsDetail
      • 優點 : 很輕易的可以作業面的轉換
      • 缺點 : 頁面間的資訊供通傳送會變得很複雜
  3. 是否有 onGenerateRoute callback function

    • callback function 傳到 MaterialApp
    • 決定哪一個頁面要被顯示

routesonGenerateRoute callback 的不同


使用home屬性

home 屬性變成 Navigator 堆疊底部的 route

1
2
3
void main() {
runApp(new MaterialApp(home: new MyAppHome()));
}

為了加入新的 route(頁面) 在 stack 上面, 必須要先創建 MaterialPageRoute 實體. 它含有 builder function 可以 build 出畫面在 screen 上面.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Navigator.push(context, new MaterialPageRoute<void>(
builder: (BuildContext context) {
return new Scaffold(
appBar: new AppBar(title: new Text('My Page')),
body: new Center(
child: new FlatButton(
child: new Text('POP'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
},
));