Android組件化開發(fā)(二)--網(wǎng)絡(luò)請(qǐng)求組件封裝

?? Hi贾惦,我是小余替裆。

本文已收錄到 GitHub · Androider-Planet 中英融。這里有 Android 進(jìn)階成長(zhǎng)知識(shí)體系,關(guān)注公眾號(hào) [小余的自習(xí)室] 夕吻,在成功的路上不迷路!

前言

前面一篇文章我們講解了maven私服的搭建菩暗,maven私服在組件化框架中有一個(gè)很重要的地位就是可以將我們的lib庫(kù)放到局域網(wǎng)中,供公司其他開發(fā)者使用旭蠕,實(shí)現(xiàn)類庫(kù)的分享停团。

下面是這個(gè)系列準(zhǔn)備實(shí)現(xiàn)的一個(gè)組件化實(shí)戰(zhàn)項(xiàng)目框架

[圖片上傳失敗...(image-1c133-1676861042632)]

  • 筆者打算從下往上依次來實(shí)現(xiàn)我們項(xiàng)目中的組件,畢竟地基穩(wěn)固了掏熬,房子才可以搭的很結(jié)實(shí)佑稠。

注意:這里不會(huì)對(duì)封裝代碼進(jìn)行長(zhǎng)篇大論,主要還是以思路點(diǎn)撥的方式進(jìn)行旗芬,如果需要看完整代碼的可以移步到github舌胶。

GitHub - ByteYuhb/anna_music_app

  • 這篇文章實(shí)現(xiàn)的是一個(gè)lib_network庫(kù):

實(shí)現(xiàn)一個(gè)組件的封裝前,我們有幾個(gè)步驟岗屏,這幾個(gè)步驟不可或缺辆琅,如果上來就直接碼代碼漱办,最后會(huì)讓你從激情到放棄

  • 1.先分析需求

    • 每個(gè)類的封裝都是有了新的需求一步一步實(shí)現(xiàn)擴(kuò)大的这刷,不可能一蹴而就,包括筆者今天講解的lib_network類庫(kù)娩井,后期也會(huì)根據(jù)用戶需求一步一步壯大暇屋。

      既然是要實(shí)現(xiàn)一個(gè)網(wǎng)絡(luò)請(qǐng)求的封裝庫(kù):主要包括get,postform表單請(qǐng)求洞辣,文件上傳和下載等一些基礎(chǔ)網(wǎng)絡(luò)功能的實(shí)現(xiàn)

  • 2.根據(jù)需求進(jìn)行技術(shù)選型

    • 技術(shù)選型在類庫(kù)封裝中也是一個(gè)重要步驟咐刨,這里我們只是實(shí)現(xiàn)一些網(wǎng)絡(luò)基礎(chǔ)功能,筆者打算在HttpUrlConnection扬霜,VollyOkHttp中選擇我們我們的封裝基礎(chǔ)庫(kù)

[圖片上傳失敗...(image-4c98bf-1676861042632)]

幾個(gè)待選型的技術(shù)對(duì)比:

  • HttpUrlConnection

這個(gè)類是一個(gè)比較底層的類庫(kù)了定鸟,如果使用這個(gè)類庫(kù)來封裝,我們需要實(shí)現(xiàn)很多輪子工作著瓶,而在另外兩個(gè)開源框架VollyOkHttp已經(jīng)實(shí)現(xiàn)了這些工作联予,沒必要重復(fù)造輪子,在這里不考慮。

如果您對(duì)庫(kù)有更深層次的要求且對(duì)自己技術(shù)比較自信沸久,可以考慮使用這個(gè)類庫(kù)去封裝實(shí)現(xiàn)季眷,畢竟開源庫(kù)也是在這些基礎(chǔ)庫(kù)上實(shí)現(xiàn)的。

  • Volly

這個(gè)類庫(kù)是在HttpUrlConnection 基礎(chǔ)上做的一層封裝卷胯,為了減少用戶使用HttpUrlConnection的復(fù)雜度子刮。

一開始出來是google主推的網(wǎng)絡(luò)請(qǐng)求框架,google希望統(tǒng)一Android網(wǎng)路請(qǐng)求庫(kù)推出的一個(gè)框架窑睁。那為什么后面用的人越來越少了呢挺峡。那就是因?yàn)橄旅嫖覀円f的OkHttp框架。

  • OkHttp

看過源碼的都知道担钮,OkHttp也是在HttpUrlConnecttion上做的封裝沙郭,其繼承了Volly的優(yōu)勢(shì),且在Volly上構(gòu)建了自己的有優(yōu)點(diǎn)裳朋,包括:連接池復(fù)用病线,重試重定向機(jī)制攔截器模式等鲤嫡、

具體關(guān)于OkHttp的介紹可以參考我的另外一篇文章:

Android體系課 之 OkHttp你想知道的都在這里了--

筆者最后選擇使用OkHttp來做我們基礎(chǔ)庫(kù)的封裝工作

  • 3.封裝思路

    前面通過對(duì)具體需求分析送挑,并且也做了技術(shù)選型

接下來就是怎么去實(shí)現(xiàn)這個(gè)封裝?需要封裝哪些類暖眼?

我們來回憶下使用OkHttp的方式:

fun testOkHttp(){
    val client = OkHttpClient()
    val r1:RequestBody = formBodyBuilder.build()
    val request = Request.Builder().get().url("http://host:port/api").build()
    val response = client.newCall(request).execute()
    print(response.body()?.bytes())
}

筆者思路:首先封裝的是和用戶直接打交道的類

Request惕耕,ResponseOkHttpClient以及異步情況下的Callback是直接和用戶打交道的地方,那我們就從這幾個(gè)類下手依次對(duì)其進(jìn)行封裝:

我們先列出來我們技術(shù)方案:

[圖片上傳失敗...(image-6c1c4e-1676861042632)]

有了上面幾個(gè)分析步驟:接下來我們就來實(shí)現(xiàn)具體的封裝流程:

  • 1.Request請(qǐng)求的封裝 Request:用戶請(qǐng)求類诫肠,在OkHttp中封裝了我們的業(yè)務(wù)請(qǐng)求信息

這里我們創(chuàng)建一個(gè)CommonRequest類來再次封裝我們的Request

筆者只列出了部分類的框架代碼:完整代碼在github上

public class CommonRequest {
    /**創(chuàng)建一個(gè)Post的請(qǐng)求司澎,不包括headers
     * @return
     */
    public static Request createPostRequest(String url, RequestParams params){
        return createPostRequest(url,params,null);
    }
    /**創(chuàng)建一個(gè)Post的請(qǐng)求,包括headers
     * @param url
     * @param params body的參數(shù)集合
     * @param headers header的參數(shù)集合
     * @return
     */
    public static Request createPostRequest(String url, RequestParams params, RequestParams headers){
        FormBody.Builder mFormBodyBuilder = new FormBody.Builder();
        if(params!=null){
            for(Map.Entry<String,String> entry:params.urlParams.entrySet()){
                mFormBodyBuilder.add(entry.getKey(),entry.getValue());
            }
        }
        Headers.Builder mHeadersBuilder = new Headers.Builder();
        if(headers!=null){
            for(Map.Entry<String,String> entry:headers.urlParams.entrySet()){
                mHeadersBuilder.add(entry.getKey(),entry.getValue());
            }
        }
        return new Request.Builder()
                .url(url)
                .headers(mHeadersBuilder.build())
                .post(mFormBodyBuilder.build())
                .build();
    }

    /**創(chuàng)建一個(gè)不包含header的get請(qǐng)求
     * @return
     */
    public static Request createGetRequest(String url,RequestParams params){
        return createGetRequest(url,params,null);
    }
    /**創(chuàng)建一個(gè)Get的請(qǐng)求栋豫,包括headers
     * @return
     */
    public static Request createGetRequest(String url,RequestParams params,RequestParams headers){
        StringBuilder stringBuilder = new StringBuilder(url).append("?");
        if(params != null){
            for(Map.Entry<String,String> entry:params.urlParams.entrySet()){
                stringBuilder.append(entry.getKey()).append("=").append(entry.getValue());
            }
        }
        Headers.Builder mHeadersBuilder = new Headers.Builder();
        if(headers!=null){
            for(Map.Entry<String,String> entry:headers.urlParams.entrySet()){
                mHeadersBuilder.add(entry.getKey(),entry.getValue());
            }
        }
        return new Request.Builder()
                .url(stringBuilder.toString())
                .headers(mHeadersBuilder.build())
                .get()
                .build();
    }
    private static final MediaType FILE_TYPE = MediaType.parse("application/octet-stream");

    /**文件上傳請(qǐng)求
     * @return
     */
    public static Request createMultiPostRequest(String url,RequestParams params){
        MultipartBody.Builder requestBuilder = new MultipartBody.Builder();
        requestBuilder.setType(MultipartBody.FORM);
        if(params != null){
            for (Map.Entry<String, Object> entry : params.fileParams.entrySet()) {
                if (entry.getValue() instanceof File) {
                    requestBuilder.addPart(Headers.of("Content-Disposition","form-data; name="" + entry.getKey() + """),
                            RequestBody.create(FILE_TYPE, (File) entry.getValue()));
                }else if (entry.getValue() instanceof String) {
                    requestBuilder.addPart(Headers.of("Content-Disposition", "form-data; name="" + entry.getKey() + """),
                            RequestBody.create(null, (String) entry.getValue()));
                }
            }
        }
        return new Request.Builder().url(url).post(requestBuilder.build()).build();
    }

    /**文件下載請(qǐng)求
     * @param url
     * @return
     */
    public static Request createFileDownLoadRequest(String url,RequestParams params){
        return createGetRequest(url,params);
    }
    /**文件下載請(qǐng)求
     * @param url
     * @return
     */
    public static Request createFileDownLoadRequest(String url){
        return createGetRequest(url,null);
    }
}

可以看到我們?cè)谶@個(gè)類里面創(chuàng)建了幾個(gè)方法:

  • 1.Get請(qǐng)求
  • 2.Post請(qǐng)求
  • 3.文件上傳
  • 4.文件下載

這幾個(gè)請(qǐng)求挤安,已經(jīng)可以基本滿足我們實(shí)戰(zhàn)項(xiàng)目的要求了

    1. response響應(yīng)的封裝

由于response請(qǐng)求是在CallBack中返回的,思路就是丧鸯,自定義業(yè)務(wù)層需要的CallBack蛤铜,盡量讓代碼輕量化

這里創(chuàng)建了兩個(gè)CallBack類:

  • 1.CommonFileResponse

這個(gè)類主要是由來對(duì)文件類型的請(qǐng)求回調(diào)進(jìn)行封裝:

CommonFileResponse封裝思路:

  • 1.對(duì)失敗相應(yīng)直接通過業(yè)務(wù)層傳遞下來的Listener回調(diào)給業(yè)務(wù)層失敗結(jié)果

  • 2.對(duì)成功的相應(yīng),我們先將輸入流中的數(shù)據(jù)寫入到文件中丛肢,并在主線程中回調(diào)文件下載進(jìn)度給業(yè)務(wù)層围肥。業(yè)務(wù)層可以在獲取文件結(jié)果的同時(shí),也可以獲取文件下載進(jìn)度蜂怎。

/**
 * 專門處理文件的回調(diào)
 */
public class CommonFileCallBack implements Callback {
    /**
     * the java layer exception, do not same to the logic error
     */
    protected final int NETWORK_ERROR = -1; // the network relative error
    protected final int IO_ERROR = -2; // the JSON relative error
    protected final String EMPTY_MSG = "";
    /**
     * 將其它線程的數(shù)據(jù)轉(zhuǎn)發(fā)到UI線程
     */
    private static final int PROGRESS_MESSAGE = 0x01;
    private Handler mDeliveryHandler;
    private DisposeDownloadListener mListener;
    private String mFilePath;
    private int mProgress;
    public CommonFileCallBack(DisposeDataHandle handle){
        this.mListener = (DisposeDownloadListener) handle.mListener;
        this.mFilePath = handle.mSource;
        this.mDeliveryHandler = new Handler(Looper.getMainLooper()){
            @Override
            public void handleMessage(@NonNull Message msg) {
                switch (msg.what) {
                    case PROGRESS_MESSAGE:
                        mListener.onProgress((int) msg.obj);
                        break;
                }
            }
        };
    }
    @Override
    public void onFailure(Call call, IOException e) {
        mDeliveryHandler.post(new Runnable() {
            @Override
            public void run() {
                mListener.onFailure(new OkHttpException(NETWORK_ERROR, e));
            }
        });
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        final File file = handleResponse(response);
        mDeliveryHandler.post(new Runnable() {
            @Override
            public void run() {
                if (file != null) {
                    mListener.onSuccess(file);
                } else {
                    mListener.onFailure(new OkHttpException(IO_ERROR, EMPTY_MSG));
                }
            }
        });
    }

    private File handleResponse(Response response) {
        ...
            while ((length = inputStream.read(buffer)) != -1) {
                fos.write(buffer, 0, length);
                currentLength += length;
                mProgress = (int) (currentLength / sumLength * 100);
                mDeliveryHandler.obtainMessage(PROGRESS_MESSAGE, mProgress).sendToTarget();
            }
            fos.flush();
        } catch (Exception e) {
            file = null;
        } finally {
            ...
        }
        return file;
    }

}
  • 2.CommonJsonResponse

這個(gè)類用處和我們的Retrofit類似穆刻,將請(qǐng)求轉(zhuǎn)換為我們需要的類,通過Gson或者fastJson等框架處理:

/**
 * @author anna
 * @function 專門處理JSON的回調(diào)
 */
public class CommonJsonCallback implements Callback {

    /**
     * the logic layer exception, may alter in different app
     */
    protected final String RESULT_CODE = "ecode"; // 有返回則對(duì)于http請(qǐng)求來說是成功的杠步,但還有可能是業(yè)務(wù)邏輯上的錯(cuò)誤
    protected final int RESULT_CODE_VALUE = 0;
    protected final String ERROR_MSG = "emsg";
    protected final String EMPTY_MSG = "";

    /**
     * the java layer exception, do not same to the logic error
     */
    protected final int NETWORK_ERROR = -1; // the network relative error
    protected final int JSON_ERROR = -2; // the JSON relative error
    protected final int OTHER_ERROR = -3; // the unknow error

    /**
     * 將其它線程的數(shù)據(jù)轉(zhuǎn)發(fā)到UI線程
     */
    private Handler mDeliveryHandler;
    private DisposeDataListener mListener;
    private Class<?> mClass;

    public CommonJsonCallback(DisposeDataHandle handle) {
        this.mListener = handle.mListener;
        this.mClass = handle.mClass;
        this.mDeliveryHandler = new Handler(Looper.getMainLooper());
    }

    @Override
    public void onFailure(final Call call, final IOException ioexception) {
        /**
         * 此時(shí)還在非UI線程氢伟,因此要轉(zhuǎn)發(fā)
         */
        mDeliveryHandler.post(new Runnable() {
            @Override
            public void run() {
                mListener.onFailure(new OkHttpException(NETWORK_ERROR, ioexception));
            }
        });
    }

    @Override
    public void onResponse(final Call call, final Response response) throws IOException {
        final String result = response.body().string();
        mDeliveryHandler.post(new Runnable() {
            @Override
            public void run() {
                handleResponse(result);
            }
        });
    }

    private void handleResponse(Object responseObj) {
        if (responseObj == null || responseObj.toString().trim().equals("")) {
            mListener.onFailure(new OkHttpException(NETWORK_ERROR, EMPTY_MSG));
            return;
        }

        try {
            /**
             * 協(xié)議確定后看這里如何修改
             */
            JSONObject result = new JSONObject(responseObj.toString());
            if (mClass == null) {
                mListener.onSuccess(result);
            } else {
                Object obj = new Gson().fromJson(responseObj.toString(), mClass);
                if (obj != null) {
                    mListener.onSuccess(obj);
                } else {
                    mListener.onFailure(new OkHttpException(JSON_ERROR, EMPTY_MSG));
                }
            }
        } catch (Exception e) {
            mListener.onFailure(new OkHttpException(OTHER_ERROR, e.getMessage()));
            e.printStackTrace();
        }
    }
}
    1. OkHttpClient請(qǐng)求的封裝

OkHttpClient是我們OkHttp的核心樞紐撰洗,業(yè)務(wù)層以及核心框架層都需要使用到這個(gè)類,我們來思考下怎么去封裝這個(gè)類:

1.我們使用裝飾器模式:在CommonOkHttpClient中封裝一個(gè)OkHttpClient對(duì)象腐芍,通過CommonOkHttpClient去代理這個(gè)OkHttpClient對(duì)象

來看下我們封裝的代碼:

public class CommonOkHttpClient {
    private static final int TIME_OUT = 30;
    private static OkHttpClient mOkHttpClient;
    static {
        OkHttpClient.Builder mOkHttpClientBuilder = new OkHttpClient.Builder();
        mOkHttpClientBuilder.hostnameVerifier(new HostnameVerifier() {
            @Override
            public boolean verify(String hostname, SSLSession session) {
                return true;
            }
        });
        //添加自定義的攔截器
        mOkHttpClientBuilder.addInterceptor(new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request()
                        .newBuilder()
                        .addHeader("User-Agent","anna-movie")
                        .build();
                return chain.proceed(request);
            }
        });
        mOkHttpClientBuilder.cookieJar(new SimpleCookieJar());
        mOkHttpClientBuilder.connectTimeout(TIME_OUT, TimeUnit.SECONDS);
        mOkHttpClientBuilder.readTimeout(TIME_OUT, TimeUnit.SECONDS);
        mOkHttpClientBuilder.writeTimeout(TIME_OUT, TimeUnit.SECONDS);
        //設(shè)置是否支持重定向
        mOkHttpClientBuilder.followRedirects(true);
        //設(shè)置代理
       // mOkHttpClientBuilder.proxy()
        mOkHttpClientBuilder.sslSocketFactory(HttpsUtils.initSSLSocketFactory(),
                HttpsUtils.initTrustManager());
        mOkHttpClient = mOkHttpClientBuilder.build();
    }
    public static OkHttpClient getOkHttpClient() {
        return mOkHttpClient;
    }

    /**
     * 通過構(gòu)造好的Request,Callback去發(fā)送請(qǐng)求
     */
    public static Call get(Request request, DisposeDataHandle handle) {
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new CommonJsonCallback(handle));
        return call;
    }
    public static Call post(Request request, DisposeDataHandle handle) {
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new CommonJsonCallback(handle));
        return call;
    }

    public static Call downloadFile(Request request, DisposeDataHandle handle) {
        Call call = mOkHttpClient.newCall(request);
        call.enqueue(new CommonFileCallBack(handle));
        return call;
    }
}

這里面封裝了一個(gè)get差导,post,以及文件下載的請(qǐng)求:

對(duì)于業(yè)務(wù)層只需要調(diào)用:

CommonOkHttpClient.get(...)
CommonOkHttpClient.post(...)
CommonOkHttpClient.downloadFile(..)

總結(jié):

本篇文章主要以封裝思路的方式進(jìn)行講解猪勇,具體代碼可以移步到github

GitHub - ByteYuhb/anna_music_app

對(duì)于大部分類庫(kù)的封裝都可以使用我們上面的思路设褐,再結(jié)合maven私服的使用∑玻可以很好的將我們代碼作為一個(gè)組件共享給開發(fā)同事使用

組件化道路長(zhǎng)遠(yuǎn),這里我們只是封裝了一個(gè)網(wǎng)絡(luò)請(qǐng)求庫(kù)助析,后面會(huì)不定期對(duì)其他類庫(kù)進(jìn)行封裝,最后整合成一個(gè)完整的組件化框架

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末椅您,一起剝皮案震驚了整個(gè)濱河市外冀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌掀泳,老刑警劉巖雪隧,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異员舵,居然都是意外死亡脑沿,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門马僻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來庄拇,“玉大人,你說我怎么就攤上這事韭邓〈虢” “怎么了?”我有些...
    開封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵女淑,是天一觀的道長(zhǎng)瞭郑。 經(jīng)常有香客問我,道長(zhǎng)诗力,這世上最難降的妖魔是什么凰浮? 我笑而不...
    開封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任我抠,我火速辦了婚禮苇本,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘菜拓。我一直安慰自己瓣窄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開白布纳鼎。 她就那樣靜靜地躺著俺夕,像睡著了一般裳凸。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上劝贸,一...
    開封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天姨谷,我揣著相機(jī)與錄音,去河邊找鬼映九。 笑死梦湘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的件甥。 我是一名探鬼主播捌议,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼引有!你這毒婦竟也來了瓣颅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤譬正,失蹤者是張志新(化名)和其女友劉穎宫补,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體曾我,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡守谓,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了您单。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斋荞。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖虐秦,靈堂內(nèi)的尸體忽然破棺而出平酿,到底是詐尸還是另有隱情,我是刑警寧澤悦陋,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布蜈彼,位于F島的核電站,受9級(jí)特大地震影響俺驶,放射性物質(zhì)發(fā)生泄漏幸逆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一暮现、第九天 我趴在偏房一處隱蔽的房頂上張望还绘。 院中可真熱鬧,春花似錦栖袋、人聲如沸拍顷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)昔案。三九已至尿贫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間踏揣,已是汗流浹背庆亡。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留捞稿,地道東北人身冀。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像括享,于是被迫代替她去往敵國(guó)和親搂根。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

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