Flutter 與 Native(iOS) 通信原理

Flutter 與 Native 通信原理

Flutter 是一個(gè)跨平臺(tái)開發(fā)框架仰禽,它使用了一種全新的方式纺蛆,自己重寫了一個(gè)平臺(tái)無關(guān)的渲染引擎勇边,它只提供畫布,所有的 UI 組件粒褒、渲染邏輯都是在這個(gè)引擎上處理的诚镰。但某些平臺(tái)獨(dú)有的功能特性,仍由平臺(tái)自己處理月杉,F(xiàn)lutter 提供了 Platform Channel 機(jī)制抠艾,讓消息能夠在 native 與 Flutter 之間進(jìn)行傳遞。

接下來我們將深入 Flutter engine 內(nèi)部检号,看看 Flutter 是如何調(diào)用 native 模塊,native 如何調(diào)用 Flutter翘盖,數(shù)據(jù)是如何在兩端傳遞。

Platform Channel

每個(gè) Channel 都有一個(gè)獨(dú)一無二的名字馍驯,Channel 之間通過 name 區(qū)分彼此玛痊。

Channel 使用 codec 消息編解碼器,支持從基礎(chǔ)數(shù)據(jù)到二進(jìn)制格式數(shù)據(jù)的轉(zhuǎn)換擂煞、解析。

Channel 有三種類型剑逃,分別是:
BasicMessageChannel:用于傳遞基本數(shù)據(jù)
MethodChannel: 用于傳遞方法調(diào)用官辽,F(xiàn)lutter 側(cè)調(diào)用 native 側(cè)的功能蛹磺,并獲取處理結(jié)果同仆。
EventChannel:用于向 Flutter 側(cè)傳遞事件,native 側(cè)主動(dòng)發(fā)消息給 Flutter俗或。
這三種類型比較相似,因?yàn)樗鼈兌际莻鬟f數(shù)據(jù)辛慰,實(shí)現(xiàn)方式也比較類似。

Channel 注冊

用官方的獲取電量的 Demo 來看看 Flutter 如何與 native通信帅腌。我們從調(diào)用處開始進(jìn)入 Flutter engine,一步步跟蹤代碼運(yùn)行過程戚篙。

在 iOS 項(xiàng)目中溺职,注冊獲取電量的 channel

- (BOOL)application:(UIApplication*)application
    didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  FlutterViewController* controller =
      (FlutterViewController*)self.window.rootViewController;

  FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
      methodChannelWithName:@"samples.flutter.io/battery"
            binaryMessenger:controller];
  __weak typeof(self) weakSelf = self;
  [batteryChannel setMethodCallHandler:^(FlutterMethodCall* call,
                                         FlutterResult result) {
    if ([@"getBatteryLevel" isEqualToString:call.method]) {
      int batteryLevel = [weakSelf getBatteryLevel];
      if (batteryLevel == -1) {
        result([FlutterError errorWithCode:@"UNAVAILABLE"
                                   message:@"Battery info unavailable"
                                   details:nil]);
      } else {
        result(@(batteryLevel));
      }
    } else {
      result(FlutterMethodNotImplemented);
    }
  }];

  FlutterEventChannel* chargingChannel = [FlutterEventChannel
      eventChannelWithName:@"samples.flutter.io/charging"
           binaryMessenger:controller];
  [chargingChannel setStreamHandler:self];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

代碼簡化一下

FlutterMethodChannel* batteryChannel = [FlutterMethodChannel
      methodChannelWithName:@"samples.flutter.io/battery"
            binaryMessenger:controller];
            
[batteryChannel setMethodCallHandler:^(FlutterMethodCall* call,
                                         FlutterResult result) {
    if ([@"getBatteryLevel" isEqualToString:call.method]) {
      int batteryLevel = [weakSelf getBatteryLevel];
      if (batteryLevel == -1) {
        result([FlutterError errorWithCode:@"UNAVAILABLE"
                                   message:@"Battery info unavailable"
                                   details:nil]);
      } else {
        result(@(batteryLevel));
      }
      ......
  }];

可以看到,首先初始化一個(gè)橋接乱灵,傳入橋接名和處理消息發(fā)送接收的類点待,橋接名為"samples.flutter.io/battery",處理消息的類為FlutterViewController癞埠。

通過setMethodCallHandler 方法,在 native 項(xiàng)目中注冊該橋接的回調(diào) handler颠区,其中參數(shù) result 表示向 Flutter 回傳的結(jié)果通铲。

- (void)setMethodCallHandler:(FlutterMethodCallHandler)handler {
  if (!handler) {
    [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:nil];
    return;
  }
  FlutterBinaryMessageHandler messageHandler = ^(NSData* message, FlutterBinaryReply callback) {
    FlutterMethodCall* call = [_codec decodeMethodCall:message];
    handler(call, ^(id result) {
      if (result == FlutterMethodNotImplemented)
        callback(nil);
      else if ([result isKindOfClass:[FlutterError class]])
        callback([_codec encodeErrorEnvelope:(FlutterError*)result]);
      else
        callback([_codec encodeSuccessEnvelope:result]);
    });
  };
  [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:messageHandler];
}

回顧上面的代碼,我們知道 _messenger 就是 FlutterViewController颅夺,此處又包裝了一個(gè)回調(diào) messageHandler,用于解碼二進(jìn)制消息 message吧黄,并向 Flutter 側(cè)回傳執(zhí)行結(jié)果 reply。

- (void)setMessageHandlerOnChannel:(NSString*)channel
              binaryMessageHandler:(FlutterBinaryMessageHandler)handler {
  NSAssert(channel, @"The channel must not be null");
  [_engine.get() setMessageHandlerOnChannel:channel binaryMessageHandler:handler];
}

FlutterViewController 不做處理廓八,將注冊事件轉(zhuǎn)發(fā)給 FlutterEngine。

- (void)setMessageHandlerOnChannel:(NSString*)channel
              binaryMessageHandler:(FlutterBinaryMessageHandler)handler {
  NSAssert(channel, @"The channel must not be null");
  FML_DCHECK(_shell && _shell->IsSetup());
  self.iosPlatformView->GetPlatformMessageRouter().SetMessageHandler(channel.UTF8String, handler);
}

FlutterEngine 又將橋接事件轉(zhuǎn)發(fā)給 PlatformViewIOS 的 PlatformMessageRouter

// PlatformMessageRouter
void PlatformMessageRouter::SetMessageHandler(const std::string& channel,
                                              FlutterBinaryMessageHandler handler) {
  message_handlers_.erase(channel);
  if (handler) {
    message_handlers_[channel] =
        fml::ScopedBlock<FlutterBinaryMessageHandler>{handler, fml::OwnershipPolicy::Retain};
  }
}

PlatformMessageRouter 的屬性 message_handlers_ 是個(gè)哈希表声功,key 是橋接名宠叼,value 放 handle先巴。原生注冊橋接方法冒冬,其實(shí)就是維護(hù)一個(gè) map 對(duì)象。

至此,注冊原生方法完成了证逻,整個(gè)流程如下

image.png

Flutter 調(diào)用 native 功能

先來看下 dart 側(cè)如何調(diào)用獲取電量的橋接方法

static const MethodChannel methodChannel =
      MethodChannel('samples.flutter.io/battery');
      
final int result = await methodChannel.invokeMethod('getBatteryLevel');

跟蹤進(jìn)去

// platform_channel.dart
const MethodChannel(this.name, [this.codec = const StandardMethodCodec()]); 
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
    assert(method != null);
    final dynamic result = await BinaryMessages.send(
      name,
      codec.encodeMethodCall(MethodCall(method, arguments)),
    );
    if (result == null)
      throw MissingPluginException('No implementation found for method $method on channel $name');
    return codec.decodeEnvelope(result);
  }

invokeMethod 方法將方法名和參數(shù)轉(zhuǎn)化為二進(jìn)制數(shù)據(jù)囚企,并通過 BinaryMessages send 方法發(fā)送出去。

// platform_messages.dart
static Future<ByteData> send(String channel, ByteData message) {
  final _MessageHandler handler = _mockHandlers[channel];
    if (handler != null)
      return handler(message);
    return _sendPlatformMessage(channel, message);
  }
  
static Future<ByteData> _sendPlatformMessage(String channel, ByteData message) {
    final Completer<ByteData> completer = Completer<ByteData>();
    ui.window.sendPlatformMessage(channel, message, (ByteData reply) {
      try {
        completer.complete(reply);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
          context: 'during a platform message response callback',
        ));
      }
    });
    return completer.future;
  }

_mockHandlers 是一個(gè)map龙宏,存放需要 mock 的橋接,如果想攔截 mock 某個(gè)橋接银酗,會(huì)在這里處理。

不是 mock 的橋接蛙讥,則轉(zhuǎn)發(fā)到 window.dart sendPlatformMessage方法灭衷。

// window.dart
  void sendPlatformMessage(String name,
                           ByteData data,
                           PlatformMessageResponseCallback callback) {
    final String error =
        _sendPlatformMessage(name, _zonedPlatformMessageResponseCallback(callback), data);
    if (error != null)
      throw new Exception(error);
  }
  
  
  String _sendPlatformMessage(String name,
                              PlatformMessageResponseCallback callback,
                              ByteData data) native 'Window_sendPlatformMessage';

_sendPlatformMessage 將調(diào)用 native 的方法 Window_sendPlatformMessage。

dart 側(cè)的代碼跟蹤結(jié)束了翔曲,接下來又到了 native 側(cè)。

void Window::RegisterNatives(tonic::DartLibraryNatives* natives) {
  natives->Register({
      {"Window_defaultRouteName", DefaultRouteName, 1, true},
      {"Window_scheduleFrame", ScheduleFrame, 1, true},
      {"Window_sendPlatformMessage", _SendPlatformMessage, 4, true},
      {"Window_respondToPlatformMessage", _RespondToPlatformMessage, 3, true},
      {"Window_render", Render, 2, true},
      {"Window_updateSemantics", UpdateSemantics, 2, true},
      {"Window_setIsolateDebugName", SetIsolateDebugName, 2, true},
  });
}

注冊 native 方法闻妓,供 dart 端調(diào)用傅蹂,其中 Window_sendPlatformMessage 被 dart 調(diào)用算凿,對(duì)應(yīng)的 _SendPlatformMessage 方法犁功。而 _SendPlatformMessage 又調(diào)用了SendPlatformMessage

// WindowClient
Dart_Handle SendPlatformMessage(Dart_Handle window,
                                const std::string& name,
                                Dart_Handle callback,
                                const tonic::DartByteData& data) {
  UIDartState* dart_state = UIDartState::Current();

  if (!dart_state->window()) {
    // Must release the TypedData buffer before allocating other Dart objects.
    data.Release();
    return ToDart("Platform messages can only be sent from the main isolate");
  }

  fml::RefPtr<PlatformMessageResponse> response;
  if (!Dart_IsNull(callback)) {
    response = fml::MakeRefCounted<PlatformMessageResponseDart>(
        tonic::DartPersistentValue(dart_state, callback),
        dart_state->GetTaskRunners().GetUITaskRunner());
  }
  if (Dart_IsNull(data.dart_handle())) {
    dart_state->window()->client()->HandlePlatformMessage(
        fml::MakeRefCounted<PlatformMessage>(name, response));
  } else {
    const uint8_t* buffer = static_cast<const uint8_t*>(data.data());

    dart_state->window()->client()->HandlePlatformMessage(
        fml::MakeRefCounted<PlatformMessage>(
            name, std::vector<uint8_t>(buffer, buffer + data.length_in_bytes()),
            response));
  }

  return Dart_Null();
}

該方法最終會(huì)調(diào)用WindowClient的HandlePlatformMessage方法。

WindowClient是父類浸卦,由子類RuntimeController具體實(shí)現(xiàn),RuntimeController又將該消息交給其代理RuntimeDelegate處理靴庆,而RuntimeDelegate的具體實(shí)現(xiàn)則是Engine類

// Engine.cc
void Engine::HandlePlatformMessage(
    fml::RefPtr<blink::PlatformMessage> message) {
  if (message->channel() == kAssetChannel) {
    HandleAssetPlatformMessage(std::move(message));
  } else {
    delegate_.OnEngineHandlePlatformMessage(std::move(message));
  }
}

首先檢查是否為了獲取資源,如果是怒医,則走獲取資源邏輯炉抒,否則走Engin的代理delegate邏輯,delegate的實(shí)現(xiàn)是Shell

// |shell::Engine::Delegate|
void Shell::OnEngineHandlePlatformMessage(
    fml::RefPtr<blink::PlatformMessage> message) {
  FML_DCHECK(is_setup_);
  FML_DCHECK(task_runners_.GetUITaskRunner()->RunsTasksOnCurrentThread());

  task_runners_.GetPlatformTaskRunner()->PostTask(
      [view = platform_view_->GetWeakPtr(), message = std::move(message)]() {
        if (view) {
          view->HandlePlatformMessage(std::move(message));
        }
      });
}

收到消息后向 PlatformTaskRunner 添加一個(gè) task稚叹,task 內(nèi)調(diào)用PlatformView(iOS中實(shí)現(xiàn)類為PlatfromViewIOS)的 HandlePlatformMessage 方法

// |shell::PlatformView|
void PlatformViewIOS::HandlePlatformMessage(fml::RefPtr<blink::PlatformMessage> message) {
  platform_message_router_.HandlePlatformMessage(std::move(message));
}

把消息轉(zhuǎn)發(fā)給PlatformMessageRouter

void PlatformMessageRouter::HandlePlatformMessage(
    fml::RefPtr<blink::PlatformMessage> message) const {
  fml::RefPtr<blink::PlatformMessageResponse> completer = message->response();
  auto it = message_handlers_.find(message->channel());
  if (it != message_handlers_.end()) {
    FlutterBinaryMessageHandler handler = it->second;
    NSData* data = nil;
    if (message->hasData()) {
      data = GetNSDataFromVector(message->data());
    }
    handler(data, ^(NSData* reply) {
      if (completer) {
        if (reply) {
          completer->Complete(GetMappingFromNSData(reply));
        } else {
          completer->CompleteEmpty();
        }
      }
    });
  } else {
    if (completer) {
      completer->CompleteEmpty();
    }
  }
}

從message_handlers_中取出channelName對(duì)應(yīng)的 handle 并執(zhí)行

handle 完成后焰薄,將結(jié)果回調(diào)給 Flutter

流程如下

image.png

Native 調(diào)用 Flutter 功能

在 Flutter 側(cè),注冊監(jiān)聽扒袖,處理接收從 native 側(cè)發(fā)來的消息

Stream<dynamic> receiveBroadcastStream([dynamic arguments]) {
    final MethodChannel methodChannel = MethodChannel(name, codec);
    StreamController<dynamic> controller;
    controller = StreamController<dynamic>.broadcast(onListen: () async {
      BinaryMessages.setMessageHandler(name, (ByteData reply) async {
        if (reply == null) {
          controller.close();
        } else {
          try {
            controller.add(codec.decodeEnvelope(reply));
          } on PlatformException catch (e) {
            controller.addError(e);
          }
        }
        return null;
      });
      try {
        await methodChannel.invokeMethod('listen', arguments);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
          context: 'while activating platform stream on channel $name',
        ));
      }
    }, onCancel: () async {
      BinaryMessages.setMessageHandler(name, null);
      try {
        await methodChannel.invokeMethod('cancel', arguments);
      } catch (exception, stack) {
        FlutterError.reportError(FlutterErrorDetails(
          exception: exception,
          stack: stack,
          library: 'services library',
          context: 'while de-activating platform stream on channel $name',
        ));
      }
    });
    return controller.stream;
  }

BinaryMessages.setMessageHandler 向 dart 側(cè)的全局map添加 key 為 name 的事件塞茅,當(dāng)接收到從 native 傳來的消息時(shí),找到就執(zhí)行對(duì)應(yīng) handle季率。

該方法向 native 側(cè)發(fā)送了一個(gè)名為 listen 的消息野瘦,這部分即是上面分析的 Flutter 調(diào)用 native 功能,不再贅述飒泻。

在 native 側(cè)鞭光,注冊key 為 listen 的橋接事件蠢络,key衰猛、handle 同樣放到 message_handlers_ 中,如下

- (void)setStreamHandler:(NSObject<FlutterStreamHandler>*)handler {
  if (!handler) {
    [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:nil];
    return;
  }
  __block FlutterEventSink currentSink = nil;
  FlutterBinaryMessageHandler messageHandler = ^(NSData* message, FlutterBinaryReply callback) {
    FlutterMethodCall* call = [_codec decodeMethodCall:message];
    if ([call.method isEqual:@"listen"]) {
      if (currentSink) {
        FlutterError* error = [handler onCancelWithArguments:nil];
        if (error)
          NSLog(@"Failed to cancel existing stream: %@. %@ (%@)", error.code, error.message,
                error.details);
      }
      currentSink = ^(id event) {
        if (event == FlutterEndOfEventStream)
          [_messenger sendOnChannel:_name message:nil];
        else if ([event isKindOfClass:[FlutterError class]])
          [_messenger sendOnChannel:_name
                            message:[_codec encodeErrorEnvelope:(FlutterError*)event]];
        else
          [_messenger sendOnChannel:_name message:[_codec encodeSuccessEnvelope:event]];
      };
      FlutterError* error = [handler onListenWithArguments:call.arguments eventSink:currentSink];
      if (error)
        callback([_codec encodeErrorEnvelope:error]);
      else
        callback([_codec encodeSuccessEnvelope:nil]);
    } else if ([call.method isEqual:@"cancel"]) {
      if (!currentSink) {
        callback(
            [_codec encodeErrorEnvelope:[FlutterError errorWithCode:@"error"
                                                            message:@"No active stream to cancel"
                                                            details:nil]]);
        return;
      }
      currentSink = nil;
      FlutterError* error = [handler onCancelWithArguments:call.arguments];
      if (error)
        callback([_codec encodeErrorEnvelope:error]);
      else
        callback([_codec encodeSuccessEnvelope:nil]);
    } else {
      callback(nil);
    }
  };
  [_messenger setMessageHandlerOnChannel:_name binaryMessageHandler:messageHandler];
}

當(dāng)接收到從 dart 側(cè)發(fā)來的 listen 事件刹孔,取出對(duì)應(yīng)的 handle 并執(zhí)行啡省。

這個(gè) handle 將 FlutterEventSink 傳給 native 調(diào)用方。

FlutterEventSink 把參數(shù)編碼后傳給 FlutterViewController髓霞,F(xiàn)lutterViewController 依次轉(zhuǎn)發(fā)給 FlutterEngine卦睹、PlatformViewIOS,最終轉(zhuǎn)發(fā)到 RuntimeController DispatchPlatformMessage方库。向 dart 側(cè)發(fā)出該消息结序。

流程如下

image.png

參考資料:
Flutter
Flutter engine
深入理解Flutter Platform Channel
Flutter與Native通信 - PlatformChannel源碼分析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市纵潦,隨后出現(xiàn)的幾起案子徐鹤,更是在濱河造成了極大的恐慌垃环,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件返敬,死亡現(xiàn)場離奇詭異遂庄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)劲赠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門涛目,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人凛澎,你說我怎么就攤上這事霹肝。” “怎么了塑煎?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵沫换,是天一觀的道長。 經(jīng)常有香客問我最铁,道長苗沧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任炭晒,我火速辦了婚禮,結(jié)果婚禮上甥角,老公的妹妹穿的比我還像新娘网严。我一直安慰自己,他們只是感情好嗤无,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布震束。 她就那樣靜靜地躺著,像睡著了一般当犯。 火紅的嫁衣襯著肌膚如雪垢村。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天嚎卫,我揣著相機(jī)與錄音嘉栓,去河邊找鬼。 笑死拓诸,一個(gè)胖子當(dāng)著我的面吹牛侵佃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奠支,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼馋辈,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了倍谜?” 一聲冷哼從身側(cè)響起迈螟,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤叉抡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后答毫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體褥民,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年烙常,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了轴捎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蚕脏,死狀恐怖侦副,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情驼鞭,我是刑警寧澤秦驯,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站挣棕,受9級(jí)特大地震影響台猴,放射性物質(zhì)發(fā)生泄漏青抛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望筹麸。 院中可真熱鬧,春花似錦佛掖、人聲如沸贴捡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽损敷。三九已至,卻和暖如春深啤,著一層夾襖步出監(jiān)牢的瞬間拗馒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來泰國打工溯街, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留诱桂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓呈昔,卻偏偏與公主長得像访诱,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子韩肝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 開篇 開局一張圖触菜,其他全靠_? 目前flutter框架還比較新哀峻,又是谷歌家的東西涡相,所以網(wǎng)上的文章基本都是講安卓和f...
    華南犀牛閱讀 17,835評(píng)論 22 47
  • Flutter的開源項(xiàng)目:http://www.reibang.com/p/7b0642a27eb0 Flutt...
    JasmineBen閱讀 6,788評(píng)論 0 18
  • 《圣經(jīng)》中認(rèn)為人一出生就背負(fù)著原罪哲泊,而人的一生就需要為自己的原罪去贖罪,這種理念實(shí)際上是教導(dǎo)人們向善催蝗。在日本寫...
    1704項(xiàng)楚閱讀 545評(píng)論 0 0
  • 1 最近小朋友感冒咳嗽切威,心里真是不舒服。昨天回來后丙号,看見小朋友好多了先朦,狀態(tài)也和平時(shí)差不多了,心里感覺就舒暢犬缨! 真希...
    甄峸閱讀 214評(píng)論 0 1
  • 在胡家悄悄搬走后喳魏,這群發(fā)了跡劉家人根本不認(rèn)大槐樹這壺酒錢,別說初一十五頂禮膜拜了怀薛,連個(gè)最起碼的尊重都沒有刺彩。大槐樹...
    槐蔭愚叟閱讀 1,294評(píng)論 0 1