组件化知识梳理(1) - Arouter 的基本使用

组件化知识梳理目录

组件化知识梳理(1) - Arouter 的基本使用
组件化知识梳理(2) - Arouter 源码分析之 Complier SDK
组件化知识梳理(3) - Arouter 源码分析之运行时 SDK

一、前言

放假几天在家看了些关于组件化的文章,目前市面上已经有许多开源的组件化框架,大家可以看一下这篇文章 总结一波组件化的实现方案优缺点,里面对目前的组件化框架做了基本的介绍。

今天,我们就以阿里巴巴开源的组件化框架Arouter为例,先对组件化有一个基本的了解,主要看的是下面的官方文档和基本思想的介绍。

本文中的所有代码都可以通过 ArouterDemo 下载并调试。

二、实践

2.1 导入依赖包

在使用Arouter的时候,需要导入两个SDK,分别针对运行时和编译时:

运行时依赖 & 编译时依赖

不同的组件类型,需要导入不同的依赖包,一般来说,一个最简单的组件化框架由以下几部分构成,由上至下可以分为四个部分,其中需要导入Arouter依赖的有两种类型的组件:

  • 业务组件:导入编译时的依赖。
  • 路由基础组件:导入运行时的依赖,还需要导入编译时的依赖。
基本的组件化框架

实际的项目结构:


实际的项目结构

2.1.1 统一各依赖库版本

在这之前,我们需要做一些前期准备。由于组件化之后,项目中会存在多个Android Library,为了保证各LibrarycompileSdkVersionbuildToolsVersion,以及第三方库版本的统一性,我们最好在同一个地方声明这些信息,然后在各modulebuild.gradle文件中引入这些变量,步骤如下:

  • 第一步:在 工程的根目录 下创建config.gradle文件,该文件中声明各版本的信息:
ext {
    android = [
            compileSdkVersion : 25,
            buildToolsVersion : "25.0.2",
            minSdkVersion : 14,
            targetSdkVersion : 25
    ]
    dependencies = [
            "support-v4"  : 'com.android.support:support-v4:25.3.1',
            "support-v7"  : 'com.android.support:appcompat-v7:25.3.1',
            "junit" : 'junit:junit:4.12',
            "arouter-api" : 'com.alibaba:arouter-api:1.2.4',
            "arouter-compiler" : 'com.alibaba:arouter-compiler:1.1.4',
            "event-bus" : 'org.simple:androideventbus:1.0.5.1',
            "fastjson" : 'com.alibaba:fastjson:1.2.9'
    ]
}
  • 第二步:在 工程的根目录build.gradle的最上面导入该文件:
//统一依赖库版本。
apply from : "config.gradle"
  • 第三步:在 各模块 中引入第一步声明的变量,我们以App壳工程为例看一下,SdkVersion和依赖包需要通过不同的方式导入:
apply plugin: 'com.android.application'
//1. 定义 config 变量
def config = rootProject.ext

android {
    //2. SdkVersion 的导入方式,直接在各关键字后面加上定义的变量名
    compileSdkVersion config.android.compileSdkVersion
    buildToolsVersion config.android.buildToolsVersion
    defaultConfig {
        applicationId "com.lizejun.demo.arouterdemo"
        minSdkVersion config.android.minSdkVersion
        targetSdkVersion config.android.targetSdkVersion
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    //3. 依赖包的引入方式,在关键字后面加上 config.dependencies,最后用括号包裹定义的变量名。
    compile config.dependencies["support-v7"]
    testCompile config.dependencies["junit"]

    if (isNeedHomeModule.toBoolean()) {
        compile project(":module-home")
    }
    if (isNeedOtherModule.toBoolean()) {
        compile project(":module-other")
    }
    if (isNeedStoreModule.toBoolean()) {
        compile project(":module-store")
    }
}

2.1.2 各项目引入的依赖

下面我们来看一下各个组件是如何引入Arouter的依赖。

(1) App 壳

App壳中不需要引入SDK的依赖,只需要引入各 业务组件 即可。

App 壳

(2) 业务组件

在业务组件中,除了需要引入Arouter编译期 依赖库,以及 路由基础组件,还需要在android界面下进行javaCompileOptions的声明。

业务组件

(3) 路由基础组件

在路由基础组件中,需要同时引入Arouter编译期 & 运行时 依赖库,也需要在android界面下进行javaCompileOptions的声明,对于 基础组件的依赖 也在这里面声明,这样 业务组件 通过依赖 路由基础组件,也可以间接地调用 基础组件 中的功能。

路由基础组件

(4) 基础组件

基础组件不需要引入Arouter相关的依赖,只需要负责实现好与业务无关的功能就好。

2.2 如何让业务组件独立运行

组件化最重要的目的就是需要 保证各业务组件能够独立调试 & 运行,下面我们就来介绍一下如何实现,在我们的项目中有三个业务组件,module-mainmodule-othermodule-store,以module-main为例看一下如何实现:

  • 第一步:在 工程根目录 下的gradle.properties下声明对应module的属性,该属性为false时表示其可以 独立运行,不依赖于App壳:
isNeedHomeModule = true
  • 第二步:在module-mainbuild.gradle文件中加上红框内的三个部分:
module-main 的 build.gradle 文件

这么做的 原理 是:当组件需要 独立运行isNeedHomeModule标志位为false)的时候,需要保证以下三点:

  • 需要被声明为com.android.application
  • 需要指定applicationId
  • 需要为其在AndroidManifest.xml中指定一个默认启动的Activity,我们通过sourceSets的方式,为其在这两种情况下指定不同的AndroidManifest.xml文件,这样就不用每次都去修改了,但是要记得,如果在其中一个AndroidManifest.xml增加了声明,那么在对应的文件中也需要加上。

其在project视图下的位置如下所示:

project 视图下的位置

  • 作为Library时,/src/main下的AndroidManifest.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.demo.lizejun.module.home">

    <application
        android:allowBackup="true"
        android:label="@string/app_name"
        android:supportsRtl="true">
        <activity android:name="com.lizejun.demo.module.home.ResultClientActivity"/>
    </application>

</manifest>
  • 作为独立运行的模块时,/src/debug下的AndroidManifest.xml文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.demo.lizejun.module.home">

    <application
        android:allowBackup="true"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat">
        <activity android:name="com.lizejun.demo.module.home.HomeActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.lizejun.demo.module.home.ResultClientActivity"/>
    </application>

</manifest>

当需要独立运行一个组件的时候,将gradle.properties中的isNeedHomeModule改为false,然后sync一下工程,在最上面选择module-home,点击绿色箭头运行即可:

选择 module-home 运行

2.3 Arouter 的使用场景

经过了前面两步,整个组件化的框架就搭建起来了,下面再来介绍一下Arouter的使用场景。

2.3.1 简单的跨模块 Activity 跳转

对跳转的 目标界面@Route进行注解,该目标界面在另一个业务组件module-other当中,RouterMap.NO_RESULT_ACTIVITY是在路由基础组件lib-base中定义的常量,其基本思想为:

页面跳转基本思想

public class RouterMap {

    public static final String HOME_FRAGMENT = "/home/main";
    public static final String NO_RESULT_ACTIVITY = "/other/no_result";
    public static final String RESULT_SERVER_ACTIVITY = "/other/result_server";
    public static final String EVENT_BUS_ACTIVITY = "/other/event_bus";
    public static final String INTERCEPT_GROUP = "intercept_group";
    public static final String INTER_MIDDLE_ACTIVITY = "/other/inter_middle";
    public static final String INTER_TARGET_ACTIVITY = "/other/inter_target";
    public static final String STORE_MODULE_SERVICE = "/store/service";
    public static final String INJECT_ACTIVITY = "/other/inject";
    public static final String JSON_SERVICE = "/base/json_service";

}

module-other中的目标界面,使用@Route注解进行声明:

@Route(path = RouterMap.NO_RESULT_ACTIVITY)
public class NoResultActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Utils.toast(this, "NoResultActivity onCreate");
    }
}

module-main中通过ARouter提供的方法进行跨模块的跳转,build的参数就是@Route所指定的名称:

ARouter.getInstance().build(RouterMap.NO_RESULT_ACTIVITY).navigation();

2.3.2 带 ActivityResult 的跨模块 Activity 跳转

当需要获得目标界面的返回结果时,调用模块需要在navigation()方法调用时,声明额外的ConstantMap.FOR_RESULT_CODE作为reququestCode,这些常量和RouterMap类似,都放在路由基础模块lib-base中进行声明,方便其它的模块访问:

public class ResultClientActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_result_client);
        Button button = (Button) findViewById(R.id.bt_result_client);
        button.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                ARouter.getInstance().build(RouterMap.RESULT_SERVER_ACTIVITY).navigation(ResultClientActivity.this, ConstantMap.FOR_RESULT_CODE);
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case ConstantMap.FOR_RESULT_CODE:
                Utils.toast(ResultClientActivity.this, "receive=" + data.getStringExtra(ConstantMap.FOR_RESULT_KEY));
                break;
            default:
                break;
        }
    }
}

跳转目标模块通过setResult方法回传数据:

@Route(path = RouterMap.RESULT_SERVER_ACTIVITY)
public class ResultServerActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_result_server);
        Button button = (Button) findViewById(R.id.bt_for_result);
        button.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.putExtra(ConstantMap.FOR_RESULT_KEY, "返回数据给 Client");
                setResult(ConstantMap.FOR_RESULT_CODE, intent);
                finish();
            }
        });
    }

}

2.3.3 在 Fragment 中接收返回结果

2.3.2中的接收返回值的方式,只适用于在Activity中启动,并通过onActivityResult接收返回结果,如果希望在Fragment中启动目标界面,并接收返回结果,那么需要使用EventBus作为桥梁,在调用模块的Fragment中对EventBus进行注册,并通过@Subscriber声明回调的方法:

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        EventBus.getDefault().register(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unregister(this);
    }

    @Subscriber(tag = ConstantMap.EVENT_BUS_KEY)
    public void onEvent(String s) {
        Utils.toast(getContext(), s);
    }

调用模块的启动方法和前面类似:

ARouter.getInstance().build(RouterMap.EVENT_BUS_ACTIVITY)
    .withInt(ConstantMap.EVENT_BUS_DATA, 1000)
    .navigation();

在目标模块中,通过EventBus发送数据:

@Route(path = RouterMap.EVENT_BUS_ACTIVITY)
public class EventBusActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_event_bus);
        final int revValue = getIntent().getIntExtra(ConstantMap.EVENT_BUS_DATA, 0);
        Button button = (Button) findViewById(R.id.bt_event_bus);
        button.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View v) {
                EventBus.getDefault().post("通过 EventBus 返回数据=" + revValue, ConstantMap.EVENT_BUS_KEY);
                finish();
            }
        });
    }
}

2.3.4 服务声明

除了跳转以外,我们有时候还需要在一个业务组件中,用到其它业务组件提供的功能,那么这时候就需要用到 服务声明,其基本思想如下:

服务声明的基本思想

  • 在路由基础框架中声明服务的接口,需要继承于IProvider接口
public interface IStoreModuleService extends IProvider {

    /**
     * 获取是否登录的状态。
     * @return 是否登录。
     */
    boolean isLogin();

    /**
     * 设置是否登录。
     * @param login 是否登录。
     */
    void setLogin(boolean login);
}
  • 在提供服务的业务模块module-store,实现在路由基础框架中声明的接口,并使用@Route注解
@Route(path = RouterMap.STORE_MODULE_SERVICE)
public class StoreModuleServiceImpl implements IStoreModuleService {

    private static final String FILE = "account";
    private static final String IS_LOGIN = "is_login";


    private Context mContext;

    @Override
    public void init(Context context) {
        mContext = context;
    }

    @Override
    public boolean isLogin() {
        return mContext.getSharedPreferences(FILE, Context.MODE_PRIVATE).getBoolean(IS_LOGIN, false);
    }

    @Override
    public void setLogin(boolean login) {
        mContext.getSharedPreferences(FILE, Context.MODE_PRIVATE).edit().putBoolean(IS_LOGIN, login).apply();
    }
}
  • 在路由基础组件中,提供静态方法给业务组件调用:
public class StoreModuleRouterService {

    public static boolean isLogin() {
        IStoreModuleService chatModuleService = (IStoreModuleService) ARouter.getInstance().build(RouterMap.STORE_MODULE_SERVICE).navigation();
        return chatModuleService != null && chatModuleService.isLogin();
    }

    public static void setLogin(boolean login) {
        IStoreModuleService chatModuleService = (IStoreModuleService) ARouter.getInstance().build(RouterMap.STORE_MODULE_SERVICE).navigation();
        if (chatModuleService != null) {
            chatModuleService.setLogin(login);
        }
    }
}
  • module-store外的业务组件要使用到它所提供的服务时,调用StoreModuleRouterService提供的静态方法即可。

2.3.5 获取 Fragment

为要发现的Fragment添加注解:

@Route(path = RouterMap.HOME_FRAGMENT)
public class HomeFragment extends Fragment {
    //....
}

通过下面的方法,获得Fragment,并添加到布局当中。

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        addFragment();
    }

    private void addFragment() {
        Fragment homeFragment = getHomeFragment();
        FragmentUtils.addFragment(this, homeFragment, R.id.fl_container);
    }

    private Fragment getHomeFragment() {
        return (Fragment) ARouter.getInstance().build(RouterMap.HOME_FRAGMENT).navigation();
    }

}

2.3.6 拦截器

拦截器用于同一管理页面之间的跳转,其基本思想如下图所示,例如module-main要启动module-other中的B界面,但是B界面要求先登录,那么我们就可以在拦截器中判断用户是否登录,如果登录了,那么可以跳转到B界面,否则就先跳转到A界面进行登录。

拦截器的基本思想

其基本的实现方式为:

  • A界面用于登录:
@Route(path = RouterMap.INTER_MIDDLE_ACTIVITY)
public class InterMiddleActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_inter_middle);
        updateStatus();
        switchStatus();
    }

    private void updateStatus() {
        TextView tvStatus = (TextView) findViewById(R.id.tv_login_status);
        tvStatus.setText("登录状态=" + StoreModuleRouterService.isLogin());
    }

    private void switchStatus() {
        final Switch switchStatus = (Switch) findViewById(R.id.sw_login);
        switchStatus.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {

            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                StoreModuleRouterService.setLogin(isChecked);
                updateStatus();
            }
        });
    }
}
  • B表示需要在登录状态下才能跳转的界面,注意这里我们在@Route注解中,新增了一个extras属性,它表示该界面是需要登录的,我们在拦截器中会用到它。
@Route(path = RouterMap.INTER_TARGET_ACTIVITY, extras = ConstantMap.LOGIN_EXTRA)
public class InterTargetActivity extends AppCompatActivity {


}
  • 在路由基础组件中定义拦截器,在拦截器中,我们通过PostcardgetExtra方法来获得目标界面@Route中声明的extras属性,该属性是一个int类型,我们可以根据拦截的需求,在路由基础组件中定义不同的常量。
@Interceptor(priority = 1, name = "重新分组进行拦截")
public class BaseInterceptor implements IInterceptor {

    @Override
    public void process(Postcard postcard, InterceptorCallback callback) {
        //getExtra() 对应目标 Activity 中的 @Route 声明。
        if (postcard.getExtra() == ConstantMap.LOGIN_EXTRA) {
            //判断是否登录。
            boolean isLogin = postcard.getExtras().getBoolean(ConstantMap.IS_LOGIN);
            if (!isLogin) {
                //如果没有登录,那么跳转到登录界面。
                ARouter.getInstance().build(RouterMap.INTER_MIDDLE_ACTIVITY).navigation();
            } else {
                //否则继续放行。
                postcard.withString(ConstantMap.IS_LOGIN_EXTRA, "登录了!");
                callback.onContinue(postcard);
            }
        } else {
            //对于其他不需要登录的界面直接放行。
            callback.onContinue(postcard);
        }
    }

    @Override
    public void init(Context context) {

    }
}

2.3.7 依赖注入

当我们通过navigation方法启动目标页面的时候,会通过withXXX方法传递额外的数据,例如下面这样:

SerialBean bean = new SerialBean();
bean.setName("SerialBean");
bean.setAge(18);
ARouter.getInstance().build(RouterMap.INJECT_ACTIVITY)
    .withInt(ConstantMap.INJECT_AGE, 18)
    .withObject(ConstantMap.INJECT_OBJECT, bean)
    .navigation();

这些数据最终会放在调起目标页面的intent当中。在目标页面里,可以通过getIntent()方法来取出里面的值,当然,还有更简便的方法,就是使用 依赖注入 的方式,使得 目标界面中的成员变量可以自动被赋值,被赋值的变量需要使用@AutoWired注解来修饰,在目标界面中,一定要加上下面这句:

ARouter.getInstance().inject(this);

目标界面的代码如下:

@Route(path = RouterMap.INJECT_ACTIVITY)
public class InjectActivity extends AppCompatActivity {
    
    //使用 AutoWired 注解。
    @Autowired(name = ConstantMap.INJECT_AGE)
    int age;

    //使用 AutoWired 注解。
    @Autowired(name = ConstantMap.INJECT_OBJECT)
    SerialBean bean;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //需要额外调用这句话来进行依赖注入。
        ARouter.getInstance().inject(this);
        Utils.toast(this, "age=" + age + ",bean.age=" + bean.getAge() + ",bean.name=" + bean.getName());
    }
}

如果我们传递的数据中包含了Object,那么还需要在路由基础组件中定义序列化的服务,例如上面的SerialBean,服务器的定义如下:

@Route(path = RouterMap.JSON_SERVICE, name = "序列化")
public class JsonServiceImpl implements SerializationService {

    @Override
    public void init(Context context) {

    }

    @Override
    public <T> T json2Object(String text, Class<T> clazz) {
        return JSON.parseObject(text, clazz);
    }

    @Override
    public String object2Json(Object instance) {
        return JSON.toJSONString(instance);
    }

    @Override
    public <T> T parseObject(String input, Type clazz) {
        return JSON.parseObject(input, clazz);
    }
}

三、小结

以上就是Arouter的基本使用方式,完整的工程可以查看 ArouterDemo

哎,每天都感觉要学的东西太多,搞着搞着又凌晨一点多了,看着To do List待学习 Android 知识点,陷入了茫茫的焦虑当中。


更多文章,欢迎访问我的 Android 知识梳理系列:

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

推荐阅读更多精彩内容