android 官方mvp框架优化:lifecycle-mvp,像前端那样组合式写页面

转载请注明出处:
android 官方mvp框架优化:lifecycle-mvp,像前端那样组合式写页面
地址:http://www.jianshu.com/p/837168325131

目录

1 前言

虽然在标题上,自己很随意的起了这么一个名字。其实并不是说它起个英文名就牛逼了。说白了,它其实就是mvp的思想加了lifecycle-component,然后加入了分层的思想,最后用TypeFactory取代presenter。为什么要这么改呢?因为用mvp框架时确实存在了一些问题,这些小修小改都是基于业务的基础上。目的就是:在这种框架下,别人用起来你写的组件更方便,沟通成本更低,移植性也更好。每个人对mvp的理解不一样,给大家提供一种思路~

2 mvp框架含义

之前写过一篇文章介绍了一下google官方提倡的mvp框架:。为什么官方要推荐这个呢?因为虽然mvp框架定义很简单:m代表model,提供数据;v即view,提供的是供presenter调用的view相关的方法;p 即presenter,提供的是页面里触发动作的逻辑方法。那么实施起来呢?并不是那么统一,因为自由度很大。用什么来做view?presenter的调用者都是谁?他们怎么关联?他们和activity又有什么关系?所以一时之间,网上有很多自创的mvp框架。

3 谷歌官方推荐的mvp框架

于是在众说纷纭之中,官方推荐了一个mvp的版本,具体详情的可看上面的那个链接。大体说下:

  • 用contract来承载view和presenter的接口定义。这一点不是重点,设计接口规范很好。但是没有也没有关系,我认为有时候还会有过度设计之嫌,因为view和presenter都是和业务相关的,a业务场景硬要用b业务的view或presenter,肯定会有不合适之处(难道要用if...else)。即便现在合适,你挡不住pm给你加场景。
  • 用fragment来实现view接口。为什么要用fragment来实现view接口呢?因为fragment有生命周期。有生命周期怎么了?这个就要从presenter的使用说起了,我们知道presenter是页面里触发动作的逻辑方法。触发动作比如页面初始化加载,加载下一页,下拉刷新,编辑,提交,删除等。这些的方法执行逻辑我们都写在presenter里,这也是mvp区分视图逻辑和业务逻辑的核心。那么presenter的在哪里调用呢?两大类:一:页面的生命周期onStart()调用presenter页面初始化加载的方法。二:页面view的监听回调onXxxListener()方法里面调用presenter的刷新,编辑,提交等方法。所以fragment做为view层的实现类的好处就体现出来了:既有生命周期方法,又有view的监听回调onXxxListener()方法,可以满足对应的presenter的调用。
  • presenter处理页面里触发动作的逻辑。在fragment使用。
  • 在activty层关联fragment和presenter,传入参数,在setContentView()塞进去对应的view。

ok,基本说完了,以上几点大体就是google官方推荐的mvp框架。

4 谷歌官方mvp框架的无奈

4.1 官方mvp的缺点

为什么说它无奈呢?前面说了,对于view层的接口,使用fragment来进行实现,因为fragment有生命周期。所以正是因为fragment有生命周期才使用的fragment作为view层。但fragment太笨重了。试想一下,我有一个页面,里面有四五块内容。为了以后的各块内容的移动、去除、移植更方面,我希望每一块内容都做成mvp形式,块与块之间不耦合。那么官方的这个mvp框架就不适用了。因为你不可能在一个页面写5个fragment把。android的activity中不建议写那么多的fragment,fragment典型的使用场景是ViewPager。

4.2 常规变通

那么变通一下,5块内容的view层,不再用fragment实现,而只是一个个普通的view,每个view监听事件的响应还是在view中进行(调用各自的presenter方法)。而对于整个页面的初始化加载或者下拉刷新加载,这5块内容共用一个fragment,在这个fragment的onStart()和下拉刷新的监听回调中加载5块内容对应的presenter的方法。然后在fragment的onCreateView()中把5块内容的view填充进来。5块内容之间可能还需要通信,数据交流,这些借助presenter在fragment中进行。

5 带生命周期的mvp:lifecycle-mvp

上面那么做完全没有问题,并且上面那种做法也存在于我们的项目中。但通过几个版本的迭代,我发现了一些问题:presenter太乱,太散。fragment需要持有所有的presenter,在onStart()时load()数据。各自的view也需要持有各自的presenter。并且view和presenter之间需要互相set()。你还需要在activty或者fragment的onDetroy()方法中管理presenter。总体让人觉得很乱。尤其是如果你的组件需要被别人使用,或组件用需要用到其他app时,其他人拿到你的组件,你要关心两个东西view和presenter,他得知道这两个东西里面的方法,并且他需要在activty/fragment的生命周期中关联他们并调用一些方法。嗯。这个过程肯定存在的大量沟通成本~

5.1 build方式

我认为一个好的组件,应该是这样的:这个组件提供了一系列的build方法,你只需要

  View component  =  new Component.Builder()
                       .setXxx()
                       .setXxx()
                       .setXxx()
                       .build();

这样这个组件就构建成功了。并且这个组件是一个view,你可以把它用在任何的地方,不局限是activty或fragment。你可以把它放到更大的一个viewGroup中,然后直接把这个viewGroup放到activty的setContentView()中就可以运行。就像小时候玩积木一样,放到页面中就可以。没有presenter,外部不需要去管这个component是怎么加载数据的,加载是在其内部进行的。

看着不错,那么会遇到几个问题:

5.2 首先就没有activty或者fragment提供生命周期的话,怎么加载数据?

以前确实存在这个问题:只有fragment和activty能够提供生命周期。这也是google官方推荐的mvp框架中view层不得不使用fragment的原因。可能是google意识到了这个问题,于是就有了lifecycle-component这个组件。这个组件能够提供给任意持有非全局context的类提供生命周期。具体怎么提供,大致的原理可以看我写的这一篇博客。

5.3 真的没有presenter吗?

如果没有presenter,那么我们就又回到以前的老路了,什么业务逻辑,视图逻辑都在一个类中处理。这肯定是不行的。所以还是需要presenter。不同的地方是,这个presenter不暴露给外部,在view层内部持有,因为view层还有了生命周期,所以对presenter的方法调用都可以在view层进行。比如:

    public class AskAndAnswerEditorBlock extends FrameLayout implements LifecycleObserver {
      
      //成员变量定义

      public AskAndAnswerEditorBlock(Context context, boolean oldContentEditable, int maxLength, ...,boolean isEditSingleLine) {
           ....//构造逻辑
           init();
            }

      private void init() {
      //new 出来对应的 controller
      askAndAnswerEditorController = new AskAndAnswerEditorController(getContext(), this, editTypeFactory);
      //添加生命周期监听
    ((LifecycleRegistryOwner) getContext()).getLifecycle().addObserver(this);
    ...//其他初始化
        }

     //生命周期方法,调用controllor进行加载数据,在controller的load()内部,加载成功数据会调用view层的 binddata()方法。
     @OnLifecycleEvent(Lifecycle.Event.ON_START)
     public void onStart() {
    askAndAnswerEditorController.load();
     }


     //view层操作view的方法。controller使用
     public void bindData(AskAndAnswerEditorBlock_Bean bean) {
     
     }

    //view层的操作view的方法。controller使用
    public void showProgress(String msg) {
    mProgressHolder.showProgressIfAlreadyExisting(msg, true);
    }
    
    //view层的操作view的方法。controller使用
    public void hideProgress() {
    if (mProgressHolder!=null){
        mProgressHolder.hideProgress();
    }
    }
    
    //供外部使用触发方法
    public void submit() {
    askAndAnswerEditorController.submit();
    }
    
    //供外部使用的触发方法
    public void refresh() {
    askAndAnswerEditorController.refresh();
    }
    
    //使用它来进行相似业务的拓展
    public  interface EditTypeFactory {
    Observable getSubmitObservable(Context context, String content);

    void updateAndSync(Context context, Object o);

    String getOldContent(Context context);
    }


    //block对应的 Builder 
    public static class Builder {

     ...//成员变量定义

    public Builder(Context context) {
        this.context = context;
    }

    public Builder setOldContentEditable(boolean isEditable) {
        oldContentEditable = isEditable;
        return this;
    }
    ...//build方法
    public AskAndAnswerEditorBlock build() {
       ...
    }
    }
     }

从上面代码中我们可以看到,presenter的初始化和数据加载等都放在了view层里面,不需要借助activty/fragment的生命周期。

5.4 提供TypeFactory。

这样复用presenter大体逻辑的同时,也能进行相似业务的拓展。在上面的例子中也看到了这么一个接口。为什么要提供这么一个接口呢?因为我们在外部不需要提供presenter,而有时候又想拓展这个block的使用场景。比如我们上面这个例子,这个是一个编辑文字的组件,我们不但想让它能够编辑问题,答案,我们还想编辑点评短评。所以我们需要提供一组接口,还对不同的场景进行不同的实现,TypeFactory就是用来做这个的。那读者可能会问了,为什么不让外界提供presenter,这样岂不是更自由。是基于这么考虑的:这个AskAndAnswerEditorBlock组件的作用是提供文字编辑,然后提供,修改,还有一些提示,如果让外界提供presenter,那么不论是电影问答的presenter还是电影影评的presenter,肯定都有很大一部分的逻辑是相同的。比如

    class AskAndAnswerEditorController  {
     ...
     public AskAndAnswerEditorController(Context context, AskAndAnswerEditorBlock dataObserver, AskAndAnswerEditorBlock.EditTypeFactory editTypeFactory) {
     ...
       }

    public void load(){
    AskAndAnswerEditorBlock.AskAndAnswerEditorBlock_Bean bean=new AskAndAnswerEditorBlock.AskAndAnswerEditorBlock_Bean();
    if(editTypeFactory==null){
        Log.e("ask_and_answer","askAndAnswerEditorController==null");
    }
    bean.content=editTypeFactory.getOldContent(context);
    view.bindData(bean);
    }

    public void submit(){
    if(editTypeFactory==null){
        Log.e("ask_and_answer","askAndAnswerEditorController==null");
    }
    if (editTypeFactory.getSubmitObservable(context,view.editTextContent)==null) return;
    submitSubscription=editTypeFactory.getSubmitObservable(context,view.editTextContent).compose(MovieSchedulersTransformer.applySchedulers()).subscribe(
            new Subscriber() {
                @Override
                public void onStart() {
                    view.submitableStatuspublishSubject.onNext(false);
                    view.showProgress("提交中...");
                }

                @Override
                public void onCompleted() {
                    view.hideProgress();
                    view.submitableStatuspublishSubject.onNext(true);
                }

                @Override
                public void onError(Throwable e) {
                    Toast.makeText(context,"提交失败",Toast.LENGTH_LONG).show();
                    view.hideProgress();
                    // TODO: 17/8/17  进行handleException
                }

                @Override
                public void onNext(Object o) {
                    Toast.makeText(context, R.string
                            .movie_detail_commentary_submit_success_first, Toast.LENGTH_LONG).show();
                    editTypeFactory.updateAndSync(context,o);
                    if(context instanceof Activity){
                        ((Activity) context).finish();
                    }
                }
            }
    );

    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
     public void onDestroy() {
    if (submitSubscription!=null){
        submitSubscription.unsubscribe();
    }
    }

这么逻辑都是共有的。如果你让外界去提供presenter,那么他们需要写这些重复的逻辑。并且,有时候你让别人去写presenter,别人不知道你的view层是怎么写的,还得花时间读你的view层代码,还得花时间去写整个的presenter,这些都是需要很大成本的。如果只让他们提供差异性的东西,其他的不用他们管,这个组件岂不是更好用吗?build的构造方式就是这么个理念,只需要set()一些需要的东西,这个组件就构造出来就能用了。比如AskAndAnswerEditorController的使用:

     public class AnswerEditPage extends LinearLayout{

     public AskAndAnswerEditorBlock editView;

     public AnswerEditPage(Context context, AskAndAnswerEditorBlock.IReaddlySubmitListener listener, long movieId, long askId, long oldAnswerId,String question,String answer) {
    super(context);
    DimenUtils dimenUtils = DimenUtils.getInstance(context.getApplicationContext());
    View view= LayoutInflater.from(getContext()).inflate(R.layout.edit_answer_pager_textview,null);
    setOrientation(LinearLayout.VERTICAL);
    ((TextView) view).setText(question);
    addView(view,new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    editView = new AskAndAnswerEditorBlock.Builder(context)
            .setcontentMaxlength(2000)
            .setcontentMinlength(5)
            .setOldContentEditable(true)
            .setEditTypeFactory(new AnswerEditTypeFactory(movieId, askId,oldAnswerId,answer))
            .setTextChangeListener(listener)
            .setTextHint("快来说说你的看法吧 (5到2000字)")
            .build();
    addView(editView,new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    }
    }

这个AnswerEditPage直接放到activty的setContentView()中,页面就可以运行了。可能还会有读者会问,我提供presenter,岂不是会更自由吗?而不像TypeFactory那样,只能实现里面的一些接口,业务场景的适用性会不会太低了?但我想说的是,业务场景已经被view层限制死了,view视图已经存在了,那么业务场景基本上也就差不多定下来了。即使你提供presenter,业务场景也就那么一个种类。这时候还需要那么自由的presenter干什么呢??

5.5 与外界交互。

一个组件肯定少不了与外界的交互。这里就有一个问题,从上面看,貌似 block的数据加载都是在内部进行的,外部无法干涉。那如果我想获取block加载后额数据怎么办?还是前面的例子,比如AskAndAnswerEditorBlock 我编辑提交成功了,成功后会返回来一些数据,这些数据我需要用来更新其他的组件,怎么做到呢?其实数据的交流不在block里面,我们并不想要侵入写好的block。而只需要动动TypeFactory,TypeFactory是我们需要提供的,所以改动它再自然不过了。比如:

     public class AnswerEditTypeFactory implements AskAndAnswerEditorBlock.EditTypeFactory {

     public AnswerEditTypeFactory(long movieId,long questionId ,long oldAnswerId,String oldAnswer) {
     ...
      }

    public Observable getSubmitObservable(Context context, String content) {
    ...
    MovieAskAndAnswerApiProxy movieDetailApi=new MovieAskAndAnswerApiProxy(context.getApplicationContext());
    Observable result= movieDetailApi.submitAnswer(context, movieId, questionId,oldAnswerId,content);
    submitObservableUseForExternal(result);
    return result;
    }

    public void submitObservableUseForExternal(Observable result) {

    }

    @Override
    public void updateAndSync(Context context, Object result) {
     ...
    }

    @Override
    public String getOldContent(Context context) {
    ...
    }
    }

我们在submitObservable这个链式操作中 插入一个submitObservableUseForExternal(result)方法。这样,我们就可以在page层对处理组件与组件的交流了,比如:

    public class AnswerEditPage extends LinearLayout{

    public AskAndAnswerEditorBlock editView;

    public AnswerEditPage(Context context, AskAndAnswerEditorBlock.IReaddlySubmitListener listener, long movieId, long askId, long oldAnswerId,String question,String answer) {
    super(context);
    DimenUtils dimenUtils = DimenUtils.getInstance(context.getApplicationContext());
    View view= LayoutInflater.from(getContext()).inflate(R.layout.edit_answer_pager_textview,null);
    setOrientation(LinearLayout.VERTICAL);
    ((TextView) view).setText(question);
    addView(view,new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    editView = new AskAndAnswerEditorBlock.Builder(context)
            .setcontentMaxlength(2000)
            .setcontentMinlength(5)
            .setOldContentEditable(true)
            .setEditTypeFactory(new AnswerEditTypeFactory(movieId, askId,oldAnswerId,answer){
                @Override
                public void submitObservableUseForExternal(Observable result) {
                    //使用result来更新其他组件
                }
            })
            .setTextChangeListener(listener)
            .setTextHint("快来说说你的看法吧 (5到2000字)")
            .build();
    addView(editView,new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
      }

     }

当然,我们的block需要提供一个refresh()方法,供外部其他组件调用。其实refresh()的就是:

           public void refresh() {
    askAndAnswerEditorController.refresh();
    }

因为我们不向外提供presenter,所以向外提供的refresh()方法我们就写在block里面了。不过像refresh()这样被外界调用的方法并不多,所以这么写也可以接受。

5.6 分层思想

其实这点从上面的代码中就可以体现出来了,只是这里明确一下。这里的分层是为了明确边界,每一层盛放的是什么逻辑,不要混乱,明确职责。这样对代码的可读性和复用都有很大的好处。举个例子,一个页面有很多组件,组件之间肯定会有交流联系。我们复用组件时,肯定不会复用这个组件与其他组件的交流逻辑,那些是特定页面特定业务的产物。我们复用的只是组件的一般性功能逻辑。所以,如果没有分层,你复用的时候怎么搞?读代码,把公用逻辑摘出来?所以分层是必要的。当然这里的分层,只是我基于我们业务的理解,层次的多少,职责,因项目而异,大家看看心里有数就可以了。

  • 首先是view,这个view和前面讲的view层不同。这个view只是单纯的view,有一个bindData(D data)方法,被其他层调用作数据绑定。这个独立出来是因为它可以作为视图的最小单位,被其他各个层复用。举个例子:

          public class MovieQuestionNumTips extends FrameLayout implements IBlock<MovieQuestionNumTips.Bean> {
    
       public MovieQuestionNumTips(Context context) {
       this(context,null,0);
       }
    
      public MovieQuestionNumTips(Context context, AttributeSet attrs) {
      this(context, attrs,0);
       }
    
       public MovieQuestionNumTips(Context context, AttributeSet attrs, int defStyleAttr) {
      super(context, attrs, defStyleAttr);
      init();
       }
    
       private void init() {
      LayoutInflater.from(getContext()).inflate(R.layout.view_movie_question_num_tips,this);
       }
    
      @Override
      public void bindData(Bean bean) {
      if (bean==null) return;
      ((TextView) findViewById(R.id.movie_question_num_tips_movie_name)).setText("《"+bean.movieName+"》");
      ((TextView) findViewById(R.id.movie_question_num_tips_num)).setText(bean.movieQuestionNum+"");
      }
    
    
      public static class Bean {
      public String movieName;
      public int movieQuestionNum;
    
      public Bean(String movieName, int movieQuestionNum) {
          this.movieName = movieName;
          this.movieQuestionNum = movieQuestionNum;
      }
      }
      }
    
  • 如果是一个列表的话,这里会有一个adapter层,里面填充数据时,拿的就是上面讲的view进行填充。

  • 具有数据加载功能的模块是一个block,比如前面讲的AskAndAnswerEditorBlock。我们通过build实例化出来,放到业务page里。在比如一个下拉刷新的列表也是一个block,set进来需要的TypeFactory(即adapter和数据加载的observable)就ok,比如:

      private MovieRcPagePullToRefreshStatusBlock getRcViewBlock(Context context, final long movieId) {
      return new MovieRcPagePullToRefreshStatusBlock.Builder(context)
              .setPulltoRefreshable(true)
              .setEnablePinned(true)
              .setTypeFactory(new MovieRcPagePullToRefreshStatusBlock.TypeFactory() {
                  @Override
                  public Observable<? extends PageBase> getListObservable(Context context, boolean isRefresh, int offset, long timestamp) {
                      MovieAskAndAnswerApiProxy movieDetailApi=new MovieAskAndAnswerApiProxy(context.getApplicationContext());
                      return movieDetailApi.getAskAndAnswerList(context,movieId,10,offset,isRefresh? CachePolicy.PREFER_NETWORK:CachePolicy.PREFER_CACHE,timestamp);
                  }
    
                  @Override
                  public HeaderFooterAdapter getAdapter(Context context) {
                      return new MovieAskAdaper(context,movieId);
                  }
              }).build();
       }
    
  • 一个页面就是一个pager,pager其实是一个view。这里面盛放一个页面所有的block。以及各个block组件之间的交流通信逻辑。
    为什么不直接用activity,而加了一层pager?因为这是为多端复用准备的。我把一个页面的逻辑写在pager中,这样我可以很方便的
    把pager放到其他app中(因为每个app都有自己的BaseActivty,如果使用你自己的activity,有兼容工作要处理)。关于这点,可以参考我之前写的美团猫眼android模块化实战-可能是最详细的模块化实战,上面有详细的介绍。

  • 最近就是Activity层。这个里面除了放pager外,还需要做和actionBar相关的逻辑。因为actionBar是与activity关联的。pager中没办法获取这个东西,也就没法写关于actionBar的视图逻辑。

6 最后的话

俗话说,优化要建立在业务场景上。以上的修修补补也都是建立在我们的业务基础之上,因为我们的业务场景很多都是需要移植,需要复用。考虑沟通成本,移植难度,我们做了以上的优化。希望对大家有借鉴意义~

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

推荐阅读更多精彩内容