Android Jetpack 架構(gòu)組件最佳實(shí)踐

Android Jetpack介紹

Android Jetpack 是一套組件、工具和指導(dǎo),可以幫助您快速構(gòu)建出色的 Android 應(yīng)用场靴。

  • Google在17年的I/O大會上推出了架構(gòu)組件(Architecture Component)队丝。

  • 隨后在18年I/O大會上發(fā)布了 Android Jetpack,Jetpack 是Android開發(fā)組件工具集腹缩,旨在幫助我們輕松構(gòu)建更穩(wěn)定屿聋、更健壯空扎、以及更可維護(hù)的應(yīng)用程序。

  • 緊接著Google推出AndroidX润讥,將許多Google認(rèn)為是正確的方案和實(shí)踐集中起來了转锈。

    • AndroidX 是對support library的重大改進(jìn)。

    • AndroidX中的所有軟件包名都以字符串a(chǎn)ndroidx.開頭楚殿,位于一致的命名空間中撮慨。

    • 與support支持庫不同,AndroidX中各個(gè)組件可單獨(dú)維護(hù)和更新脆粥。

    • 所有新的支持庫開發(fā)都將在AndroidX庫中進(jìn)行甫煞。

目前很多組件庫新版本都遷移到了androidx。比如Lifecycle2.0.0+冠绢、Paging2.0.0+抚吠、ViewPager2等,非官方庫也積極響應(yīng)弟胀,比如lottie2.8.0+版本之后就使用了androidx實(shí)現(xiàn)楷力。由此可以得出,官方在已有組件新版本實(shí)現(xiàn)和全新組件的開發(fā)都將只支持androidx孵户,所以盡快將自己的項(xiàng)目遷移到androidx吧萧朝。

  • Jetpack結(jié)構(gòu)圖(2018年版本),目前圖中大部分組件都推出了穩(wěn)定的release版本夏哭。
android-jetpack.png

Jetpack主要分為4個(gè)部分检柬,基礎(chǔ)、架構(gòu)竖配、行為何址、界面。從圖中得知进胯,Jetpack并不全是些新東西用爪,只要是能夠幫助開發(fā)者更好更方便地構(gòu)建應(yīng)用程序的組件,Google都將其歸納入了Jetpack胁镐,可以看出Google對jetpack很重視偎血,對開發(fā)者很上心。

剛剛結(jié)束的Google I/O 2019大會上盯漂,Jetpack又迎來了新組件CameraX颇玷、SavedStateViewModelJetpack Compose等等就缆,提出Kotlin first 帖渠,并強(qiáng)調(diào)大部分新的Jetpack Apis和功能將會優(yōu)先提供 Kotlin 版本。參考文章

每個(gè) Jetpack 組件均可單獨(dú)采用违崇,并且它們可以流暢地協(xié)作阿弃。Android開發(fā)請一定關(guān)注和使用Jetpack+Kotlin诊霹。

本文重點(diǎn)是在架構(gòu)組件的使用上,我們先來看看官方推薦的架構(gòu)實(shí)現(xiàn)渣淳。

官方推薦架構(gòu)

官方架構(gòu)圖

使用此架構(gòu)能帶來什么好處脾还?

  • UI和業(yè)務(wù)邏輯解耦。
  • 有效避免生命周期組件內(nèi)存泄漏入愧。
  • 提高模塊可測試性鄙漏。
  • 提高應(yīng)用穩(wěn)定性,有效降低以下異常發(fā)生概率棺蛛。
    • Can not perform this action after onSaveInstanceState
    • WindowManager$BadTokenException, is your activity running?
    • OOM 怔蚌、 NullPointerException
    • ...

測試每個(gè)組件

  • 界面和交互:使用 Android 界面插樁測試∨陨蓿基于此架構(gòu)只需mock 一個(gè)ViewModel即可完成界面測試桦踊。

  • ViewModel:使用 JUnit 測試。只需mock一個(gè)類终畅,即 Repository籍胯。

  • Repository:使用 JUnit 測試。只需mock兩個(gè)類离福,XxxDao杖狼,XxxService;由于XxxDao妖爷,XxxService都是接口蝶涩,還可以創(chuàng)建虛擬實(shí)現(xiàn)來完成復(fù)雜測試用例。

  • XxxDao:可以使用插樁測試來測試 DAO 類絮识。這里注意對于每個(gè)測試绿聘,都請創(chuàng)建內(nèi)存中數(shù)據(jù)庫以確保測試沒有任何副作用(例如更改磁盤上的數(shù)據(jù)庫文件)。

  • XxxService:就Retrofit而言可以使用MockWebServer模擬本地服務(wù)器笋除。

評價(jià)一個(gè)架構(gòu)好不好主要看三點(diǎn):穩(wěn)定性斜友、易維護(hù)、可測試程度垃它。

提到架構(gòu)組件庫,不得不說的是Lifecycle庫烹看。文章后面部分就分別從該庫中的Lifecycle国拇、ViewModel、LiveData這三個(gè)類來簡要分析其實(shí)現(xiàn)原理以及使用建議惯殊。

Lifecycle

Lifecycle是一個(gè)類酱吝,它包含組件(Activity或Fragment)生命周期狀態(tài)的信息,并允許其他對象觀察此狀態(tài)土思。

那lifecycle是如何跟蹤組件生命周期的呢务热?

lifecycle-states

上圖中states表示組件狀態(tài)忆嗜,events表示組件生命周期事件。其實(shí)Lifecycle代碼內(nèi)部使用了兩個(gè)主要枚舉(Event崎岂、State)來跟蹤其關(guān)聯(lián)組件的生命周期狀態(tài)捆毫。

  • 其中枚舉Event的值和Activity或Fragment組件的生命周期回調(diào)事件一一對應(yīng)。
  • 而枚舉State則表示被跟蹤組件的當(dāng)前狀態(tài)冲甘,其中 STARTEDRESUMED 為活躍狀態(tài)绩卤,配合LiveData使用時(shí),只有組件處于活躍狀態(tài)才能接受到數(shù)據(jù)更新通知江醇。

實(shí)踐示例:工具類LifecycleHandler濒憋,一個(gè)具有生命周期感知的Handler。

LifecycleOwner和LifecycleRegistry

  • LifecycleOwner是一個(gè)單一的方法接口陶夜,表示該類具有生命周期凛驮。support包從26.1.0版本開始,F(xiàn)ragment和Activity默認(rèn)實(shí)現(xiàn)了該接口条辟,這樣直接和LiveData使用就能獲取組件的生命周期感知能力黔夭。
  • LifecycleRegistry: Lifecycle接口的實(shí)現(xiàn)類,協(xié)助組件處理生命周期捂贿,可處理多個(gè)觀察者纠修。如果你想自定義LifecyclerOwner請參考support包中Fragment和Activity實(shí)現(xiàn)。

ViewModel

ViewModel 是用來保存應(yīng)用UI數(shù)據(jù)的類厂僧,它會在配置變更(Configuration Change)后繼續(xù)存在扣草。

先看看官方給出的ViewModel生命周期圖解

viewmodel-lifecycle.png

關(guān)于ViewModel的生命周期就一句話:在Activity、Fragment等組件整個(gè)生命周期過程中颜屠,ViewModel的實(shí)例有且只有一個(gè)辰妙。

這樣設(shè)計(jì)好處在哪呢?
  • 可用ViewModel存儲數(shù)據(jù)甫窟,它能安全度過手機(jī)旋轉(zhuǎn)等配置變更場景密浑。

  • ViewModel能很好的實(shí)現(xiàn)多個(gè)Fragment之間的數(shù)據(jù)共享。

如果界面和業(yè)務(wù)都比較復(fù)雜粗井,ViewModel會不會爆掉尔破?對于這種場景,官方也給出了解決思路:單一責(zé)任原則浇衬。

viewmodel_duty

上圖為官方中文視頻截圖懒构,從單一責(zé)任原則考慮提出了實(shí)現(xiàn)建議。

  • Actvity或Fragment只顯示UI和接收互動(dòng)耘擂。

  • 為避免ViewModel臃腫胆剧,可創(chuàng)建presenter處理UI數(shù)據(jù)。

  • Repository 數(shù)據(jù)源操作入口醉冤。(便于單元測試)

  • 還可配合其它架構(gòu)組件使用秩霍。

關(guān)于ViewModel的最佳實(shí)踐

  • 如何時(shí)候都不要將Context傳入ViewModel篙悯。

  • 如果要在ViewModel中使用Application實(shí)例,請使用AndroidViewModel類铃绒。

  • ViewModel+LiveData+Databinding 可構(gòu)建反應(yīng)式UI鸽照。(請查看文末提供的參考資料)

  • ViewModel與onSaveInstanceState要配合使用。

    ViewModel onSaveInstanceState
    能度過配置變更 能度過配置變更和進(jìn)程關(guān)閉
    能存儲大量數(shù)據(jù) 只可存儲少量數(shù)據(jù)
    xx 必須序列化

    其實(shí)ViewModel和onSaveInstanceState是相輔相成的匿垄,當(dāng)進(jìn)程被關(guān)閉時(shí)移宅,ViewModel會被銷毀,而onSaveInstanceState不會受影響椿疗,所以可用onSaveInstanceState存儲少量關(guān)鍵數(shù)據(jù)(如userId)漏峰,并在該場景恢復(fù)時(shí)用來加載頁面數(shù)據(jù)。

在使用ViewModel時(shí)届榄,如果頁面僅僅是簡單的展示數(shù)據(jù)沒什么交互浅乔,一個(gè)LiveData就能輕松搞定,但實(shí)際情況是大多數(shù)頁面復(fù)雜且交互多铝条,就想著怎樣更好的處理ViewModel和View之間的通信靖苇,直到看到了這篇文章,參考之后得出了下圖實(shí)現(xiàn)班缰。

ViewModel和View之間通信模型
Communication between ViewModel and View
  • UserProfileActivity引用UserViewModel贤壁,可觀察其提供的UserLiveData、StatusLiveData埠忘、PageStateLiveData數(shù)據(jù)源變更分別處理數(shù)據(jù)顯示脾拆、頁面loading、跳轉(zhuǎn)等UI操作莹妒。
  • 注意Activity和ViewModel之間是單向引用名船。為避免內(nèi)存泄漏,ViewModel不能持有任何Context引用旨怠。

該模型如何響應(yīng)用戶事件的渠驼?比如點(diǎn)擊某個(gè)按鈕,需要提交信息給server鉴腻,并在成功響應(yīng)后刷新UI迷扇,這個(gè)過程中ViewModel和View是如何通信的?這里簡單描述下該過程爽哎,首先是Activity將更新事件傳遞給ViewModel谋梭,ViewModel有將其委托給Presenter處理,Presenter將處理狀態(tài)和結(jié)果倦青,通過給圖中指定的LiveData設(shè)置數(shù)據(jù),liveData就能將新數(shù)據(jù)回調(diào)給Activity盹舞,這樣頁面上所有操作就都能通過數(shù)據(jù)來驅(qū)動(dòng)了产镐。

另外隘庄,如果Activity中存在多個(gè)View組件(Fragment),這些組件可直接依賴Activity的ViewModel進(jìn)行交互癣亚。

LiveData

LiveData是一個(gè)具有生命周期感知特性的可觀察的數(shù)據(jù)保持類丑掺。

  • LiveData只通知活躍狀態(tài)( STARTED or RESUMED )的Observer更新,并在 DESTROYED狀態(tài)時(shí)自動(dòng)移除Observers述雾,來避免內(nèi)存泄漏街州。
  • 始終保持最新數(shù)據(jù)。舉例:1.退后臺的Activity在返回前臺后會立即收到最新數(shù)據(jù)玻孟。2. 配置變更導(dǎo)致Activity重建后也會立即收到最新數(shù)據(jù)唆缴。
  • 共享資源。單利模式共享同一個(gè)LiveData黍翎。
  • SingleLiveEvent 只通知一次事件面徽,適用于Navigation event、SnackBar等等場景匣掸。
    參考文章: SingleLiveEvent or EventWrapper

LiveData趟紊、MutableLiveData、MediatorLiveData三者關(guān)系?

  • 繼承關(guān)系:MediatorLiveData -> MutableLiveData -> LiveData碰酝。 所以MediatorLiveData功能最強(qiáng)大霎匈。
  • LiveData 是一個(gè)具有生命周期感知的可觀察的數(shù)據(jù)保持類。
  • MutableLiveData 在LiveData基礎(chǔ)上打開了修改Value的方法權(quán)限送爸。
  • MediatorLiveData 可管理多個(gè)LiveData铛嘱。

Transformations

用過RxJava的朋友都知道,它可以很方便地在Observable之間轉(zhuǎn)換碱璃。LiveData也提供了類似的功能弄痹。

  • map : 將一種數(shù)據(jù)類型的LiveData<A> 轉(zhuǎn)換為另一種類型的 LiveData<B>

    // 示例代碼:觀察將被轉(zhuǎn)換LiveData<User>,待其數(shù)據(jù)源變更后轉(zhuǎn)換為LiveData<String>并通知訂閱者嵌器。
    LiveData<User> userLiveData = ...;
      LiveData<String> userName = Transformations.map(userLiveData, user -> {
          user.name + " " + user.lastName
    });
    
  • switchMap : 和map類似肛真。差別在于triggerLiveData變更后,會觸發(fā)和等待另外一個(gè)LiveData獲取數(shù)據(jù)爽航。

    // 示例代碼:將addressInputLiveData轉(zhuǎn)換為postalCodeLiveData.
    class MyViewModel extends ViewModel {
      private final PostalCodeRepository repository;
      private final MutableLiveData<String> addressInput = new MutableLiveData();
      public final LiveData<String> postalCode =
              Transformations.switchMap(addressInput, (address) -> {
                  return repository.getPostCode(address);
               });
    
      public MyViewModel(PostalCodeRepository repository) {
        this.repository = repository
      }
    
     // addressInputLiveData變更時(shí)觸發(fā)repository.getPostCode蚓让,
     // 待其回去成功后,再將數(shù)據(jù)設(shè)置給postalCodeLiveData讥珍。
      private void setInput(String address) {
              addressInput.setValue(address);
          }
    }
    
    

以上為使用示例代碼历极,其內(nèi)部使用的MediatorLiveData實(shí)現(xiàn),較簡單衷佃,感興趣的朋友請自行查閱源碼趟卸。

幾個(gè)問題

LifecycleOwner組件是如何與liveData通信的?

  • SupportActivity 通過添加一個(gè)空的ReportFragment來處理生命周期狀態(tài)變更回調(diào);Fragment則在自身生命周期函數(shù)中處理锄列。
  • LifecycleOwner組件图云,通過LifecycleRegistry類中handleLifecycleEvent -> dispatchEvent方法與liveData通信,從而使liveData具有感知組件生命周期的能力邻邮。
  • 組件銷毀時(shí)竣况,LifecycleRegistry會通知liveData移除observer。

ViewModel如何做到一直在內(nèi)存中筒严,直到Activity銷毀或Fragment被移除時(shí)才被清除的丹泉?

1.x.x版本實(shí)現(xiàn)

  • Activity或Fragment會添加一個(gè)空的HolderFragment,而ViewModelStore實(shí)例被HolderFragment持有鸭蛙,所以就保證了整個(gè)生命周期中ViewModelStore實(shí)例始終唯一摹恨,也就保證了其緩存的ViewModel實(shí)例會一直存在直到組件銷毀(在onDestroy中會調(diào)用ViewModelStore.clear()方法清除其緩存的ViewModel實(shí)例)。
  • 由于這個(gè)HolderFragment設(shè)置了setRetainInstance(true)规惰, 這樣在Activity重建時(shí)它不會執(zhí)行onDestroy回調(diào)睬塌,這就是它能度過配置變更的原因

2.x.x版本-對應(yīng)androidx-fragment-v1.0.0

  • Activity
    • 緩存:在onRetainNonConfigurationInstance()回調(diào)方法中將ViewModelStore實(shí)例緩存到NonConfigurationInstances中歇万。
    • 恢復(fù):在onCreate中通過getLastNonConfigurationInstance()獲取重建前的狀態(tài)并回復(fù)ViewModelStore揩晴。
  • Fragment
    • 緩存:在FragmentActivity.onSaveInstanceState -> FragmentManager.saveAllState -> FragmentManager.saveNonConfig方法中,將ViewModelStore實(shí)例緩存到了FragmentManagerNonConfig中贪磺,最終通過FragmentActivity將其緩存到NonConfigurationInstances中硫兰。
    • 恢復(fù):方法調(diào)用棧FragmentActivity.onCreate -> FragmentManager.restoreAllState(arg1, nonConfig) -> FragmentState.instantiate(x,x,x,nonConfig, viewModelStore);其中在FragmentState.instantiate(x,x,x,nonConfig, viewModelStore)方法會創(chuàng)建一個(gè)新的Fragment并將ViewModelStore變量賦值寒锚。

以上結(jié)論是我分別從lifecycle庫1.x和2.x版本源碼分析后得出劫映。關(guān)于ViewModel的生命周期實(shí)現(xiàn)原理,各個(gè)版本實(shí)現(xiàn)略有不同刹前,其中l(wèi)ifecycle2.x版本已改用Androidx實(shí)現(xiàn)泳赋,所以ViewModel的緩存實(shí)現(xiàn)還和androidx組件版本有關(guān)系。感興趣的朋友喇喉,請自行查閱源碼祖今。

當(dāng)Fragment被detach后再attach回來,會導(dǎo)致添加多個(gè)Observer拣技?

  • 分析出現(xiàn)的原因

    由于Fragment默認(rèn)實(shí)現(xiàn)是在onDestroy才通知liveData 移除observers千诬,而我們每次在onCreateView都會add新的observer實(shí)例,這樣就會導(dǎo)致數(shù)據(jù)更新時(shí)膏斤,LiveData會同時(shí)通知多個(gè)Observer徐绑,界面就會快速刷新多次。

  • 解決方案

    • 當(dāng)你在onActivityCreated方法中添加LiveData.observer(LifecycleOwner owner, Observer<T> observer)時(shí)莫辨,第一個(gè)參數(shù)使用Fragment.getViewLifecycleOwner()方法返回值傲茄。(如果你沒有找到該方法毅访,請更新你依賴的support包版本,Google已在新版本中提供該方法)
    • LiveData.observer(LifecycleOwner owner, Observer<T> observer)放在onCreate回調(diào)中烫幕,在Fragment顯示時(shí)手動(dòng)觸發(fā)數(shù)據(jù)刷新俺抽,當(dāng)然最好還是更新support版本來解決。

總結(jié)

Android Jetpack是一套組件開發(fā)工具集较曼,旨在幫助我們輕松構(gòu)建更穩(wěn)定、更健壯振愿、以及更可維護(hù)的應(yīng)用程序捷犹。對于Google而言,推出Jetpack可以更好的管理和維護(hù)組件庫冕末;對開發(fā)者而言萍歉,使用Jetpack可以快速開發(fā)出高質(zhì)量應(yīng)用,也能看到官方在不同技術(shù)方案上的選擇档桃,以及新技術(shù)發(fā)展方向枪孩。從目前Jetpack組件布局來看,AndroidX藻肄、kotlin是需要我們使用和掌握的蔑舞。

本文后半部分介紹了架構(gòu)組件中l(wèi)ifecycle庫的一些原理和最佳實(shí)踐,但還不夠全面深入嘹屯,后面我會一一從源碼角度分析各種組件實(shí)現(xiàn)原理攻询,敬請關(guān)注。

既然是最佳實(shí)踐州弟,怎么能沒有代碼钧栖,這里分享下作者使用架構(gòu)組件實(shí)現(xiàn)的項(xiàng)目代碼,重點(diǎn)關(guān)注wanandroid模塊婆翔,其實(shí)現(xiàn)使用到了ViewModel+LiveData+Lifecycle+Room拯杠,按照官方推薦的架構(gòu)實(shí)現(xiàn),并完成各個(gè)組件獨(dú)立的單元測試啃奴。

image.png

限于作者水平有限潭陪,文中定有錯(cuò)誤和疏漏之處,懇請指出纺腊,與君共勉畔咧;若有不明白之處,歡迎隨時(shí)評論交流揖膜。

參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末誓沸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子壹粟,更是在濱河造成了極大的恐慌拜隧,老刑警劉巖宿百,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異洪添,居然都是意外死亡垦页,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門干奢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痊焊,“玉大人,你說我怎么就攤上這事忿峻”∩叮” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵逛尚,是天一觀的道長垄惧。 經(jīng)常有香客問我,道長绰寞,這世上最難降的妖魔是什么到逊? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮滤钱,結(jié)果婚禮上觉壶,老公的妹妹穿的比我還像新娘。我一直安慰自己菩暗,他們只是感情好掰曾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著停团,像睡著了一般旷坦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上佑稠,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天秒梅,我揣著相機(jī)與錄音,去河邊找鬼舌胶。 笑死捆蜀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的幔嫂。 我是一名探鬼主播辆它,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼履恩!你這毒婦竟也來了锰茉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤切心,失蹤者是張志新(化名)和其女友劉穎飒筑,沒想到半個(gè)月后片吊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡协屡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年俏脊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肤晓。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡爷贫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出材原,到底是詐尸還是另有隱情沸久,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布余蟹,位于F島的核電站,受9級特大地震影響子刮,放射性物質(zhì)發(fā)生泄漏威酒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一挺峡、第九天 我趴在偏房一處隱蔽的房頂上張望葵孤。 院中可真熱鬧,春花似錦橱赠、人聲如沸尤仍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宰啦。三九已至,卻和暖如春饼拍,著一層夾襖步出監(jiān)牢的瞬間赡模,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工师抄, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留漓柑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓叨吮,卻偏偏與公主長得像辆布,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子茶鉴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

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