Platform-specific code with MethodChannel – Flutter
Flutter & DartContents
Chào mừng bạn đến với Fx Studio. Bài viết này sẽ tiếp tục chuyến phiêu lưu vào đa vũ trụ Flutter. Chủ đề là MethodChannel. Nó giúp bạn tương tác với từng nền tảng code (native code) trong một số trường hợp cần thiết. Đây cũng là một chủ đề quan trọng khi bạn muốn chinh phục Flutter.
Tác giả bài viết là bạn Hồng Vân. Mọi người có thể theo dõi thêm các bài viết từ GitHub chính chủ của bạn nhóe!
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Về mặt kiến thức, bạn cần thông thạo Flutter cơ bản trước nhóe. Còn về mặt tools & versions, mình sẽ sử dụng các công cụ như sau:
-
IDE:
-
Visual Studio Code version 1.67.0
-
Android Studio Chipmunk 2021.2.1
-
XCode version 13.3.1
-
-
Flutter SDK version 2.10.5
Ngoài ra, đây cũng là phần tiếp theo của 2 bài viết liên quan tới Flavor trong Flutter. Nên nếu bạn chưa đọc qua nó, thì có thể tham khảo ở link sau:
- Flavor Configurations:
- Github doc: https://github.com/vanle57/flutter-flavor
- FxStudio post: https://fxstudio.dev/flavor-configurations-flutter/
- Customize Run and Debug:
- Github doc: https://github.com/vanle57/flutter-customize-run
- FxStudio post: https://fxstudio.dev/customize-run-and-debug-flutter/
1. MethodChannel là gì?
Flutter cho phép 1 base code có thể build được cho nhiều nền tảng. Tuy nhiên, trong lập trình, đôi khi bạn sẽ gặp phải tình huống cần truy cập API dành riêng cho nền tảng bằng ngôn ngữ hoạt động trực tiếp với các API đó (native code). Mình lấy ví dụ một số tình huống như:
-
Lấy lượng pin của device
-
Truy cập camera hoặc thư viện ảnh của device
-
…
MethodChannel sẽ giúp bạn làm điều đó!
Cách hoạt động của MethodChannel được mô tả theo mô hình bên dưới:
MethodChannel hoạt động dựa trên tin nhắn nhị phân (binnary message) và kênh nền tảng (platform channel).
Ở phía client (Flutter UI), MethodChannel cho phép gửi tin nhắn qua các cuộc gọi phương thức. Ở phía platform, MethodChannel trên Android và FlutterMethodChannel trên iOS nhận các lời gọi method này và trả kết quả về lại. Nên nhớ một điều rằng API sẽ không thực sự “gọi hàm” thay cho bạn. Phần kiểm tra các phương thức được gọi và trả kết quả về sẽ do bạn thực hiện. MethodChannel chỉ “lắng nghe” các lời gọi này mà thôi.
Để đảm bảo cho tương tác của người dùng trên app vẫn mượt mà, các tin nhắn và phản hồi sẽ được gửi 1 cách bất đồng bộ.
2. Tương tác với native code
Bạn tải project template tại đây về để thực hành.
Mình sẽ làm 1 cái demo nho nhỏ là đọc package name (Android) / bundle identifier (iOS) để các bạn biết hình dung ra cách làm việc với MethodChannel nhé!
2.1. Cấu hình cho iOS
Bạn vào iOS/Runner/AppDelegate.swift
và cấu hình cho FlutterMethodChannel như sau:
import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) guard let controller = window?.rootViewController as? FlutterViewController else { return super.application(application, didFinishLaunchingWithOptions: launchOptions) } // 1 let flavorChannel = FlutterMethodChannel(name: "demo", binaryMessenger: controller.binaryMessenger) // 2 flavorChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch call.method { // 3 case "getPackage": let bundleId = Bundle.main.infoDictionary?["CFBundleIdentifier"] result(bundleId) default: // 4 result(FlutterMethodNotImplemented) } }) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }
Giải thích:
-
Khởi tạo FlutterMethodChannel với name là demo. Name giống như là id cho kênh, vậy nên nó phải giống nhau ở cả iOS, Android và Flutter.
-
Đăng ký lắng nghe các lời gọi hàm.
-
Với lời gọi hàm là
getPackage
, ta thực hiện việc trả về CFBundleIdentifier được đọc từInfo.plist
. -
Xử lý những lời gọi hàm không xác định.
2.2. Cấu hình cho Android
Bạn mở file android/app/src/main/kotlin/MainActivity.kt
và thay thế đoạn code này vào sau dòng package.
// 1 import androidx.annotation.NonNull; import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant class MainActivity: FlutterActivity() { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine); //2 MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "demo").setMethodCallHandler { // 3 call, result -> when (call.method) { "getPackage" -> { result.success(BuildConfig.APPLICATION_ID) } else -> result.notImplemented() } } } }
Giải thích:
-
Bạn nhớ import tất cả các package cần thiết nha!
-
Tương tự như ở iOS, bạn khởi tạo MethodChannel với name là demo.
-
Trả về giá trị BuildConfig.APPLICATION_ID, là giá trị có sẵn của Android với lời gọi hàm là
getPackage
. -
Xử lý những lời gọi hàm không xác định.
2.3. Cấu hình cho phía Flutter
Bạn tiếp tục vào file lib/main.dart
và chỉnh sửa hàm main
// 1 Future<void> main() async { // 2 WidgetsFlutterBinding.ensureInitialized(); // 3 final package = await MethodChannel('demo').invokeMethod<String>("getPackage"); print(package); runApp(const MyApp()); }
Giải thích:
-
Mình sửa lại kiểu trả về của hàm
main
là Future với từ khoáasync
-
Bước này đặc biệt quan trọng. Điều này là bắt buộc để truy cập kênh nền tảng trước khi khởi chạy ứng dụng.
-
Khời tạo đối tượng của MethodChannel với name demo. Gọi hàm
getPackage
với kiểu trả về là String.invokeMethod()
phải đi kèm từ khoáawait
vì nó sẽ trả về 1Future
.
2.4. Kết quả
Bạn run thử và xem kết quả nào trên Debug Console nào!
flutter run
Hoặc bấm nút Run ở tab Run and Debug
Kết quả:
-
iOS:
com.demoFlavor.dev
-
Android:
com.example.demo_flavor.dev
3. Ứng dụng MethodChannel vào Flutter Flavor
Nếu chưa biết Flutter Flavor là gì?
Bạn có thể tham khảo bài viết tại GitHub – vanle57/flutter-flavor: Guide to flavoring a Flutter app.
3.1. Sử dụng MethodChannel để đọc flavor của mỗi platform
3.1.1. Cấu hình cho iOS
Bạn phải thực hiện trên XCode nha!
-
Bước 1: Thêm APP_FLAVOR vào User-Defined Setting
- Vào project Runner -> chọn tab Build Settings -> click vào dấu + chọn Add User-Defined Setting.
-
- Đặt tên là APP_FLAVOR và định nghĩa các giá trị là các flavor tương ứng cho từng cấu hình.
- Bước 2: Định nghĩa AppFlavor trong file Info.plist. Có 2 cách các bạn có thể làm ở bước này.
- Cách 1: Thêm vào trên giao diện
file .plist
- Cách 1: Thêm vào trên giao diện
Giải thích 1 chút: $(APP_FLAVOR) sẽ tương ứng với biến APP_FLAVOR bạn đã tạo ở bước 1 nhé.
-
- Cách 2: Thêm dưới dạng source code. Các bạn có thể mở file Info.plist bằng VSCode hoặc click chuột phải vào file Info.plist -> chọn Open as source code trên giao diện XCode.
<key>AppFlavor</key> <string>$(APP_FLAVOR)</string>
-
Bước 3: Cấu hình cho FlutterMethodChannel trong AppDelegate. Tham khảo đoạn code sau nhóe!
import UIKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) guard let controller = window?.rootViewController as? FlutterViewController else { return super.application(application, didFinishLaunchingWithOptions: launchOptions) } let flavorChannel = FlutterMethodChannel(name: "demo", binaryMessenger: controller.binaryMessenger) flavorChannel.setMethodCallHandler({(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch call.method { case "getPackage": let bundleId = Bundle.main.infoDictionary?["CFBundleIdentifier"] result(bundleId) // NOTE: Add new case case "getFlavor": let flavor = Bundle.main.infoDictionary?["AppFlavor"] result(flavor) default: result(FlutterMethodNotImplemented) } }) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }
Giải thích:
Ở đây chúng ta thêm case là “getFlavor” để khi nhận được lời gọi hàm này, ta sẽ xử lý trả về AppFlavor được đọc từ Info.plist.
3.1.2. Cấu hình cho Android
Tương tự, bạn mở file cũng android/app/src/main/kotlin/MainActivity.kt
. Bạn muốn làm giao diện VSCode hay Android Studio đều được.
import androidx.annotation.NonNull; import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel import io.flutter.plugins.GeneratedPluginRegistrant class MainActivity: FlutterActivity() { override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine); MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "demo").setMethodCallHandler { call, result -> when (call.method) { "getPackage" -> { result.success(BuildConfig.FLAVOR) } // NOTE: Add new case "getFlavor" -> { result.success(BuildConfig.FLAVOR) } else -> result.notImplemented() } } } }
Giải thích code:
Ở đây, bạn cũng add thêm case “getFlavor” và trả về BuildConfig.FLAVOR.
3.2. Thực hiện việc gọi hàm đọc flavor ở phía Flutter client
Ý tưởng ở đây là mình sẽ xây dựng lớp FlavorConfig và sử dụng MethodChannel để gọi phương thức getFlavor
.
import 'package:flutter/services.dart'; class FlavorConfig { // 1 Future<void> getFlavor() async { // 2 const methodChannel = MethodChannel('demo'); // 3 final flavor = await methodChannel.invokeMethod<String>('getFlavor'); // 4 if (flavor == 'dev') { print('Flavor: dev'); } else if (flavor == 'staging') { print('Flavor: staging'); } else if (flavor == 'product') { print('Flavor: product'); } } }
Giải thích:
-
Khai báo hàm
getFlavor
trả về 1 Future với từ khoáasync
(Bạn hãy nhớ lại mình đã đề cập ở phần 1 là tin nhắn và phản hồi sẽ được gửi bất đồng bộ nhé!) -
Khởi tạo MethodChannel với
name
giống như bạn đã đặt ở phần cấu hình cho iOS và Android. -
Gọi hàm
getFlavor
với kiểu trả về là String. -
So sánh kết quả trả về để
print
ra console.
Okay! Tiếp theo là mình sẽ gọi hàm getFlavor()
của lớp FlavorConfig
để xem nó hoạt động như thế nào. Bạn vào hàm main
để gọi nhé!
// 1 Future<void> main() async { // 2 WidgetsFlutterBinding.ensureInitialized(); final package = await MethodChannel('demo').invokeMethod<String>("getPackage"); print(package) // 3 await FlavorConfig().getFlavor(); runApp(const MyApp()); }
Bây giờ thì run app và xem kết quả thôi!
- iOS
- Android
Biến tấu một chút.
Giả sử như có 3 cái api url khác nhau cho mỗi flavor, thì mình sẽ xử lý như thế nào?
// 1 enum AppFlavor { dev, stg, prod } // 2 extension AppFlavorExtension on AppFlavor { String get apiURL { switch (this) { case AppFlavor.dev: return "https://example.dev.com/"; case AppFlavor.stg: return "https://example.stg.com/"; case AppFlavor.prod: return "https://example.com/"; } } }
Giải thích:
-
Mình sẽ tạo enum
AppFlavor
với 3 case tương ứng với 3 flavor hiện có trong app. -
Tạo extension của
AppFlavor
để định nghĩa biếnapiURL
kiểu String là đại diện cho các link api khác nhau của mỗi flavor.
Mình sửa lại 1 tí ở hàm getFlavor
của lớp FlavorConfig
là sẽ trả về kiểu Future<AppFlavor?>
thay vì kiểu Future như lúc nãy.
Future<AppFlavor?> getFlavor() async { const methodChannel = MethodChannel('flavor'); final flavor = await methodChannel.invokeMethod<String>('getFlavor'); if (flavor == 'dev') { print('Flavor: dev'); return AppFlavor.dev; } else if (flavor == 'staging') { print('Flavor: staging'); return AppFlavor.stg; } else if (flavor == 'product') { print('Flavor: product'); return AppFlavor.prod; } }
Tiếp theo, sửa thêm 1 chút ở hàm main
để print
ra api url.
final appFlavor = await FlavorConfig().getFlavor(); print(appFlavor?.apiURL);
Run và xem kết quả thôi! (Mình build trên iOS nha).
Tạm kết
- Tìm hiểu về MethodChannel
- Cách tương tác với các native code ở các nền tảng khác nhau
- Cấu hình và sử dụng kết hợp với Flavor
Vậy là đã kết thúc những vấn đề liên quan đến cấu hình flavor hoàn chỉnh cho 1 project. Nếu các bạn thấy cách này quá quằn quại và phức tạp, thì có thể sử dụng dart define. Hãy đón chờ bài viết tiếp theo của mình về chủ đề này nhé!
Okay! Tới đây, mình xin kết thúc bài viết về MethodChannel trong Flutter . Nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
- Bạn có thể checkout source code tại đây.
- Nguồn tham khảo:
-
Follow me more:
- Facebook: https://www.facebook.com/van.may.750/
- Email : lehongvan.develop@gmail.com
- LinkedIn: https://www.linkedin.com/in/le-van-935231150/
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by Hồng Vân Lê
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
You may also like:
Archives
- December 2024 (3)
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)