Flutter与原生端的通信


在 Flutter 的实际开发过程中,单靠 Dart 层往往无法满足所有业务需求,尤其是当涉及到底层系统能力、第三方原生 SDK、硬件访问或平台特有功能时(如相机、蓝牙、权限管理等),就需要与原生平台进行通信。Flutter 提供了多种与原生端交互的机制,MethodChannelEventChannelBasicMessageChannel,允许在 Dart 层与原生代码之间高效地传递数据和调用方法。

本文将通过一个完整的 Demo,详细介绍这三种 Channel 的使用方法和最佳实践。

MethodChannel

MethodChannel 是 Flutter 与原生通信中最常用的一种方式,主要用于实现 Dart 层对原生方法的调用 ,以及原生端主动调用 Dart 层的方法。它采用异步消息传递机制,非常适合一次性的请求-响应式交互。

核心应用场景

  1. Dart 调用原生
    • 获取设备信息(如电池电量、网络状态、设备型号)
    • 调用原生 SDK 功能(如地图、支付、分享)
    • 触发平台特定操作(如打开系统设置、显示原生弹窗)
  2. 原生调用 Dart
    • 将原生页面的操作结果通知给 Dart
    • 在原生端完成某个任务后,更新 Flutter UI

实现步骤

1. 初始化 MethodChannel

在 Dart 和原生端都需要创建一个具有相同名称的 MethodChannel 实例。

Dart 端 ( lib/main.dart)

dart 复制代码
final MethodChannel _methodChannel = MethodChannel("method_channel");

iOS 端 ( ios/Runner/AppDelegate.swift)

swift 复制代码
let methodChannel = FlutterMethodChannel(name: "method_channel", binaryMessenger: controller.binaryMessenger)

methodChannel.setMethodCallHandler(methodCallHandler)

2. Dart 调用原生

获取设备电量

dart 复制代码
Future<void> _getBatteryLevel() async {
  final int batteryLevel = await _methodChannel.invokeMethod("getBatteryLevel");
  setState(() {
    _batteryLevel = batteryLevel;
  });
}

打开原生页面

dart 复制代码
Future<void> _openNativePage() async {
  await _methodChannel.invokeMethod("openNativePage");
}

iOS 端实现

swift 复制代码
// AppDelegate.swift
private func methodCallHandler(call: FlutterMethodCall, result: @escaping FlutterResult) {
    switch call.method {
    case "getBatteryLevel": getBatteryLevel(result)
    case "openNativePage": openNativePage(result)
    // ... 其他方法 ...
    default: result(FlutterMethodNotImplemented)
    }
}

// 获取电量
private func getBatteryLevel(_ result: @escaping FlutterResult) {
    let device = UIDevice.current
    device.isBatteryMonitoringEnabled = true
    
#if targetEnvironment(simulator)
    result(100)
#else
    if device.batteryState == UIDevice.BatteryState.unknown {
        result(FlutterError(
            code: "Error", message: "BatteryState is unkonw", details: nil
        ))
    } else {
        let level = Int(device.batteryLevel * 100)
        result(level)
    }
#endif
}

// 打开原生页面
private func openNativePage(_ result: @escaping FlutterResult) {
    DispatchQueue.main.async {
        guard let flutterViewController = self.flutterViewController else {
            result(
                FlutterError(
                    code: "ERROR",
                    message: "FlutterViewController is not initialized",
                    details: nil
                )
            )
            return
        }
        
        let nativePage = NativePage()
        
        let navigationController = UINavigationController(rootViewController: nativePage)
        navigationController.modalPresentationStyle = .fullScreen
        flutterViewController.present(navigationController, animated: true)
        
        result(nil)
    }
}

3. 原生调用 Dart

从原生页面更新 Flutter 计数器

iOS 端 ( ios/Runner/NativePage.swift)

swift 复制代码
private func increment() {
    count += 1
    // 从单例中获取 channel 实例
    guard let methodChannel = ChannelManager.shared.methodChannel else {
        return
    }
    
    // 调用 Dart 方法并传递参数
    methodChannel.invokeMethod("setFlutterCount", arguments: ["count": count])
}

Dart 端接收

dart 复制代码
void _setupChannels() {
    _methodChannel.setMethodCallHandler(methodCallHandler);
    // ...
}

Future<void> methodCallHandler(MethodCall call) async {
    switch (call.method) {
      case "setFlutterCount":
        _setFlutterCount(call);
        break;
    }
}

void _setFlutterCount(MethodCall call) {
    setState(() {
        _count = call.arguments['count'] as int;
    });
}

EventChannel

EventChannel 专门用于 原生端向 Dart 端发送持续的数据流。它就像一个数据管道,一旦建立连接,原生端可以随时向 Dart 端发送事件,直到连接被关闭。

核心应用场景

  1. 传感器数据:如加速度计、陀螺仪、GPS 位置更新
  2. 实时数据流:如网络状态变化、下载进度、蓝牙设备扫描结果
  3. 定时器和计时器:原生端定时器触发,持续向 Flutter 发送更新事件

实现步骤

1. 初始化 EventChannel

Dart 端 ( lib/main.dart)

dart 复制代码
final EventChannel _eventChannel = EventChannel("event_channel");

iOS 端 ( ios/Runner/AppDelegate.swift)

swift 复制代码
// event sink
var timerEventSink: FlutterEventSink?

let eventChannel = FlutterEventChannel(name: "event_channel", binaryMessenger: controller.binaryMessenger)

eventChannel.setStreamHandler(self)

2. Dart 端监听事件流

dart 复制代码
void _setupChannels() {
    // ...
    // 监听名为 "timer" 的事件流
    _eventChannel.receiveBroadcastStream("timer").listen(timerEvent);
}

void timerEvent(dynamic event) {
    setState(() {
        _timeValue = event.toString();
    });
}

3. iOS 端实现 FlutterStreamHandler

swift 复制代码
// AppDelegate.swift
extension AppDelegate: FlutterStreamHandler {
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        guard let args = arguments as? String else {
            return FlutterError(code: "INVALID_ARGUMENTS", message: "Arguments must be a string", details: nil)
        }
        
        switch args {
        case "timer":
            timerEventSink = events
        default:
            return FlutterError(code: "UNKNOWN_STREAM", message: "Unknown stream: \(args)", details: nil)
        }
        
        return nil
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        if let args = arguments as? String {
            switch args {
            case "timer":
                timerEventSink = nil
            default:
                break
            }
        }
        return nil
    }
}

4. 原生端发送事件

swift 复制代码
private func startTimer(_ result: @escaping FlutterResult) {
    startTimer = Date()
    timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
        if let startTimer = self.startTimer {
            let elapsed = Date().timeIntervalSince(startTimer)
            // ... 计算时间 ...
            let timeString = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
            
            // 通过 event sink 发送事件
            self.timerEventSink?(timeString)
        }
    })
    result(nil)
}

BasicMessageChannel

BasicMessageChannel 提供了一个非常灵活的双向消息通道,它使用 Codec(编解码器)来序列化和反序列化消息,支持传输字符串、数字、布尔值、列表、字典等复杂数据结构。

核心应用场景

  1. 传输复杂数据:当需要传递自定义对象或嵌套的 JSON 数据时
  2. 双向通信:在一次交互中,既需要发送数据,又需要接收回复
  3. 高性能数据传输 :可以使用二进制编解码器(如 BinaryCodec)来提高性能

实现步骤

1. 初始化 BasicMessageChannel

Dart 端 ( lib/main.dart)

dart 复制代码
final BasicMessageChannel _basicMessageChannel = BasicMessageChannel(
  "basic_message_channel",
  JSONMessageCodec(), // 使用 JSON 编解码器
);

iOS 端 ( ios/Runner/AppDelegate.swift)

swift 复制代码
let basicMessageChannel = FlutterBasicMessageChannel(
    name: "basic_message_channel",
    binaryMessenger: controller.binaryMessenger,
    codec: FlutterJSONMessageCodec.sharedInstance()
)

basicMessageChannel.setMessageHandler(basicMessageHandler)

2. Dart 端发送和接收消息

发送消息

dart 复制代码
Future<void> _sendUserInfoToNative() async {
    Map<String, dynamic> userInfo = {
      "name": "John",
      "age": 20,
      "email": "john@example.com",
    };

    try {
      final result = await _basicMessageChannel.send(userInfo);
      setState(() {
        _basicMessageInfo = Map<String, dynamic>.from(result);
      });
    } catch (e) {
      print("Error sending user info to native: $e");
    }
}

接收消息

dart 复制代码
void _setupChannels() {
    // ...
    _basicMessageChannel.setMessageHandler(basicMessageHandler);
}

Future<void> basicMessageHandler(dynamic message) async {
    setState(() {
        _basicMessageInfo = Map<String, dynamic>.from(message);
    });
    return Future.value(message); // 返回确认消息
}

3. iOS 端接收和回复消息

接收消息

swift 复制代码
private func basicMessageHandler(message: Any?, reply: FlutterReply) {
    guard let messageDict = message as? [String: Any] else {
        reply(FlutterError(...))
        return
    }
    
    // 模拟处理后,返回一个包含更多信息的新字典
    let response: [String: Any] = [
        "received": true,
        "receivedData": messageDict,
        "platform": "iOS"
    ]
    
    reply(response)
}

从原生页面发送消息

swift 复制代码
// NativePage.swift
private func sendComplexDataToFlutter() {
    let complexData: [String: Any] = [
        "fromNative": true,
        "timestamp": ISO8601DateFormatter().string(from: Date()),
        // ...
    ]
    
    ChannelManager.shared.basicMessageChannel?.sendMessage(complexData) { _ in
        status = "yes"
    }
}

总结与建议

Channel 类型 主要用途 数据流向 适用场景
MethodChannel 方法调用 双向(请求/响应) 获取数据、触发动作、简单的同步/异步任务
EventChannel 事件流 单向(原生 -> Dart) 传感器数据、实时更新、持续的状态变化
BasicMessageChannel 复杂数据交换 双向(消息/回复) 自定义数据结构、文件传输、复杂的双向通信
  • 优先选择 MethodChannel:对于大多数常见的交互场景,MethodChannel 已经足够强大且易于使用。
  • 使用 EventChannel 处理持续数据 :当需要从原生端持续接收数据时,EventChannel 是最佳选择,它避免了轮询,提高了效率。
  • BasicMessageChannel用于复杂场景 :当 MethodChannel 的参数和返回值无法满足你的数据结构需求时,或者需要更灵活的双向通信时,可以考虑使用 BasicMessageChannel

通过合理地选择和使用这三种 Channel,可以轻松地在 Flutter 和原生平台之间构建起高效、稳定的通信桥梁,从而充分发挥 Flutter 的跨平台优势,同时利用原生平台的强大能力。

Demo演示

完整Demo代码

  • main.dart
dart 复制代码
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final MethodChannel _methodChannel = MethodChannel("method_channel");
  final EventChannel _eventChannel = EventChannel("event_channel");
  final BasicMessageChannel _basicMessageChannel = BasicMessageChannel(
    "basic_message_channel",
    JSONMessageCodec(),
  );

  int _batteryLevel = 0;
  int _count = 0;
  String _timeValue = "00:00:00";
  Map<String, dynamic> _basicMessageInfo = {};

  StreamSubscription? _eventSubscription;

  void _setupChannels() {
    _methodChannel.setMethodCallHandler(methodCallHandler);
    _eventChannel.receiveBroadcastStream("timer").listen(timerEvent);
    _basicMessageChannel.setMessageHandler(basicMessageHandler);
  }

  Future<void> methodCallHandler(MethodCall call) async {
    switch (call.method) {
      // 接受来自原生端的方法
      case "setFlutterCount":
        _setFlutterCount(call);
        break;
    }
  }

  void _setFlutterCount(MethodCall call) {
    setState(() {
      _count = call.arguments['count'] as int;
    });
  }

  void timerEvent(dynamic event) {
    setState(() {
      _timeValue = event.toString();
    });
  }

  Future<void> basicMessageHandler(dynamic message) async {
    setState(() {
      _basicMessageInfo = Map<String, dynamic>.from(message);
    });
    return Future.value(message);
  }

  Future<void> _getBatteryLevel() async {
    final int batteryLevel = await _methodChannel.invokeMethod(
      "getBatteryLevel",
    );
    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

  Future<void> _openNativePage() async {
    await _methodChannel.invokeMethod("openNativePage");
  }

  Future<void> _startTimer() async {
    await _methodChannel.invokeMethod("startTimer");
  }

  Future<void> _stopTimer() async {
    await _methodChannel.invokeMethod("stopTimer");
  }

  Future<void> _sendUserInfoToNative() async {
    Map<String, dynamic> userInfo = {
      "name": "John",
      "age": 20,
      "email": "john@example.com",
    };

    try {
      final result = await _basicMessageChannel.send(userInfo);
      setState(() {
        _basicMessageInfo = Map<String, dynamic>.from(result);
      });
    } catch (e) {
      print("Error sending user info to native: $e");
    }
  }

  @override
  void initState() {
    super.initState();

    _setupChannels();

    _getBatteryLevel();
  }

  @override
  void dispose() {
    super.dispose();
    _eventSubscription?.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary,
       title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Count: $_count',
              style: Theme.of(context).textTheme.headlineMedium,
            ),

            SizedBox(height: 20),

            const Text('Device Battery:'),
            Text(
              '$_batteryLevel',
              style: Theme.of(context).textTheme.headlineMedium,
            ),

            SizedBox(height: 20),

            Text(
              'Time: $_timeValue',
              style: Theme.of(context).textTheme.bodyLarge,
            ),

            SizedBox(height: 10),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: _startTimer,
                  child: Text('Start Timer'),
                ),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: _stopTimer,
                  child: Text('Stop Timer'),
                ),
              ],
            ),

            SizedBox(height: 10),

            Text('User Info:'),
            Text(
              _basicMessageInfo.toString(),
              style: Theme.of(context).textTheme.bodyLarge,
            ),

            ElevatedButton(
              onPressed: _sendUserInfoToNative,
              child: Text('Send User Info to Native'),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _openNativePage,
        tooltip: 'Increment',
        child: const Icon(Icons.navigation_rounded),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}
  • AppDelegate.swift
swift 复制代码
import Flutter
import UIKit

// MARK: ChannelManager
public class ChannelManager {
    static let shared = ChannelManager()
    
    var methodChannel: FlutterMethodChannel?
    var basicMessageChannel: FlutterBasicMessageChannel?
    var eventChannel: FlutterEventChannel?
    
    private init() {}
}



// MARK: AppDelegate
@main
@available(iOS 13.0, *)
@objc class AppDelegate: FlutterAppDelegate {
    var flutterViewController: FlutterViewController?
    
    // event sink
    var timerEventSink: FlutterEventSink?
    
    // timer
    var timer: Timer?
    var startTimer: Date?
    
    
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
            GeneratedPluginRegistrant.register(with: self)
            
            // 获取FlutteViewController
            guard let flutterViewController = window?.rootViewController as? FlutterViewController else {
                fatalError("RootViewController is not FlutterViewController")
            }
            self.flutterViewController = flutterViewController
            
            // 设置Channe通道
            setupChannel(with: flutterViewController)
            
            return super.application(application, didFinishLaunchingWithOptions: launchOptions)
        }
    
    
    // 设置Channel通道
    private func setupChannel(with controller: FlutterViewController) {
        let methodChannel = FlutterMethodChannel(name: "method_channel", binaryMessenger: controller.binaryMessenger)
        let eventChannel = FlutterEventChannel(name: "event_channel", binaryMessenger: controller.binaryMessenger)
        let basicMessageChannel = FlutterBasicMessageChannel(
            name: "basic_message_channel",
            binaryMessenger: controller.binaryMessenger,
            codec: FlutterJSONMessageCodec.sharedInstance()
        )
        
        ChannelManager.shared.methodChannel = methodChannel
        ChannelManager.shared.eventChannel = eventChannel
        ChannelManager.shared.basicMessageChannel = basicMessageChannel
        
        methodChannel.setMethodCallHandler(methodCallHandler)
        eventChannel.setStreamHandler(self)
        basicMessageChannel.setMessageHandler(basicMessageHandler)
    }
}

// MARK: Handler
@available(iOS 13.0, *)
extension AppDelegate: FlutterStreamHandler {
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        guard let args = arguments as? String else {
            return FlutterError(code: "INVALID_ARGUMENTS", message: "Arguments must be a string", details: nil)
        }
        
        switch args {
        case "timer":
            timerEventSink = events
        default:
            return FlutterError(code: "UNKNOWN_STREAM", message: "Unknown stream: \(args)", details: nil)
        }
        
        return nil
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        if let args = arguments as? String {
            switch args {
            case "timer":
                timerEventSink = nil
            default:
                break
            }
        }
        return nil
    }
    
    private func methodCallHandler(call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "getBatteryLevel": getBatteryLevel(result)
        case "openNativePage": openNativePage(result)
        case "startTimer": startTimer(result)
        case "stopTimer": stopTimer(result)
        default: result(FlutterMethodNotImplemented)
        }
    }
    
    private func basicMessageHandler(message: Any?, reply: FlutterReply) {
        guard let messageDict = message as? [String: Any] else {
            reply(FlutterError(code: "INVALID_MESSAGE", message: "Invalid message format", details: nil))
            return
        }
        
        let response: [String: Any] = [
            "received": true,
            "receivedData": messageDict
        ]
        
        reply(response)
    }
}

// MARK: Founction
@available(iOS 13.0, *)
extension AppDelegate {
    private func getBatteryLevel(_ result: @escaping FlutterResult) {
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        
#if targetEnvironment(simulator)
        result(100)
#else
        if device.batteryState == UIDevice.BatteryState.unknown {
            result(FlutterError(
                code: "Error", message: "BatteryState is unkonw", details: nil
            ))
        } else {
            let level = Int(device.batteryLevel * 100)
            result(level)
        }
#endif
    }
    
    private func openNativePage(_ result: @escaping FlutterResult) {
        DispatchQueue.main.async {
            guard let flutterViewController = self.flutterViewController else {
                result(
                    FlutterError(
                        code: "ERROR",
                        message: "FlutterViewController is not initialized",
                        details: nil
                    )
                )
                return
            }
            
            let nativePage = NativePage()
            
            let navigationController = UINavigationController(rootViewController: nativePage)
            navigationController.modalPresentationStyle = .fullScreen
            flutterViewController.present(navigationController, animated: true)
            
            result(nil)
        }
    }
    
    private func startTimer(_ result: @escaping FlutterResult) {
        startTimer = Date()
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { _ in
            if let startTimer = self.startTimer {
                let elapsed = Date().timeIntervalSince(startTimer)
                let hours = Int(elapsed) / 3600
                let minutes = (Int(elapsed) % 3600) / 60
                let seconds = Int(elapsed) % 60
                let timeString = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
                
                self.timerEventSink?(timeString)
            }
        })
        result(nil)
    }
    
    private func stopTimer(_ result: @escaping FlutterResult) {
        timer?.invalidate()
        timer = nil
        startTimer = nil
        result(nil)
    }
}
  • NativePage.swift
swift 复制代码
import SwiftUI
import UIKit

@available(iOS 13.0, *)
class NativePage: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        title = "Native Page"
        
        setNavigation()
        
        setupView()
        
    }
    
    @objc private func dismissPage() {
        dismiss(animated: true)
    }
    
    private func setNavigation() {
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .close,
            target: self,
            action: #selector(dismissPage)
        )
    }
    
    
    private func setupView() {
        let swiftUIView = NativeSwiftUIView()
        
        let hostingController = UIHostingController(rootView: swiftUIView)
        
        addChild(hostingController)
        
        view.addSubview(hostingController.view)
        
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        hostingController.didMove(toParent:self)
    }
}


@available(iOS 13.0, *)
struct NativeSwiftUIView: View {
    @State private var count: Int = 0
    @State private var status: String = "no"
    
    var body: some View {
        VStack(spacing: 15) {
            
            Text("Native Page")
                .font(.title.bold())
            
            Text("Count: \(count)")
                .font(.headline)
            
            
            Button {
                increment()
            } label: {
                Text("Increment")
                    .foregroundColor(.white)
                    .padding(.horizontal, 20)
                    .padding(.vertical, 8)
                    .background(
                        RoundedRectangle(
                            cornerRadius: 20,
                            style: .continuous
                        )
                        .fill(Color(.systemBlue))
                    )
            }
            
            Text("BasicMessageSendStatus: \(status)")
            
            
            Button {
                sendComplexDataToFlutter()
            } label: {
                Text("Send Complex Data")
                    .foregroundColor(.white)
                    .padding()
                    .background(
                        RoundedRectangle(cornerRadius: 10)
                            .fill(Color.green)
                    )
            }
        }
    }
    
    private func increment() {
        count += 1
        guard let methodChannel = ChannelManager.shared.methodChannel else {
            return
        }
        
        methodChannel.invokeMethod("setFlutterCount", arguments: ["count": count])
    }
    
    private func sendComplexDataToFlutter() {
        let complexData: [String: Any] = [
            "fromNative": true,
            "timestamp": ISO8601DateFormatter().string(from: Date()),
            "deviceInfo": [
                "model": UIDevice.current.model,
                "systemVersion": UIDevice.current.systemVersion,
                "name": UIDevice.current.name
            ],
            "randomData": Array(1...5).map { _ in Int.random(in: 1...100) }
        ]
        
        ChannelManager.shared.basicMessageChannel?.sendMessage(complexData) { _ in
            status = "yes"
        }
    }
}
相关推荐
iReaShare7 小时前
如何将 iPhone 备份到云端:完整指南
ios
小赵小赵福星高照~8 小时前
iOS UI视图面试相关
ui·ios·面试
杂雾无尘11 小时前
苹果高管揭示苹果背后秘密:苹果为何不涉足搜索引擎领域?
ios·apple
二流小码农11 小时前
鸿蒙开发:一键更新,让应用无需提交应用市场即可下载安装
android·ios·harmonyos
张风捷特烈13 小时前
Flutter 知识集锦 | 如何得到图片主色
android·flutter
2501_915921431 天前
没有Mac如何完成iOS 上架:iOS App 上架App Store流程
android·ios·小程序·https·uni-app·iphone·webview
你听得到111 天前
揭秘Flutter图片编辑器核心技术:从状态驱动架构到高保真图像处理
android·前端·flutter
wilinz1 天前
Flutter Android 端接入百度地图踩坑记录
android·flutter