---
tags: Flutter
---
# Flutter Develop
最近小組內希望嘗試使用Flutter進行android與ios的Sample app開發,可以直接將公司產品(SDK)套用至Flutter之下這樣就同時維護兩的平台的sample app。
## Install Flutter
首先是對Flutter進行安裝,我自己是用mac進行開發,所以我就很單純的下載Flutter的SDK下來直接使用,相關的設定與使用都可以透過官方網站來了解。
https://flutter.dev/docs/get-started/install/macos
接下來,我們就必須要確認一下自己的環境是否符合Flutter所需要的開發環境,Flutter很貼心的可以透過指令的方式替你進行確認,當你的環境是符合的就會顯示如下圖:

如果出現警告或是錯誤,也會在提示裡面說明建議的做法:

當一切都就緒後,我們就可以開始進行Flutter的開發。
## Develop Android use Flutter
首先,我們要先建立一個Flutter專案。直接使用command建立就可以了(要留意先切換到想要存放專案的資料夾之下),如下:
```
格式: flutter create [project name]
flutter create testfluuterproject
```
要留意project name要用小寫的字母,另一個要注意的是,在Android開發上預設建立時會使用kotlin,如果你是想要說java開發,則可以改用以下command:
```
flutter create -i objc -a java testfluuterproject
```
完成後顯示如下:

現在可以用Android studio開啟專案看看裡面的架構:

有上圖可以看到裡面包含了Android與iOS的專案,另一個main.dart則是主要編輯顯示界面的檔案,使用的是Dart語法。點開後可以看一下該語法是透過巢狀式結構進行編輯。
現在我們可以直接開啟一個模擬器,然後開始build專案進模擬器裡,如果成功就可以看到以下顯示:

這是Flutter的起始專案,下方的按鈕點擊後,中間的數字計數就會加1。
現在來大致說明一下這個畫面的結構是如何透過Dart呈現出來。先來開啟main.dart。
程式進入點:
``` =dart
void main() {
runApp(MyApp());
}
```
這裡餵給runapp()一個名叫物件MyApp的Widget物件,再細看這物件繼承一個StatelessWidget物件,表示這個widget在被建立之後就固定住,不會再更改。
``` =Dart
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
```
當widget被建立時會呼叫build這個function,回傳的就是要顯示的內容。它回傳的MaterialApp有三個參數title、theme以及home,可以把MaterialApp當成是Material Design入口的Widget,這裡可以設定標題、樣式以及導向的路由等。如果只有一個頁面的話,就可以如同範例中設定home即可,但要注意如果設定home的話,就不可以設定routs屬性。
和MyApp不同,MyHomePage繼承自StatefulWidget,它的狀態會隨著時間做改變。因此可以看到有createState這個function,就是用來建立及控管它的狀態的。
``` =Dart
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
//當資料有變動時需要變更畫面就需要呼叫該function才能重新建構畫面。
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
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),
),
);
}
}
```
在_MyHomePageState的build function中則回傳一個Scaffold物件。元件的相關詳細資料與用法都可以透過官方網站查找。
https://api.flutter.dev/flutter/material/material-library.html
## Flutter call android native function
以下會用官方的一個電池電量的小範例來解釋如何呼叫android native的function。
### 第一步 建立channel
第一步就是建立一個channel,這個channel name因該是唯一的。
``` =Dart
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('samples.flutter.dev/battery');
// Get battery level.
}
```
下一步就是建立調用channel method回傳時的實作。
``` =Dart
// Get battery level.
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
```
await為非同步方法,可以等待platform.invokeMwthod('getBatteryLevel')執行完成並拿到結果後再往下執行。但要留意await需要在async的method中使用。getBatteryLevel則是在Android內的method name。在拿到電池數據後,呼叫setState來改變UI顯示。
最後調整一下要顯示的UI,把剛剛的Scaffold更換成以下的程式碼。
``` =Dart
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
child: Text('Get Battery Level'),
onPressed: _getBatteryLevel,
),
Text(_batteryLevel),
],
),
),
);
}
```
以上,就在Flutter UI顯示部分已經建置完成,些下來開始建立Android native的部分。但是要對Android專案進行編輯的話,則需要另外開一個Android studio來開啟Android的專案,或是也可以直接透過以下方式快速的開啟android專案。

直接在Android那個資料夾上點右鍵,在Flutter的選項中點選Open Android module in Android Studio。
剛才我們已經在Flutter那一面建立了method的channel,並且也給了channel name,同樣的我們在Android這端也要做同樣的設定來令Flutter呼叫到對應的method。
``` =java
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "samples.flutter.dev/battery";
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
(call, result) -> {
// Note: this method is invoked on the main thread.
// TODO
}
);
}
}
```
上面就是告訴Flutter當執行samples.flutter.dev/battery這個channel時,就是呼叫這裡的method。
現在我們先建立好取得電池電量的method。
``` =java
private int getBatteryLevel() {
int batteryLevel = -1;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return batteryLevel;
}
```
最後,在setMethodCallHandler中增加對call.method的判斷,找到對應的method name後則執行對應的方法。
``` =java
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler(
(call, result) -> {
//如果flutter call 的 method name 是 getBatteryLevel
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
//回傳資料到Flutter
result.success(batteryLevel);
} else {
//錯誤訊息回傳
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
//沒有找到對應的method
result.notImplemented();
}
}
);
}
```
執行後的成果如下:

## Permission
權限的問題也可以在Flutter這層就做處理,原本我很直覺地認為應該要在Android native處理,在好奇下發現原來Fultter就可以處理了。
想要透過Flutter去處理android權限問題,我們要使用到permission_handler這個plugin,所以需要留意一下幾點:
1. 在3.1.0版以後,permission_handler改支援Androidx,所以需要留意專案是否有相關設定在gradle.properties中:
```
android.useAndroidX=true
android.enableJetifier=true
```
2. android/app/build.gradle中注意以下設定
```
android {
compileSdkVersion 30
...
}
```
3. 最後在AndroidManifest.xml加入想要使用的權限。
```
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET"/>
```
完成以上步驟後,Android端這邊的設定就算是設定完成了,接下來就要去Flutter端進行權限的請求實作。
首先,我們要在pubspec.yaml裡面增加dependencies,打開檔案後找到dependencies,加入以下程式:
``` =yaml
permission_handler: ^3.1.0
```
接著,我們在main.dart檔案的最上面import剛剛說的plugin:
``` =Dart
import 'package:permission_handler/permission_handler.dart';
```
最後建立一個專門用來處理權限的class:
``` =Dart
class PermissionsService {
final PermissionHandler _permissionHandler = PermissionHandler();
Future<bool> _requestPermission(PermissionGroup permission) async {
var result = await _permissionHandler.requestPermissions([permission]);
if (result[permission] == PermissionStatus.granted) {
return true;
}
return false;
}
}
```
我個人喜歡在前面先檢查一下權限,沒有的話再向使用者請求:
``` =Dart
Future<bool> isHasPermission(PermissionGroup permission) async {
var permissionStatus = await _permissionHandler.checkPermissionStatus(permission);
return permissionStatus == PermissionStatus.granted;
}
void getCameraPermission(){
if(isHasPermission(PermissionGroup.camera) != true){
requestCamaraPermission();
}
}
```
最後我們就可以程式啟動的地方呼叫權限得請求:
``` =Dart
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
PermissionsService().getCameraPermission();
...
}
}
```
結果如下:

## build apk
1. 要在properties檔中新增Sign Key當相關資訊(可以自行建立,目前為求方便直接使用local.properties):
```
storeFile = your key path
keyAlias = alias
keyPassword = password
storePassword = password
```
2. 在build.gradle中加入以下code
```
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
signingConfigs {
release {
keyAlias localProperties['keyAlias']
keyPassword localProperties['keyPassword']
storeFile localProperties['storeFile'] ? file(localProperties['storeFile']) : null
storePassword localProperties['storePassword']
v1SigningEnabled true
v2SigningEnabled true
}
}
buildTypes {
release {
signingConfig signingConfigs.release
//關閉混淆
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
```
3. 接著在命令視窗中執行以下command:
```
flutter build apk --split-per-abi --no-sound-null-safety
```
如果不想區分cpu架構,可以去掉--split-per-abi。
也可以使用其他進入點進行來達到產出不同設定的apk。
```
flutter build apk -t lib/main_other.dart
```
## flutter 切換版本
可以根據以下方法隨時切換flutter版本,就如同git切換分支一樣。
1. cd [flutter path]
2. git checkout 2.10.5
3. flutter --version
這樣就可以將flutter切換到 2.10.5版。
flutter 版本List可以參考以下網址
https://docs.flutter.dev/development/tools/sdk/releases?tab=macos
## flutter 切換預設編譯的jdk路徑
```
flutter config --jdk-dir '/Applications/Android Studio Koala 功能推送.app/Contents/jbr/Contents/Home'
```
完成後使用以下指令確認是否修改完成
```
flutter doctor -v
```

## 參考資料
https://flutter.dev/docs/get-started/flutter-for/android-devs
https://flutter.dev/docs/development/platform-integration/platform-channels?tab=android-channel-java-tab
https://morioh.com/p/e7fdb2fea1f5
https://pub.dev/packages/permission_handler
https://medium.com/flutter-community/request-permissions-in-flutter-as-a-consumable-service-e6cd243f882f
https://ithelp.ithome.com.tw/articles/10215523