手把手為你封裝一個(gè)MVP+RxJava+Retrofit2+Dagger2+BaseRecyclerView快速開(kāi)發(fā)框架刹悴,兩周一個(gè)版本不是夢(mèng)

距離我上次發(fā)表文章都有超過(guò)半年時(shí)間了提针,年前一直在復(fù)習(xí),年后一段時(shí)間都在找工作施敢,期間還去了一家公司三天周荐,覺(jué)得不合適就溜了,感覺(jué)挺對(duì)不起那家公司的僵娃。最后等了一個(gè)多月(期間自己也有一段時(shí)間去了復(fù)習(xí)怎么做網(wǎng)頁(yè))才入職一家比較知名的國(guó)企概作,拿到自己想要的薪水,也是對(duì)上一年自己學(xué)習(xí)成果的回報(bào)吧默怨,也實(shí)現(xiàn)了自己不想再待在外包公司小小的愿望⊙堕牛現(xiàn)在回想起2016剛畢業(yè)真的覺(jué)得有點(diǎn)苦,白天在外包公司工作量成倍的增長(zhǎng)匙睹,晚上還堅(jiān)持看書(shū)學(xué)習(xí)做筆記愚屁,最后真正實(shí)現(xiàn)了逃亡的目標(biāo)的時(shí)候自己也想放松一段日子,所以在2017年的上半年自己基本處于一種半頹廢的狀態(tài)垃僚,直到最近在新公司接到一個(gè)搭建新框架的任務(wù)才重新投入安卓的懷抱集绰,2017年自己的計(jì)劃也正式的算是開(kāi)始了规辱。
好吧谆棺,不扯個(gè)人經(jīng)歷這種無(wú)聊的話題了,回歸正題罕袋,通過(guò)一段時(shí)間的歸納和總結(jié)網(wǎng)上很多我覺(jué)得很好的代碼改淑,就自己搭建了一套快速開(kāi)發(fā)的框架,下面我就把自己的愚見(jiàn)和網(wǎng)上收集的資料分享一下浴讯。
如果不想聽(tīng)我多啰嗦的話可以在GitHub上直接下載源碼下載鏈接

1.首先朵夏,介紹一下BaseRecyclerView:

BaseRecyclerView適配器的使用.png

可以看到,封裝之后的Adapter榆纽,我們只需要重寫(xiě)convert一個(gè)方法就可以實(shí)現(xiàn)該有的效果仰猖,比原來(lái)要重寫(xiě)多個(gè)方法代碼簡(jiǎn)潔多了捏肢。
我們只用了

viewHolder.setText(R.id.tv_title,item.getTitle());

一句代碼就完成了對(duì)View的初始化和運(yùn)用,下面我們看看ViewHolder是怎么寫(xiě)的:

public class BaseViewHolder extends RecyclerView.ViewHolder {

    private SparseArray<View> mViews;
    public View mConvertView;
    public Context mContext;

    public BaseViewHolder(Context context,View itemView) {
        super(itemView);
        mViews = new SparseArray<>();
        mConvertView = itemView;
        mContext = context;
    }

    public View getConvertView()
    {
        return mConvertView;
    }

    public static BaseViewHolder createViewHolder(Context context, View itemView){
        BaseViewHolder holder = new BaseViewHolder(context, itemView);
        return holder;
    }

    public static BaseViewHolder createViewHolder(Context context, ViewGroup parent, int layoutId){
        View itemView = LayoutInflater.from(context).inflate(layoutId, parent,
                false);
        BaseViewHolder holder = new BaseViewHolder(context, itemView);
        return holder;
    }

    public BaseViewHolder setText(@IdRes int viewId, CharSequence value) {
        TextView view = getView(viewId);
        view.setText(value);
        return this;
    }

    public BaseViewHolder setBackgroundColor(@IdRes int viewId, @ColorInt int color) {
        View view = getView(viewId);
        view.setBackgroundColor(color);
        return this;
    }

    public BaseViewHolder setBackgroundRes(@IdRes int viewId, @DrawableRes int backgroundRes) {
        View view = getView(viewId);
        view.setBackgroundResource(backgroundRes);
        return this;
    }

    public BaseViewHolder setTextColor(@IdRes int viewId, @ColorInt int textColor) {
        TextView view = getView(viewId);
        view.setTextColor(textColor);
        return this;
    }

    public BaseViewHolder setImageDrawable(@IdRes int viewId, Drawable drawable) {
        ImageView view = getView(viewId);
        view.setImageDrawable(drawable);
        return this;
    }

    public BaseViewHolder setImageBitmap(@IdRes int viewId, Bitmap bitmap) {
        ImageView view = getView(viewId);
        view.setImageBitmap(bitmap);
        return this;
    }

    public BaseViewHolder setVisible(@IdRes int viewId, boolean visible) {
        View view = getView(viewId);
        view.setVisibility(visible ? View.VISIBLE : View.GONE);
        return this;
    }

    //關(guān)于事件
    public BaseViewHolder setOnClickListener(@IdRes int viewId, View.OnClickListener listener) {
        View view = getView(viewId);
        view.setOnClickListener(listener);
        return this;
    }



    public BaseViewHolder setOnTouchListener(@IdRes int viewId,View.OnTouchListener listener)
    {
        View view = getView(viewId);
        view.setOnTouchListener(listener);
        return this;
    }

    public BaseViewHolder setOnLongClickListener(@IdRes int viewId,View.OnLongClickListener listener)
    {
        View view = getView(viewId);
        view.setOnLongClickListener(listener);
        return this;
    }



    public <T extends View> T getView(int viewId){
        View view = mViews.get(viewId);
        if(view == null){
            view = itemView.findViewById(viewId);
            mViews.put(viewId,view);
        }
        return (T) view;
    }
}

關(guān)鍵的三步:

1.用SparseArray來(lái)存儲(chǔ)各個(gè)View饥侵;
2.用getView的方法去集體聲明View鸵赫;
3.用系統(tǒng)給出的setText等方法達(dá)到你想要的效果。

簡(jiǎn)單的封裝就能省略了開(kāi)發(fā)中初始化ViewHolder的很多重復(fù)繁雜的代碼~
在BaseAdapter躏升,我也做了一些小小的封裝處理辩棒,把a(bǔ)dapter的onCreateViewHolder(),onBindViewHolder()膨疏,getItemCount()三個(gè)必要寫(xiě)的方法也減少為只需要重寫(xiě)convert()一個(gè)方法一睁。

BaseRecyclerView其他用法.png

可以看到,在adapter我同樣封裝了增加頭部佃却、尾部和子項(xiàng)監(jiān)聽(tīng)事件者吁,在下拉刷新,上拉加載方面饲帅,我用了之前自己寫(xiě)過(guò)的一個(gè)SwipeRecyclerView融合在一起砚偶,有興趣的朋友可以去看看SwipeRecyclerView源碼下載鏈接
至于怎么實(shí)現(xiàn),我是參考了網(wǎng)上幾個(gè)對(duì)RecyclerView封裝的很好的框架進(jìn)行修改的洒闸。以下是參考的鏈接:
BaseRecyclerViewHelperAdapter
為RecyclerView打造通用Adapter 讓RecyclerView更加好用

2.RxJava和Retrofit2的兩種網(wǎng)絡(luò)鏈接方式:

因?yàn)楦魅硕加凶约旱膼?ài)好染坯,所以我聽(tīng)取別人給我的建議,選擇去封裝了兩種不同鏈接方式丘逸。

(1)分參數(shù)上傳:

分參數(shù)上傳.png

分參數(shù)上傳-View代碼.png

分參數(shù)上傳-Presenter代碼.png

這種方式是RxJava和Retrofit2在MVP模式中最普遍的用法单鹿,它跟以前很多網(wǎng)絡(luò)框架不一樣要先確定參數(shù)和JavaBean,所以也有習(xí)慣了以前模式的人吐槽這種方式深纲,特別是Presenter的代碼量太多仲锄,冗余得代碼也多,但是這種封裝卻是最能體現(xiàn)RxJava和Retrofit2特性的封裝湃鹊,以后查詢接口可以一目了然地知道鏈接和所需要的參數(shù)儒喊。
這種鏈接方式比較常見(jiàn),所以我也不做太多的解釋币呵。
以下是參考鏈接:
GeekNews

(2)批量參數(shù)上傳:
批量參數(shù)上傳-View層請(qǐng)求代碼.png
批量參數(shù)上傳-Presenter層請(qǐng)求代碼.png

很明顯怀愧,這種封裝Presenter層的代碼量看上去會(huì)減少很多,同時(shí)余赢,這樣的封裝跟以前很多網(wǎng)絡(luò)框架的模式很像:

(1)用一個(gè)HashMap裝載所有的請(qǐng)求參數(shù)芯义;
(2)url和參數(shù)直接在View層顯示;
(3)在請(qǐng)求的時(shí)候返回是一個(gè)callback妻柒。

下面我們看看代碼:
一開(kāi)始我還以為Retrofit不能批量參數(shù)上傳的扛拨,只能確定好每個(gè)參數(shù),但是后來(lái)在網(wǎng)上才發(fā)現(xiàn)原來(lái)還有這種操作举塔,所以就參照著幾個(gè)好的框架來(lái)實(shí)現(xiàn)了一次~

public interface BaseServerApi {

    @POST("{path}")
    Observable<ResponseBody> post(
            @Path(value = "path", encoded = true) String path,
            @QueryMap Map<String, Object> map);

    @GET("{path}")
    Observable<ResponseBody> get(
            @Path(value = "path", encoded = true) String path,
            @QueryMap Map<String, Object> map);
}

Retrofit2的相關(guān)配置和Get绑警、Post上傳方法

public class RetrofitHelper {

    private static final int CONNECT_TIME_OUT = 20;
    private static final int READ_TIME_OUT = 20;
    private static final int WRITE_TIME_OUT = 20;
    private static final int MAX_CACHE_SIZE = 1024 * 1024 * 50;
    private static final String HTTP_CACHE_DIR = Environment.getExternalStorageDirectory() + "/HTTP_LIBRARY_CACHE";

    private static Retrofit retrofit;
    private static BaseServerApi baseServerApi;

    private static OkHttpClient mOkHttpClient = null;

    @Inject
    //初始化Retrofit
    public RetrofitHelper() {
        initOkHttp();
        retrofit = new Retrofit.Builder()
                .baseUrl(AppURL.BaseURL)
                .client(mOkHttpClient)
                .addConverterFactory(GsonConverterFactory.create()) //自定義的gsonConverter 在里面處理了登錄失效處理
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();
        baseServerApi = retrofit.create(BaseServerApi.class);

    }

    //get方法
    public <T> T Get(String url, Map<String, Object> maps, BaseSubscriber<T> subscriber){

        return (T) baseServerApi.get(url,maps)
                .compose(RxUtil.<ResponseBody>rxSchedulerHelper())
                .compose(RxUtil.<T>handleResult())
                .subscribe(subscriber);
    }

    //post方法
    public <T> T Post(String url, Map<String, Object> maps, BaseSubscriber<T> subscriber){

        return (T) baseServerApi.post(url,maps)
                .compose(RxUtil.<ResponseBody>rxSchedulerHelper())
                .compose(RxUtil.<T>handleResult())
                .subscribe(subscriber);
    }

    //配置okHttp的數(shù)據(jù)
    public static void initOkHttp() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        if (Logger.DEBUG) {
            HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
            loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
            builder.addInterceptor(loggingInterceptor);
        }

        File cacheFile = new File(HTTP_CACHE_DIR);
        Cache cache = new Cache(cacheFile, MAX_CACHE_SIZE);
        Interceptor cacheInterceptor = new CacheInterceptor();
        //設(shè)置緩存
        builder.addNetworkInterceptor(cacheInterceptor);
        builder.addInterceptor(cacheInterceptor);
        builder.cache(cache);
        //設(shè)置超時(shí)
        builder.connectTimeout(CONNECT_TIME_OUT, TimeUnit.SECONDS);
        builder.readTimeout(READ_TIME_OUT, TimeUnit.SECONDS);
        builder.writeTimeout(WRITE_TIME_OUT, TimeUnit.SECONDS);

        //錯(cuò)誤重連
        builder.retryOnConnectionFailure(true);
        mOkHttpClient = builder.build();
    }
}

按照一位以前班里的大神說(shuō):
這種方式來(lái)說(shuō)求泰,可讀性可能比較差點(diǎn),但是如果以后你想換一種網(wǎng)絡(luò)框架计盒,只需要把callback稍微改一下就好了而不需要像第一種那樣整個(gè)項(xiàng)目都要傷筋動(dòng)骨拜秧,但是這樣做卻把RxJava的鏈?zhǔn)浇Y(jié)構(gòu)的特性給抹殺掉了。章郁。枉氮。
其實(shí)兩種鏈接方式各有各的優(yōu)點(diǎn),看你喜歡哪一種暖庄,或者按照個(gè)人習(xí)慣去選擇聊替,兩種方式我都放在框架里面了。
以下是參考鏈接:
Android 巧妙封裝培廓,基于Retrofit+RxJava網(wǎng)絡(luò)框架“Leopard”---完整淺析
Novate:Retrofit2.0和RxJava的又一次完美改進(jìn)加強(qiáng)惹悄!

其實(shí),很多時(shí)候我們請(qǐng)求數(shù)據(jù)回來(lái)都需要有第一層封裝去處理肩钠,譬如說(shuō)超時(shí)登錄等等泣港,所以接下來(lái)我們也來(lái)封裝一下:
第二種網(wǎng)絡(luò)鏈接方式的處理:

第二種網(wǎng)絡(luò)鏈接方式的第一層數(shù)據(jù)封裝.png
public static <T> Observable.Transformer<ResponseBody, T> handleResult() {
        return new Observable.Transformer<ResponseBody, T>() {
            @Override
            public Observable<T> call(Observable<ResponseBody> tObservable) {
                return tObservable.flatMap(
                        new Func1<ResponseBody, Observable<T>>() {
                            @Override
                            public Observable<T> call(ResponseBody responseBody) {
                                try {
                                    String jstr = new String(responseBody.bytes());
                                    Type type = new TypeToken<WXHttpResponse>() {
                                    }.getType();
                                    Log.e("responseBody",jstr);
                                    //需要手動(dòng)解析Json,WXHttpResponse相當(dāng)于BaseBean价匠,自己可以修改
                                    WXHttpResponse wxHttpResponse = new Gson().fromJson(jstr,type);
                                    //驗(yàn)證成功返回碼
                                    if (wxHttpResponse.getCode() == 200) {
                                        Log.e("wxHttpResponse",wxHttpResponse.getNewslist().toString());
                                        return (Observable<T>) Observable.just(wxHttpResponse.getNewslist());
                                    } else {
                                        // 處理被踢出登錄情況
                                        return Observable.error(new ReloginException("服務(wù)器返回error"));
                                    }
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                    }
                                return Observable.error(new ReloginException("服務(wù)器返回error"));
                            }
                        }
                );
            }
        };
    }
public abstract class BaseSubscriber<T> extends Subscriber<T> {

    private Task<T> resultTask;
    private String msg;

    public BaseSubscriber(){
        Type type = getSuperClassGenricType(getClass(),0);
        this.resultTask = new Task(type);
    }

    @Override
    public void onCompleted() {

    }

    @Override
    public void onError(Throwable throwable) {
        throwable.printStackTrace();

        if(throwable instanceof ReloginException){
            // 踢出登錄
        }else if (throwable instanceof UnknownHostException) {
            msg = "沒(méi)有網(wǎng)絡(luò)...";
        } else if (throwable instanceof SocketTimeoutException) {
            // 超時(shí)
            msg = "超時(shí)...";
        }else{
            msg = "請(qǐng)求失敗当纱,請(qǐng)稍后重試...";
        }
        onErrorT(msg);
    }

    @Override
    public void onNext(T t) {
        if(t != null) {
            Gson gson = new Gson();
            String result = gson.toJson(t);
            resultTask.setJson(result);
            onNextT(resultTask.getResult());
        }
    }

    public abstract void onNextT(T t);

    public abstract void onErrorT(String msg);

    // 獲取超類(lèi)類(lèi)型
    private Type getSuperclassTypeParameter(Class<?> clazz, int index)
    {
        Type genType = clazz.getGenericSuperclass();

        if (!(genType instanceof ParameterizedType)) {
            return Object.class;
        }
        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

        if (index >= params.length || index < 0) {
            return Object.class;
        }
        if (!(params[index] instanceof Class)) {
            return Object.class;
        }

        return (Class) params[index];
    }
  
    public static Type getSuperClassGenricType(final Class clazz, final int index) {

        //返回表示此 Class 所表示的實(shí)體(類(lèi)、接口踩窖、基本類(lèi)型或 void)的直接超類(lèi)的 Type坡氯。
        Type genType = clazz.getGenericSuperclass();

        if (!(genType instanceof ParameterizedType)) {
            return Object.class;
        }
        //返回表示此類(lèi)型實(shí)際類(lèi)型參數(shù)的 Type 對(duì)象的數(shù)組。
        Type[] params = ((ParameterizedType) genType).getActualTypeArguments();

        if (index >= params.length || index < 0) {
            return Object.class;
        }
        if (!(params[index] instanceof Type)) {
            return Object.class;
        }

        return params[index];
    }

}

第一種連接方式跟第二種在這里是有區(qū)別的洋腮,第二種批量參數(shù)上傳對(duì)比起第一種相對(duì)比較復(fù)雜箫柳,是因?yàn)檫@里沒(méi)有用到retrofit2自動(dòng)解析json的功能,要自己手動(dòng)去解析啥供,所以這里就只列出第二種連接方式給大家看看悯恍。
以下是參考鏈接:
RxJava簡(jiǎn)潔封裝之道

3.Dagger2

對(duì)于Dagger2,我只能說(shuō)自己的理解能力有限伙狐,一直都是靠文章和大神的幫助才慢慢理解涮毫,所以這里就不發(fā)表自己的愚見(jiàn)了,直接上幾篇寫(xiě)的比較好的文章給大家參考吧~
Android:dagger2讓你愛(ài)不釋手-基礎(chǔ)依賴(lài)注入框架篇
Dagger2從入門(mén)到放棄再到恍然大悟
Dagger2 入門(mén),以初學(xué)者角度

4.相關(guān)工具類(lèi)

相關(guān)工具類(lèi).png

應(yīng)有盡有的相關(guān)工具類(lèi)滿足你的一切要求~
由衷感謝我的好友兼我的大神jaminchanks對(duì)我安卓學(xué)習(xí)提供的巨大支持鳞骤,我有很多安卓資料都是從他那里學(xué)習(xí)的窒百。他比較低調(diào),但是他知道的比我多太多了豫尽,這是他的簡(jiǎn)書(shū)賬號(hào)
接下來(lái)可能我還會(huì)更新圖片框架的封裝和其他的一些改善顷帖。
如果有哪位朋友喜歡這個(gè)框架的美旧,可以下載源碼渤滞,一起交流一下哪里可以改進(jìn)的,共同進(jìn)步~
源碼下載鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末榴嗅,一起剝皮案震驚了整個(gè)濱河市妄呕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嗽测,老刑警劉巖绪励,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異唠粥,居然都是意外死亡疏魏,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)晤愧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)大莫,“玉大人,你說(shuō)我怎么就攤上這事官份≈焕澹” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵舅巷,是天一觀的道長(zhǎng)羔味。 經(jīng)常有香客問(wèn)我,道長(zhǎng)钠右,這世上最難降的妖魔是什么介评? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮爬舰,結(jié)果婚禮上们陆,老公的妹妹穿的比我還像新娘。我一直安慰自己情屹,他們只是感情好坪仇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著垃你,像睡著了一般椅文。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上惜颇,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天皆刺,我揣著相機(jī)與錄音,去河邊找鬼凌摄。 笑死羡蛾,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的锨亏。 我是一名探鬼主播痴怨,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼忙干,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了浪藻?” 一聲冷哼從身側(cè)響起捐迫,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎爱葵,沒(méi)想到半個(gè)月后施戴,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萌丈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年赞哗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浓瞪。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡懈玻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出乾颁,到底是詐尸還是另有隱情涂乌,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布英岭,位于F島的核電站湾盒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏诅妹。R本人自食惡果不足惜罚勾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吭狡。 院中可真熱鬧尖殃,春花似錦、人聲如沸划煮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)弛秋。三九已至器躏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蟹略,已是汗流浹背登失。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挖炬,地道東北人揽浙。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親捏萍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子太抓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

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