Retrofit使用詳解(一)

本文翻譯自https://futurestud.io/tutorials/retrofit-getting-started-and-android-client
OKHttp(一)之Calls

OKHttp(二)之Connections

OkHttp(三)之使用方法

OkHttp(四)之攔截器

OkHttp(五)之HTTPS

Retrofit使用詳解(一)

Retrofit使用詳解(二)

Retrofit使用詳解(三)

Retrofit使用詳解(四)

Retrofit使用詳解(五)

Retrofit使用詳解(六)

一、創建安卓客戶端

什么是Retrofit

官方是這樣的描述的

A type-safe REST client for Android and Java.

使用注解來描述HTTP請求,默認會集成URL參數替換。還提供了自定義頭信息,多請求體,文件上傳下載,模擬響應等功能。

準備你的安卓工程

可以通過Android Studio的Gradle來添加依賴。

定義依賴:Gradle

此處只介紹retrofit2的用法

Retrofit默認使用OkHttp作為網絡層,并在其之上建立。

如果你在retrofit2中想要將JSON映射到GSON上,添加以下依賴:

dependencies {  
    // Retrofit & OkHttp
    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.1.0'
}

Android 網絡權限

Retrofit針對Internet上某個服務器上運行的API執行HTTP請求。 從Android應用程序執行這些請求需要Internet權限才能打開網絡套接字。 您需要在AndroidManifest.xml文件中定義權限。 如果您尚未設置Internet權限,請在您的AndroidManifest.xml定義中添加以下行:

<uses-permission android:name="android.permission.INTERNET" />  

怎樣描述API終端

在你發起第一個請求之前,你需要描述你需要與之交互的API終端。首先你需要創建一個接口并且定義一個方法。

GitHubClient

下面的代碼定義了一個接口GitHubClient和一個方法reposForUser來請求給定用戶的倉庫列表。
@GET注解聲明此請求使用HTTP GET方法。在定義的方法中,當調用reposForUser方法時,{user}路徑將替換為給定的變量值。

public interface GitHubClient {  
    @GET("/users/{user}/repos")
    Call<List<GitHubRepo>> reposForUser(
        @Path("user") String user
    );
}

定義了一個類,GitHubRepo,這個類包括返回數據的所有屬性。

public class GitHubRepo {  
    private int id;
    private String name;

    public GitHubRepo() {
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

關于前面提到的JSON映射:GitHubClient接口定義了一個名為reposForUser的方法,返回類型為List <GitHubRepo>。 Retrofit確保服務器響應得到正確映射。

Retrofit REST Client

在描述完APi接口和對象模型后,下面準備創建請求。Retrofit的所有請求的基礎是Retrofit(2.0+)這個類。你可以使用構造器為所有請求設置一些常規選項,包括BaseURl和converter。

創建adapter后,可以創建一個客戶端來執行請求。

String API_BASE_URL = "https://api.github.com/";

OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

Retrofit.Builder builder =  
    new Retrofit.Builder()
            .baseUrl(API_BASE_URL)
            .addConverterFactory(
                GsonConverterFactory.create()
            );

Retrofit retrofit =  
    builder
        .client(
            httpClient.build()
        )
        .build();

GitHubClient client =  retrofit.create(GitHubClient.class);  

在上邊的代碼中我們定義了BaseURl是"https://api.github.com/",并且使用了最少的配置。Retrofit可以有更多的配置,但是在本例中不使用。

JSON Mapping

在大多數情況下,對服務器的請求和來自服務器的響應不是Java對象。 它們映射到一些其他格式中,如JSON。 GitHub的API使用JSON,在Retrofit2中,你需要顯式的將一個轉換器(convert)田家達Retrofit對象。

Retrofit in Use

在Retrofit 2中,您使用客戶端獲取call對象。一旦你對創建的call對象調用了.enqueue(異步請求),請求將由Retrofit進行。

// Create a very simple REST adapter which points the GitHub API endpoint.
GitHubClient client =  retrofit.create(GitHubClient.class);

// Fetch a list of the Github repositories.
Call<List<GitHubRepo>> call =  
    client.reposForUser("fs-opensource");

// Execute the call asynchronously. Get a positive or negative callback.
call.enqueue(new Callback<List<GitHubRepo>>() {  
    @Override
    public void onResponse(Call<List<GitHubRepo>> call, Response<List<GitHubRepo>> response) {
        // The network call was a success and we got a response
        // TODO: use the repository list and display it
    }

    @Override
    public void onFailure(Call<List<GitHubRepo>> call, Throwable t) {
        // the network call was a failure
        // TODO: handle error
    }
});

Retrofit將返回一個方便的列表<GitHubRepo>,你可以進一步使用它來顯示您的應用程序中的數據。

二、API終端

怎樣描述一個API終端

我們在一個接口文件中描述我們的API終端。

public interface GitHubClient {  
    @GET("/users/{user}/repos")
    Call<List<GitHubRepo>> reposForUser(
        @Path("user") String user
    );
}

現在讓我們來看看這些選項中的細節。

HTTP Method

在Java接口中使用注解來描述每一個API終端,最終處理請求。第一件事就是定義HTTP請求方法,如GET,POST,PUT,DELETE等。Retrofit為每個請求方法都提供了注解,你只需要為每個HTTP方法添加下面的注解即可:
@GET,@PSOT,@PUT,@DELETE,@PATCH,@HEAD。

下面是幾個簡單的例子:

public interface FutureStudioClient {  
    @GET("/user/info")
    Call<UserInfo> getUserInfo();

    @PUT("/user/info")
    Call<UserInfo> updateUserInfo(
        @Body UserInfo userInfo
    );

    @DELETE("/user")
    Call<Void> deleteUser();
}

HTTP Resource Location

此外,你需要為你的注解添加相對與BaseURL的String參數來完成路徑,例如 @GET(“/ user / info”)。 在大多數情況下,您只會傳遞相對網址,而不傳遞完整網址(例如http://futurestud.io/api/user/info)。 這具有的優點是,Retrofit只需要一次請求基本URL(http://futurestud.io)。 如果你要更改API基本網址,則只需在一個位置更改它。 此外,它使一些更高端的事情,如動態基本URL,更容易。 不過,你可以指定完整的URL。

public interface FutureStudioClient {  
    @GET("/user/info")
    Call<UserInfo> getUserInfo();

    @PUT("/user/info")
    Call<UserInfo> updateUserInfo(
        @Body UserInfo userInfo
    );

    @DELETE("/user")
    Call<Void> deleteUser();

    // example for passing a full URL
    @GET("https://futurestud.io/tutorials/rss/")
    Call<FutureStudioRssFeed> getRssFeed();
}

Function Name & Return Type

Java方法聲明:Call<UserInfo> getUserInfo(); 這包含三個部分:

  • 方法名---getUserInfo

你可以自由定義方法名稱。 Retrofit不在乎,它不會對功能產生任何影響。不過,你應該選擇一個名稱,這將有助于你和其他開發人員了解什么是API請求。

  • 方法返回的類型---UserInfo

你必須定義你期望從服務器的什么樣的數據。例如,當您請求某些用戶信息時,您可以將其指定為Call <UserInfo>。 UserInfo類包含將保存用戶數據的屬性。 Retrofit會自動映射它,您不必進行任何手動解析。如果你想要原始響應,你可以使用ResponseBody而不是像UserInfo這樣的特定類。如果你根本不在乎服務器響應什么,你可以使用Void。在所有這些情況下,你必須將它包裝到一個類型的Retrofit Call <>類中。

  • 方法傳遞的參數---此處為空

您可以將參數傳遞給方法。有各種各樣的可能選項,此處列舉一些:

@Body: 發送Java對象作為請求體
@Url: 使用動態地址
@Field: 以表單形式發送數據
public interface FutureStudioClient {  
    @GET("/user/info")
    Call<UserInfo> getUserInfo();

    @PUT("/user/info")
    Call<Void> updateUserInfo(
        @Body UserInfo userInfo
    );

    @GET
    Call<ResponseBody> getUserProfilePhoto(
        @Url String profilePhotoUrl
    );
}

Path Parameters

REST API是基于動態URL構建的。 您可以通過替換部分URL來訪問資源,例如獲取我們網頁上的第三個教程可能是http://futurestud.io/api/tutorials/3。 最后的3指定您要訪問的頁面。 Retrofit提供了一種簡單的方法來替換路徑參數。 例如:

public interface GitHubClient {  
    @GET("/users/{user}/repos")
    Call<List<GitHubRepo>> reposForUser(
        @Path("user") String user
    );
}

這里,{user}值是動態的,并且將在請求發生時設置。 如果在URL中包含路徑參數,則需要添加@Path()函數參數,其中@Path值與URL中的占位符匹配(在本例中為@Path(“user”))。 如有必要,您可以使用多個占位符。 只需確保您始終具有匹配參數的確切數量。 您甚至可以使用可選的路徑參數。

Query Parameters

動態URL的另一個大部分是查詢參數 與路徑參數不同,您不需要將它們添加到注釋URL。 你可以簡單地添加一個方法參數@Query()和查詢參數名稱,描述類型,你很好去。 Retrofit會自動將其附加到請求。 如果傳遞一個空值作為查詢參數,Retrofit將忽略它。 您還可以添加多個查詢參數。

public interface FutureStudioClient {  
    @GET("/tutorials")
    Call<List<Tutorial>> getTutorials(
        @Query("page") Integer page
    );

    @GET("/tutorials")
    Call<List<Tutorial>> getTutorials(
            @Query("page") Integer page,
            @Query("order") String order,
            @Query("author") String author,
            @Query("published_at") Date date
    );
}    

在上面的例子中,你可以使用第二個方法來替換第一個方法,只需要把其他的值設置為null即可。

創建一個可復用的客戶端

The ServiceGenerator

Retrofit對象及其構建器是所有請求的核心。 在這里,你可以配置和準備請求,響應,認證,日志記錄和錯誤處理。
讓我們從簡單的代碼開始。 在其當前狀態下,它僅定義一種方法為給定類/接口創建基本REST客戶端,該接口從接口返回服務類。

public class ServiceGenerator {

    private static final String BASE_URL = "https://api.github.com/";

    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create());

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    public static <S> S createService(Class<S> serviceClass) {
        return retrofit.create(serviceClass);
    }
}

ServiceGenerator類使用Retrofit的Retrofit構造器創建具有BaseURL(BASE_URL)的新REST客戶端。 例如,GitHub的API的BaseURL位于https://api.github.com/

createService方法將serviceClass(它是API請求的注釋接口)作為參數,并從中創建一個可用的客戶端。 在生成的客戶端上,您將能夠執行網絡請求。

Why Is Everything Declared Static Within the ServiceGenerator?

我們想在整個應用程序中使用相同的對象(OkHttpClient,Retrofit,...),只打開一個套接字連接,處理所有的請求和響應,包括緩存和更多。 通常的做法是只使用一個OkHttpClient實例來重用開放套接字連接。 這意味著,我們需要通過依賴注入或使用靜態字段將OkHttpClient注入到此類中。 正如你所看到的,我們選擇使用靜態字段。 并且因為我們在這個類中使用OkHttpClient,我們需要使所有字段和方法靜態。

除了加快速度,我們可以在移動設備上節省一些有價值的內存,當我們不必一遍又一遍地重新創建相同的對象。

Using the ServiceGenerator

GitHubClient client = ServiceGenerator.createService(GitHubClient.class);  

Preparing Logging

使用Retrofit 2進行日志記錄是由稱為HttpLoggingInterceptor的攔截器完成的。 您需要向OkHttpClient添加此攔截器的實例。 例如,您可以通過以下方式解決它:

public class ServiceGenerator {

    private static final String BASE_URL = "https://api.github.com/";

    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create());

    private static HttpLoggingInterceptor logging =
            new HttpLoggingInterceptor()
                    .setLevel(HttpLoggingInterceptor.Level.BODY);

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    public static <S> S createService(
        Class<S> serviceClass) {
        if (!httpClient.interceptors().contains(logging)) {
            httpClient.addInterceptor(logging);
            builder.client(httpClient.build());
            retrofit = builder.build();
        }

        return retrofit.create(serviceClass);
    }
}

有一些事情你必須知道。 首先,確保你沒有不小心多次添加攔截器!通過httpClient.interceptors().contains(logging)來檢查日志攔截器已經存在。 其次,確保不在每次createService調用上創建新的對象。 否則,將會使ServiceGenerator失去意義。

Prepare Authentication

需要在創建客戶端時傳遞附加參數到createService。

讓我們看一個Hawk身份驗證的示例:

public class ServiceGenerator {

    private static final String BASE_URL = "https://api.github.com/";

    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .baseUrl(BASE_URL)
                    .addConverterFactory(GsonConverterFactory.create());

    private static HttpLoggingInterceptor logging =
            new HttpLoggingInterceptor()
                    .setLevel(HttpLoggingInterceptor.Level.BODY);

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    public static <S> S createService(
            Class<S> serviceClass, final HawkCredentials credentials) {
        if (credentials != null) {
            HawkAuthenticationInterceptor interceptor =
                    new HawkAuthenticationInterceptor(credentials);

            if (!httpClient.interceptors().contains(interceptor)) {
                httpClient.addInterceptor(interceptor);

                builder.client(httpClient.build());
                retrofit = builder.build();
            }
        }

        return retrofit.create(serviceClass);
    }
}

createService現在具有HawkCredentials的第二個參數。 如果傳遞非空值,它將創建必要的Hawk身份驗證攔截器并將其添加到Retrofit客戶端。 我們還需要重建Retrofit以將更改應用到下一個請求。

URL的處理與解析

Url Handling Introduction

在Retrofit 2中,所有的URL都是使用由HttpUrl類來解決的,它是由OkHttp3提供給你的。 盡管如此,它引入了您需要處理您的應用程序中的所有網址的方式的更改:BaseURL和endpointURl以及為特定請求定義的任何動態網址。

請記住:Retrofit 2中的網址會像網頁上的鏈接一樣處理:<a href="..."> ... </a>。

baseUrl Resolution

使用Retrofit,您需要一個總是具有相同BaseURL的特定API。這個BaseURl共享相同的方案和主機,您可以在一個地方(使用Retrofit.Builder())定義它,并在必要時更改它,而不必觸及應用程序中的每個終端。

Retrofit.Builder builder = new Retrofit.Builder()  
            .baseUrl("https://your.base.url/api/");

BaseURL用于每個請求,任何終端(如@GET等)都將針對此地址解析。BaseURL必修以斜杠結尾:/。具有相對路徑地址的每個終端定義將正確解析,因為它將自身附加到已經定義或包括路徑參數的BaseURL。

讓我們來看一個例子:

# Good Practice
base url: https://futurestud.io/api/  
endpoint: my/endpoint  
Result:   https://futurestud.io/api/my/endpoint

# Bad Practice
base url: https://futurestud.io/api  
endpoint: /my/endpoint  
Result:   https://futurestud.io/my/endpoint  

上面的示例說明了如果你不以斜杠結尾你的BaseURl的api路徑參數將被忽略,并從生成的請求網址中刪除。

實際上,Retrofit幫助你,如果你傳遞一個基本url沒有尾部斜線。 它會拋出異常,告訴你,你的基本url需要以斜杠結尾。

Absolute Urls

你可以將絕對url傳遞給你的端點url。 盡管如此,這種技術可能需要在您的應用程序中調用適當的端點。 隨著時間的推移,您的后端將發布一個新的API版本。 根據版本控制的類型,讓我們假設您的后端開發人員選擇在網址中的API版本。 您需要將基本網址從v2壓縮到v3。 此時,您必須處理API v3引入的所有突變。 要依賴于所選的v2端點,可以使用絕對URL來直接指定API版本。

# Example 1
base url: https://futurestud.io/api/v3/  
endpoint: my/endpoint  
Result:   https://futurestud.io/api/v3/my/endpoint

# Example 2
base url: https://futurestud.io/api/v3/  
endpoint: /api/v2/another/endpoint  
Result:   https://futurestud.io/api/v2/another/endpoint  

在更改BaseURL的情況下,你將自動更需吧所有端點以使用新的URL和請求。 可以看到,示例1的工作原理與預期一樣,只是將端點url附加到針對v3的API調用的基本URL。

示例2說明了將基本URL升級到v3并仍然使用所選端點的API v2的情況。 這可能是由于您的客戶端的巨大升級造成的,并且您仍然希望從其他端點的API v3的所有其他好處中獲益。

Dynamic Urls or Passing a Full Url

使用Retrofit 2,您可以將給定的URL傳遞到端點,然后用于請求。也就是說,如果您已使用https定義了BaseURL,并且想要確保應用程序中的所有其他請求也都使用https,那么你只需使用雙斜線//開始請求網址即可。 這是Web中的常見做法,以避免瀏覽器在同一頁面上使用安全和不安全的資源時發出警告。

# Example 3 — completely different url
base url: http://futurestud.io/api/  
endpoint: https://api.futurestud.io/  
Result:   https://api.futurestud.io/

# Example 4 — Keep the base url’s scheme
base url: https://futurestud.io/api/  
endpoint: //api.futurestud.io/  
Result:   https://api.futurestud.io/

# Example 5 — Keep the base url’s scheme
base url: http://futurestud.io/api/  
endpoint: //api.github.com  
Result:   http://api.github.com  

示例3顯示了在使用完全不同的URL時替換BaseURL。 此示例在請求具有不同位置的文件或圖像時很有用,例如某些文件在您自己的服務器上,而其他文件或圖像存儲在Amazon的S3上。 您只將該位置作為網址接收并使用Retrofit,您可以傳遞請求端點的完整網址。

如前所述,您可以保留BaseURL的方案。 在示例4中,我們不使用API的路徑段,而是使用子域。 我們仍然想保留以前定義的方案,因此只傳遞帶有前導//的完整網址。

示例5使用與定義的BaseURL相同的方案,但用給定的端點url替換主機和路徑段。

在運行時更換BaseURL

The Core: ServiceGenerator

ServiceGenerator使用多個靜態字段和一個String常量API_BASE_URL,它保存API基址url:

public class ServiceGenerator {  
    public static final String API_BASE_URL = "http://futurestud.io/api";
    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .addConverterFactory(GsonConverterFactory.create())
                    .baseUrl(API_BASE_URL);

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    // No need to instantiate this class.
    private ServiceGenerator() {

    }

    public static <S> S createService(Class<S> serviceClass, AccessToken token) {
        String authToken = token.getTokenType().concat(token.getAccessToken());
        return createService(serviceClass, authToken);
    }

    // more methods
    // ...
}

Adjusting the ServiceGenerator

通過此設置,您無需在運行時更改API_BASE_URL常量。假如你在源代碼中改變它,編譯一個新的.apk并再次測試它,這是非常不方便,如果你正在使用多個API部署,我們將對ServiceGenerator類進行小的更改:

public class ServiceGenerator {  
    public static String apiBaseUrl = "http://futurestud.io/api";
    private static Retrofit retrofit;

    private static Retrofit.Builder builder =
            new Retrofit.Builder()
                    .addConverterFactory(GsonConverterFactory.create())
                    .baseUrl(apiBaseUrl);

    private static OkHttpClient.Builder httpClient =
            new OkHttpClient.Builder();

    // No need to instantiate this class.
    private ServiceGenerator() {
    }

    public static void changeApiBaseUrl(String newApiBaseUrl) {
        apiBaseUrl = newApiBaseUrl;

        builder = new Retrofit.Builder()
                        .addConverterFactory(GsonConverterFactory.create())
                        .baseUrl(apiBaseUrl);
    }

    public static <S> S createService(Class<S> serviceClass, AccessToken token) {
        String authToken = token.getTokenType().concat(token.getAccessToken());
        return createService(serviceClass, authToken);
    }

    // more methods
    // ...
}

將常量API_BASE_URL重命名為非最終字段apiBaseUrl。添加新的靜態方法changeApiBaseUrl(String newApiBaseUrl),它將更改apiBaseUrl變量。 它還會創建一個新版本的Retrofit.Builder實例構建器。 因為我們正在為請求重新使用構建器,如果我們不創建一個新的實例,所有的請求仍然會違反原來的apiBaseUrl值。

Example Usage

public class DynamicBaseUrlActivity extends AppCompatActivity {

    public static final String TAG = "CallInstances";
    private Callback<ResponseBody> downloadCallback;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file_upload);

        downloadCallback = new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                Log.d(TAG, "server contacted at: " + call.request().url());
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                Log.d(TAG, "call failed against the url: " + call.request().url());
            }
        };

        // first request
        FileDownloadService downloadService = ServiceGenerator.create(FileDownloadService.class);
        Call<ResponseBody> originalCall = downloadService.downloadFileWithFixedUrl();
        originalCall.enqueue(downloadCallback);

        // change base url
        ServiceGenerator.changeApiBaseUrl("http://development.futurestud.io/api");

        // new request against new base url
        FileDownloadService newDownloadService = ServiceGenerator.create(FileDownloadService.class);
        Call<ResponseBody> newCall = newDownloadService.downloadFileWithFixedUrl();
        newCall.enqueue(downloadCallback);
    }
}

在第一個請求執行后,我們使用新的ServiceGenerator.changeApiBaseUrl()方法將基本url更改為我們的開發環境。 最后,我們將再次提出相同的下載請求。 當我們啟動應用程序時,我們會收到以下日志:

D/CallInstances: server contacted at: http://futurestud.io/resource/example.zip  
D/CallInstances: server contacted at: http://development.futurestud.io/resource/example.zip 

When To Change the Base Url at Runtime

上面的演示代碼簡化了一些東西。我們通常在應用程序的調試版本中實現一個按鈕,測試人員可以在其中選擇所需的服務器環境。因此,根據您的情況,您可能需要編寫更多的代碼來決定何時以及到哪個基本URL Retrofit應該更改。

此外,我們只建議這樣做的調試目的。我們不認為這是一個讓您的應用程序與各種服務器同時工作的好方法。如果您的應用程序需要處理多個API,請查找其他版本。動態網址教程可能是一個好的開始。

最后,請測試您是否可以使用應用程序簡單切換環境。例如,如果在服務器端存儲用戶和認證信息,切換環境可能會導致問題。您的生產數據庫很可能不包含與您的開發數據庫相同的用戶,是否正確?在我們的應用中,我們刪除所有相關的用戶數據,并在測試人員通過新的基本網址更改環境后強制進行全新的登錄。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容