理解Android Architecture Components系列(二)

第一期的文章比較匆忙囤屹,遺留了好多問題。最明顯的一個(gè)是ViewModel如何獲取詳細(xì)的個(gè)人信息逢渔。假設(shè)用戶信息是從網(wǎng)絡(luò)獲取肋坚,那么我們調(diào)用后臺(tái)接口即可獲取數(shù)據(jù)。如果后臺(tái)是REST API的,使用Retrofit作為網(wǎng)絡(luò)請(qǐng)求就再合適不過了智厌。

至于什么是REST API诲泌,簡(jiǎn)言之就是把后臺(tái)所有請(qǐng)求看成是一種對(duì)資源的操作。比如關(guān)注一個(gè)人就是@Post把被關(guān)注的人的信息放到我的關(guān)注列表里铣鹏,取關(guān)一個(gè)人就是@Delete從我的關(guān)注列表里刪除這個(gè)人的資源敷扫。而這些接口的api都是相同,參數(shù)也是一樣的诚卸,不同的訪問方式不同葵第,產(chǎn)生不同的效果。更好的的解釋可以點(diǎn)擊鏈接合溺。

獲取數(shù)據(jù)

那好卒密,讓我們用Retrofit的WebService獲取數(shù)據(jù)吧。

public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}

是不是立刻想在ViewModel里面獲取數(shù)據(jù)辫愉,然后根據(jù)LiveData展示數(shù)據(jù)呢栅受?這樣做是沒有錯(cuò)的,但是隨著項(xiàng)目的增長(zhǎng)恭朗,這會(huì)讓項(xiàng)目變得難以維護(hù)屏镊。比如現(xiàn)在的問題來了(這個(gè)例子不一定合理),如果在查詢網(wǎng)絡(luò)之前我想先查詢下本地?cái)?shù)據(jù)庫中有沒有這個(gè)用戶的信息痰腮,那我就要在ViewModel中這段代碼之前添加查詢數(shù)據(jù)庫的操作而芥,這樣ViewModel的代碼就會(huì)變得非常臃腫。更重要的是這違反了上面提到的Soc原則膀值,ViewModel承擔(dān)了太多的功能棍丐。此外ViewModel的作用域是和Activity/Fragment綁定的,如果Activity/Fragment正常退出沧踏,相應(yīng)的ViewModel就會(huì)釋放掉相關(guān)的數(shù)據(jù)歌逢,當(dāng)用戶再次進(jìn)入這個(gè)頁面就會(huì)再次進(jìn)行網(wǎng)絡(luò)請(qǐng)求,這顯然在某種程度上是浪費(fèi)的翘狱。所以View的生命周期一結(jié)束就失去了所有數(shù)據(jù)秘案,這在用戶體驗(yàn)上會(huì)造成困擾。那怎么辦呢潦匈?

把這部分功能通過一個(gè)新的Repository類代理執(zhí)行阱高。這個(gè)Repository類的作用就是獲取并提供各種來源的數(shù)據(jù)(數(shù)據(jù)庫中的數(shù)據(jù),網(wǎng)絡(luò)數(shù)據(jù)茬缩,緩存數(shù)據(jù)等)赤惊,并且在來源數(shù)據(jù)更新時(shí)通知數(shù)據(jù)的獲取方。

可以這樣來寫UserRepository

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below(這不是最好的實(shí)現(xiàn)凰锡,以后會(huì)修復(fù)的)
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity(為了簡(jiǎn)單起見未舟,簡(jiǎn)化了錯(cuò)誤處理的情況)
                data.setValue(response.body());
            }
        });
        return data;
    }
}

目前來看就是把網(wǎng)絡(luò)請(qǐng)求的代碼從ViewModel中移到了UserRepository中而已圈暗,似乎顯得非常多余。但是考慮到數(shù)據(jù)來源的多重性处面,這樣做可以簡(jiǎn)化ViewModel中的代碼厂置。更重要的是他抽象出了數(shù)據(jù)來源這一層,使更換數(shù)據(jù)來源變得非常簡(jiǎn)單(參考上面那個(gè)不一定合理的請(qǐng)求用戶信息的例子)魂角,對(duì)于ViewModel來說只是獲取到了數(shù)據(jù),并不知道數(shù)據(jù)的是從網(wǎng)絡(luò)獲取的還是從本地獲取的智绸。

上面的代碼中為了簡(jiǎn)單起見忽略了錯(cuò)誤處理的情況野揪,這在實(shí)際應(yīng)用中是不合理的。詳細(xì)的解決方案在后面的文章中有提供瞧栗,先讓我們跳過這一部分斯稳。

管理組件間的依賴關(guān)系

從上面的代碼不難看出UserRepository里需要WebService這個(gè)類來完成網(wǎng)絡(luò)請(qǐng)求。所以就要在UserRepository里創(chuàng)建WebService這個(gè)類迹恐,這就會(huì)帶來很多麻煩挣惰,假如WebService的構(gòu)造函數(shù)變了,那么用到WebService的地方都需要更改殴边,這會(huì)讓代碼變得難以維護(hù)憎茂。

有兩個(gè)解決這個(gè)問題的辦法:

連接ViewModel和Repository

接下來使用Dagger2和UserRepository來提供數(shù)據(jù)來源。代碼如下:

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}

緩存數(shù)據(jù)

UserRepository很好的解決了數(shù)據(jù)層抽象的問題蛋铆,但是每次請(qǐng)求數(shù)據(jù)都只有網(wǎng)絡(luò)請(qǐng)求一種方法馋评,會(huì)顯得功能上有點(diǎn)單一。而且ViewModel每請(qǐng)求一次用戶信息都會(huì)重新請(qǐng)求網(wǎng)絡(luò)一次刺啦,即使是已經(jīng)請(qǐng)求過的用戶信息留特。這會(huì)造成兩個(gè)問題:

  • 浪費(fèi)流量
  • 讓用戶有不必要的等待

為了解決這個(gè)問題,可以在UserRepository里添加內(nèi)存級(jí)的緩存UserCache來解決這個(gè)問題洪燥。
代碼如下:

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity(簡(jiǎn)單的內(nèi)存緩存)
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}

持久化數(shù)據(jù)

上面的做法已經(jīng)可以解決用戶重復(fù)打開相同的頁面數(shù)據(jù)獲取的問題磕秤,但是如果用戶退出了App,那么這些數(shù)據(jù)也就被銷毀了捧韵。隔了一段時(shí)間用戶重新打開相同的頁面仍然需要從網(wǎng)絡(luò)獲取相同的數(shù)據(jù)市咆。解決這個(gè)問題的最好辦法就是提供本地持久化的數(shù)據(jù)Model。說了這么多再来,終于到了Android Architecture Components的另一重要內(nèi)容:Room蒙兰。

Room is an object mapping library that provides local data persistence with minimal boilerplate code. At compile time, it validates each query against the schema, so that broken SQL queries result in compile time errors instead of runtime failures. Room abstracts away some of the underlying implementation details of working with raw SQL tables and queries. It also allows observing changes to the database data (including collections and join queries), exposing such changes via LiveData objects. In addition, it explicitly defines thread constraints that address common issues such as accessing storage on the main thread.

簡(jiǎn)略一下就是:Room是一個(gè)對(duì)象映射庫磷瘤,(姑且按GreenDAO的功能來理解吧)可以在在編譯的時(shí)候就能檢測(cè)出SQL語句的異常。還能夠在數(shù)據(jù)庫內(nèi)容發(fā)生改變時(shí)通過LiveData的形式發(fā)出通知(想想這有什么用搜变?一個(gè)例子就是通知列表采缚,比如App調(diào)某個(gè)接口可以返回消息通知,App需要把返回的數(shù)據(jù)存儲(chǔ)在本地挠他,方便以后查看扳抽。那么問題來了,在顯示服務(wù)器返回的數(shù)據(jù)之前殖侵,還需要把之前的數(shù)據(jù)展示在界面上贸呢。之前用過的做法是先查數(shù)據(jù)庫展示->請(qǐng)求網(wǎng)絡(luò)->把網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)插到最前面->把網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)存儲(chǔ)在數(shù)據(jù)庫中,光是看這個(gè)做法就知道這里的代碼多到想吐拢军,用Room就可以很好的解決這個(gè)問題楞陷,當(dāng)然其他ORM框架有類似的解決方案。Room可以使過程大大簡(jiǎn)化茉唉,查詢顯示->請(qǐng)求網(wǎng)絡(luò)->本地存儲(chǔ)->收到改變通知固蛾,顯示新的數(shù)據(jù)(自動(dòng)發(fā)生,不需要人為操作)度陆。

那么來看看如何使用Room吧艾凯。以上面用戶信息為例:

  1. 定義User類,并且使用@Entity注解:
@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}
  1. 創(chuàng)建RoomDatabase:
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}
  1. 創(chuàng)建DAO
@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}
  1. 在RoomDatabase中訪問DAO
@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

注意UserDaoload()方法返回的是LiveData<User>坚芜。這就是為什么Room能夠在數(shù)據(jù)庫內(nèi)容發(fā)生改變的時(shí)候通知對(duì)數(shù)據(jù)庫內(nèi)容感興趣的類览芳。

現(xiàn)在可以把Room和UserRepository結(jié)合起來:

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.(從數(shù)據(jù)庫中直接返回LiveData)
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread(在后臺(tái)線程運(yùn)行)
            // check if user was fetched recently(檢查數(shù)據(jù)庫中是否有數(shù)據(jù))
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database(不用其他代碼就能實(shí)現(xiàn)數(shù)據(jù)自動(dòng)更新)
                userDao.save(response.body());
            }
        });
    }
}

從上面的代碼可以看出,即使是改變了UserRepository中的數(shù)據(jù)來源鸿竖,也無需對(duì)UserProfileViewModel或者UserProfileFragment作任何更改沧竟。這就是抽象出了數(shù)據(jù)層的靈活性。

現(xiàn)在基本上就解決之前提到的一些問題缚忧。如果已經(jīng)查看過某個(gè)用戶的數(shù)據(jù)悟泵,那么再次打開這個(gè)用戶的頁面時(shí),數(shù)據(jù)可以瞬間被展示闪水。如果數(shù)據(jù)過期的話糕非,還可以立刻去更新數(shù)據(jù)。當(dāng)然球榆,如果你規(guī)定了數(shù)據(jù)過期就不允許展示朽肥,也可以展示從網(wǎng)絡(luò)獲取的數(shù)據(jù)。

在某些情況下持钉,需要顯示是否本次網(wǎng)絡(luò)請(qǐng)求的進(jìn)度(是否完成)衡招,比如下拉刷新,需要根據(jù)網(wǎng)絡(luò)請(qǐng)求的進(jìn)度展示相應(yīng)的UI每强。最好把UI顯示和實(shí)際的數(shù)據(jù)來源分開始腾,因?yàn)閿?shù)據(jù)可能會(huì)因?yàn)楦鞣N原因被更新州刽。(實(shí)際上我也沒有很好的理解這段話,下面貼下這段話的原文:In some use cases, such as pull-to-refresh, it is important for the UI to show the user if there is currently a network operation in progress. It is good practice to separate the UI action from the actual data since it might be updated for various reasons (for example, if we fetch a list of friends, the same user might be fetched again triggering a LiveData<User> update). From the UI's perspective, the fact that there is a request in flight is just another data point, similar to any other piece data (like the User object).

數(shù)據(jù)來源的唯一性

在上面提供的代碼中浪箭,數(shù)據(jù)庫是App數(shù)據(jù)的唯一來源穗椅。Google推薦采用這種方式。

最終的架構(gòu)形態(tài)

下面這張圖展示了使用Android Architecture Components來構(gòu)建的App整體的架構(gòu):


image.png

一些App架構(gòu)設(shè)計(jì)的推薦準(zhǔn)則

  • 不要把在Manifest中定義的組件作為提供數(shù)據(jù)的來源(包括Activity奶栖、Services匹表、Broadcast Receivers等),因?yàn)樗麄兊纳芷谙鄬?duì)于App的生命周期是相對(duì)短暫的驼抹。
  • 嚴(yán)格的限制每個(gè)模塊的功能桑孩。比如上面提到的不要再ViewModel中增加如何獲取數(shù)據(jù)的代碼。
  • 每個(gè)模塊盡可能少的對(duì)外暴露方法框冀。
  • 模塊中對(duì)外暴露的方法要考慮單元測(cè)試的方便。
  • 不要重復(fù)造輪子敏簿,把精力放在能夠讓App脫穎而出的業(yè)務(wù)上明也。
  • 盡可能多的持久化數(shù)據(jù),因?yàn)檫@樣即使是在網(wǎng)絡(luò)條件不好的情況下惯裕,用戶仍然能夠使用App
  • 保證數(shù)據(jù)來源的唯一性(即:提供一個(gè)類似UserRepository的類)

附加建議:在網(wǎng)絡(luò)請(qǐng)求過程中提供網(wǎng)絡(luò)請(qǐng)求的狀態(tài)

在上面的文章中温数,刻意忽略了網(wǎng)絡(luò)錯(cuò)誤和處于loading狀態(tài)的處理。下面的代碼將會(huì)提供一個(gè)包含網(wǎng)絡(luò)狀態(tài)的示例代碼蜻势。

//a generic class that describes a data with a status
public class Resource<T> {
    @NonNull public final Status status;
    @Nullable public final T data;
    @Nullable public final String message;
    private Resource(@NonNull Status status, @Nullable T data, @Nullable String message) {
        this.status = status;
        this.data = data;
        this.message = message;
    }

    public static <T> Resource<T> success(@NonNull T data) {
        return new Resource<>(SUCCESS, data, null);
    }

    public static <T> Resource<T> error(String msg, @Nullable T data) {
        return new Resource<>(ERROR, data, msg);
    }

    public static <T> Resource<T> loading(@Nullable T data) {
        return new Resource<>(LOADING, data, null);
    }
}

下面這幅圖展示請(qǐng)求數(shù)據(jù)的通用流程

image.png

總體的思路就是:請(qǐng)求數(shù)據(jù)之前先查看本地有沒有數(shù)據(jù)撑刺,再根據(jù)本地?cái)?shù)據(jù)的狀態(tài)決定是否進(jìn)行網(wǎng)絡(luò)操作。網(wǎng)絡(luò)操作回來的數(shù)據(jù)緩存在本地握玛。

根據(jù)流程圖的思路可以抽象出NetworkBoundResource這個(gè)類:

// ResultType: Type for the Resource data
// RequestType: Type for the API response
public abstract class NetworkBoundResource<ResultType, RequestType> {
    // Called to save the result of the API response into the database(存儲(chǔ)網(wǎng)絡(luò)請(qǐng)求返回的數(shù)據(jù))
    @WorkerThread
    protected abstract void saveCallResult(@NonNull RequestType item);

    // Called with the data in the database to decide whether it should be
    // fetched from the network.(根據(jù)數(shù)據(jù)庫檢索的結(jié)果決定是否需要從網(wǎng)絡(luò)獲取數(shù)據(jù))
    @MainThread
    protected abstract boolean shouldFetch(@Nullable ResultType data);

    // Called to get the cached data from the database(從數(shù)據(jù)中獲取數(shù)據(jù))
    @NonNull @MainThread
    protected abstract LiveData<ResultType> loadFromDb();

    // Called to create the API call.(創(chuàng)建API)
    @NonNull @MainThread
    protected abstract LiveData<ApiResponse<RequestType>> createCall();

    // Called when the fetch fails. The child class may want to reset components
    // like rate limiter.(獲取數(shù)據(jù)失敗回調(diào))
    @MainThread
    protected void onFetchFailed() {
    }

    // returns a LiveData that represents the resource, implemented
    // in the base class.(獲取LiveData形式的數(shù)據(jù))
    public final LiveData<Resource<ResultType>> getAsLiveData();
}

和數(shù)據(jù)獲取相關(guān)的類只要繼承這個(gè)類够傍,就能按照上面流程圖高效獲取數(shù)據(jù)。值得注意的是NetworkBoundResource<ResultType, RequestType>有兩種數(shù)據(jù)類型挠铲,這是因?yàn)榉?wù)端返回的數(shù)據(jù)類型和本地存儲(chǔ)的數(shù)據(jù)類型可能是不一致的冕屯。(再來一個(gè)不一定合理的例子,以用戶信息為例拂苹,服務(wù)器為了擴(kuò)展考慮返回的User會(huì)包含一個(gè)年齡字段安聘,但是本地對(duì)這個(gè)字段并沒有任何用處,就沒有必要存儲(chǔ)瓢棒,這樣一來就會(huì)出現(xiàn)不一致的情況

代碼中的ApiResponse是對(duì)Retrofit2.Call的包裝浴韭,主要的作用是把call類型轉(zhuǎn)換成LiveData。

下面是NetworkBoundResource的詳細(xì)實(shí)現(xiàn):

public abstract class NetworkBoundResource<ResultType, RequestType> {
    private final MediatorLiveData<Resource<ResultType>> result = new MediatorLiveData<>();

    @MainThread
    NetworkBoundResource() {
        result.setValue(Resource.loading(null));
        LiveData<ResultType> dbSource = loadFromDb();
        result.addSource(dbSource, data -> {
            result.removeSource(dbSource);
            if (shouldFetch(data)) {
                fetchFromNetwork(dbSource);
            } else {
                result.addSource(dbSource,
                        newData -> result.setValue(Resource.success(newData)));
            }
        });
    }

    private void fetchFromNetwork(final LiveData<ResultType> dbSource) {
        LiveData<ApiResponse<RequestType>> apiResponse = createCall();
        // we re-attach dbSource as a new source,
        // it will dispatch its latest value quickly
        result.addSource(dbSource,
                newData -> result.setValue(Resource.loading(newData)));
        result.addSource(apiResponse, response -> {
            result.removeSource(apiResponse);
            result.removeSource(dbSource);
            //noinspection ConstantConditions
            if (response.isSuccessful()) {
                saveResultAndReInit(response);
            } else {
                onFetchFailed();
                result.addSource(dbSource,
                        newData -> result.setValue(
                                Resource.error(response.errorMessage, newData)));
            }
        });
    }

    @MainThread
    private void saveResultAndReInit(ApiResponse<RequestType> response) {
        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... voids) {
                saveCallResult(response.body);
                return null;
            }

            @Override
            protected void onPostExecute(Void aVoid) {
                // we specially request a new live data,
                // otherwise we will get immediately last cached value,
                // which may not be updated with latest results received from network.
                result.addSource(loadFromDb(),
                        newData -> result.setValue(Resource.success(newData)));
            }
        }.execute();
    }

    public final LiveData<Resource<ResultType>> getAsLiveData() {
        return result;
    }
}

現(xiàn)在我們就可以在UserRepository里通過NetworkBoundResource來獲取并且緩存User信息:

class UserRepository {
    Webservice webservice;
    UserDao userDao;

    public LiveData<Resource<User>> loadUser(final String userId) {
        return new NetworkBoundResource<User,User>() {
            @Override
            protected void saveCallResult(@NonNull User item) {
                userDao.insert(item);
            }

            @Override
            protected boolean shouldFetch(@Nullable User data) {
                return rateLimiter.canFetch(userId) && (data == null || !isFresh(data));
            }

            @NonNull @Override
            protected LiveData<User> loadFromDb() {
                return userDao.load(userId);
            }

            @NonNull @Override
            protected LiveData<ApiResponse<User>> createCall() {
                return webservice.getUser(userId);
            }
        }.getAsLiveData();
    }
}

通過上面的文章脯宿,大致可以建立起對(duì)整個(gè)Android Architecture Components的認(rèn)識(shí)念颈,同時(shí)也能窺見Google在設(shè)計(jì)這套架構(gòu)后面的思想。整體介紹的文章已經(jīng)結(jié)束了嗅绰,在接下來的文章中舍肠,會(huì)詳細(xì)的介紹涉及的各個(gè)類搀继。

相關(guān)文章:
理解Android Architecture Components系列(一)
理解Android Architecture Components系列(二)
理解Android Architecture Components系列之Lifecycle(三)
理解Android Architecture Components系列之LiveData(四)
理解Android Architecture Components系列之ViewModel(五)
理解Android Architecture Components系列之Room(六)
理解Android Architecture Components系列之Paging Library(七)
理解Android Architecture Components系列之WorkManager(八)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市翠语,隨后出現(xiàn)的幾起案子叽躯,更是在濱河造成了極大的恐慌,老刑警劉巖肌括,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件点骑,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡谍夭,警方通過查閱死者的電腦和手機(jī)黑滴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來紧索,“玉大人袁辈,你說我怎么就攤上這事≈槠” “怎么了晚缩?”我有些...
    開封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)媳危。 經(jīng)常有香客問我荞彼,道長(zhǎng),這世上最難降的妖魔是什么待笑? 我笑而不...
    開封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任鸣皂,我火速辦了婚禮,結(jié)果婚禮上暮蹂,老公的妹妹穿的比我還像新娘寞缝。我一直安慰自己,他們只是感情好椎侠,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開白布第租。 她就那樣靜靜地躺著,像睡著了一般我纪。 火紅的嫁衣襯著肌膚如雪慎宾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天浅悉,我揣著相機(jī)與錄音趟据,去河邊找鬼。 笑死术健,一個(gè)胖子當(dāng)著我的面吹牛汹碱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播荞估,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼咳促,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼稚新!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起跪腹,我...
    開封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤褂删,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后冲茸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體屯阀,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年轴术,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了难衰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逗栽,死狀恐怖盖袭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情彼宠,我是刑警寧澤苍凛,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站兵志,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏宣肚。R本人自食惡果不足惜想罕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望霉涨。 院中可真熱鬧按价,春花似錦、人聲如沸笙瑟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽往枷。三九已至框产,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間错洁,已是汗流浹背秉宿。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留屯碴,地道東北人描睦。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像导而,于是被迫代替她去往敵國和親忱叭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子隔崎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

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