Android依賴注入框架-Hilt詳解您访,官方基于Dagger封裝適配Android而開發(fā),史上最詳細解析

Android依賴注入框架-Hilt詳解剪决,官方基于Dagger封裝適配Android而開發(fā)灵汪,史上最詳細解析

記得2年前檀训,我發(fā)布過一篇關于Android依賴注入框架的文章,Dagger在Android開發(fā)中上手難度較高识虚,當時就給大家推薦了Koin,感興趣的同學可以去看看那篇文章肢扯。http://www.reibang.com/p/bccb93a78cee。(Koin與Hilt各有優(yōu)勢)

現(xiàn)在担锤,Android有了官方的依賴注入工具蔚晨,那就是Hilt,基于Dagger上的封裝,所以Hilt跟Dagger很相似肛循,谷歌官方也有專門的Hilt中文文檔铭腕,不過版本比較老,是2.28版本多糠,地址為:https://developer.android.google.cn/training/dependency-injection/hilt-android累舷,感興趣的同學可以去看看。本例是基于當前最新的版本:2.40.5版本所寫夹孔,最新的版本跟老的版本有很多的差別被盈,方法和參數(shù)都變動挺大,老的文檔可能不適用于最新的版本搭伤。正所謂用新不用舊只怎,如果想了解最新用法少走歪路的,可以看我這篇文章怜俐,算是比較詳細的吧身堡。

依賴注入是什么,依賴注入有什么好處拍鲤,這些就不多講了贴谎,直接去看我上面以前寫的文章,我們直接進入Hilt的用法講解季稳。本篇的例子在我的github上擅这,地址為:https://github.com/CaesarShao/CaesarHit,感興趣的可以對照著理解景鼠。

Hilt圖片.png

本文目錄

依賴方式
開始使用
Activity作用域--最基礎調用
全局作用域及單例模式
ViewModel中調用
Fragment作用域使用
Service作用域使用
自定義View作用域
嵌套使用
重名的用法
接口未指明用法
其他類中獲取方式

依賴方式

首先在根級的build.gradle中加入

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.5'
    }
}

然后在app/build.gradle中加入

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

dependencies {
    implementation "com.google.dagger:hilt-android:2.40.5"
    kapt "com.google.dagger:hilt-compiler:2.40.5"
    
    implementation "androidx.activity:activity-ktx:1.2.3"http://在activity中可以便捷獲取ViewModel
    implementation 'androidx.fragment:fragment-ktx:1.3.4'//在fragment中可以便捷獲取ViewModel
}
android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

在文檔中有寫這么一段話:Hilt 使用 Java 8 功能仲翎。如需在項目中啟用 Java 8,請將以下代碼添加到 app/build.gradle 文件中莲蜘。不過目前我創(chuàng)建項目的時候谭确,好像會自動增加這個。

開始使用

首先在我們項目的Application中加上@HiltAndroidApp注釋

@HiltAndroidApp
class MyApp : Application() {
}

接下來就可以開始使用依賴注入了票渠。

我們先來總體講解下逐哈,后面會有詳細的例子說明。依賴注入的方式有2種问顷,一種就是我們自己創(chuàng)建的類昂秃,只要在構造函數(shù)中加上@Inject這個標簽注釋禀梳,就可以在我們需要注入的界面通過@Inject注釋來調用。第二種就是專門給第三方類使用的肠骆,因為第三方的類無法用@Inject標簽注釋算途,我們就需要在項目中添加@Module注解塊,然后在對應的@Module中將需要用到的提供對象new出來蚀腿,用@Provides注釋來標識出嘴瓤,之后我們就可以在其他注入的界面調用。

Hilt的依賴注入也有作用域的概念莉钙,就是你在注入一個對象的時候廓脆,已經(jīng)規(guī)定好了,這個對象在哪里可以被調用磁玉,然后當這個被調用的區(qū)域被銷毀了停忿,那作用域中被注入的對象也會自動銷毀,是不是很方便蚊伞。

Hilt目前提供了以下幾個作用域席赂,并且對應使用的地方我也一并寫出來

Singleton -- SingletonComponent -> Application (這個作用域看著有點像單例,但是就是應用全局作用域时迫,我們單例的對象颅停,必須要在這個作用域中注入才能使用)

ActivityRetainedScoped -- ActivityRetainedComponent ->跟Activity生命周期有聯(lián)系的,例如viewmodel或者其他關聯(lián)的

ActivityScoped -- ActivityComponent -> Activity

ViewModelScoped -- ViewModelComponent -> ViewModel

FragmentScoped -- FragmentComponent -> Fragment

ViewScoped -- ViewWithFragmentComponent -> View(從名字上看别垮,應該是自定義view與fragment的生命周期相關)

ViewScoped -- ViewComponent -> View(一般用于自定義view便监,不過從里面的繼承關系來看扎谎,應該是view與activity的生命周期相關)

ServiceScoped -- ServiceComponent -> Service

好碳想,所有的作用域都已經(jīng)寫出來了,接下來我針對不同的作用域毁靶,來講解胧奔,其實用法都很簡單,每個作用域预吆,我都會用2種注入方法來寫龙填,代表著自己寫的類和第三方類的使用。(其中帶有Global字樣的bean都可以理解為是第三方類來使用)

Activity作用域--最基礎調用

@ActivityScoped
class SimpleData @Inject constructor(){
    init {
        CaesarHitLog.I("SimpleData類的構造函數(shù)被調用了")
    }
    fun deal(){
        CaesarHitLog.I("SimpleData調用了方法")
    }
}

第一個SimpleData類中拐叉,用@ActivityScoped標簽注釋代表是Activity的作用域岩遗,如果什么都不寫就代表是全局的作用域,在構造函數(shù)中用@Inject標簽注入凤瘦,就完成了一個簡單類的注入宿礁。

class SimpleGlobalData {
    init {
        CaesarHitLog.I("SimpleGlobalData類的構造函數(shù)被調用了")
    }
    fun deal(){
        CaesarHitLog.I("SimpleGlobalData調用了方法")
    }
}
@InstallIn(ActivityComponent::class)
@Module
class MyActModule {
    @Provides
    fun providerSimpleGlobal(): SimpleGlobalData {
        return SimpleGlobalData()
    }
 }

第二個可以理解是第三方類的注入,我們創(chuàng)建一個MyActModule蔬芥,用@Module標簽注釋梆靖,然后再加上@InstallIn(ActivityComponent::class)注解控汉,代表這個作用域是Activity的,最后在里面寫上provider的方法返吻,要用@Provides注釋姑子,記住方法名不能重復,然后返回你需要的對象就可以了测僵,跟Dagger一毛一樣街佑。

如何在Activity中調用呢,也非常簡單捍靠。

@AndroidEntryPoint
class SimpleActivity : AppCompatActivity() {
    @Inject
    lateinit var simpleData1: SimpleData
    @Inject
    lateinit var simpleData2: SimpleData
    @Inject
    lateinit var simpleGlobalData1: SimpleGlobalData
    @Inject
    lateinit var simpleGlobalData2: SimpleGlobalData
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_simple)
        simpleData1.deal()
        CaesarHitLog.I("simpleData1的地址:" + simpleData1.toString())
        CaesarHitLog.I("simpleData2的地址:" + simpleData2.toString())
        simpleGlobalData1.deal()
        CaesarHitLog.I("simpleGlobalData1的地址:" + simpleGlobalData1.toString())
        CaesarHitLog.I("simpleGlobalData2的地址:" + simpleGlobalData2.toString())
    }
}

在Activity中加上@AndroidEntryPoint注解舆乔,然后用@Inject注解獲取對象就可以了,我這邊打印了4個對象的地址剂公,都是不同的希俩,說明獲取成功,并且每次注入獲取都是一個新的對象纲辽。

全局作用域及單例模式

接著就是全局的使用和單例的使用

@Singleton
class SingleData @Inject constructor(){
    init {
        CaesarHitLog.I("SingleData類的構造函數(shù)被調用了")
    }
}

上面的代碼有個@Singleton標簽颜武,代表是全局單例的作用域,如果單單想要全局非單例拖吼,那就去掉@Singleton作用域就可以了鳞上。

class SingleGlobalData {
    init {
        CaesarHitLog.I("SingleData類的構造函數(shù)被調用了")
    }
}
@InstallIn(SingletonComponent::class)
@Module
 class MyAppModule {
    @Provides
    @Singleton
    fun providerSingleGlobal(): SingleGlobalData {
        return SingleGlobalData()
    }
 }

第二個就是全局的另外一種寫法,其他的大同小異吊档,就改了一個作用域SingletonComponent篙议,然后,如果你是要單例的怠硼,就要加上@Singleton鬼贱,如果不是,就去掉即可香璃。如果你去看過官方的文檔这难,就會發(fā)現(xiàn),跟他的不同葡秒,因為我這個是最新的版本姻乓,官方上的版本還是比較老的,很多的方法和類都變了眯牧。最后使用的方法都是一樣的蹋岩,就不多說了。

...
@Inject
lateinit var singleData: SingleData
@Inject
lateinit var singleGlobalData: SingleGlobalData

override fun onCreate(savedInstanceState: Bundle?) {
   CaesarHitLog.I("singleData地址:"+singleData)
   CaesarHitLog.I("singleGlobalData地址:"+singleGlobalData)
}

ViewModel中調用

這個作用域是ViewModelScoped及ViewModelComponent学少,用起來也很簡單剪个。

@ViewModelScoped
class VMData @Inject constructor(){
    init {
        CaesarHitLog.I("VMData類的構造函數(shù)被調用了")
    }
}
class VMGlobalData {
    init {
        CaesarHitLog.I("VMGlobalData類的構造函數(shù)被調用了")
    }
}
@InstallIn(ViewModelComponent::class)
@Module
 class MyModelModule {
    @Provides
    fun providerVM(): VMGlobalData {
        return VMGlobalData()
    }
}

然后在viewmodel中調用如下:

@HiltViewModel
class MyViewModel @Inject constructor() :ViewModel() {
    @Inject
    lateinit var vm: VMData
    @Inject
    lateinit var vmg: VMGlobalData
    fun check(){
        CaesarHitLog.I("VMData地址:"+vm)
        CaesarHitLog.I("VMGlobalData:"+vmg)
    }
}

這邊ViewModel要用@HiltViewModel標簽注釋,另外要注意旱易,在ViewModel的構造方法中要加一個@Inject標簽禁偎,不然會報錯腿堤。還有這邊ViewModel官方已經(jīng)給我們注入過了,所以我們可以用便捷的方式來獲取ViewModel如暖。這邊要注意笆檀,不能在Activity中用@Inject的方式來獲取,這種獲取的方式會將ViewModel變成一個普通的類盒至,會失去它的生命周期酗洒。

@AndroidEntryPoint
class MyViewModelActivity : AppCompatActivity() {

//    @Inject
//    lateinit var viewmodel: MyViewModel  錯誤的獲取方式

    //    val viewmodel: MyViewModel by  lazy {
//        ViewModelProvider(this).get(MyViewModel::class.java)
//    }//這個是老的方式
    val viewmodel by viewModels<MyViewModel>()//這個是注入獲取的方式

當然了,用便捷的方式獲取的話還需要另外2個依賴包

implementation "androidx.activity:activity-ktx:1.2.3"http://在activity中獲取
implementation 'androidx.fragment:fragment-ktx:1.3.4'//在fragment中獲取

Fragment作用域使用

@FragmentScoped
class FragmentData @Inject constructor(){
    init {
        CaesarHitLog.I("FragmentData類的構造函數(shù)被調用了")
    }
}
class FragmentGlobalData {
    init {
        CaesarHitLog.I("FragmentGlobalData類的構造函數(shù)被調用了")
    }
}
@InstallIn(FragmentComponent::class)
@Module
class MyFragmentModule {
    @Provides
    fun providerFrag(): FragmentGlobalData {
        return FragmentGlobalData()
    }
}
@AndroidEntryPoint
class BlankFragment : Fragment() {
    @Inject
    lateinit var fragData: FragmentData
    @Inject
    lateinit var fragData2: FragmentGlobalData
}

在fragment中也超級簡單枷遂。用FragmentScoped作用域和FragmentComponent樱衷,記得在碎片中,要加上@AndroidEntryPoint注釋酒唉。

Service作用域使用

@ServiceScoped
class ServiceData @Inject constructor(){
    init {
        CaesarHitLog.I("ServiceData類的構造函數(shù)被調用了")
    }
}
class ServiceGlobalData {
    init {
        CaesarHitLog.I("ServiceGlobalData類的構造函數(shù)被調用了")
    }
}
@InstallIn(ServiceComponent::class)
@Module
class MyServiceModule {
    @Provides
    fun providerSerFrag(): ServiceGlobalData {
        return ServiceGlobalData()
    }
}
@AndroidEntryPoint
class MyService:Service(){
    @Inject
    lateinit var data: ServiceData
    @Inject
    lateinit var data2: ServiceGlobalData
}

在Service中矩桂,別忘記加上@AndroidEntryPoint注釋

自定義View作用域

在自定義view中,有時我們也需要獲取對象使用痪伦。只要指定viewScoped作用域即可

@ViewScoped
class CusViewData @Inject constructor(){
    init {
        CaesarHitLog.I("CusViewData類的構造函數(shù)被調用了")
    }
}
class CusViewGlobalData {
    init {
        CaesarHitLog.I("CusViewGlobalData類的構造函數(shù)被調用了")
    }
}
@AndroidEntryPoint
class CusView :View {
    @Inject
    lateinit var data: CusViewData
    @Inject
    lateinit var data2: CusViewGlobalData
}

調用跟上面都是大同小異侄榴。

嵌套使用

我們平常使用的時候,不單單一個類會這么簡單网沾,他肯定構造函數(shù)中包含了其他的類癞蚕,那這種情況應該怎么使用,下面也給出2種注入方式

class MoreDate  @Inject constructor(val simpleData: SimpleData,@ActivityContext val context: Context){
    init {
        CaesarHitLog.I("MoreDate類的構造函數(shù)被調用了,context是否為空:"+(context==null))
    }
}

第一種就是自己寫的方法辉哥,MoreDate的構造函數(shù)中桦山,有2個參數(shù),一個是SimpleData醋旦,這個類我們在剛才已經(jīng)注入過了恒水,所以在這邊就可以直接使用,系統(tǒng)會自動為我們在構造函數(shù)中注入浑度。另外一個是Context上下文寇窑,其中Context我們用了@ActivityContext來修飾鸦概,Hilt已經(jīng)為我們注入提供了2種上下文箩张,另外一個是ApplicationContext,我們也能直接使用窗市。接下來我們來看看另外一個注入方法:

class MoreGlobalData(val singleData: SingleData, var context: Context){
    init {
        CaesarHitLog.I("MoreGlobalData類的構造函數(shù)被調用了,context是否為空:"+(context==null))
    }
}
@Provides
fun providerMoreGlobal( singleData: SingleData,@ApplicationContext context: Context): MoreGlobalData {
    return MoreGlobalData(singleData,context)
}
@AndroidEntryPoint
class MoreActivity : AppCompatActivity() {
    @Inject
    lateinit var fragData: MoreDate
    @Inject
    lateinit var fragData2: MoreGlobalData
}

在Provides中先慷,MoreGlobalData需要SingleData和Context對象,Hilt會自動為我們找對象咨察,我們不需要去主動賦值论熙,只需要在方法參數(shù)中有定義即可。

重名的用法

有時候我們要創(chuàng)建2個相同的對象摄狱,那我們應該如何去區(qū)分他們脓诡?

class UserNameGlobalBean constructor(val name: String) {
    init {
        CaesarHitLog.I("UserNameBean構造了")
    }
}
@Named("name1")
@Provides
fun providerUserName1(): UserNameGlobalBean {
    return  UserNameGlobalBean("111")
}
@Named("name2")
@Provides
fun providerUserName2(): UserNameGlobalBean {
    return  UserNameGlobalBean("222")
}
@AndroidEntryPoint
class NamesActivity : AppCompatActivity() {
    @Inject
    @Named("name1")
    lateinit var nameOne: UserNameGlobalBean
    @Inject
    @Named("name2")
    lateinit var nameTwo: UserNameGlobalBean
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_names)
        CaesarHitLog.I("名字1為:"+nameOne.name)
        CaesarHitLog.I("名字2為:"+nameTwo.name)
    }
}

通過加入@Named限定符无午,就可以指定我們需要對象,調用的時候祝谚,也指定一下即可宪迟。

接口未指明用法

這個跟上面的有點不太一樣,先看例子吧交惯。

interface ICallBack {
    fun onData()
    fun onDes()
}
class CallBackImpl @Inject constructor(@ActivityContext var context: Context):ICallBack{
    override fun onData() {
        Toast.makeText(context,"onData調用了",Toast.LENGTH_SHORT).show()
        CaesarHitLog.I("數(shù)據(jù)1調用額")
    }
    override fun onDes() {
        CaesarHitLog.I("數(shù)據(jù)2調用額")
    }
}

我們有一個接口ICallBack次泽,然后有一個實現(xiàn)該接口的類CallBackImpl,這邊注意席爽,這個類的構造函數(shù)中要加上@Inject注釋意荤。然后接口類的注入也有點不同,因為是抽象的只锻,所以在綁定的時候玖像,類也要是抽象類。然后這邊不再是provider標簽了齐饮,要用Binds標簽御铃。

@InstallIn(ActivityComponent::class)
@Module
abstract class MyAbsModule {
    @Binds
    abstract fun provideCallback(callback: CallBackImpl):ICallBack

}
@AndroidEntryPoint
class InterActivity : AppCompatActivity() {
    @Inject
    lateinit var calBack: ICallBack
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_inter)
        calBack.onData()
        calBack.onDes()
    }
}

在綁定的時候,我們的參數(shù)是一個實現(xiàn)類沈矿,返回是一個接口上真。所以最后,我們獲取調用的時候羹膳,可以用它的接口方法睡互,然后在實現(xiàn)類中,就會自動調用了對應的方法陵像。

其他非注入的類中獲取

Android應用有4大組件就珠,目前Hilt只支持了其中2個的依賴注入,那剩余2個的組件甚至于其他不直接支持注入的類中難道就不能使用了么醒颖,當然可以妻怎,接下來我這邊再舉一個在廣播中獲取注入的方式,注冊廣播和發(fā)送就簡略了泞歉,直接上核心代碼:

class MyBroadReceiver:BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if ("com.caesar.hit.normal.broad" == intent?.action){
           val singletom =  EntryPointAccessors.fromApplication<MySingleTom>(context!!)
            CaesarHitLog.I("收到了和廣播:"+singletom.getSingleData())
        }
    }
    @EntryPoint
    @InstallIn(SingletonComponent::class)
    interface MySingleTom {
        fun getSingleData(): SingleData
    }
}

這邊我們用EntryPoint限定符創(chuàng)建一個入口逼侦,在里面定義好你要獲取的那個類,當然了腰耙,那個類是已經(jīng)被注入好的榛丢。然后我們通過EntryPointAccessors這個類的方法,它有4種獲取的方法挺庞,對應4種作用域晰赞,分別為fromApplication,fromActivity,fromFragment掖鱼,fromView然走,獲取的類型就是下面定義的接口MySingleTom,然后通過里面的方法戏挡,就可以獲取到你想要的對象了丰刊。通過這種辦法,就可以在其他你想要的任何類中來獲取注入的對象了增拥。

至此啄巧,HIlt基本講完了它的基礎用法。不過我看Hilt每次更新的變動挺大的掌栅,目前應該還不是它的完全版本秩仆,不過Hilt相對于Dagger是大大簡化了難度,還是強烈推薦大家去使用猾封。

轉載請標明出處澄耍。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市晌缘,隨后出現(xiàn)的幾起案子齐莲,更是在濱河造成了極大的恐慌,老刑警劉巖磷箕,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件选酗,死亡現(xiàn)場離奇詭異,居然都是意外死亡岳枷,警方通過查閱死者的電腦和手機芒填,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來空繁,“玉大人殿衰,你說我怎么就攤上這事∈⑴荩” “怎么了闷祥?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長傲诵。 經(jīng)常有香客問我凯砍,道長,這世上最難降的妖魔是什么掰吕? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任果覆,我火速辦了婚禮,結果婚禮上殖熟,老公的妹妹穿的比我還像新娘。我一直安慰自己斑响,他們只是感情好菱属,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布钳榨。 她就那樣靜靜地躺著,像睡著了一般纽门。 火紅的嫁衣襯著肌膚如雪薛耻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天赏陵,我揣著相機與錄音饼齿,去河邊找鬼。 笑死蝙搔,一個胖子當著我的面吹牛缕溉,可吹牛的內容都是我干的。 我是一名探鬼主播吃型,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼证鸥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了勤晚?” 一聲冷哼從身側響起枉层,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赐写,沒想到半個月后鸟蜡,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡挺邀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年矩欠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悠夯。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡癌淮,死狀恐怖,靈堂內的尸體忽然破棺而出沦补,到底是詐尸還是另有隱情乳蓄,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布夕膀,位于F島的核電站虚倒,受9級特大地震影響,放射性物質發(fā)生泄漏产舞。R本人自食惡果不足惜魂奥,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望易猫。 院中可真熱鬧耻煤,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至炮赦,卻和暖如春怜跑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背吠勘。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工性芬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人剧防。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓植锉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親诵姜。 傳聞我的和親對象是個殘疾皇子汽煮,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容