---
# System prepended metadata

title: Flutter Develop
tags: [Flutter]

---

---
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很貼心的可以透過指令的方式替你進行確認，當你的環境是符合的就會顯示如下圖：

![](https://i.imgur.com/ZcSr1NH.png)

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

![](https://i.imgur.com/6v3O6S2.png)

當一切都就緒後，我們就可以開始進行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
```

完成後顯示如下：
![](https://i.imgur.com/DhmWDUQ.png)

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

![](https://i.imgur.com/9kOWJrv.png)

有上圖可以看到裡面包含了Android與iOS的專案，另一個main.dart則是主要編輯顯示界面的檔案，使用的是Dart語法。點開後可以看一下該語法是透過巢狀式結構進行編輯。

現在我們可以直接開啟一個模擬器，然後開始build專案進模擬器裡，如果成功就可以看到以下顯示：
![](https://i.imgur.com/ZFWh0NQ.png)

這是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專案。

![](https://i.imgur.com/Gg8RjRV.png)

直接在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();
                            }
                        }
                );
    }
```

執行後的成果如下：

![](https://i.imgur.com/muadBTd.jpg)


## 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();
    
    ...
    }
}
```
結果如下：
![](https://i.imgur.com/785Vkb7.jpg)



## 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
```
![截圖 2025-05-08 上午10.58.34](https://hackmd.io/_uploads/rkOgA5Yxgl.png)



## 參考資料
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