--- 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