采用kotlin+rxjava+retrofit+mvp打造基礎(chǔ)架構(gòu),快速開發(fā)必備

最近項目忙完了漓柑,開始有一定的時間優(yōu)化自己的架構(gòu)教硫,我一直寫代碼都有一種感覺,每次寫完一個項目辆布,然后開始優(yōu)化瞬矩,等優(yōu)化完,再看看自己寫的代碼锋玲,就發(fā)現(xiàn)我封裝的框架真的有點辣雞景用,然后又開始寫個基礎(chǔ)的lib進(jìn)行架構(gòu)的優(yōu)化

簡介

  • 關(guān)于Android程序的構(gòu)架,主要有MVC,MVP和MVVM惭蹂。MVC相對于較為落后伞插,耦合度太高、職責(zé)不明確盾碗;MVVM其實就是在mvp的基礎(chǔ)上采用DataBind媚污,普及性不如MVP,況且Google官方提供了Sample代碼來展示MVP模式的用法廷雅,所以目前大部分項目采用的還是MVP耗美,當(dāng)然根據(jù)項目的情況以及項目的大小來采用合適的結(jié)構(gòu)才是合理的。

  • Kotlin是由JetBrains創(chuàng)建的基于JVM的編程語言航缀,IntelliJ正是JetBrains的杰作商架,而Android Studio是基于IntelliJ修改而來的。Kotlin是一門包含很多函數(shù)式編程思想的面向?qū)ο缶幊陶Z言芥玉。Kotlin生來就是為了彌補Java缺失的現(xiàn)代語言的特性蛇摸,并極大的簡化了代碼,使得開發(fā)者可以編寫盡量少的樣板代碼灿巧。所以目前來說kotlin的Android開發(fā)者中的普及率越來越大赶袄,這應(yīng)該是一個很大的趨勢诬烹。所以學(xué)習(xí)和使用kotlin是一個Android開發(fā)者必備的技能

  • Retrofit: Retrofit是Square 公司開發(fā)的一款正對Android 網(wǎng)絡(luò)請求的框架。底層基于OkHttp 實現(xiàn)弃鸦。
    RxJava:RxJava 在 GitHub 主頁上的自我介紹是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM"(一個在 Java VM 上使用可觀測的序列來組成異步的绞吁、基于事件的程序的庫)。這就是 RxJava 唬格,概括得非常精準(zhǔn)家破。總之就是讓異步操作變得非常簡單购岗。
    各自的職責(zé):Retrofit 負(fù)責(zé)請求的數(shù)據(jù)和請求的結(jié)果汰聋,使用接口的方式呈現(xiàn),OkHttp 負(fù)責(zé)請求的過程喊积,RxJava 負(fù)責(zé)異步烹困,各種線程之間的切換。
    RxJava + Retrofit 已成為當(dāng)前Android 網(wǎng)絡(luò)請求最流行的方式乾吻。

MVP具體實現(xiàn)

首先既然采用了MVP髓梅,肯定必不可少具M(jìn)層,V層绎签,P層的基礎(chǔ)接口枯饿,封裝一個公共的操作,看一下具體實現(xiàn)
我把頂級的接口分成了兩層诡必,這樣有利于在寫泛型的時候沒那么麻煩
先看下目錄結(jié)構(gòu)


aa.png
  • 頂級接口
    第一層
interface ITopView : LifecycleOwner {
    fun getCtx(): Context?
    fun inited()
    fun finish(resultCode: Int = Activity.RESULT_CANCELED)
    fun showLoading(@NotNull msg: String)
    fun showLoading(@StringRes srtResId: Int)
    fun dismissLoading()
    fun showToast(@StringRes srtResId: Int)
    fun showToast(@NotNull message: String)
}

interface ITopPresenter : LifecycleObserver {
    fun attachView(view: ITopView)
    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun detachView()
}


interface ITopModel {
    fun onDetach()
}

第二層

interface IView<P : ITopPresenter> : ITopView {
    var mPresenter: P
    override fun inited() {
        mPresenter.attachView(this)
    }
}

interface IPresenter<V : ITopView, M : IModel> : ITopPresenter {
    var mView: V?
    var mModel: M?
    fun getContext() = mView?.getCtx()

    @Suppress("UNCHECKED_CAST")
    override fun attachView(view: ITopView) {
        mView = view as V
        mView?.lifecycle?.addObserver(this)
    }

    override fun detachView() {
        mModel?.onDetach()
        mModel = null
        mView = null
    }

    //判斷是否初始化View
    private val isViewAttached: Boolean
        get() = mView != null

    fun checkViewAttached() {
        if (!isViewAttached) throw MvpViewNotAttachedException()
    }
    private class MvpViewNotAttachedException internal constructor() : RuntimeException("Please call IPresenter.attachView(IBaseView) before" + " requesting data to the IPresenter")

}

interface IModel : ITopModel {
    val mDisposablePool: CompositeDisposable

    fun addDisposable(disposable: Disposable) {
        mDisposablePool.add(disposable)
    }

    override fun onDetach() {
        if (!mDisposablePool.isDisposed) {
            mDisposablePool.clear()
        }
    }
}


還有額外的一個列表的V層奢方,主要是對列表界面數(shù)據(jù)統(tǒng)一處理

interface IListView<P : ITopPresenter> :IView<P>{
    val mRecyclerView: RecyclerView?
    val mStateView: IStateView?
    val mRefreshLayout:SmartRefreshLayout
    fun loadMoreFail(isRefresh: Boolean)
}

然后M的基類

open class BaseModelKt {
    val mDisposablePool: CompositeDisposable by lazy { CompositeDisposable() }
}

然后P的基類

open class BasePresenterKt<V : ITopView> {
    var mView: V? = null
}
  • Activity和Fragment的封裝

    首先的MVPActivity的實現(xiàn)

abstract class BaseMvpActivity<V : ITopView, P : ITopPresenter> : BaseActivity(), IView<P> {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        inited()
    }
    override fun getCtx() = this
    override fun showLoading(msg: String) {
        progressDialog?.showProgressDialogWithText(msg)
    }

    override fun finish(resultCode: Int) {
        finish()
    }

    override fun showLoading(srtResId: Int) {
        progressDialog?.showProgressDialogWithText(resources.getString(srtResId))
    }

    override fun dismissLoading() {
        progressDialog?.dismissProgressDialog()
    }

    override fun showToast(message: String) {
        showToastBottom(message)
    }

    override fun showToast(srtResId: Int) {
        showToast(resources.getString(srtResId))
    }
}

然后MVPFragment的實現(xiàn)

abstract class BaseMvpFragment<V : ITopView, P : ITopPresenter> : BaseFragment(), IView<P> {
    override fun getCtx() = context

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        inited()
    }
    override fun finish(resultCode: Int) {
    }
    override fun showToast(message: String) {
        showToastBottom(message)
    }
    override fun showToast(srtResId: Int) {
        showToast(resources.getString(srtResId))
    }
    override fun showLoading(msg: String) {
        showProgressDialog(msg)
    }

    override fun showLoading(srtResId: Int) {
        showProgressDialog(resources.getString(srtResId))
    }

    override fun dismissLoading() {
        dismissProgressDialog()
    }
}

然后封裝一個帶toolBar的MVPTitleActivity,它是MVPActivity的子類

abstract class BaseMvpTitleActivity<V : ITopView, P : ITopPresenter> : BaseMvpActivity<V, P>() {
    private var rightMenuTexts: String? = null
    private var rightMenuIcons: Int? = null
    private var titleTv: TextView? = null
    @LayoutRes
    protected abstract fun childView(): Int

    override fun getContentView() = R.layout.activtiy_base_title
    override fun initView() {
        val container = this.findViewById<FrameLayout>(R.id.base_container)
        container.addView(layoutInflater.inflate(childView(), null))
        val toolbar = this.findViewById<Toolbar>(R.id.base_toolbar)
        titleTv = this.findViewById(R.id.base_title_tv)
        toolbar.title = ""
        setSupportActionBar(toolbar)
        if (hasBackIcon()) {
            toolbar.setNavigationIcon(R.drawable.return_icon)
            toolbar.setNavigationOnClickListener { finish() }
        }
    }

    open fun hasBackIcon() = true
    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        rightMenuIcons?.let {
            val item = menu.add(0, 0, 0, "")
            item.icon = ContextCompat.getDrawable(this, it)
            item.setShowAsAction(Menu.FLAG_ALWAYS_PERFORM_CLOSE)
        }
        rightMenuTexts?.let {
            val item = menu.add(0, 0, 0, "")
            item.title = it
            item.setShowAsAction(Menu.FLAG_ALWAYS_PERFORM_CLOSE)
        }
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        onRightMenuClick(item.itemId)
        return false
    }

    /**
     * 設(shè)置toolbar右邊的文字
     */
    fun setRightMenuTexts(rightMenuText: String) {
        this.rightMenuTexts = rightMenuText
    }

    /**
     * 設(shè)置toolbar右邊的icon
     */
    fun setRightMenuIcons(@DrawableRes rightIconResId: Int) {
        this.rightMenuIcons = rightIconResId
    }

    /**
     * 當(dāng)toolbar右邊的icon爸舒,被點擊蟋字,數(shù)據(jù)0,1,2,3
     */
    open fun onRightMenuClick(itemId: Int) {
    }

    /**
     * 設(shè)置中間的title
     */
    protected fun setActivityTitle(@StringRes strResId: Int) {
        titleTv?.setText(strResId)
    }

    protected fun setActivityTitle(text: String) {
        titleTv?.text = text
    }

    /**
     * 設(shè)置中間title的顏色
     */
    fun setActivityTitleColor(@ColorRes colorId: Int) {
        titleTv?.setTextColor(resources.getColor(colorId))
    }
}

這樣基本的封裝基本就結(jié)束了

但是還可以對列表進(jìn)行封裝,封裝了視圖狀態(tài)扭勉,下拉刷新
來看看MVPListActivity,MVPListFragment,MvpTitleListAcitivty的封裝

abstract class BaseMvpListActivity<V : ITopView, P : ITopPresenter> : BaseMvpActivity<V, P>(), IListView<P> {
    override fun getContentView() = R.layout.layout_list
    override val mStateView: IStateView by lazy { list_sv }
    override val mRecyclerView: RecyclerView by lazy { list_rv }
    override val mRefreshLayout: SmartRefreshLayout by lazy { refreshLayout }

    override fun initView() {
        //設(shè)置列表背景色
        list_rv.setBackgroundColor(ContextCompat.getColor(this, setRecyclerViewBgColor))
        //重試
        list_sv.onRetry = { onRetry() }
        //刷新
        refreshLayout.setOnRefreshListener { onRefresh() }
        //設(shè)置下拉刷新是否可用
        refreshLayout.isEnabled = setRefreshEnable
    }

    abstract fun onRefresh()
    abstract fun onRetry()
    open val setRecyclerViewBgColor = R.color.white
    open val setRefreshEnable = true
}

abstract class BaseMvpListFragment<V : ITopView, P : ITopPresenter> : BaseMvpFragment<V, P>(), IListView<P> {
    override fun getContentView() = R.layout.layout_list
    override val mStateView: IStateView by lazy { list_sv }
    override val mRecyclerView: RecyclerView by lazy { list_rv }
    override val mRefreshLayout: SmartRefreshLayout by lazy { refreshLayout }
    override fun initData() {
        //設(shè)置背景色
        context?.let { list_rv.setBackgroundColor(ContextCompat.getColor(it, setRecyclerViewBgColor)) }
        //重試
        list_sv.onRetry = { onRetry() }
        //刷新
        refreshLayout.setOnRefreshListener { onRefresh() }
        //設(shè)置下拉刷新是否可用
        refreshLayout.isEnabled = setRefreshEnable
    }

    abstract fun onRefresh()
    abstract fun onRetry()
    open val setRecyclerViewBgColor = R.color.white
    open val setRefreshEnable = true
}

abstract class BaseMvpTitleListActivity<V : ITopView, P : ITopPresenter> : BaseMvpTitleActivity<V, P>(), IListView<P> {
    override fun childView()= R.layout.layout_list
    override val mStateView: IStateView by lazy { list_sv }
    override val mRecyclerView: RecyclerView by lazy { list_rv }
    override val mRefreshLayout: SmartRefreshLayout by lazy { refreshLayout }
    override fun initView() {
        super.initView()
        //設(shè)置背景色
        list_rv.setBackgroundColor(ContextCompat.getColor(this, setRecyclerViewBgColor))
        //重試
        list_sv.onRetry = { onRetry() }
        //刷新
        refreshLayout.setOnRefreshListener { onRefresh() }
        //設(shè)置下拉刷新是否可用
        refreshLayout.isEnabled = setRefreshEnable
    }
    abstract fun onRefresh()
    abstract fun onRetry()
    open val setRecyclerViewBgColor = R.color.white
    open val setRefreshEnable = true
}

這樣MVP的大致架構(gòu)基本已經(jīng)封裝好了

網(wǎng)絡(luò)框架的具體實現(xiàn)

  • retrofit的封裝
    這個apiService我才用泛型回調(diào)鹊奖,這樣可以根據(jù)不同的模塊創(chuàng)建不同的retrofit工廠類,這個也有利用模塊化開發(fā)
abstract class RetrofitFactory<T> {
    private val time_out: Long = 15//超時時間
    var apiService: T

    init {
        val httpClient = OkHttpClient.Builder()
                .addInterceptor { chain ->
                    val builder = chain.request().newBuilder()
                    // 添加請求頭header
                    if (getToken().isNotEmpty()) {
                        builder.header("userToken", getToken())
                    }
                    val build = builder.build()
                    chain.proceed(build)
                }
                .addInterceptor(HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { message ->
                    if (message.contains("{")||message.contains("=")||message.contains("http")
                            ||message.contains("userToken")){
                        Logger.e("${message}")
                    }
                }).setLevel(HttpLoggingInterceptor.Level.BODY))//設(shè)置打印得日志內(nèi)容
                .connectTimeout(time_out, TimeUnit.SECONDS)
                .readTimeout(time_out, TimeUnit.SECONDS)
                .build()

        apiService = Retrofit.Builder()
                .baseUrl(URLConstant.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(buildGson())) // 添加Gson轉(zhuǎn)換器
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // 添加Retrofit到RxJava的轉(zhuǎn)換器
                .client(httpClient)
                .build()
                .create(getApiService())
    }

    abstract fun getApiService(): Class<T>
    abstract fun getToken(): String
    private fun buildGson(): Gson {
        return GsonBuilder()
                .setDateFormat("yyyy-MM-dd HH:mm:ss")
                .create()
    }

    fun getService(): T {
        return apiService
    }
}
  • Rxjava+Retrofit的封裝
    這個部分是網(wǎng)絡(luò)請求的部分剖效,我是封裝在kotlin的拓展方法里面嫉入,這樣就可以使用lambda表達(dá)式進(jìn)行網(wǎng)絡(luò)請求焰盗,代碼量賊少璧尸,用起來賊舒服,一行代碼一個請求
    具體使用例子
  PersonRetrofit.apiService.getIdentityCode(phone).mySubscribe(view, "正在獲取驗證碼...") {
            view.getCodeSuccess()
        }

是不是賊簡單熬拒,賊方便爷光,這個得感謝我的同學(xué)大強哥,這招都是他教我的澎粟,把lambda用到極致蛀序;

再來看看kotlin的拓展內(nèi)部實現(xiàn)欢瞪;

fun <T : BaseBean, P : ITopPresenter> Observable<T>.mSubscribe(
        iBaseView: IView<P>? = null
        , iModel: IModel? = null
        , msg: String = ""
        , onSuccess: (T) -> Unit) {
    this.compose(SchedulerUtils.ioToMain())
            .subscribe(object : Observer<T> {
                override fun onComplete() {
                    iBaseView?.dismissLoading()
                }

                override fun onSubscribe(d: Disposable) {
                    iModel?.addDisposable(d)
                    iBaseView?.showLoading(if (msg.isEmpty()) "請求中..." else msg)
                    if (!NetworkUtils.isConnected()) {
                        showToastBottom("連接失敗,請檢查網(wǎng)絡(luò)狀況!")
                        onComplete()
                    }
                }

                override fun onNext(t: T) {
                    if (t.code == CodeStatus.SUCCESS) {
                        onSuccess.invoke(t)
                    } else if (t.code == CodeStatus.LOGIN_OUT) {//重新登錄
//                val currentActivity = ActivityUtils.currentActivity()
//                UserManager.getInstance().clear()
//                EMClient.getInstance().logout(true)
//                showToastBottom("登錄過期,請重新登錄")
//                val intent = Intent(currentActivity, LoginActivity::class.java)
//                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
//                currentActivity?.startActivity(intent)
                    } else {
                        if (!t.msg.isNullOrEmpty()) {
                            t.msg?.let { showToastBottom(it) }
                        } else {
                            showToastBottom("請求失敗")
                        }
                    }
                }

                override fun onError(e: Throwable) {
                    iBaseView?.dismissLoading()
                    if (e is SocketTimeoutException || e is ConnectException) {
                        showToastBottom("連接失敗,請檢查網(wǎng)絡(luò)狀況!")
                    } else if (e is JsonParseException) {
                        showToastBottom("數(shù)據(jù)解析失敗")
                    } else {
                        showToastBottom("請求失敗")
                    }
                }
            })
}

fun <T : BaseBean, P : ITopPresenter> Observable<T>.listSubcribe(
        iBaseView: IListView<P>? = null
        , iModel: IModel? = null
        , isRefresh: Boolean
        , isLoadMore: Boolean
        , onSuccess: (T) -> Unit) {
    this.compose(SchedulerUtils.ioToMain())
            .subscribe(object : Observer<T> {
                override fun onComplete() {}
                override fun onSubscribe(d: Disposable) {
                    iModel?.addDisposable(d)
                    if (!isRefresh && !isLoadMore) {
                        iBaseView?.mStateView?.showLoading()
                    }
                }

                override fun onNext(t: T) {
                    if (t.code == CodeStatus.SUCCESS) {
                        iBaseView?.mStateView?.showSuccess()
                        onSuccess.invoke(t)
                    } else if (t.code == CodeStatus.LOGIN_OUT) {//重新登錄
//                    UserManager.getInstance().clear()
//                    showToastBottom("登錄過期徐裸,請重新登錄")
//                    EMClient.getInstance().logout(true)
//                    val intent = Intent(currentActivity, LoginActivity::class.java)
//                    intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
//                    currentActivity.startActivity(intent)
                    } else {
                        iBaseView?.mStateView?.showError()
                    }
                }

                override fun onError(e: Throwable) {
                    if (!isLoadMore) {
                        iBaseView?.mStateView?.showError()
                    } else {
                        iBaseView?.loadMoreFail(isRefresh)
                    }
                }
            })
}

配合插件使用遣鼓,快速開發(fā)必備

這里我推薦一個我同學(xué)的插件,結(jié)合這種lib使用賊方便
插件的名字叫MvpAutoCodePlus重贺,github地址 插件地址骑祟,,气笙,這個low比名字還是我?guī)退〉摹?br> 具體使用

a.png

b.png
c.png

這樣就生成了次企,真的很方便

最后我寫了一個demo放在github上面 項目地址
原文地址

歡迎大家掃描關(guān)注作者公眾號,長期推送Android技術(shù)干貨潜圃,感謝大家支持:


qrcode_for_gh_c5f1738c50f5_344.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缸棵,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子谭期,更是在濱河造成了極大的恐慌堵第,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件隧出,死亡現(xiàn)場離奇詭異型诚,居然都是意外死亡,警方通過查閱死者的電腦和手機鸳劳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進(jìn)店門狰贯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赏廓,你說我怎么就攤上這事涵紊。” “怎么了幔摸?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵摸柄,是天一觀的道長。 經(jīng)常有香客問我既忆,道長驱负,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任患雇,我火速辦了婚禮跃脊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘苛吱。我一直安慰自己酪术,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绘雁,像睡著了一般橡疼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上庐舟,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天欣除,我揣著相機與錄音,去河邊找鬼挪略。 笑死耻涛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瘟檩。 我是一名探鬼主播抹缕,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼墨辛!你這毒婦竟也來了卓研?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤睹簇,失蹤者是張志新(化名)和其女友劉穎奏赘,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體太惠,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡磨淌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了凿渊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梁只。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖埃脏,靈堂內(nèi)的尸體忽然破棺而出搪锣,到底是詐尸還是另有隱情,我是刑警寧澤彩掐,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布构舟,位于F島的核電站,受9級特大地震影響堵幽,放射性物質(zhì)發(fā)生泄漏狗超。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一朴下、第九天 我趴在偏房一處隱蔽的房頂上張望努咐。 院中可真熱鬧,春花似錦桐猬、人聲如沸麦撵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽免胃。三九已至,卻和暖如春惫撰,著一層夾襖步出監(jiān)牢的瞬間羔沙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工厨钻, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留扼雏,地道東北人。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓夯膀,卻偏偏與公主長得像诗充,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子诱建,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

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