組件化+Jetpack+MVVM項(xiàng)目實(shí)戰(zhàn)司志,涉及協(xié)程+Retrofit,Paging3+Room等

一降宅、項(xiàng)目簡(jiǎn)介

微信截圖_20210521163936.png

該項(xiàng)目主要以組件化+Jetpack+MVVM為架構(gòu)骂远,使用Kotlin語(yǔ)言,集合了最新的Jetpack組件腰根,如Navigation激才、Paging3Room等额嘿,另外還加上了依賴(lài)注入框架Koin和圖片加載框架Coil瘸恼。

網(wǎng)絡(luò)請(qǐng)求部分使用OkHttp+Retrofit,配合Kotlin的協(xié)程册养,完成了對(duì)Retrofit和協(xié)程的請(qǐng)求封裝东帅,結(jié)合LoadSir進(jìn)行狀態(tài)切換管理,讓開(kāi)發(fā)者只用關(guān)注自己的業(yè)務(wù)邏輯捕儒,而不要操心界面的切換和通知冰啃。

對(duì)于具體的網(wǎng)絡(luò)封裝思路,可參考【Jetpack篇】協(xié)程+Retrofit網(wǎng)絡(luò)請(qǐng)求狀態(tài)封裝實(shí)戰(zhàn)【Jetpack篇】協(xié)程+Retrofit網(wǎng)絡(luò)請(qǐng)求狀態(tài)封裝實(shí)戰(zhàn)(2)

項(xiàng)目地址:github.com/fuusy/wanan…

如果此項(xiàng)目對(duì)你有幫助和價(jià)值刘莹,煩請(qǐng)給個(gè)star??,或者有什么好的建議或意見(jiàn)阎毅,可以發(fā)個(gè)issues,感謝点弯!

二扇调、項(xiàng)目詳情

2.1、組件化搭建項(xiàng)目時(shí)暴露出的問(wèn)題

2.1.1抢肛、如何獨(dú)立運(yùn)行一個(gè)Module狼钮?

運(yùn)行總App時(shí),子Module是屬于library捡絮,而獨(dú)立運(yùn)行時(shí)熬芜,子Module是屬于application。那么我們只需要在根目錄下gradle.properties中添加一個(gè)標(biāo)志位來(lái)區(qū)分一下子Module的狀態(tài)福稳,例如singleModule = false 涎拉,該標(biāo)志位可以用來(lái)表示當(dāng)前Module是否是獨(dú)立模塊,true表示處于獨(dú)立模塊,可單獨(dú)運(yùn)行鼓拧,false則表示是一個(gè)library半火。

image-20210425094424273.png

如何使用呢?

在每個(gè)Modulebuild.gradle中加入singleModule的判斷季俩,以區(qū)分是application還是library钮糖。如下:

if (!singleModule.toBoolean()) {
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

......
dependencies {
}
復(fù)制代碼

如果需要獨(dú)立運(yùn)行只需要修改gradle.properties標(biāo)志位singleModule的值。

2.1.2酌住、編譯運(yùn)行后店归,桌面會(huì)出現(xiàn)多個(gè)相同圖標(biāo);

當(dāng)新建多個(gè)Moudle的時(shí)候赂韵,運(yùn)行后你會(huì)發(fā)現(xiàn)桌面上會(huì)出現(xiàn)多個(gè)相同的圖標(biāo)娱节,

image-20210425100807316.png

其實(shí)每個(gè)圖標(biāo)都能夠獨(dú)立運(yùn)行,但是到最后App發(fā)布的時(shí)候祭示,肯定是只需要一個(gè)總?cè)肟诰涂梢粤恕?/p>

發(fā)生這種情況的原因很簡(jiǎn)單,因?yàn)樾陆ㄒ粋€(gè)Module谴古,結(jié)構(gòu)相當(dāng)于一個(gè)project质涛,AndroidManifest.xml包括Activity都存在,在AndroidManifest.xml為Activity設(shè)置了actioncategory掰担,當(dāng)app運(yùn)行時(shí)汇陆,也就在桌面上為webview這個(gè)模塊生成了一個(gè)入口。

image-20210425102207853.png

解決方案很簡(jiǎn)單带饱,刪除上圖紅色框框中的代碼即可毡代。

但是...... 問(wèn)題又雙叒叕來(lái)了,刪除了中代碼勺疼,確實(shí)可以解決多個(gè)圖標(biāo)的問(wèn)題教寂,但是當(dāng)該子Moudle需要獨(dú)立運(yùn)行時(shí),由于缺少<intent-filter>中的聲明执庐,該Module就無(wú)法正常運(yùn)行酪耕。

以下圖項(xiàng)目為例:

image-20210425103221979.png

我們可以在”webview“Module中,新建一個(gè)和java同層級(jí)的包轨淌,取名:manifest迂烁,將AndroidManifest.xml復(fù)制到該包下,并且將/manifest/AndroidManifest.xml中內(nèi)容進(jìn)行刪除修改递鹉。

image-20210425104829329.png

只留有一個(gè)空殼子盟步,原來(lái)的AndroidManifest.xml則保持不變。同時(shí)在webview的build.gradle中利用sourceSets進(jìn)行區(qū)分躏结。

android{
    sourceSets{
        main {
            if (!singleModule.toBoolean()) {
                //如果是library却盘,則編譯manifest下AndroidManifest.xml
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            } else {
                //如果是application,則編譯主目錄下AndroidManifest.xml
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}
復(fù)制代碼

通過(guò)修改SourceSets中的屬性,可以指定需要被編譯的源文件谷炸,根據(jù)singleModule.toBoolean()來(lái)判斷當(dāng)前Module是屬于application還是library北专,如果是library,則編譯manifest下AndroidManifest.xml旬陡,反之則直接編譯主目錄下AndroidManifest.xml拓颓。

上述處理后,子Moudule當(dāng)作library時(shí)不會(huì)出現(xiàn)多個(gè)圖標(biāo)的情況描孟,同時(shí)也可以獨(dú)立運(yùn)行驶睦。

2.1.3、組件間通信

主要借助阿里的路由框架ARouter匿醒,具體使用請(qǐng)參考github.com/alibaba/ARo…

2.2场航、Jetpack組件

2.2.1、Navigation

Navigation是一個(gè)管理Fragment切換的組件廉羔,支持可視化處理溉痢。開(kāi)發(fā)者也完全不用操心Fragment的切換邏輯”锼基本使用請(qǐng)參考官方說(shuō)明

在使用Navigation的過(guò)程中孩饼,會(huì)出現(xiàn)點(diǎn)擊back按鍵,界面會(huì)重新走了onCreate生命周期竹挡,并且將頁(yè)面重構(gòu)镀娶。例如Navigation與BottomNavigationView結(jié)合時(shí),點(diǎn)擊tab揪罕,F(xiàn)ragment會(huì)重新創(chuàng)建梯码。目前比較好的解決方法是自定義FragmentNavigator,將內(nèi)部replace替換為show/hide好啰。

另外轩娶,官方對(duì)于與BottomNavigationView結(jié)合時(shí)的情況也提供了一種解決方案。 官方提供了一個(gè)BottomNavigationView的擴(kuò)展函數(shù)NavigationExtensions坎怪,

將之前共用一個(gè)navigation分為每個(gè)模塊單獨(dú)一個(gè)navigation罢坝,例如該項(xiàng)目分為首頁(yè)項(xiàng)目搅窿、我的三個(gè)tab嘁酿,相應(yīng)的新建了三個(gè)navigation:R.navigation.navi_home, R.navigation.navi_project, R.navigation.navi_personal, Activity中BottomNavigationViewNavigation進(jìn)行綁定時(shí)也做出了相應(yīng)的改變男应。

    /**
     * navigation綁定BottomNavigationView
     */
    private fun setupBottomNavigationBar() {
        val navGraphIds =
            listOf(R.navigation.navi_home, R.navigation.navi_project, R.navigation.navi_personal)

        val controller = mBinding?.navView?.setupWithNavController(
            navGraphIds = navGraphIds,
            fragmentManager = supportFragmentManager,
            containerId = R.id.nav_host_container,
            intent = intent
        )

        currentNavController = controller
    }
復(fù)制代碼

官方這么做的目的在于讓每個(gè)模塊單獨(dú)管理自己的Fragment棧闹司,在tab切換時(shí),不會(huì)相互影響沐飘。

2.2,2游桩、Paging3

Paging是一個(gè)分頁(yè)組件牲迫,主要與Recyclerview結(jié)合分頁(yè)加載數(shù)據(jù)。具體使用可參考此項(xiàng)目“每日一問(wèn)”部分借卧,如下:

UI層:

class DailyQuestionFragment : BaseFragment<FragmentDailyQuestionBinding>() {
...

private fun loadData() {
        lifecycleScope.launchWhenCreated {
            mViewModel.dailyQuestionPagingFlow().collectLatest {
                dailyPagingAdapter.submitData(it)
            }
        }
    }
...
}
復(fù)制代碼

ViewModel層:

class ArticleViewModel(private val repo: HomeRepo) : BaseViewModel(){
    /**
     * 請(qǐng)求每日一問(wèn)數(shù)據(jù)
     */
    fun dailyQuestionPagingFlow(): Flow<PagingData<DailyQuestionData>> =
        repo.getDailyQuestion().cachedIn(viewModelScope)

}
復(fù)制代碼

Repository層

class HomeRepo(private val service: HomeService, private val db: AppDatabase) : BaseRepository(){
    /**
     * 請(qǐng)求每日一問(wèn)
     */
    fun getDailyQuestion(): Flow<PagingData<DailyQuestionData>> {

        return Pager(config) {
            DailyQuestionPagingSource(service)
        }.flow
    }
}
復(fù)制代碼

PagingSource層:

/**
 * @date:2021/5/20
 * @author fuusy
 * @instruction: 每日一問(wèn)數(shù)據(jù)源盹憎,主要配合Paging3進(jìn)行數(shù)據(jù)請(qǐng)求與顯示
 */
class DailyQuestionPagingSource(private val service: HomeService) :

    PagingSource<Int, DailyQuestionData>() {
    override fun getRefreshKey(state: PagingState<Int, DailyQuestionData>): Int? = null

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DailyQuestionData> {
        return try {
            val pageNum = params.key ?: 1
            val data = service.getDailyQuestion(pageNum)
            val preKey = if (pageNum > 1) pageNum - 1 else null
            LoadResult.Page(data.data?.datas!!, prevKey = preKey, nextKey = pageNum + 1)

        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}
復(fù)制代碼
2.2.3、Room

Room是一個(gè)管理數(shù)據(jù)庫(kù)的組件铐刘,此項(xiàng)目主要將Paging3與Room相結(jié)合陪每。2.3小節(jié)主要介紹了Paging3從網(wǎng)絡(luò)上加載數(shù)據(jù)分頁(yè),而這不同的是镰吵,結(jié)合Room需要RemoteMediator的協(xié)同處理檩禾。

RemoteMediator主要作用是:可以使用此信號(hào)從網(wǎng)絡(luò)加載更多數(shù)據(jù)并將其存儲(chǔ)在本地?cái)?shù)據(jù)庫(kù)中,PagingSource 可以從本地?cái)?shù)據(jù)庫(kù)加載這些數(shù)據(jù)并將其提供給界面進(jìn)行顯示疤祭。 當(dāng)需要更多數(shù)據(jù)時(shí)盼产,Paging 庫(kù)從 RemoteMediator 實(shí)現(xiàn)調(diào)用load()方法。具體使用方法可參考此項(xiàng)目首頁(yè)文章列表部分勺馆。

RoomPaging3結(jié)合時(shí)戏售,UI層ViewModel層的操作與2.3小節(jié)一致,主要修改在于Repository層谓传。

Repository層:

class HomeRepo(private val service: HomeService, private val db: AppDatabase) : BaseRepository() {
   /**
     * 請(qǐng)求首頁(yè)文章蜈项,
     * Room+network進(jìn)行緩存
     */
    fun getHomeArticle(articleType: Int): Flow<PagingData<ArticleData>> {
        mArticleType = articleType
        return Pager(
            config = config,
            remoteMediator = ArticleRemoteMediator(service, db, 1),
            pagingSourceFactory = pagingSourceFactory
        ).flow
    }
}

復(fù)制代碼

DAO:

@Dao
interface ArticleDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertArticle(articleDataList: List<ArticleData>)

    @Query("SELECT * FROM tab_article WHERE articleType =:articleType")
    fun queryLocalArticle(articleType: Int): PagingSource<Int, ArticleData>

    @Query("DELETE FROM tab_article WHERE articleType=:articleType")
    suspend fun clearArticleByType(articleType: Int)

}
復(fù)制代碼

RoomDatabase:

@Database(
    entities = [ArticleData::class, RemoteKey::class],
    version = 1,
    exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {

    abstract fun articleDao(): ArticleDao
    abstract fun remoteKeyDao(): RemoteKeyDao

    companion object {
        private const val DB_NAME = "app.db"

        @Volatile
        private var instance: AppDatabase? = null

        fun get(context: Context): AppDatabase {
            return instance ?: Room.databaseBuilder(context, AppDatabase::class.java,
                DB_NAME
            )
                .build().also {
                    instance = it
                }
        }
    }
}
復(fù)制代碼

自定義RemoteMediator:

/**
 * @date:2021/5/20
 * @author fuusy
 * @instruction:RemoteMediator 的主要作用是:在 Pager 耗盡數(shù)據(jù)或現(xiàn)有數(shù)據(jù)失效時(shí),從網(wǎng)絡(luò)加載更多數(shù)據(jù)续挟。
 * 可以使用此信號(hào)從網(wǎng)絡(luò)加載更多數(shù)據(jù)并將其存儲(chǔ)在本地?cái)?shù)據(jù)庫(kù)中,PagingSource 可以從本地?cái)?shù)據(jù)庫(kù)加載這些數(shù)據(jù)并將其提供給界面進(jìn)行顯示侥衬。
 * 當(dāng)需要更多數(shù)據(jù)時(shí)诗祸,Paging 庫(kù)從 RemoteMediator 實(shí)現(xiàn)調(diào)用 load() 方法。這是一項(xiàng)掛起功能轴总,因此可以放心地執(zhí)行長(zhǎng)時(shí)間運(yùn)行的工作鳄乏。
 * 此功能通常從網(wǎng)絡(luò)源提取新數(shù)據(jù)并將其保存到本地存儲(chǔ)空間课舍。
 * 此過(guò)程會(huì)處理新數(shù)據(jù),但長(zhǎng)期存儲(chǔ)在數(shù)據(jù)庫(kù)中的數(shù)據(jù)需要進(jìn)行失效處理(例如,當(dāng)用戶(hù)手動(dòng)觸發(fā)刷新時(shí))眷细。
 * 這由傳遞到 load() 方法的 LoadType 屬性表示。LoadType 會(huì)通知 RemoteMediator 是需要刷新現(xiàn)有數(shù)據(jù)坝疼,還是提取需要附加或前置到現(xiàn)有列表的更多數(shù)據(jù)翠拣。
 */
@OptIn(ExperimentalPagingApi::class)
class ArticleRemoteMediator(
    private val api: HomeService,
    private val db: AppDatabase,
    private val articleType: Int
) : RemoteMediator<Int, ArticleData>() {

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, ArticleData>
    ): MediatorResult {

        /*
        1.LoadType.REFRESH:首次訪問(wèn) 或者調(diào)用 PagingDataAdapter.refresh() 觸發(fā)
        2.LoadType.PREPEND:在當(dāng)前列表頭部添加數(shù)據(jù)的時(shí)候時(shí)觸發(fā),實(shí)際在項(xiàng)目中基本很少會(huì)用到直接返回 MediatorResult.Success(endOfPaginationReached = true) 虑灰,參數(shù) endOfPaginationReached 表示沒(méi)有數(shù)據(jù)了不在加載
        3.LoadType.APPEND:加載更多時(shí)觸發(fā)吨瞎,這里獲取下一頁(yè)的 key, 如果 key 不存在,表示已經(jīng)沒(méi)有更多數(shù)據(jù)穆咐,直接返回 MediatorResult.Success(endOfPaginationReached = true) 不會(huì)在進(jìn)行網(wǎng)絡(luò)和數(shù)據(jù)庫(kù)的訪問(wèn)
         */
        try {
            Log.d(TAG, "load: $loadType")
            val pageKey: Int? = when (loadType) {
                LoadType.REFRESH -> null
                LoadType.PREPEND -> return MediatorResult.Success(true)
                LoadType.APPEND -> {
                    //使用remoteKey來(lái)獲取下一個(gè)或上一個(gè)頁(yè)面颤诀。
                    val remoteKey =
                        state.lastItemOrNull()?.id?.let {
                            db.remoteKeyDao().remoteKeysArticleId(it, articleType)
                        }

                    //remoteKey' null '字旭,這意味著在初始刷新后沒(méi)有加載任何項(xiàng)目,也沒(méi)有更多的項(xiàng)目要加載崖叫。
                    if (remoteKey?.nextKey == null) {
                        return MediatorResult.Success(true)
                    }
                    remoteKey.nextKey
                }
            }

            val page = pageKey ?: 0
            //從網(wǎng)絡(luò)上請(qǐng)求數(shù)據(jù)
            val result = api.getHomeArticle(page).data?.datas
            result?.forEach {
                it.articleType = articleType
            }
            val endOfPaginationReached = result?.isEmpty()

            db.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    //清空數(shù)據(jù)
                    db.remoteKeyDao().clearRemoteKeys(articleType)
                    db.articleDao().clearArticleByType(articleType)
                }
                val prevKey = if (page == 0) null else page - 1
                val nextKey = if (endOfPaginationReached!!) null else page + 1
                val keys = result.map {
                    RemoteKey(
                        articleId = it.id,
                        prevKey = prevKey,
                        nextKey = nextKey,
                        articleType = articleType
                    )
                }
                db.remoteKeyDao().insertAll(keys)
                db.articleDao().insertArticle(articleDataList = result)
            }
            return MediatorResult.Success(endOfPaginationReached!!)
        } catch (e: IOException) {
            return MediatorResult.Error(e)
        } catch (e: HttpException) {
            return MediatorResult.Error(e)
        }

    }
}
復(fù)制代碼

另外新創(chuàng)建了RemoteKeyRemoteKeyDao來(lái)管理列表的頁(yè)數(shù)遗淳,具體請(qǐng)參考此項(xiàng)目home模塊。

2.2.4心傀、LiveData

關(guān)于LiveData的使用和原理屈暗,可參考【Jetpack篇】LiveData取代EventBus?LiveData的通信原理和粘性事件刨析

還有很多好用的Jetpack組件剧包,將在后續(xù)更新恐锦。

三、感謝

API: 鴻洋大大提供的 WanAndroid API

第三方開(kāi)源庫(kù):

??Retrofit

??OkHttp

??Gson

??Coil

??Koin

??Arouter

??LoadSir

另外還有上面沒(méi)列舉的一些優(yōu)秀的第三方開(kāi)源庫(kù)疆液,感謝開(kāi)源一铅。

四、License??

License Copyright 2021 fuusy

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

www.apache.org/licenses/LI…

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

項(xiàng)目地址github.com/fuusy/wanan…

本文在開(kāi)源項(xiàng)目:https://github.com/Android-Alvin/Android-LearningNotes 中已收錄堕油,里面包含不同方向的自學(xué)編程路線潘飘、面試題集合/面經(jīng)、及系列技術(shù)文章等掉缺,資源持續(xù)更新中...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末卜录,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子眶明,更是在濱河造成了極大的恐慌艰毒,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搜囱,死亡現(xiàn)場(chǎng)離奇詭異丑瞧,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)蜀肘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)绊汹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人扮宠,你說(shuō)我怎么就攤上這事西乖。” “怎么了坛增?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵获雕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我轿偎,道長(zhǎng)典鸡,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任坏晦,我火速辦了婚禮萝玷,結(jié)果婚禮上嫁乘,老公的妹妹穿的比我還像新娘。我一直安慰自己球碉,他們只是感情好蜓斧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著睁冬,像睡著了一般挎春。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上豆拨,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天直奋,我揣著相機(jī)與錄音,去河邊找鬼施禾。 笑死脚线,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的弥搞。 我是一名探鬼主播邮绿,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼攀例!你這毒婦竟也來(lái)了船逮?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤粤铭,失蹤者是張志新(化名)和其女友劉穎挖胃,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體梆惯,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡冠骄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了加袋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡抱既,死狀恐怖职烧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情防泵,我是刑警寧澤蚀之,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站捷泞,受9級(jí)特大地震影響足删,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锁右,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一失受、第九天 我趴在偏房一處隱蔽的房頂上張望讶泰。 院中可真熱鬧,春花似錦拂到、人聲如沸痪署。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)狼犯。三九已至,卻和暖如春领铐,著一層夾襖步出監(jiān)牢的瞬間悯森,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工绪撵, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓢姻,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓莲兢,卻偏偏與公主長(zhǎng)得像汹来,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子改艇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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