Flutter插件分享

新建Flutter工程的几种方式







1.Flutter Application

Application的目标是最终产出一个apk或者ipa包
->android:Android平台相关代码(其实就是一个完整的android项目)
->ios:iOS平台相关代码(也是一个完整的IOS项目)
->lib:flutter相关代码(lib包下的代码文件最终会被渲染到android和ios两个平台)
->pubspec.yaml: 整个Flutter项目的配置文件,配置FlutterSDK,图片、字体、插件等





2.Flutter Plugin

plugin是产出一个抽象层的组件,会包含原生代码





3.Flutter Package

package是产出一个dart组件





4.Flutter Module

Module最终是产出一个library
->.android:Android的宿主工程
->.ios:iOS的宿主工程
->lib:flutter相关代码
->pubspec.yaml:配置文件
因为宿主工程的存在,这个flutter工程是可以独立运行的,
通过安装了flutter与dart插件的Androidstudio 打开这个flutter_module项目,并且是可以运行的



理解flutter package



纯Dart插件工程,仅包含Dart层的实现,往往定义一些公共Widget和工具组件

1.在pubspec.yaml 添加描述信息
    name:组件名字
    description: 组件描述
    version: 版本号(如:0.0.1)
    author:作者-邮箱    
    homepage:github地址
2.README.md 添加使用说明 (非必要操作LICENSE摘抄)
3.CHANGELOG.md 添加版本变更记录
4.发布插件 flutter package pub publish --dry-run(预检查)
          flutter package pub publish(发布命令,第一次发布可能需要账号)


理解flutter plugin(介绍AndroidLog案例)

<table>
<tr>
<td bgcolor="black">
<font color="white">
当你在开发flutter应用的时候,有时会需要调用native的api,往往遇到flutter并没有相应的package


这时候flutter plugin就开始发挥作用了
</font>
</td>
</tr>
</table>

    ```
    1.lib文件夹添加 FlutterNativeLogPlugin.dart文件
    import 'dart:async';
    
    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    enum Log { DEBUG, WARNING, ERROR }
    
    class FlutterNativeLogPlugin {
        static const MethodChannel _channel =
            const MethodChannel('flutter_native_log_plugin');
    
        static Future<String> printLog({Log logType, String tag, String msg}) async {
            String log = "debug";
            if (logType == Log.WARNING) {
                log = "warning";
            } else if (logType == Log.ERROR) {
                log = "error";
            } else {
                log = "debug";
            }
    
            final Map<String, dynamic> params = <String, dynamic>{
                'tag': tag,
                'msg': msg,
                'logType': log
            };
    
            //如果params为null 则用 params ?? {}
            final String result = await _channel.invokeMethod('printLog', params);
            return result;
        }
    }   
    
    2.Android端Module需要依赖Flutter的方式 在该Module的build.gradle中添加
        def localProperties = new Properties()
        def localPropertiesFile = rootProject.file('local.properties')
        if (localPropertiesFile.exists()) {
            localPropertiesFile.withReader('UTF-8') { reader ->
                localProperties.load(reader)
            }           
        }   
        
        def flutterRoot = localProperties.getProperty('flutter.sdk')
        if (flutterRoot == null) {
            throw new GradleException("Flutter SDK not found, Define location with flutter.sdk in the local.properties file.")
        }
        
        //找到该脚本 然后可以依赖上flutter sdk
        apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
    
        flutter {
            source '../..'
        }
    
    3.在该Android的module中 新增FlutterNativePlugin.java
    package com.xxx.xxx;
    
    import android.util.Log;
    
    import io.flutter.plugin.common.MethodCall;
    import io.flutter.plugin.common.MethodChannel;
    import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
    import io.flutter.plugin.common.MethodChannel.Result;
    import io.flutter.plugin.common.PluginRegistry.Registrar;
    
    //FlutterNativeLogPlugin需要实现flutter中的MethodCallHandler类
    public class FlutterNativeLogPlugin implements MethodCallHandler {
        
        //注册插件
        public static void registerWith(Registrar registrar) {
            final MethodChannel channel = new MethodChannel(registrar.messenger(),"flutter_native_log_plugin");
            channel.setMethodCallHandler(new FlutterNativeLogPlugin());
        }
    
        @Override
        public void onMethodCall(MethodCall call, Result result) {
            if (call.method.equals("printLog")) {
                String msg = call.argument("msg");
                String tag = call.argument("tag");
                String logType = call.argument("logType");
    
                if (logType.equals("warning")) {
                    Log.w(tag, msg);
                } else if (logType.equals("error")) {
                    Log.e(tag, msg);
                } else {
                    Log.d(tag, msg);
                }
    
                result.success("Logged Successfully!");
            } else {
                result.notImplemented();
            }
        }
    }

    4.注册插件
    在GeneratedPluginRegistant.registerWith(this); (注册pubspec里面的插件)下面
    //注意registerFor方法
    FlutterNativeLogPlugin.registerWith(registerFor("包名.FlutterNativeLogPlugin"))
    ```

新的插件加载逻辑


Android工程与Flutter Module融合

1.新建一个Android Project(目录地址../flutter/projects/AndroidBindingFlutter)
2.新建一个Flutter Module(目录地址../flutter/projects/flutter_module)
3.在Android工程的setting.gradle配置关联flutter_module信息
    //主module名是‘app’,如果当前项目主module名不是‘app’,那就找不到目录,无法将相应产物打进apk中
    //改成 setBinding(new Binding([gradle: this, mainModuleName: '主module名']))即可
    setBinding(new Binding([gradle: this]))
    //File文件路径表示flutter_module的绝对路径
    evaluate(new File(settingsDir.parentFile, 'flutter_module/.android/include_flutter.groovy'))
4.app的build.gradle添加配置信息
    android {
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }

    dependencies {
        implemention project(':flutter')
    }


Android与Flutter混合开发

1.原生页面<font color=#ff4800>跳转</font>Flutter页面

**现在清单文件注册FlutterActivity**
<manifest>
    <application>
        <activity
            android:name="io.flutter.embedding.android.FlutterActivitu
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|                           fontScale|screenLayout|density|uiMode"
            android:hardworeAccelerated="true"
            android:theme="@style/AppTheme"
            android:windowSoftInputMode="adjustResize"/>
    </application>
</manifest>
**启动方式 FutterActivity默认路由名称为"/",默认打开main.dart的main方法**
//使用的默认路由"/"
startActivity(
    FlutterActivity.createDefaultIntent(this)
);
//FutterActivity路由名称为“route”,创建一个新的FlutterEngine对象
startActivity(
    FlutterActivity
        .withNewEngine()
        .initialRoute("route1")
        .build(this)
);
//FutterActivity 使用缓存的引擎对象
startActivity(
    FlutterActivity
        .withCachedEngine("engine_id")
        .build(this)
);
**flutter接收路由**
//在runApp里面获取路由
void main() => runApp(_widgetForRoute(window.defaultRouteName));

Widget _widgetForRoute(String Route) {
    根据不同Route返回对应的Widget    
}

2.原生页面<font color=#ff4800>嵌套</font>Flutter页面
<table>
<tr>
<td bgcolor="#cccccc">
<font color="white">
新版本的FlutterSDK已经没有Flutter类


不再支持诸如Flutter.createView()、Flutter.createFragment()等用法
</font>
</td>
</tr>
</table>

**FlutterView 案例**
public class DemoFlutterViewActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.demo_flutter_view_layout);
        FrameLayout frameLayout=findViewById(R.id.flutter_view_container);
        ProgressBar progress=findViewById(R.id.progress);

        //创建FlutterView
        FlutterView flutterView = new FlutterView(this);
        //创建FlutterView首帧渲染完成监听
        flutterView.addOnFirstFrameRenderedListener(new FlutterUiDisplayListener() {
            @Override
            public void onFlutterUiDisplayed() {
                //显示FlutterView
                frameLayout.setVisibility(View.VISIBLE);
            }
            @Override
            public void onFlutterUiNoLongerDisplayed() {
                //隐藏进度条
                progress.setVisibility(View.GONE);
            }
        });
        
        //将FlutterView添加到布局中
        ViewGroup.LayoutParams layoutParams=new LinearLayout.LayoutParam(
            FrameLayout.LayoutParams.MATCH_PARENT, 
            FrameLayout.LayoutParams.MATCH_PARENT);
        frameLayout.addView(flutterView, layoutParams);
        
        //关键代码 将Flutter页面显示到FlutterView中
        //一个engine只能加载一次flutter界面
        //一个flutteractivity需要一个flutterengine来加载flutter界面,
        //可以指定activity初始化flutterengine的模式:使用缓存或新建
        flutterView.attachToFlutterEngine(需要创建一个FlutterEngine);
    }
}
//布局文件 demo_flutter_view_layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:id="@+id/flutter_view_container"
        android:visibility="invisible"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    <ProgressBar
        android:id="@+id/progress"
        android:layout_width="45dp"
        android:layout_height="45dp"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true"
        android:visibility="visible"/>
</RelativeLayout>

**FlutterFragment 案例**  
public class DemoFlutterFragmentActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.demo_flutter_fragment_layout);
        FlutterFragment fragment = FlutterFragment.withNewEngine().initialRoute("home").build();
        getSupportFragmentManager()
            .beginTransaction()
            .add(R.id.flutter_fragment_container, fragment)
            .commit();
    }
}
//布局文件 demo_flutter_fragment_layout
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:id="@+id/flutter_fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

3.Flutter页面<font color=#ff4800>跳转</font>原生页面

案例:使用plugin的方式
//别忘记注册插件
//FlutterPluginJumpToAct.registerWith(registrarFor(FlutterPluginNavigator.CHANNEL));
public class FlutterPluginNavigator implements MethodChannel.MethodCallHandler {
    public static String CHANNEL = "com.flutter.plugin/activity";
    static MethodChannel channel;
    private Activity activity;

    private FlutterPluginNavigator(Activity activity) {
        this.activity = activity;
    }

    public static void registerWith(PluginRegistry.Registrar registrar) {
        channel = new MethodChannel(registrar.messenger(), CHANNEL);
        FlutterPluginNavigator instance = new FlutterPluginNavigator(registrar.activity());
        //setMethodCallHandler在此通道上接收方法调用的回调
        channel.setMethodCallHandler(instance);
    }

    @Override
    public void onMethodCall(MethodCall call, MethodChannel.Result result) {
        if (call.method.equals("定义的方法标识")) {
            //跳转到指定Activity
            Intent intent = new Intent(activity, XXXActivity.class);
            activity.startActivity(intent);
            //返回给flutter的参数
            result.success("success");
        } else {
            result.notImplemented();
        }   
    }
}

4.Flutter的路由与导航

管理多个页面时有两个核心概念和类:Route和 Navigator
Flutter中通过定义Route, 使用Navigator来跳转界面(通过route入栈和出栈来实现页面之间的跳转)
一个route是一个屏幕或页面的抽象、Navigator是管理route的Widget
>导航到新页面
    **显示跳转**
    Navigator.of(context).push(new MaterialPageRoute(builder: (context) {
        //指定跳转的页面
        return new Demo1();
        },));

    **隐式跳转**
    需要先定义,
        @override
        Widget build(BuildContext context) {
            return new MaterialApp(
                home: new Scaffold(
                body: ...,
            ),
            //定义路由
            routes: <String,WidgetBuilder>{
                "/xxx": (BuildContext context)=>new XXXPage(),
            },
        );
    后使用
        Navigator.of(context).pushNamed("/xxx");

    **打开路由映射页面,并销毁当前页**
    Navigator.of(context).pushReplacementNamed('/路由标识');

>移除页面
    Navigator.pop(...)
    **表示当前页面是否可以退出**
    Navigator.of(context).canPop()返回一个bool值
    **一种很友善的退出方式,如果能退出就退出**
    Navigator.of(context).maybePop() 《等价于 maybePop() => canPop() == true?pop() : do nothing》
    **点击退出当前页面,并将 路由映射的页面 压入栈中**
    Navigator.of(context).popAndPushNamed('/路由标识');
    **一直退出直到某一个页面 在路由页面上的Page都退出**
    Navigator.of(context).popUntil(ModalRoute.withName('/路由标识'));   
    **案例**
    例如我们的跳转顺序是Screen1—>Screen2—>Screen3—>Screen4
    Navigator.of(context).pushAndRemoveUntil(
          MaterialPageRoute(builder: (context) => Screen4()),
          ModalRoute.withName('/screen1'));
    表示跳转到screen4,并且移除所有的页面直到screen1
    newRoute和参数表示将要加入栈中的页面,predicate参数表示栈中要保留的页面底线


Flutter与Native通信

1.MethodChannel:用于传递方法调用

实现flutter与原生 双向通信

[图片上传失败...(image-898c07-1614331541410)]

Flutter端
**定义MethodChannel**
static const MethodChannel methodChannel = MethodChannel("包名/test");
**异步方法调用methodChannel中的invokeMethod方法,指定要调用的原生中的方法**
Future<void> _getActivityResult() async {
    //结果值
    String result;
    try {
        final int level = await methodChannel.invokeMethod('方法标志'); 
        result = 'success msg';
    } on PlatformException {
        result = 'error msg';
    }
    
    setState(() {
        _result = result;
    });
}   

Android端
**定义MethodChannel**
public static String CHANNEL = "包名/test";
**创建 MethodChannel 并通过 setMethodCallHandler 方法来区分 Flutter 的不同调用方法名和返回对应的回调**
//绑定对应的FlutterView
MethodChannel channel = new MethodChannel((FlutterView)flutterView, CHANNEL)
    .setMethodCallHandler(new MethodChannel.MethodCallHandler(){
        @Override
        public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
            if(methodCall.equals("方法标志")){
                //回调给flutter的参数
                result.success("success");
            }
        }
    });

//Android端也可以调用Flutter端方法
//channel.invoke(String method, Object arguments, Result callback)
//Flutter端 methodChannel.setMethodCallHandler(Future<dynamic> handler(MethodCall call)) { ... }

2.EventChannel:用于数据流信息通信

描述:native到flutter的单向调用(支持一对多调用)


案例:把电池状态变化由Native"推送"给Flutter

public class FlutterEventChannel implements EventChannel.StreamHandler {

    private static final String EVENT_CHANNEL_NAME = 包名/event名称";

    private FlutterEventChannel(FlutterView flutterView) {
        EventChannel eventChannel = new EventChannel(flutterView, EVENT_CHANNEL_NAME);
        eventChannel.setStreamHandler(this);
    }

    //创建EventChannel 把FlutterView传进来
    public static FlutterEventChannel create(FlutterView flutterView) {
        return new FlutterEventChannel(flutterView);
    }

    private EventChannel.EventSink eventSink;

    //onListen则代表通道已经建好,Native可以发送数据了
    @Override
    public void onListen(Object o, EventChannel.EventSink eventSink) {
        this.eventSink = eventSink;
    }

    //onCancel代表对面不再接收 应该做一些clean up的事情
    @Override
    public void onCancel(Object o) {
        eventSink = null;
    }
}

private BroadcastReceiver createChargingStateChangeReceiver(final EventSink events) {
    return new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);

            if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) {
                events.error("UNAVAILABLE", "Charging status unavailable", null);
            } else {
                boolean isCharging = 
                    status == BatteryManager.BATTERY_STATUS_CHARGING ||
                    status == BatteryManager.BATTERY_STATUS_FULL;
                // 把电池状态发给Flutter
                // 在Native这里转化为约定好的字符串
                events.success(isCharging ? "charging" : "discharging");
            }
        }
    };
}
    
//Flutter端  
static const EventChannel eventChannel = const EventChannel('com.meetyou.flutter/event');
  
@override
void initState() {
    super.initState();
    //接受native推送的消息
    eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
}
        
//events.success(isCharging ? "charging" : "discharging");的消息
void _onEvent(Object event) {
    setState(() {
        _chargingStatus = "Battery status: ${event == 'charging' ? '' : 'dis'}charging.";
    });
}

//events.error("UNAVAILABLE", "Charging status unavailable", null);的消息
void _onError(Object error) {
    setState(() {
        _chargingStatus = 'Battery status: unknown.';
    });
}

3.BasicMessageChannel:用于传递字符串和半结构化的信息

实现 Flutter 与 原生(Android 、iOS)双向通信

**Flutter端**
//创建 BasicMessageChannel
// flutter_and_native_100 为通信标识
// StandardMessageCodec() 为参数传递的 编码方式
static const messageChannel = const BasicMessageChannel('flutter_and_native_100', StandardMessageCodec());

//发送消息
Future<Map> sendMessage(Map arguments) async {
    Map reply = await messageChannel.send(arguments);
    //解析 原生发给 Flutter 的参数
    int code = reply["code"];
    String message = reply["message"];

    //更新 Flutter 中页面显示
    setState(() {
        recive = "code:$code message:$message";
    });
    return reply;
}
sendMessage({"method": "test", "ontent": "flutter 中的数据", "code": 100});

**Android端**
private BasicMessageChannel<Object> mMessageChannel;

private void messageChannelFunction() {
    //消息接收监听
    //BasicMessageChannel (主要是传递字符串和一些半结构体的数据)
    //创建通道
    mMessageChannel = new BasicMessageChannel<Object>(getFlutterView(), "flutter_and_native_100", StandardMessageCodec.INSTANCE);
    // 接收消息监听
    mMessageChannel.setMessageHandler(new BasicMessageChannel.MessageHandler<Object>() {
        @Override
        public void onMessage(Object o, BasicMessageChannel.Reply<Object> reply) {
            
            Map<Object, Object> arguments = (Map<Object, Object>) o;
            //方法名标识
            String lMethod = (String) arguments.get("method");
            //测试 reply.reply()方法 发消息给Flutter
            if (lMethod.equals("test")) {
                Toast.makeText(mContext, "flutter 调用到了 android test", Toast.LENGTH_SHORT).show();
                //回调Flutter  
                Map<String, Object> resultMap = new HashMap<>();
                resultMap.put("message", "reply.reply 返回给flutter的数据");
                resultMap.put("code", 200);
                //回调 此方法只能使用一次
                reply.reply(resultMap); 
            } 
        }
    });
}


Android中相关Flutter类理解

1.分析FlutterApplication

public class FlutterApplication extends Application {
    public void onCreate() {
        super.onCreate();
        //1. 检查方法必须在主线程中执行,否则抛异常
        //2. 初始化配置,通过从manifest XML文件获取Flutter配置值来初始化这些值
        //3. 初始化资源
        //4. 加载flutter.so动态库
        FlutterMain.startInitialization(this);
    }
}

2.分析FlutterActivity(android/io/flutter/embedding/android/)

//android/io/flutter/app/FlutterActivity.java已经废弃
public class FlutterActivity extends Activity 
    implements FlutterActivityAndFragmentDelegate.Host, LifecycleOwner {
    
    //主要职责
    //同步Activity生命周期(Lifecycle)
    //选择Flutter的初始化路由
    //提供子类钩子,提供和配置FlutterEngine
}

3.分析FlutterFragment

Flutter UI的一种方式

4.分析FlutterView

FlutterView的作用是将Flutter UI通过对应的FlutterEngine绘制后,显示在Android设备上

5.分析FlutterEngine

FlutterEngine是一个独立的Flutter运行环境,是Dart代码运行在Android应用的容器
可以通过FlutterEngineCache来管理FlutterEngine
    FlutterEngineCache
        .getInstance()
        .apply {
            put(engineId, FlutterEngineXXX)
            put(engineId, FlutterEngineXXX)
        }

    FlutterEngineCache的onDestroy
    需要手动释放 并且清空缓存
    创建FlutterUI的时候需要重新创建
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容