Background the problem
Flutter’s advantage is an increase in overall development efficiency, but its lack of components greatly limits its advantage.
Here’s an example:
After the requirement function is developed, it is necessary to report and recover data. With native development, these functional components are already available, but there are many things to consider if we use Flutter for discovery.
- Design of client dotting module. Reporting strategy, error prevention, loss prevention, encryption…
- Server logs are reported and stored in the database…
- Data side log automatic report generation, log format verification, log quantity monitoring…
- Handle the difference between Android and iOS logs
So instead of writing from scratch, bridge the Android and iOS logging modules directly. Similarly, during the development of Flutter components, repeat wheel building should be avoided as much as possible. For example, the network, crash reporting, some tools can be reused, reuse, use Flutter technology to consider whether the current work is necessary, whether it meets its technical goals (improvement of overall development efficiency).
The following is an example of how to make a Flutter central component using the Flutter log component
Preparation of the Flutter layer
Create plug-in project
Select Kotlin and Swift as the development languages
Implements message communication with Dart as the initiator and natively as the receiver (MethodChannel callback)
I define two methods. One is to obtain client parameters stored in LogSDK, such as user UID, system version information, third-party ID of Appsflyer, etc. The second is to collect the information according to the product’s established format and forward it to the native report. The definition code is as follows:
class Logsdk {
const MethodChannel _channel = const MethodChannel('logsdk'); Future<BaseInfo> get BaseInfo async {BaseInfo BaseInfo = baseinfo._frommap (await) _channel.invokeMapMethod<String, dynamic>('getBaseInfo'));
returnbaseInfo; } act(int event, {String count, String act1, String act2, String act3, String act4, String act5}) { _channel.invokeMethod("act", {
'event': event,
'count': count,
'act1': act1,
'act2': act2,
'act3': act3,
'act4': act4,
'act5': act5,
});
Copy the code
then
- How does a native implement the Dart method?
- How does the native end call back results to Dart?
Two problems need to be solved.
Implements message communication with Dart as receiver natively as initiator (MethodChannel listening)
How the native end actively sends messages to Dart.
For example, there is such a scenario: the user reports that the App is stuck, the operation contacts the user and gets his UID, then pushes the message to the user’s mobile phone according to the UID, and the push module is at the native end. Then the native layer sends the message to the Dart layer, and the Dart layer collects necessary information and sends it to the server with the help of the native ability. Thus a Log collection link is realized. Message receiving:
class Logsdk {
static const MethodChannel _channel = const MethodChannel('logsdk');
static StreamController<int> _msgCodeController;
static Stream<int> get msgCodeUpdated =>
_msgCodeController.stream;
init() {
if (_msgCodeController == null) {
_msgCodeController = new StreamController.broadcast();
}
_channel.setMethodCallHandler((call) {
switch (call.method) {
case "errorCode":
_msgCodeController.add(call.arguments as int);
break;
case "msgCode":
_msgCodeController.add(call.arguments);
break;
case "updateCode":
_msgCodeController.add(new ConnectionResult.fromJSON(result));
default:
throw new ArgumentError('Unknown method ${call.method}');
}
return null;
});
}
Copy the code
External mode of use
Logsdk.msgCodeUpdated.listen((event) {
if(event==1) {// do it}});Copy the code
So the question here is how does the client send the information to Dart?
Android implementation
Open the create Plugin project to automatically find onMethodCall for our generated logsdkplugin.kt file. There are two arguments (@nonnull Call: MethodCall, @nonnull result: result). The call parameter contains the dart layer method name and additional information that the method carries. The result argument is used to call back the execution results to the DART layer. The concrete implementation is as follows:
override fun onMethodCall(@NonNull call: MethodCall.@NonNull result: Result) {
when (call.method) {
"act"- > {val event: Int? = call.argument("event") event? .let {val act1: String? = call.argument("act1")
val act2: String? = call.argument("act2")
val act3: String? = call.argument("act3")
val act4: String? = call.argument("act4")
val act5: String? = call.argument("act5")
val count: String? = call.argument("count")
if (count == null) { StatisticLib.getInstance().onCountReal(event!! .1, act1 ? :"", act2 ? :"", act3 ? :"", act4 ? :"", act5 ? :"")}else{ StatisticLib.getInstance().onCountReal(event!! , count.toLong(), act1 ? :"", act2 ? :"", act3 ? :"", act4 ? :"", act5 ? :"")}}}"getBaseInfo"- > {val build: MutableMap<String, Any> = StatisticUtil.uuParams
build["idfa"] = AppsCache.get().sp().getString(AppSpConstants.GAID, "")
build["afid"] = AppsFlyerLib.getInstance().getAppsFlyerUID(GlobalLib.getContext())
build["isNew"] = StatisticUtil.isNew().toString()
build["pkg"] = GlobalLib.getContext().packageName
build["device"] = "android"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
build["sys_lang"] = Locale.getDefault().toLanguageTag()
} else {
build["sys_lang"] = ""
}
build["sdk_version"] = Build.VERSION.SDK_INT.toString()
build["channel"] = ""
result.success(build)
}
else -> {
result.notImplemented()
}
}
}
Copy the code
So how do you send a message to DART?
lateinit var channel: MethodChannel
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "logsdk")
channel.setMethodCallHandler(this);
}
fun sendMsg(code: Int) {
channel.invokeMethod("errorCode", code);
}
Copy the code
The first argument to the channel.invokemethod (“errorCode”, code) method is the name of the method sent to dart, and the second argument can be of any type. For object passing, see the function’s comment documentation. Map/Json is recommended.
/** * Arguments forthe call.
*
* <p>Consider using {@link #arguments()} for cases where aparticular run-time type is expected. * Consider using {@link #argument(String)} when that run-time type is {@link Map} or {@link * JSONObject}. */
Copy the code
For example,
// Send on Android native
JSONObject item = new JSONObject();
item.put("connected".false);
channel.invokeMethod("connection-updated", item.toString());
/ / the dart to receive
Map<String, dynamic> result = jsonDecode(call.arguments);
_purchaseController.add(new PurchasedItem.fromJSON(result));
Copy the code
That is to say, the solution of the above three problems is to use the method channel. That’s what it’s all about
- How does a native implement the Dart method?
Uses the first argument to onMethodCall(@nonNULL Call: MethodCall, @nonNULL Result: result)
- How does the native end call back results to Dart?
Uses the second argument to onMethodCall(@nonNULL Call: MethodCall, @nonNULL Result: result)
- How the native end actively sends messages to Dart.
Using MethodChannel. InvokeMethod () method
The iOS implementation
Open the automatically generated classSwiftLogsdkPlugin on xCode and edit the automatically generated method public func Handle (_ call: FlutterMethodCall, result: @escaping FlutterResult) {. FlutterMethodCall contains method names and parameter information that dart passes. FlutterResult is used to do the callback, which is passed to the DART layer
// The result() method must be executed regardless of success or failure otherwise the flutter end will wait because the flutter is a single-threaded model that causes the flutter to stall.
This code is executed on the UI thread of the Flutter. If the Flutter operation is time-consuming, remember to switch the thread
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS \(UIDevice.current.systemVersion)")
result(nil)
case "uu":
/ / reporting UU
handleReportUU(call, result)
case "act":
handleLogAct(call, result)
case "report":
handleReportUU(call, result)
case "getBaseInfo":
handleGetBaseInfo(call, result)
default:
result(FlutterMethodNotImplemented)}}private func handleLogAct(_ call:FlutterMethodCall,_ result: FlutterResult){
let arguments = call.arguments as? NSDictionary
if let _args = arguments{
let event = _args["event"] as! Int
let act1 = _args["act1"] as? String
let act2 = _args["act2"] as? String
let act3 = _args["act3"] as? String
let act4 = _args["act4"] as? String
let act5 = _args["act5"] as? String
let count = _args["count"] as? String
if let count = count {
if let countValue = Int64(count) {
// Report the statistics duration
EventLogger.shared.logEventCount(EventCombine(code: event), act1 ?? "",act2 ?? "" , act3 ?? "", act4 ?? "",act5 ?? "".count: countValue)
} else{
// This is a normal report
EventLogger.shared.logEventCount(EventCombine(code: event), act1 ?? "",act2 ?? "" , act3 ?? "", act4 ?? "",act5 ?? "")
}
}
result(nil)}}private func handleGetBaseInfo(_ call:FlutterMethodCall,_ result: FlutterResult){
var infos:Dictionary<String.String> = Dictionary(a)if let config = UULogger.shared.config{
infos["vendor_id"] = config.vendor_id
// Android system is provided by Google uid corresponding to iOS?
infos["idfa"] = config.idfa
// The uid provided by Appsflyers
infos["afid"] = config.afid
infos["device"] = "ios";
// System language
infos["sys_lang"] = UIDevice.current.accessibilityLanguage;
// System version
infos["sdk_version"] = UIDevice.current.systemVersion;
/ / channel
infos["channel"] = config.referrer;
// Whether the user is new
infos["isNew"] = SwiftLogsdkPlugin.isNew ? "1":"0"
/ / package name
infos["pkg"] = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ?? ""
}
result(infos)
}
}
Copy the code
Then actively send a message to DART using the FlutterMethodChannel.
public static var channel:FlutterMethodChannel? = nil
public static func register(with registrar: FlutterPluginRegistrar) {
channel = FlutterMethodChannel(name: "logsdk", binaryMessenger: registrar.messenger())
let instance = SwiftLogsdkPlugin()
registrar.addMethodCallDelegate(instance, channel: channel!)
}
public static func sendMsg(_ errorCode:Int){ channel? .invokeMethod("errorCode", arguments: errorCode)
}
Copy the code
Component engineering
Master-slave relationship between dart layer and native
Commercially, Flutter is hosted by Android and iOS, but a look at the code dependencies shows that native flutter is the host of flutter, as well as the various components. It is just that these components need to follow some protocol to support flutter components. MethodChannel is a basic communication protocol.
- Open the Android host Gradle file. see
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
This is the dependency of flutter in. - Open setting.gradle to see
This is where the components of Flutter are introduced into the dependencies of the host.
How does Android configure dependencies
- Open the native development environment.
Knowing that native is the host of the Flutter component, you can open the project to develop native code in a traditional Android/iOS project.
Android: AS opens directly, the example below the flutter component folder
- Relying on existing modules
Statisticlib.getinstance ().oncountreal, statisticlib.getInstance (). StatisticLib is basically my existing upload code. Open the build.gradle file in the Android folder of the component and add the existing dependencies directly
- Component release
Open pubspec.yaml and now the component depends on the local file path.
logsdk:
path: ../flutter-plugins/packages/plugin_logsdk
Copy the code
Modified to
How to configure dependencies in iOS
- Open the native development environment
Execute the flutter pub get
Run pod Install in example/ios
Open the Runner. Xcworkspace
- Open dependent on existing modules
logsdk.podspec
Pod 'Flutter', :path => 'Flutter' KeychainAccess' pod 'FBSDKCoreKit', '~> 5.6.0' pod 'AppsFlyerFramework', '5.2.0' pod 'SwiftyUserDefaults', '~>1.0' pod 'Copy the code
This enables the plug-in project to introduce dependencies on the EventLog native module.
- Component release
Open pubspec.yaml and now the component depends on the local file path.
logsdk:
path: ../flutter-plugins/packages/plugin_logsdk
Copy the code
Modified to
Component debugging
-
Android and the Flutter can be debugged using Attach. You don’t have to recompile
-
Run it again on iOS, using Debug to print the Po + object content
Breakpoint debugging should not be done simultaneously.
conclusion
- The goal of Flutter is to improve the overall client development efficiency
- I had no prior experience with iOS development, but I worked with Xcode and Swift. Most of these problems can be solved in androidstudio and kotlin.