AllenHsu的技術手扎

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

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 »

CI/CD,全名叫做Continuous Integration / Continuous Delivery,中文名叫持續集成與持續交付,從2014年開始,得以有幸接觸了從零開始的CI/CD的搭建以及流程梳理,也有在一個已經既有的開發體系中,如何去逐步導入CI/CD的相關經驗,透過這篇文章分享一下自己多年以來對於CI/CD的經驗與心路歷程。

關於持續集成與持續交付

在目前軟體業界普遍以敏捷開發作為精神,必須透過連續的小型的開發循環(Sprint)來迭代我們的產品以及驗證需求,以這樣為前提之下,我們必須不斷地從需求、研發、測試、交付,中多個階段不斷地循環迭代。所以我們為了上述的概念,我們必須有了持續集成與持續交付。

持續集成與持續交付,對於我個人的理解來說有以下來幾個重點。

  • 讓開發團隊可以在一個不斷提升開發品質的體系當中。
  • 從**需求研發測試交付**四個階段中能夠有一套完整且嚴格的體系及框架。
  • 自動化節省時間 - 讓開發團隊從一些瑣事的泥沼中解放出來。
  • 減少風險 - 因為有了上述幾點,所以可以有效提升軟體品質,而不會因為迭代過快造成軟體工程的不穩定。
Read more »
0%