在已有項目中集成Flutter

本文翻譯自 Add-Flutter-to-existing-apps

在已有項目中繼承Flutter

約定:
本文中hostApp翻譯成: 宿主App, 主App,主應用程序,在示例中可以理解為MyApp

介紹

正在進行的一項為了更加容易地將Flutter添加到現有應用程序中的工作??梢栽?a target="_blank" rel="nofollow">Add2App project查看進度。

此頁面記錄了該工作的當前狀態,并將在我們構建必要的工具時進行更新。

最后更新時間為2018年11月26日。

“add2app”支持處于預覽狀態,目前僅在master分支上可用。

免責

由于Flutter的“Add2App”功能處于預覽狀態,因此相關的API和工具不穩定且可能會發生變化。

Flutter module項目模板

使用flutter create xxx 創建的含有Flutter/Dart代碼的Flutter項目包含非常簡單的原生應用程序(單一Activity的Android host和單一ViewController的iOS host)。你可以修改這些主應用程序以滿足你的需求并構建它們。

但是,如果你開始使用的是某一平臺的現有原生程序,你可能希望將Flutter項目作為某種形式的庫包含在該應用程序中。

這就是Flutter module模板提供的內容。執行 flutter create -t module xxx會生成一個Flutter項目,其中包含專為現有原生應用程序使用而設計的一個Android庫和一個Cocoapods pod。

Android部分

創建Flutter module

假設你已有的一個Android應用程序some/path/MyApp,并且希望將Flutter項目放在該同級目錄下:

$ cd some/path/
$ flutter create -t module my_flutter

這將在some/path/my_flutter/Flutte創建一個Flutter module,其中包含一個lib/main.dart文件作為入口,以及一個.android/的隱藏的子文件夾,它包含了Android庫中的模塊項目。

(雖然以下不需要,但如果您愿意,可以使用Gradle構建該庫:

$ cd .android/
$ ./gradlew flutter:assembleDebug

這會在.android/Flutter/build/outputs/aar/下生成一個flutter-debug.aar歸檔文件)

為宿主App添加Flutter module的依賴

將Flutter module作為子項目包含在主應用程序的settings.gradle中:

// MyApp/settings.gradle
include ':app'                                     // assumed existing content
setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
  settingsDir.parentFile,                                               // new
  'my_flutter/.android/include_flutter.groovy'                          // new
))

通過settings.gradle的綁定和script evaluation允許Flutter module include 自己(如:flutter)和模塊所使用的任意Flutter插件(如:package_info, :video_player等)

在你的應用程序中實現引入對Flutter module的依賴:

// MyApp/app/build.gradle
:
dependencies {
  implementation project(':flutter')
  :
}

在Java代碼中的Flutter module

使用Flutter module的Java API將Flutter視圖添加到主應用程序??梢酝ㄟ^直接使用Flutter.createView:

// MyApp/app/src/main/java/some/package/MainActivity.java
fab.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    View flutterView = Flutter.createView(
      MainActivity.this,
      getLifecycle(),
      "route1"
    );
    FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(600, 800);
    layout.leftMargin = 100;
    layout.topMargin = 200;
    addContentView(flutterView, layout);
  }
});

也可以創建一個自己處理生命周期FlutterFragment:

// MyApp/app/src/main/java/some/package/SomeActivity.java
fab.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
    tx.replace(R.id.someContainer, Flutter.createFragment("route1"));
    tx.commit();
  }
});

上面我們使用字符串"route1"告訴Dart代碼在Flutter視圖中顯示哪個widget。Flutter module項目模板中的lib/main.dart文件應該switch(或以其他方式解釋)提供的路由字符串,也可使用window.defaultRouteName,來確定要創建和傳遞到哪個widget到runApp。示例:

mport 'dart:ui';
import 'package:flutter/material.dart';

void main() => runApp(_widgetForRoute(window.defaultRouteName));

Widget _widgetForRoute(String route) {
  switch (route) {
    case 'route1':
      return SomeWidget(...);
    case 'route2':
      return SomeOtherWidget(...);
    default:
      return Center(
        child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
      );
  }
}

完全取決于你想要的路由字符串以及如何解釋它們。

構建和運行應用程序

構建和運行MyApp的方式與添加Flutter module依賴項之前的方式完全相同,通常使用Android Studio。編輯,調試和分析Android代碼也是如此。

熱重啟/重新加載和調試Dart代碼

完整的IDE集成以支持使用混合應用程序的Flutter / Dart代碼正在進行中。但目前已經提供了Flutter命令行工具和Dart Observatory Web用戶界面。

連接設備或啟動模擬器。然后使Flutter CLI工具監聽應用程序:

$ cd some/path/my_flutter
$ flutter attach
Waiting for a connection from Flutter on Nexus 5X...

從Android Studio以調試模式啟動MyApp。導航到使用Flutter的應用程序區域。然后回到終端,應該看到類似于以下內容的輸出:

Done.
Syncing files to device Nexus 5X...                          5.1s

??  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on Nexus 5X is available at: http://127.0.0.1:59556/
For a more detailed help message, press "h". To quit, press "q".

你現在可以編輯my_flutter的Dart代碼,按下r終端可以重新加載更改。還可以將上面的URL粘貼到瀏覽器中,以使用Dart Observatory設置斷點,分析內存和其他調試任務。

iOS部分

創建Flutter工程模塊

假設已有iOS項目在some/path/MyApp路徑上,我們在該同級目錄上創建一個Flutter工程模塊:

$ cd some/path/
$ flutter create -t module my_flutter

這將會在some/path/my_flutter/目錄下創建一個以lib/main.dart為入口的Flutter模塊項目。其中隱藏的.ios/子目錄中包含了一些Cocopods和幫助類的Ruby腳本。

為宿主App添加Flutter依賴模塊

下面的描述是假設你現有的iOS應用程序是通過使用Xcode 10.0且使用Objective-C來創建"Single View App"的項目。如果你現有應用程序具有不同的文件夾結構和/或/ .xcconfig文件,則可以重復使用這些文件,但可能需要相應地調整下面提到的一些相對路徑。

假定的文件夾結構如下:

.
├── MyApp
│   ├── MyApp
│   │   ├── AppDelegate.h
│   │   ├── AppDelegate.m
│   │   └── ...
│   ...
└── my_flutter
    ├── .ios
    │   ├── Config
    │   ├── Flutter
    │   ├── Runner
    │   ├── Runner.xcodeproj
    │   └── Runner.xcworkspace
    ├── lib
    │   └── main.dart
    ├── my_flutter.iml
    ├── my_flutter_android.iml
    ├── pubspec.lock
    ├── pubspec.yaml
    └── ..
將Flutter app添加到Podfile中

集成Flutter框架需要使用CocoaPods依賴項管理器。這是因為Flutter框架也需要可用于你可能包含在my_flutter中的任何Flutter插件。

如果需要,請參考cocoapods.org了解如何在開發設備上安裝CocoaPods。

如果你的主應用程序(MyApp)已在使用Cocoapods,只需執行以下操作即可與my_flutterapp集成:

1.將以下幾行添加到Podfile:

flutter_application_path = 'path/to/flutter_app/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)

注意:根據示例的項目結構這里path/to/flutter_app/需要替換為../my_flutter/

2.執行pod install

每當你在some/path/my_flutter/pubspec.yaml中更改了Flutter插件的依賴關系后,你都需要運行flutter packages get來通過podhelper.rb腳本更新some/path/my_flutter的插件列表。然后再在some/path/MyApp目錄下運行pod install。

podhelper.rb腳本將確保你的插件和Flutter.framework被添加到項目中,并禁用所有target的bitcode選項。

為Dart代碼添加構建階段

MyApp項目的導航器中最頂層,在主視圖的左側選擇TARGETS列表中的MyApp,然后選擇Build Phases選項卡。單擊主視圖左上角的+添加新構建階段。選擇New Run Script Phase并展開新添加到構建階段列表的Run Script。

將以下內容粘貼到Shell字段正下方的文本區域中:

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed

最后,將新構建階段拖到Target Dependencies phase下面。

你現在可以能夠使用?B來構建項目。

以上操作如下圖:

1.添加RunScript
2.添加構建命令
3.拖動Run Script
幕后操作

如果你因為某些理由要手動執行此操作或調試這些步驟無法正常工作的原因,請參閱以下內容:

  1. Flutter.framework(引擎庫)已嵌入到你的app中。這必須與發布類型(debug/profile/release)以及app的架構(arm *,i386,x86_64等)相匹配。Cocoapods將其作為一個框架,并確保它嵌入到你的原生app中。
  2. App.framework (你的Flutter應用程序二進制文件)已嵌入到你的app中。
  3. flutter_assets 文件夾作為資源嵌入 - 它包含字體,圖像,并且在某些構建模式下,它還包含引擎在運行時所需的二進制文件。 此文件夾如果有問題可能導致運行時錯誤,例如“無法運行引擎進行配置”(Could not run engine for configuration) - 通常表示文件夾未嵌入,或者你嘗試通過啟用AOT的引擎交叉JIT應用程序,反之亦然!
  4. 任何插件都會被添加為Cocoapods依賴,這么做對插件本身來說更加具體。不過從理論上講,應該可以手動合并它們。
  5. 對項目中的每個target禁用Bitcode。這是與Flutter引擎鏈接的必要條件。
  6. Generated.xcconfig(包含特定于Flutter的環境變量)包含在Cocoapods生成的release和debug.xcconfig文件中。

構建階段腳本(xcode_backend.sh)確保你構建的二進制文件與實際位于文件夾中的Dart代碼保持同步。

在宿主app中使用FlutterViewController

你應當根據你的宿主app的實際情況進行操作。下面是一個對Xcode 10.0生成的宿主app的空白屏幕示例(SingleViewApp)。

首先聲明你的app delegate繼承自FlutterAppDelegate。

AppDelegate.h

#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>

@interface AppDelegate : FlutterAppDelegate
@end

這種情況下AppDelegate.m的內容非常簡單,除非你的宿主App需要覆蓋其他方法:

#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins

#include "AppDelegate.h"

@implementation AppDelegate

// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

@end

如果你使用的是Swift,則可以在以下位置執行以下操作AppDelegate.swift:

import UIKit
import Flutter
import FlutterPluginRegistrant // Only if you have Flutter Plugins.

@UIApplicationMain
class AppDelegate: FlutterAppDelegate {

  // Only if you have Flutter plugins.
  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    GeneratedPluginRegistrant.register(with: self);
    return super.application(application, didFinishLaunchingWithOptions: launchOptions);
  }

}

<
<details>
<summary>如果app delegate已經從其他地方繼承,該如何處理? </summary>

需要為你的app delegate 實現 FlutterAppLifeCycleProvider協議例如:

#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins

@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@end

具體實現則應該委托給FlutterPluginAppLifeCycleDelegate


@implementation AppDelegate
{
  FlutterPluginAppLifeCycleDelegate *_lifeCycleDelegate;
}

- (instancetype)init {
  if (self = [super init]) {
      _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
  }
  return self;
}

- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
  [GeneratedPluginRegistrant registerWithRegistry:self]; // Only if you are using Flutter plugins.
  return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}

// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {
  UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
  if ([viewController isKindOfClass:[FlutterViewController class]]) {
      return (FlutterViewController*)viewController;
  }
  return nil;
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
  [super touchesBegan:touches withEvent:event];
  
  // Pass status bar taps to key window Flutter rootViewController.
  if (self.rootFlutterViewController != nil) {
      [self.rootFlutterViewController handleStatusBarTouches:event];
  }
}

- (void)applicationDidEnterBackground:(UIApplication*)application {
  [_lifeCycleDelegate applicationDidEnterBackground:application];
}

- (void)applicationWillEnterForeground:(UIApplication*)application {
  [_lifeCycleDelegate applicationWillEnterForeground:application];
}

- (void)applicationWillResignActive:(UIApplication*)application {
  [_lifeCycleDelegate applicationWillResignActive:application];
}

- (void)applicationDidBecomeActive:(UIApplication*)application {
  [_lifeCycleDelegate applicationDidBecomeActive:application];
}

- (void)applicationWillTerminate:(UIApplication*)application {
  [_lifeCycleDelegate applicationWillTerminate:application];
}

- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
  [_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}

- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
  [_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
  [_lifeCycleDelegate application:application
     didReceiveRemoteNotification:userInfo
           fetchCompletionHandler:completionHandler];
}

- (BOOL)application:(UIApplication*)application
          openURL:(NSURL*)url
          options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
  return [_lifeCycleDelegate application:application openURL:url options:options];
}

- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
  return [_lifeCycleDelegate application:application handleOpenURL:url];
}

- (BOOL)application:(UIApplication*)application
          openURL:(NSURL*)url
sourceApplication:(NSString*)sourceApplication
       annotation:(id)annotation {
  return [_lifeCycleDelegate application:application
                                 openURL:url
                       sourceApplication:sourceApplication
                              annotation:annotation];
}

- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
  [_lifeCycleDelegate application:application
     performActionForShortcutItem:shortcutItem
                completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
completionHandler:(nonnull void (^)(void))completionHandler {
  [_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
                completionHandler:completionHandler];
}

- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
  [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}

- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
  [_lifeCycleDelegate addDelegate:delegate];
}
@end

</details>

ViewController.m:


#import <Flutter/Flutter.h>
#import "ViewController.h"

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    [button addTarget:self
               action:@selector(handleButtonAction)
     forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Press me" forState:UIControlStateNormal];
    [button setBackgroundColor:[UIColor blueColor]];
    button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
    [self.view addSubview:button];
}

- (void)handleButtonAction {
    FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
    [self presentViewController:flutterViewController animated:false completion:nil];
}
@end

或者,使用Swift:

ViewController.swift:

import UIKit
import Flutter

class ViewController: UIViewController {
  override func viewDidLoad() {
    super.viewDidLoad()
    let button = UIButton(type:UIButtonType.custom)
    button.addTarget(self, action: #selector(handleButtonAction), for: .touchUpInside)
    button.setTitle("Press me", for: UIControlState.normal)
    button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
    button.backgroundColor = UIColor.blue
    self.view.addSubview(button)
  }

  @objc func handleButtonAction() {
    let flutterViewController = FlutterViewController()
    self.present(flutterViewController, animated: false, completion: nil)
  }
}

你現在應該能夠在模擬器或設備上構建和啟動MyApp。按下按鈕會全屏顯示帶有標準Flutter Demo計數應用程序的Flutter視圖。你可以使用路由在應用中的不同位置顯示不同的widgets,如上面的Android部分所述。要設置路由,需要調用:

  • Objective-C:
[flutterViewController setInitialRoute:@"route1"];
  • Swift:
flutterViewController.setInitialRoute("route1")

一旦你在創建FlutterViewController之后(并在presenting顯示前)。

你可以在Dart代碼中通過調用SystemNavigator.pop()讓Flutter應用程序消失。

構建和運行應用程序

使用Xcode構建和運行MyApp的方式與添加Flutter模塊依賴項之前完全相同。編輯,調試和分析iOS代碼也是如此。

熱重啟/重新加載和調試Dart代碼

連接設備或啟動模擬器。然后使Flutter CLI工具監聽應用程序:

$ cd some/path/my_flutter
$ flutter attach
Waiting for a connection from Flutter on iPhone X...

從Xcode以調試模式啟動MyApp。導航到使用Flutter的應用程序區域。然后回到終端,你應該看到類似于以下內容的輸出:

Done.
Syncing files to device iPhone 8...                          1.3s

??  To hot reload changes while running, press "r". To hot restart (and rebuild
state), press "R".
An Observatory debugger and profiler on iPhone 8 is available at:
http://127.0.0.1:54467/
For a more detailed help message, press "h". To detach, press "d"; to quit,
press "q".

你現在可以編輯my_flutter下的Dart代碼,按下r終端可以熱重啟加載更改。也可以將上面的URL粘貼到瀏覽器中,以使用Dart Observatory設置斷點,分析內存保留和其他調試任務。

調試特定的Flutter實例

可以將多個Flutter(root isolates)實例添加到應用程序中。flutter attach默認情況下連接到所有可用的isolates。然后,從連接的CLI發送的任何命令都會轉發到每個連接的isolates。

通過flutterCLI工具鍵入l來列出所有附加的isolates。如果未指定,則會從dart入口點文件和函數名稱自動生成isolates名稱。

l同時顯示兩個Flutter isolates的應用程序的示例輸出:

Connected views:
  main.dart$main-517591213 (isolates/517591213)
  main.dart$main-332962855 (isolates/332962855)

通過兩個步驟連接到特定的isolates:

1.在其Dart源文件中命名Flutter的根isolate。

// main.dart
import 'dart:ui' as ui;

void main() {
  ui.window.setIsolateDebugName("debug isolate");
  // ...
}

2.通過flutter attach--isolate-filter選項運行。

$ flutter attach --isolate-filter='debug'
Waiting for a connection from Flutter...
Done.
Syncing files to device...      1.1s

??  To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler is available at: http://127.0.0.1:43343/
For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".

Connected view:
  debug isolate (isolates/642101161)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容