AllenHsu的技術手扎

學海無涯,努力做個永遠年輕的人

最近在 Telegram Dev Channel 中蠻常看到有人提出這幾種問題,如果我在 TON 上發送了一筆交易,那麼我要如何知道這筆交易的結果呢?或者是使用 TonClient 發送交易後,TonClient 回傳的結果裡面的 Boc 又是什麼,我如何取得更有用的資訊呢?這篇文章就來解答這些問題,讓你更好的了解 TON。

TON 的 Transaction

在一開始我們先理解在 TON 的 Transaction 是什麼,TON 的 Transaction 包含了以下幾個部分:

  • 觸發合約的初始傳入訊息(存在特殊的觸發方式)
  • 傳入訊息引起的合約行為,例如對合約存儲的更新(可選)
  • 發送給其他參與者的生成輸出訊息(可選)

img

從上圖可以得知,如果我要發起一筆 TON 轉帳交易,本質上就是傳送一個 Message 給錢包合約,錢包合約根據這個 Message 內容做出相應的轉帳操作。

準備交易的 Message 內容

使用 TonConnect 發送交易前,我們需要定義有效的 Message 內容,使用 JS 版本的 TonConnect SDK 時,可以看到傳入的交易參數如下:

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
export declare interface SendTransactionRequest {
/**
* Sending transaction deadline in unix epoch seconds.
*/
validUntil: number;
/**
* The network (mainnet or testnet) where DApp intends to send the transaction. If not set, the transaction is sent to the network currently set in the wallet, but this is not safe and DApp should always strive to set the network. If the network parameter is set, but the wallet has a different network set, the wallet should show an alert and DO NOT ALLOW TO SEND this transaction.
*/
network?: CHAIN;
/**
* The sender address in '<wc>:<hex>' format from which DApp intends to send the transaction. Current account.address by default.
*/
from?: string;
/**
* Messages to send: min is 1, max is 4.
*/
messages: {
/**
* Receiver's address.
*/
address: string;
/**
* Amount to send in nanoTon.
*/
amount: string;
/**
* Contract specific data to add to the transaction.
*/
stateInit?: string;
/**
* Contract specific data to add to the transaction.
*/
payload?: string;
}[];
}

所以當我今天想要做一筆簡單的傳送交易時,我只需要構建下列的交易參數即可

1
2
3
4
5
6
7
8
9
10
11
const transaction = {
// The transaction is valid for 10 minutes from now, in unix epoch seconds.
validUntil: Math.floor(Date.now() / 1000) + 600,
messages: [
{
address:
"0:412410771DA82CBA306A55FA9E0D43C9D245E38133CB58F1457DFB8D5CD8892F", // destination address
amount: "20000000", //Toncoin in nanotons
},
],
};

如果我需要一個附上 Comment 的交易,在 TON 中,對於特定的 Custom Transaction,必須定義特定的 Payload,這個 Payload 必須是一個由 Cell 組成,並轉成 Base64 格式的字串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { beginCell } from "@ton/ton";

const body = beginCell()
.storeUint(0, 32) // write 32 zero bits to indicate that a text comment will follow
.storeStringTail("Hello, TON!") // write our text comment
.endCell();

const transaction = {
// The transaction is valid for 10 minutes from now, in unix epoch seconds.
validUntil: Math.floor(Date.now() / 1000) + 600,
messages: [
{
address:
"0:412410771DA82CBA306A55FA9E0D43C9D245E38133CB58F1457DFB8D5CD8892F", // destination address
amount: "20000000", //Toncoin in nanotons,
payload: body.toBoc().toString("base64"), // payload with comment in body
},
],
};

更多的 Message 類型定義與參考,可以參考 Message Build

發送交易後的結果

接下來進入我們的主題,發送交易後,我要怎麼等待交易完成,得到最後的 Transaction Result 呢?
首先我們先看一下我們使用 TonConnect 發送交易後,回傳的結果

1
2
const [tonConnectUi] = useTonConnectUI();
const result = await tonConnectUi.sendTransaction(transaction);

這個 sendTransaction 函數會回傳一個 SendTransactionResponse 物件,這個物件包含了一個 boc 字串

1
2
3
4
5
6
export declare interface SendTransactionResponse {
/**
* Signed boc
*/
boc: string;
}

這個 boc 其實就是我們前面提到的,他是一個 觸發交易傳入的初始內容,這個 boc 會被用來發送到 TON 網路上,進行交易的驗證與執行。在這中間我們需要等待交易完成,這時候我們可以使用 TonClient 來進行交易的查詢。並且需要去解析目前最新 Transaction 中他的 InMessage 的 Base64 是否與我們發送的 boc 相同,如果相同,則代表這筆交易是我們發送的交易,代表這筆交易已經完成,我們也可以取得這筆交易的內容,包含 TxHash 等。

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
const waitForTransaction = async (
options: WaitForTransactionOptions,
client: TonClient
): Promise<Transaction | null> => {
const { hash, refetchInterval = 1000, refetchLimit, address } = options;

return new Promise((resolve) => {
let refetches = 0;
const walletAddress = Address.parse(address);
const interval = setInterval(async () => {
refetches += 1;

console.log("waiting transaction...");
const state = await client.getContractState(walletAddress);
if (!state || !state.lastTransaction) {
clearInterval(interval);
resolve(null);
return;
}
const lastLt = state.lastTransaction.lt;
const lastHash = state.lastTransaction.hash;
const lastTx = await client.getTransaction(
walletAddress,
lastLt,
lastHash
);

if (lastTx && lastTx.inMessage) {
const msgCell = beginCell()
.store(storeMessage(lastTx.inMessage))
.endCell();

const inMsgHash = msgCell.hash().toString("base64");
console.log("InMsgHash", inMsgHash);
if (inMsgHash === hash) {
clearInterval(interval);
resolve(lastTx);
}
}
if (refetchLimit && refetches >= refetchLimit) {
clearInterval(interval);
resolve(null);
}
}, refetchInterval);
});
};

首先裡面的 getContractState 是為了獲取最後一筆的 txHash 以及 txLt,接著使用 getTransaction 去查詢最後一筆的交易內容,並且解析這筆交易的 InMessage,取得 InMessage 的 Hash,最後比對這個 Hash 是否與我們發送的交易相同,如果相同,則代表這筆交易已經完成,我們可以取得這筆交易的內容。當然你也可以使用其他方法,像是 getTransactions 等方式,去監聽鏈上交易的變化,然後根據最新結果的 InMessage 去比對是否是我們發送的交易。

結論

相關代碼範例可以參考 Github,這個是我 fork 官方 tonconnect sdk 的範例之上做了修改,Demo 可以訪問 Demo

Ton 身為一個以消息通訊為基礎的區塊鏈,且是異步的,所以我們在實作相關 dApp 的功能時,需要以一個不同於之前開發以太方的思維去思考,這篇文章希望能夠幫助你更好的了解 TON 的交易機制,以及如何獲取交易後的結果。

什麼是 TON Blockchain

TON 早期是由 Telegram 開發的一個區塊鏈項目,2018 年由 Telegram 的創辦人 Pavel 和 Nikolai Durov 兄弟探索適用 Telegram 的區塊鏈技術,但他們發現當時的 L1 區塊鏈,並沒有辦法滿滿足支撐
Telegram 的億級用戶群,所以他們開始設計自己的 L1 區塊鏈,並且將其命名為 TON(Telegram Open Network)。當時出售 TON 的代幣 GRAM(一開始代幣的名字),也是一度成為了 ICO 的熱門項目。並募集了 17 億美元,
2019 年,TON 的測試網如期上線,但美國 SEC 認為 TON 是一種證券,且初期有 10 億的代幣向美國的投資者出售,違反了美國的證卷法,所以 Telegram 被迫取消了 TON 的項目。但 TON 並沒有因此而消失,
而是改用社群的方式繼續運作下去,由核心開發者 Tolya 帶領,並將 TON 的開發工作委託給了 Telegram 的 Open Source 社群。

更多的關於 TON 的歷史可以參考這篇文章:TON 歷史和路線圖

TON 的挖礦歷史

由於上面的轉變,在 2020 年 6 月,經過社群的決策,決定使用傳統的 PoW 模式進行代幣分發,由於 TON 區塊鏈的底層是使用了 PoS 的共識機制,所以這次的 PoW 分發是為了讓更多的人參與到 TON 的生態中。為了
實現這一個目標,核心團隊在 TON 上開發了一個名為 Giveaway 的智能合約,這個智能合約可以讓用戶通過 PoW 的方式來獲得 TON 代幣。初始流動數量為 50 億顆,可流通的 98.55% 開放挖礦,剩下的 1.45%
用於團隊和社群建設。最後的一顆 TON 已經在 2022 年 6 月挖出,這也意味著 TON 的 PoW 挖礦已經結束。

TON 的技術架構特點

在講述 TON 的技術架構之前,我們可以先看 TON 的幾個區塊鏈關鍵指標 :

  • TON 的出塊時間 : 2-5s
  • TON 的 TPS : 100k (是的,你沒看錯,100K,這是一個非常驚人的數字)
  • TON 的 平均區塊大小 2mb
  • TON 的 區塊確認時間 < 6s (只需要一個區塊確認)

更多的 Performance 指標可以參考這個網站:TON Performance Test

這些指標讓 TON 在技術上有了很大的優勢,這也是為什麼 TON 能夠支撐 Telegram 這樣的大型應用的原因。那麼 TON 是如何實現這些指標的呢?這就要從 TON 的技術架構來講起了。
首先我們可以先看 TON 白皮書內提到的與其他區塊鏈的比較 :

Project Year G. Cons. Sm. Ch. R. Sh. Int.
Bitcoin 2009 1 PoW no 1
Ethereum 2013, 2015 2 PoW yes 1
NXT 2014 2+ PoS no 1
Tezos 2017,? 2+ PoS yes 1
Casper 2015, (2017) 3 PoW/PoS yes 1
BitShares 2013, 2014 3’ DPoS no m ht. no L
EOS 2016, (2018) 4 DPoS yes m ht. no L
PolkaDot 2016, (2019) 4 PoS BFT yes m ht. no L
Cosmos 2017, ? 4 PoS BFT yes m ht. no L
TON 2017, (2018) 5 PoS BFT yes m mix dyn. T

G. - Generation, (世代)
Cons. - Consensus, (共識演算法)
Sm. - Smart contracts, (是否具備圖靈完善的智能合約)
Ch. - Sharding, (單鏈/多鏈/混合架構)
R. - Rollups, (同質/異質架構)
Sh. - Sharding, (是否支持分片)
Int. - Interoperability, (區塊鏈間交互, L 鬆耦合, T 緊耦合)

TON 本身在區塊鏈擴容機制上,採用了一種稱做無限分片的機制,這種機制可以讓 TON 的區塊鏈在需要的時候,可以無限的擴容,分片其實是來自資料庫設計的概念,將一個超大的 Dataset 分割,並且分布在多個不同的資料庫當中,實現水平擴展的能力。TON 在區塊鏈分片上,分為下列四種類型的分片鏈:

  • MasterChain: TON 上的主鏈,用來儲存基礎訊息,主要包含協議的參數、驗證者集合,以及對應的分片鏈的資訊。紀錄當前活躍的 WorkChains 和它們的分片 (shards) 組,以及最重要的是所有 WorkChains 和 ShardChains 裡最新的區塊 Hash,這條鏈是唯一一條,且確定了系統的全局狀態
  • WorkChain: 最多可達 2^32 條,但目前只有一條,理論上每個人可以構建自己的 WorkChain,不同的 WorkChain 可以有不同的規則,不同的帳戶格式、交易格式、VMs。
  • ShardChain: 最多可達 2^60 條,是屬於 AccountChain 的集合
  • AccountChain: 最低層級,按理想情況,每個帳戶都有自己的 ShardChain,但實際上,一個 ShardChain 可能包含多個帳戶。AccountChain 則包含帳戶一系列的交易。

img

TON 錢包

在 TON 上面的錢包帳戶,跟傳統以太坊的 EOA 帳戶非常不一樣,在 TON 上面,每個錢包都是一個智能合約,這代表著 TON 的錢包帳戶一開始就支持了帳戶抽象化,同一套註記詞可以根據不同合約類型建立不同的帳戶,根據合約類型不同,可以支持不同的版本,當前最新的版本為 Wallet V5 (Beta),由 TonKeeper 推出,下列是幾個不同版本的錢包合約比對

錢包合約版本 功能
Wallet V1 一次只能發送一筆交易
Wallet V2 加入交易時間限制,防止交易長時間未被確認
Wallet V3 加入 SubWalletId 功能,可以創建子錢包
Wallet V4 加入插件和訂閱功能,且兼容 V3 所有功能
Wallet V5(Beta)
  • 減少 25% gas fee
  • 類似 AA 錢包可讓第三方支付 gas (目前 USDT 轉帳減免)
  • 更靈活的 plug interface
  • TON 智能合約地址格式

    TON 的帳戶地址格式基本上分為兩種類型,一種稱為用戶友好類型,另外一種則是原始格式的類型

    • 用戶友好: EQDKbjIcfM6ezt8KjKJJLshZJJSqX7XOA4ff-W72r5gqPrH
    • 原始格式: 0:ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e

    在原始格式的部分,由工作鏈 ID 和帳戶 ID 組成
    [0] : 工作鏈 ID
    [ca6e321c7cce9ecedf0a8ca2492ec8592494aa5fb5ce0387dff96ef6af982a3e] : 帳戶 ID

    原始地址無法在發送交易前驗證地址,所以 TON 有了用戶友好的地址 (但其實對於使用者來說也還是一段亂碼)。但用戶友好地址提供地址驗證功能,可以很好的防止用戶輸入不合法的地址,跟信用卡使用的算法很像,防止用戶輸入不合法的卡號,更多細節可以參考這篇文章:TON Address

    其中用戶友好地址最終要的其實是第一個標誌位,開頭的第一個符號,可以代表智能合約收到消息的反應,目前用戶友好地址格式的符號類型包括

    • isBounceable: 是否可以回彈,當目標智能合約不存在,或者交易過程中發生某些問題,消息將被彈回給發送者,包含了扣掉 gas 費的資產部分
      isBounceable=false,通常意味著接收者是一個錢包
      isBounceable=true,通常意味著接收者具有自己應用邏輯的自定義智能合約(可能是 Dex)
    • isTestnetOnly: 表示僅用於測試網的類型,不應該被主網上的合約所接受
    isTestnetOnly isBounceable 地址類型 描述
    false true EQ.. 主網智能合約地址
    false false UQ.. 主網錢包合約地址
    true true KQ.. 測試網智能合約地址
    true false 0Q.. 測試網錢包合約地址

    總結

    上面從 TON 的發展歷史,到 TON 的技術特點以及部分技術細節做了一個簡單的通盤介紹,希望可以讓大家更能理解 TON 多一點,之後會深入探討更多 TON 的部分,包括 TON 的智能合約開發,TON 的錢包開發,以及 TON 的生態建設等等。

    TLDR

    這篇主要是介紹如何透過 GetIt 和 Injectable 兩個套件,來完成 Flutter Dependency Injection 依賴注入(簡稱 DI)的功能。 這邊不多說明 DI 的概念,如果有興趣可以參考之前寫的文章 Android 的依賴注入框架-Dagger2(一)。有稍微提到什麼是 DI。

    在過往 Android 開發中,一開始早期使用 Dagger2,到後來的 Koin,以及最近的 Hilt,所以在這邊也會把做法以及相似的部分做一個比較,也能讓 Android 開發者更容易理解。

    以下我們會用一個簡單的例子來說明如何使用 GetIt 和 Injectable 來完成 DI 的功能。

    GetIt

    首先可以到 GetIt 的網站 GetIt 去將 GetIt 套件安裝至專案中,這邊不做多餘的贅述。GetIt 是一個簡單的 Service Locator,可以用來管理依賴的實例,這邊先來看一下 GetIt 的基本用法。

    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
    import 'package:get_it/get_it.dart';

    final getIt = GetIt.instance;

    void setup() {
    <!-- DI provider -->
    getIt.registerSingleton<CounterService>(CounterService());
    }

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

    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {

    final counterService = getIt<CounterService>();
    return MaterialApp(
    home: Scaffold(
    body: Center(
    child: Text(counterService.count.toString()),
    ),
    ),
    );
    }
    }

    這邊我們先註冊一個 CounterService 的實例,然後在 Appbuild 方法中,透過 getIt<CounterService>() 來取得 CounterService 的實例,這樣就完成了一個簡單的 DI。

    在這個簡單的例子中,可以想像一下我們的 setup 是一個 DI Provider,在 App 初始化之前提供了許多我們需要的物件實例,然後在 App 初始化的時候,透過 getIt 來取得這些實例,讓我們的代碼可以更加地解除耦合,那 get_it 也提供了更多的功能,像是我們在範例中,使用 registerSingleton 來註冊一個 Singleton 的實例,但有時候我們不是一開始就需要這些物件實例,而是希望等到後續有相關的業務代碼呼叫到的時候再去實例化,這時候我們可以使用 registerLazySingleton 來註冊一個 Lazy Singleton 的實例。

    但是你會發現,當我們如果有很多的依賴,這樣的寫法會讓我們的 setup 方法變得很大,所以這時候我們可以使用 Injectable 來幫助我們更好的管理我們的依賴。這邊也會將加入 Injectable 來改寫上面的例子,以及比較一下跟 Koin, Heilt 的差異。

    Injectable

    Injectable 是一個 透過 Annotation 來生成 GetIt 的 相關代碼,讓我們可以更好的管理我們的依賴,這邊先來看一下 Injectable 的基本用法。

    首先可以到 Injectable 的網站 Injectable 去將 Injectable 套件安裝至專案中,這邊不做多餘的贅述。

    首先我們建立一個 injection.dart

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import 'package:injectable/injectable.dart';
    import 'inject.config.dart';

    final getIt = GetIt.instance;

    @InjectableInit(
    initializerName: 'init',
    preferRelativeImports: true,
    asExtension: false,
    )

    Future<void> configureDependencies() => $init(getIt);

    然後我們建立一個 service_module.dart

    1
    2
    3
    4
    5
    6
    7
    import 'package:injectable/injectable.dart';

    @module
    abstract class ServiceModule {
    @singleton
    CounterService get counterService => CounterService();
    }

    接下來其實只要執行一下 flutter pub run build_runner build 就可以生成相關的代碼,然後我們就可以在我們的 App 中使用了。

    1
    2
    3
    4
    5
    void main() async {
    WidgetsFlutterBinding.ensureInitialized();
    await configureDependencies();
    runApp(MyApp());
    }

    生成後的代碼我們可以發現,在 injection.config.dart 中,會生成一個 GetItInjectableX 的 Extension,這個 Extension 會幫助我們初始化我們的依賴。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    extension GetItInjectableX on _i1.GetIt {
    // initializes the registration of main-scope dependencies inside of GetIt
    Future<_i1.GetIt> init({
    String? environment,
    _i2.EnvironmentFilter? environmentFilter,
    }) async {
    final gh = _i2.GetItHelper(
    this,
    environment,
    environmentFilter,
    );
    final serviceModule = _$ServiceModule();
    gh.singleton<_i3.CounterService>(serviceModule.counterService);
    return this;
    }
    }

    class _$ServiceModule extends _i7.ServiceModule {}

    熟悉 koin 跟 Hilt 的朋友應該會發現,這邊的 @module@singletonkoin / Hlit 的 provider module 實作非常類似。

    koin

    1
    2
    3
    4
    5
    6
    7
    8
    9
    val myModule = module {
    single { CounterService() }
    }

    fun main() {
    startKoin {
    modules(myModule)
    }
    }

    Hilt

    1
    2
    3
    4
    5
    6
    7
    @Module
    @InstallIn(SingletonComponent::class)
    object ServiceModule {
    @Singleton
    @Provides
    fun provideCounterService() = CounterService()
    }

    由上面的案例可能不覺得 Injectable 帶來什麼好處,那我們接下來來看一下這個複雜一點的案例,如果我們只使用 GetIt,我有一個 ServiceA 以及 ServiceB,然後 ServiceB 依賴 ServiceA,這時候我們可以這樣寫。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    class ServiceA {}

    class ServiceB {
    ServiceB(ServiceA serviceA);
    }

    void setup() {
    getIt.factory<ServiceA>(() => ServiceA());
    getIt.factory<ServiceB>(ServiceB(getIt<ServiceA>()));
    }

    如果今天有更多依賴,我們必須手動去維護這些關係在 getIt 的 setup 中,透過 Injectable,可以透過註解快速的生成這些依賴關係。

    1
    2
    3
    4
    5
    6
    7
    8
    @module
    abstract class ServiceModule {
    @singleton
    ServiceA get serviceA => ServiceA();

    @singleton
    ServiceB getServiceB(ServiceA serviceA) => ServiceB(serviceA);
    }

    其他 Injectable 還有更多類似 Koin/Hlit 的功能,像是 Binding abstract class to implementation class、name、scope 等相關功能, 這邊就不多做贅述,有興趣的朋友可以參考 Injectable 的官方文件。

    以上就是一些在 Flutter 中使用 GetIt 以及 Injectable 來完成 DI 的功能,這邊也有提到一些跟 Android 開發中的 DI 框架的比較,希望這篇文章對大家有所幫助。

    TLDR

    最近想要打造一些自己的小 App,由於工作上使用的是 React Native,之前又比較專注於 Android 開發,所以想要找一個可以跨平台的框架,在 Flutter 剛釋出的時候有嘗試了一下,經過了多年 Flutter 變得穩定且有許多套件,嘗試了一下覺得開發體驗非常棒,這篇文章主要是想要分享一下我在學習 Flutter 過程中的一些心得,希望可以幫助到有興趣的人。

    Flutter 是什麼?

    Flutter 是 Google 開發的一個跨平台框架,可以讓開發者使用一套程式碼同時開發 Android 與 iOS App,這幾年更可以開發 Web 以及桌面 App, Flutter 使用 Dart 這個程式語言,Dart 是 Google 開發的程式語言,它的特色是 JIT(Just In Time) 與 AOT(Ahead Of Time) 編譯,這讓 Dart 在開發時可以快速的編譯,並且在發布時可以編譯成 Native Code,這讓 Flutter 在執行時有著非常好的效能。

    近幾年的 Mobile 與前端開發

    在近年的 Mobile 以及前端開發來說,從 React 到 Android 的 Compose ,以及 iOS 的 SwiftUI,
    還有今天這篇文章所說的 Flutter,都從原本的 Xml、Xib、Storyboard 轉變成了一個以描述式 UI 或稱宣告式的 UI,讓我們可以很快速的將 UI 的元件用 Code 的方式描述出來,在下面貼下四個平台的 UI Code,讓大家可以感受一下。

    Android Compose

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Composable
    fun MyApp() {
    Column(
    modifier = Modifier
    .fillMaxSize()
    .background(Color.Blue)
    ) {
    Text(
    text = "Hello World",
    modifier = Modifier
    .padding(16.dp)
    .background(Color.Red)
    )
    }
    }

    iOS SwiftUI

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct ContentView: View {
    var body: some View {
    VStack {
    Text("Hello World")
    .padding(16)
    .background(Color.red)
    }
    .background(Color.blue)
    }
    }

    React Native

    1
    2
    3
    4
    5
    6
    7
    export default function App() {
    return (
    <View style={{ flex: 1, backgroundColor: "blue" }}>
    <Text style={{ padding: 16, backgroundColor: "red" }}>Hello World</Text>
    </View>
    );
    }

    Flutter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return Container(
    color: Colors.blue,
    child: Column(
    children: [
    Container(
    color: Colors.red,
    padding: EdgeInsets.all(16),
    child: Text('Hello World'),
    ),
    ],
    ),
    );
    }
    }

    大家可以發現,即使在不同平台上,都是用一樣的方式,宣告組件,用巢狀的方式來描述 UI,這讓我們可以很快速的將 UI 的元件用 Code 的方式描述出來,

    Flutter 環境安裝

    Flutter 也提供了非常強大的工具,以及良好的開發體驗,這點 React Native 真的跟屎一樣 (個人感受),以下是 Flutter 的環境安裝方式,可以在官方網站的說明參考一下 : Flutter 官方網站

    Flutter 可以選擇兩種 VSCode,以及 Android Studio 這兩種 IDE 來開發,我之前偏好 VSCode,因為他更輕量,但是在 Android Studio 升級到最新版本之後,整個 UI 變得更加的好用,所以我目前是使用 Android Studio 來開發 Flutter。這邊看個人喜好就好了

    進入正題 - Widget

    在 Flutter 中,所有的一切皆是 Widget, Widget 是 Flutter 的基本元件,所有的 UI 都是由 Widget 來組成的,Flutter 提供了許多的 Widget,我們可以透過這些 Widget 來建構我們的 UI,這邊先來看一下 StatelessWidget 與 StatefulWidget 的差異。

    StatelessWidget

    StatelessWidget 是一個無狀態的 Widget,這代表這個 Widget 在生命週期中不會改變,這個 Widget 通常用來顯示靜態的 UI,例如一個 Text Widget,一個 Icon Widget,這些 Widget 在生命週期中不會改變,這邊我們來看一下一個簡單的 StatelessWidget。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class MyApp extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
    return Container(
    color: Colors.blue,
    child: Column(
    children: [
    Container(
    color: Colors.red,
    padding: EdgeInsets.all(16),
    child: Text('Hello World'),
    ),
    ],
    ),
    );
    }

    StatefulWidget

    StatefulWidget 是一個有狀態的 Widget,這代表這個 Widget 在生命週期中會改變,這個 Widget 通常用來顯示動態的 UI,例如一個計數器,一個下拉更新的 Widget,這些 Widget 在生命週期中會改變,這邊我們來看一下一個簡單的 StatefulWidget。

    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
    class CounterApp extends StatefulWidget {
    @override
    _CounterAppState createState() => _CounterAppState();
    }

    class _CounterAppState extends State<CounterApp> {
    int _counter = 0;

    void _incrementCounter() {
    setState(() {
    _counter++;
    });
    }

    @override
    Widget build(BuildContext context) {
    return Scaffold(
    appBar: AppBar(
    title: Text('Counter App'),
    ),
    body: Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
    Text(
    'You have pushed the button this many times:',
    ),
    Text(
    '$_counter',
    style: Theme.of(context).textTheme.headline4,
    ),
    ],
    ),
    ),
    floatingActionButton: FloatingActionButton(
    onPressed: _incrementCounter,
    tooltip: 'Increment',
    child: Icon(Icons.add),
    ),
    );
    }
    }

    生命週期

    在 Android 開發中,不管是 Activity/Fragments/View 都有自己的生命週期,Flutter 也是一樣,每個 Widget 都有自己的生命週期,這邊簡單的介紹一下 StatelessWidget 與 StatefulWidget 的生命週期。

    StatelessWidget

    StatelessWidget 是一個無狀態的 Widget,代表狀態創建後沒有改變,所以它的生命週期只有一個 build 方法,這個方法是用來建立 Widget 的 UI,當這個 Widget 被繪製到畫面上時,這個方法就會被呼叫,這個方法會回傳一個 Widget,這個 Widget 就是這個 StatelessWidget 的 UI。

    StatefulWidget

    StatefulWidget 是一個有狀態的 Widget,代表狀態創建後會改變,所以他有相關的生命週期方法,更多詳細的介紹可以參閱官方文件,下面列出幾個比較重要的方法。

    • createState: 該函數為 StatefulWidget 創建一個 State 對象,當 StatefulWidget 被調用時,會立刻執行 createState。
    • initState: 該函數為 State 初始化調用,所以可以在這邊做變量的初始化等操作。
    • setState: 這個方法是用來通知 Flutter 這個 Widget 的狀態有改變,需要重新繪製,這個方法會觸發 build 方法。
    • didChangeDependencies: 當 State 對象的依賴發生變化時,會被調用。
    • build: 主要建立 StatefulWidget 的 UI,build 會被調用多次,所以要確保在該函數中只能返回 Widget UI 相關邏輯。
    • reassemble: debug 模式下, hot reload 時會被調用。所以在 debug 階段可以增加一些 debug code 來做一些排查。
    • didUpdateWidget: 當 Widget 重建時,會調用 Widget.canUpdate 來檢查 Widget Tree 中同一位置的新舊節點,來決定是否更新,如果
      Widget.canUpdate 返回 true,則會調用 didUpdateWidget。
    • deactivate: 當 Widget 被移除時,會被調用,如果該 Widget 被移除,且未插入到新的 Widget Tree 中,則會調用 dispose。
    • dispose: 當 Widget 被銷毀時,會被調用,這個方法通常用來釋放一些資源,例如一些監聽器的移除。

    alt Lifecycle

    生命週期整個過程可以分成四個階段

    • 初始化階段 : createState -> initState
    • 組件構建階段 : didChangeDependencies -> build
    • 更新組件重建 : didChangeDependencies、setState 或者 didUpdateWidget 都會引發組件重繪
    • 組件銷毀 : deactivate -> dispose

    由於效能的關係,如果沒有需要狀態的變更,都是靜態資料,可以盡可能的使用 StatelessWidget,這樣可以減少不必要的重繪。StatefulWidget 盡量不要放在越上層級使用,只放在有需要變更狀態的地方使用,這點跟 React 是很相似的。

    結論

    這篇文章主要是想要分享一下我在學習 Flutter 過程中的一些心得,希望可以幫助到有興趣的人 Flutter 的 Widget 非常多,這邊只是簡單的介紹了 StatelessWidget 與 StatefulWidget 這兩個核心 Widget,未來會持續分享更多的心得,希望大家可以一起來學習 Flutter。

    Android View 渲染效能優化

    好久沒寫文章了,應該至少兩年多了,這兩年工作轉換了很多,現在又重回 Android 開發的行列,最近工作上剛好遇到需要優化一個頁面的渲染性能(解決卡頓問題),所以剛好透過這篇文章來覆盤跟總結一下 Android 性能優化

    造成 View 渲染緩慢的原因

    在這個 Case 中,有幾個非常常見的 View 卡頓原因,這邊就列出這幾個常見的 Case

    Layout 佈局過於複雜

    因為 inflate 需要花費的時間過久,如果你的 Layout 層次過於複雜的話,所以需要盡量減少 Layout 佈局的複雜度,善用ConstraintLayout去減少佈局的複雜度

    RecycleView 頻繁全局刷新

    這個蠻容易在開發過程中出現的一個錯誤,RecyclerView 主要的更新流程是透過

    GetData –> setAdapter –> 通知 RecyclerView 更新
    在通知 RecyclerView 更新這個環節有許多的方法

    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
    final void	notifyDataSetChanged()
    Notify any registered observers that the data set has changed.

    final void notifyItemChanged(int position, Object payload)
    Notify any registered observers that the item at position has changed with an optional payload object.

    final void notifyItemChanged(int position)
    Notify any registered observers that the item at position has changed.

    final void notifyItemInserted(int position)
    Notify any registered observers that the item reflected at position has been newly inserted.

    final void notifyItemMoved(int fromPosition, int toPosition)
    Notify any registered observers that the item reflected at fromPosition has been moved to toPosition.

    final void notifyItemRangeChanged(int positionStart, int itemCount, Object payload)
    Notify any registered observers that the itemCount items starting at position positionStart have changed.

    final void notifyItemRangeChanged(int positionStart, int itemCount)
    Notify any registered observers that the itemCount items starting at position positionStart have changed.

    final void notifyItemRangeInserted(int positionStart, int itemCount)
    Notify any registered observers that the currently reflected itemCount items starting at positionStart have been newly inserted.

    final void notifyItemRangeRemoved(int positionStart, int itemCount)
    Notify any registered observers that the itemCount items previously located at positionStart have been removed from the data set.

    final void notifyItemRemoved(int position)
    Notify any registered observers that the item previously located at position has been removed from the data set.

    但是這邊常犯的錯誤是直接呼叫 notifyDataSetChanged,這個會直接使整個 RecyclerView 全局刷新,但我們有時候獲得的資料只是一小部分的改動,所以我們應該根據當下情況去使用其他的 function,或者使用 Android 官方提供的DiffUtil去幫助做資料更新的比對

    ref : https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil

    局部更新造成全局更新

    這個在這個 case 上,很容易發生的是在動態改變 text 內容的 TextView 之上,假設今天有一個場景,是有一個倒數計時的 UI 呈現,那麼就會定時地去更新該 TextView,但是如果 TextView 的 width 跟 height 是wrap_content,那就會造成這個 TextView 有機會的去更新全局的 UI。

    onDraw, onMeasure, onLayout 時做了耗時操作

    這幾個關鍵函數,都應該只做他們的自我權責,不應該在這些 Function 裡面做不必要的耗時操作,尤其是 onDraw,這也會造成 UI 渲染的效能瓶頸

    頻繁 Add/Remove

    頻繁 Add/Remove 也是會觸發整個佈局的重新繪製,所以應該避免

    如何找出緩慢的貧頸

    上述就是常見的幾種錯誤使用方式,但是我們在開發的時候,可能會遇到難以查找是什麼原因造成整體重繪(有可能上述狀況都有,不好定義),可以透過 Android 自帶的工具或者一些方式

    在 RootView 的 requestLayout print stacktree

    可以複寫 RootView 的 requestLayout function,並且在裡面打印 stacktree,這樣可以查找出是哪一個 UI 元件呼叫 stacktree,

    透過開發者工具測量速度

    在開發者選項裡有一個”硬件加速渲染“,裡面有一個“調試 GPU 過度繪製”,這個會在屏幕上以顏色來區分 overdraw(過度繪製,也就是進行了不必要的繪製)的嚴重重度:

    • 藍色 1 倍 overdraw
    • 綠色 2 倍 overdraw
    • 紅色 3 倍 overdraw
    • 紫色 4 倍 overdraw

    在開發者選項面有一個是監控
    選擇GPU使用情況,選擇第二個
    在螢幕上顯示條型,就可以在手機上即時觀測到目前的使用情況

    參考文件 :

    React中的虛擬DOM詳解

    在 React 還沒出現前的前端開發,我們需要直接操作DOM結構,操作既複雜,兼容性又差,當我們有了jQuery之後,透過jQuery封裝的API,我們能夠更方便的操作DOM,但是DOM是很慢的,其元素非常巨大,頁面的性能問題常常會因為操作DOM而出現瓶頸,於是乎有什麼方法可以讓操作DOM的效率提升呢? ReactJs 提出了虛擬DOM的概念

    Read more »

    React組件拆分與組間之間的傳值

    React 是一個基於組件化開發的前端框架,那什麼是組件化的概念呢,以下圖來做比喻的話

    img

    圖中的每一個區塊都可以拆分成一個一個的組件,像是上面的Header,下面內容的每一個區塊,都會是一個組件。為什麼我們需要拆分組件呢,在開發前端頁面的時候,這個頁面的邏輯可能相對來得相當複雜,如果還是採用原有的開發方式,那就會造成我們在寫代碼之中的困擾。而當我們將頁面拆分成一塊一塊的組件之後,相對來說,每個組件的邏輯就會比整個頁面上來的簡單。

    Read more »

    Golang 初心者筆記-變數類型

    Golang 在近代算是一個成長快速的語言,這邊簡單記錄一下 Golang 學習的過程,所以從最簡單的變量類型開始,希望透過邊學習邊紀錄的過程,讓自己可以快速入門這個語言,並且實做一個小型的side project

    Read more »

    設計模式-代理模式(一)

    從出社會開始,因緣際會的讀了一本深入淺出設計模式(用Java實踐的),當時就覺得設計模式很有趣,索幸用C++把裡面的範例都實踐了一遍,最近因為開始SSM的練習,為了了解Mybatis,想從裡面的設計模式開始著手導讀,知道裡面用了動態代理的模式,但這邊先不談動態代理,而是想先從簡單的靜態代理開始,在下一篇才會繼續深度探索Mybatis的動態代理怎麼實踐的,這邊就先拿靜態代理的設計模式來練練手吧。

    Read more »
    0%