我們真的需要使用RxJava+Retrofit嗎?

Android.jpg

前言

可以說RxJava+Retrofit是整個2016年Android 開發圈內最受關注的的組合。各大Android論壇上有大量以RxJava+Retrofit+xxx 為標題的文章,此類文章也備受大家的關注。這個組合仿佛已經成為了Android開發的必備組件,項目里沒使用這個組合好像自己都out了似的。

平心而論,RxJava和Retrofit 相較于以往的各種框架(如 AsyncHttpClient,Volley等 )學習和使用起來會有一些難度;RxJava 強大而又龐大的操作符,Retrofit采用注解風格定義接口,都會讓初學者花費不少功夫,繞不少圈子,踩大量的坑。既然這樣,那么就會有人懷疑,我們真的需要學習RxJava和Retrofit嗎?

任意一款需要聯網的APP,最典型的套路就是請求后端數據,解析數據進行UI更新;響應用戶操作,再次請求數據,更新UI。這里我們就從最基礎的網絡請求出發,帶著疑問,逐步了解一下Retrofit的前生今世,看一看RxJava和Retrofit的價值所在。

Android Http

最基礎的實現方式

初學Android開發時,還在上大學,那會兒還不知有AsyncHttpClient,Volley,OKHttp 這么方便的框架;一個簡單的網絡請求通常要寫一大段代碼。

使用HttpURLConnection實現網絡請求####

class MyTask extends AsyncTask<String, Void, String> {

        @Override
        protected String doInBackground(String... params) {
            InputStream mInputStream = null;
            HttpURLConnection connection = getHttpUrlConnection(params[0]);
            String result = "";
            try {
                connection.connect();
                int statusCode = connection.getResponseCode();
                String response = connection.getResponseMessage();
                mInputStream = connection.getInputStream();
                InputStreamReader inputStreamReader = new InputStreamReader(mInputStream);
                BufferedReader reader = new BufferedReader(inputStreamReader);
                StringBuffer sb = new StringBuffer();
                String line;
                while ((line = reader.readLine()) != null) {
                    sb.append(line + "\n");
                }

                result = "StatusCode: " + statusCode + "\n"
                        + "Response" + response + "\n"
                        + sb.toString();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return result;
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            tv.setText(s);
        }
    }

    private HttpURLConnection getHttpUrlConnection(String url) {
        HttpURLConnection connection = null;
        try {
            URL mUrl = new URL(url);
            connection = (HttpURLConnection) mUrl.openConnection();
            connection.setConnectTimeout(20000);
            connection.setReadTimeout(40000);
            connection.setRequestMethod("GET");
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Accept", "application/json");
            connection.setRequestProperty("Charset", "utf-8");
            connection.setRequestProperty("Content-Length", "0");

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return connection;
    }
new MyTask().execute(BASE_URL);

這段代碼的邏輯很簡單,就是將網絡請求的結果顯示在一個TextView上。很大一部分的內容都是在執行HttpURLConnection 相關的配置及初始化工作。

記得第一次通過網絡請求把數據顯示的Android模擬器(那時候還是窮學生,買不起Android手機)的屏幕上時,雖然只是一大堆別人看不懂的json字符串,但是感覺自己就要上天了,現在想想真是。。。。。

即便是這么長的一段代碼,還沒有包含網絡請求異常的內容,如果加上網絡請求失敗處理的邏輯,將使得整個代碼結構更加臃腫龐大。

網絡請求框架的涌現###

一款聯網的APP至少會有十幾次的網絡請求,更多的就無法估計了。因此,每一次的網絡請求不可能像上面那樣寫。因此,我們需要封裝,將一些固定的操作統一處理,當然已經有許多大神比我早想到了這個問題,因此便出現了許多對網絡請求進行封裝的庫。

  • AsyncHttpClient(底層基于HttpClient)
  • afinal(FinalHttp,同樣是基于HttpClient封裝)
  • xUtils (基于afinal)
  • Volley(Google官方出品)
  • okHttp
  • NoHttp (個人開發)

這里列出的幾個庫當中,個人使用AsyncHttpClient較多,AsyncHttpClient 的確非常好用,但是后來伴隨著Android sdk 23 中HttpClient的廢棄也逐漸被遺忘。
afinal和xUtils 都沒有在實際項目中沒用過,不做評價。

Volley作為Google官方在2013年I/O 大會上推出的庫,相較于AsyncHttpClient 更強大。

下面簡單列舉一個使用Volley進行get請求的demo。

Volley 簡單使用####

添加依賴:

compile 'com.mcxiaoke.volley:library:1.0.19'
protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mContext = this;
        queue = Volley.newRequestQueue(mContext);
        setContentView(R.layout.activity_http_volley_demo);
        tv = (TextView) findViewById(R.id.editText);

        final StringRequest request = new StringRequest(Request.Method.GET, BASE_URL,
                new ResponseSuccessListener(), new ResponseFailListener());
        findViewById(R.id.volley).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                queue.add(request);
            }
        });

    }

    private class ResponseSuccessListener implements com.android.volley.Response.Listener<String> {

        @Override
        public void onResponse(String response) {
            tv.setText(response);
        }
    }

    private class ResponseFailListener implements Response.ErrorListener {

        @Override
        public void onErrorResponse(VolleyError error) {
            Toast.makeText(mContext, error.toString(), Toast.LENGTH_SHORT).show();
        }
    }

這段代碼和上面的功能一樣,都是將網絡請求的結果顯示在TextView。但是通過Volley對http請求進行一次封裝后,我們不再關注網絡請求的具體細節,而是將重點放在了對請求結果的處理上;網絡請求無論成功還是失敗,我們都可以很多好的應對。

而且在Volley中,異步網絡請求的回調方法已然處于UI線程中,這樣我們就可以直接在回調方法中進行UI更新了。

可以說,使用Volley已經可以非常方便的處理Android 網絡請求的相關內容了。既然如此,為什么還會有OKHttp和Retrofit的出現呢?他們的優勢又在哪里呢?

OKHttp 簡單介紹####

okHttp 是由squire 推出的一個網絡請求庫,包括Retrofit也是由其開發,這里為squire點個贊。

使用之前加入依賴

    compile 'com.squareup.okhttp3:okhttp:3.4.1'
    compile 'com.squareup.okio:okio:1.11.0'

okHttp 網絡請求實現

findViewById(R.id.get).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tv.setText("");
                loading.setVisibility(View.VISIBLE);
                client = new OkHttpClient();
                Request.Builder builder = new Request.Builder()
                        .url(BASE_URL)
                        .method("GET", null);

                request = builder.build();
                Call mCall = client.newCall(request);
                mCall.enqueue(new MyCallback());
            }
        });

private class MyCallback implements Callback {

        @Override
        public void onFailure(Call call, IOException e) {
            Message msg = new Message();
            msg.what = 100;
            msg.obj = e;
            handler.sendMessage(msg);
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            Message msg = new Message();
            msg.what = 200;
            msg.obj = response.body().string();
            handler.sendMessage(msg);
        }
    }

class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            loading.setVisibility(View.GONE);
            switch (msg.what) {
                case 100:
                    Object e = msg.obj;
                    Toast.makeText(mContext, e.toString(), Toast.LENGTH_SHORT).show();
                    break;
                case 200:
                    String response = (String) msg.obj;
                    tv.setText(response);
                    break;
                case 300:
                    int percent = msg.arg1;
                    Log.e("llll", "the percent is " + percent);
                    if (percent < 100) {
                        progressDialog.setProgress(percent);
                    } else {
                        progressDialog.dismiss();
                        Glide.with(mContext).load(FILE_PATH).into(imageView);
                    }
                    break;
                default:
                    break;
            }
        }
    }

這里必須了解的是,okHttp的回調方法,并不處于UI 線程中,對網絡請求結果如果涉及UI 線程的操作,需要使用Handler。這么看來,okHttp 貌似反而不如Volley了。其實不然,okhttp的封裝套路和Volley,AsyncHttp不是一個級別的,不能和后兩者作比較,okhttp 和HttpClient、HttpUriConneciton 才是一個級別的產物,相較于這兩者,okhttp顯然強大了許多。

所以,OKHttp不僅僅可以用于Android開發,Java開發也是OK的。

Retrofit

A type-safe HTTP client for Android and Java

一個針對Android和Java類型安全的http客戶端

上面這句話,就是Squire對Retrofit的說明,言簡意賅。Retrofit其實是對okhttp 做了進一步的封裝,有了okhttp 的基礎,使用Retrofit會很容易。

下面就來看看,使用Retrofit做網絡請求又是一種怎樣的體驗。

這里為了方便我們使用"https://api.github.com/"作為網絡請求的接口基地址

使用之前加入依賴:

compile 'com.squareup.retrofit2:retrofit:2.1.0'

定義接口

public interface GithubService {

    @GET("users/{user}")
    Call<ResponseBody> getUserString(@Path("user") String user);

}

這里我們使用http中的get 方法獲取users這個接口下,當前user的具體信息,參數為當前user名。返回內容為Http請求的ResponseBody。

Retrofit 返回ResponseBody

private void SimpleRetrofit() {
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        Retrofit.Builder builder = new Retrofit.Builder()
                .baseUrl(BASE_URL);
        Retrofit retrofit = builder.client(httpClient.build()).build();
        GithubService simpleService = retrofit.create(GithubService.class);
        Call<ResponseBody> call = simpleService.getUserString(name);
        call.enqueue(new Callback<ResponseBody>() {
            @Override
            public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
                loading.dismiss();
                try {
                    String result = response.body().string();
                    Gson gson = new Gson();
                    GithubUserBean bean = gson.fromJson(result, GithubUserBean.class);
                    setUserView(bean);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onFailure(Call<ResponseBody> call, Throwable t) {
                loading.dismiss();
            }
        });
    }

private void setUserView(GithubUserBean user) {
        if (user != null) {
            viewShell.removeAllViews();
            View view = LayoutInflater.from(mContext).inflate(R.layout.user_item_layout, null);
            TextView title = (TextView) view.findViewById(R.id.title);
            TextView id = (TextView) view.findViewById(R.id.userId);
            TextView creteaTime = (TextView) view.findViewById(R.id.createTime);
            TextView updateTime = (TextView) view.findViewById(R.id.updateTime);
            TextView bio = (TextView) view.findViewById(R.id.bio);
            ImageView avatar = (ImageView) view.findViewById(R.id.avatar);

            title.setText("Name: " + user.getLogin());
            bio.setText("Bio: " + user.getBio());
            id.setText("Id: " + String.valueOf(user.getId()));
            creteaTime.setText("createTime: " + user.getCreated_at());
            updateTime.setText("updateTime: " + user.getUpdated_at());
            Glide.with(mContext).load(user.getAvatar_url()).into(avatar);

            viewShell.addView(view);
        } else {
            Toast.makeText(mContext, "result is null", Toast.LENGTH_SHORT).show();
        }
    }

GitHubUserBean 為網絡請求結果json數據所對應的實體類。

通過這段代碼,我們在最終的回調方法里可以友好的處理請求結果,失敗時onFailure方法執行。成功時,onResponse方法執行,我們在這里用Gson解析返回的數據,并進行UI更新操作(setUserView(bean)),

這里我們這樣做有些啰嗦,Gson轉換的方式都是類似,唯一不同的只是每次網絡請求結果對應的實體類;因此我們可以借助強大的Retrofit幫助我們完成Gson轉換的步驟。當然,如果在你所在的開發環境中,接口返回的并不是json格式的數據,也沒有問題的。

convert
convert

上圖是Retrofit官網對可轉換類型給出的介紹。有這么多種,當然了如果你們家服務器返回的數據格式比較神奇,你也可以自定義轉換類。

好了,言歸正傳,這里還是以Json 格式數據為例。

添加依賴:

compile 'com.squareup.retrofit2:converter-gson:2.1.0'

注意這里converter-gson 的版本號,要和之前Retrofit的版本號保持一致。

我們重新定義接口:

public interface GithubService {
    @GET("users/{user}")
    Call<GithubUserBean> getUser(@Path("user") String user);

}

這里我們用GithubUserBean取代ResponseBody,直接將其作為返回類型。

Retrofit 返回對象

private void LazyRetrofit() {
        OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
        Retrofit.Builder builder = new Retrofit.Builder()
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create());
        Retrofit retrofit = builder.client(httpClient.build()).build();
        GithubService service = retrofit.create(GithubService.class);
        Call<GithubUserBean> call = service.getUser(name);
        call.enqueue(new Callback<GithubUserBean>() {
            @Override
            public void onResponse(Call<GithubUserBean> call, Response<GithubUserBean> response) {
                GithubUserBean bean = response.body();
                setUserView(bean);
                loading.dismiss();
            }

            @Override
            public void onFailure(Call<GithubUserBean> call, Throwable t) {
                loading.dismiss();
            }
        });
    }

這里的實現方式和上面基本相似,只是多了一行

.addConverterFactory(GsonConverterFactory.create());

這樣,我們在onResponse中獲得就是對象,不再需要做額外的轉換工作,可以直接使用。

Retrofit 簡單封裝

這里我們可以看到,Retrofit使用有著一定的套路,所以我們可以將Retrofit初始化相關得內容做一次簡單的封裝。

public class GenServiceUtil {
    private static final String BASE_URL = "https://api.github.com/";

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

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

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

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

}

private void EasyRetrofit() {
        GithubService service = GenServiceUtil.createService(GithubService.class);
        Call<GithubUserBean> call = service.getUser(name);
        call.enqueue(new Callback<GithubUserBean>() {
            @Override
            public void onResponse(Call<GithubUserBean> call, Response<GithubUserBean> response) {
                GithubUserBean bean = response.body();
                loading.dismiss();
                setUserView(bean);
            }

            @Override
            public void onFailure(Call<GithubUserBean> call, Throwable t) {
                loading.dismiss();
            }
        });
    }

我們只需傳入定義好的借口,會使代碼簡介許多。看到這里可以發現,Retrofit的確很厲害,那為什么又要將他和RxJava結合在一起呢?下面我們就來看看。

RxJava+Retrofit

關于什么是RxJava,這里不再贅述,不了解的看以看看這里。這里我們就看看將RxJava 和我們之前的內容結合在一起會有怎樣的效果。

首先,加入依賴

    compile 'io.reactivex:rxjava:1.1.7'
    compile 'io.reactivex:rxandroid:1.2.1'

RxJava+Retrofit 實現###

private void RxRetrofit() {
        GithubService service = GenServiceUtil.createService(GithubService.class);
        final Call<GithubUserBean> call = service.getUser(name);
        final Observable myObserable = Observable.create(new Observable.OnSubscribe<GithubUserBean>() {
            @Override
            public void call(Subscriber<? super GithubUserBean> subscriber) {
                Response<GithubUserBean> bean = null;
                try {
                    bean = call.execute();
                    subscriber.onNext(bean.body());

                } catch (IOException e) {
                    e.printStackTrace();
                    subscriber.onError(e);
                }

                subscriber.onCompleted();
            }
        });

        myObserable
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .map(new Func1<GithubUserBean, GithubUserBean>() {
                    @Override
                    public GithubUserBean call(GithubUserBean o) {
                        if (TextUtils.isEmpty(o.getBio())) {
                            o.setBio("nothing !");
                        }
                        return o;
                    }
                })
                .subscribe(new Subscriber<GithubUserBean>() {
                    @Override
                    public void onCompleted() {
                        loading.dismiss();
                    }

                    @Override
                    public void onError(Throwable e) {
                        loading.dismiss();
                    }

                    @Override
                    public void onNext(GithubUserBean o) {
                        setUserView(o);
                    }
                });

    }

這里有幾點需要注意:

  • RxJava 本身最大的特定就是異步,因此這里我們Retrofit執行網絡請求的時候,使用了execute(同步請求),而不再是enqueue。
  • RxJava 可以使用subscribeOn和observeOn完美處理Observeable和Subscribe的執行線程問題。
  • 這里使用RxJava中map操作符,對返回內容中的為null或“” 的對象做了簡單的處理。

我們引入RxJava實現了同樣的功能,卻使得代碼量增加了很多。不禁要問,RxJava的價值到底在哪里呢?

RxJava + Retrofit 到底好在哪里

好了,為了說明為題,我們添加一個接口

public interface GithubService {

    @GET("users/{user}")
    Call<GithubUserBean> getUser(@Path("user") String user);

    @GET("users/{user}/followers")Observable<List<UserFollowerBean>> followers(@Path("user") String usr);

}

當然這里依舊需要添加依賴:

compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'

同時在Service的封裝方法中添加

.addCallAdapterFactory(RxJavaCallAdapterFactory.create())

這樣,RxJava就和Retrofit完美的關聯在了一起。

我們在接口中,定義followers()方法直接返回了Observable,因為Observable是RxJava的源頭,而且Retrofit可以很好的支持RxJava,這樣就非常方便了。

    private void RxRetrofitList() {
        GithubService service = GenServiceUtil.createService(GithubService.class);
        Observable<List<UserFollowerBean>> myObserve = service.followers(name);
        myObserve
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<List<UserFollowerBean>>() {
                    @Override
                    public void onCompleted() {
                        loading.dismiss();
                    }

                    @Override
                    public void onError(Throwable e) {
                        loading.dismiss();
                        e.printStackTrace();
                    }

                    @Override
                    public void onNext(List<UserFollowerBean> userFollowerBeen) {
                        setFollowersView(userFollowerBeen);
                    }
                });

    }

在接口中返回的內容就是Observable,因此不用再像之前一樣單獨定義Observable;在onNext 方法中,接收到返回的對象,更新UI。 這里如果我們不使用RxJava,單獨使用Retrofit實現這個過程是沒有任何問題的; RxJava看似沒有價值;但是假設現在出現如下之一的情景

  • 需要對返回的userFollowerBeen 這個list 進行按用戶名從小到大的排序
  • 需要對返回的userFollowerBeen 這個list 進行按用戶ID從小到大的排序
  • 如果返回的userFollowerBeen 這個list 中,某一項的頭像地址為空,則不顯示該項

.....

這種情景在實際開發中太常見了,試想如果沒有RxJava;那么每一次需求的變更都意味著我們需要去修改setFollowersView這個方法,需求一旦變更,就去修改這個方法,這樣會不可避免的產生各種bug。那有沒有辦法不去修改這個方法呢?這個時候,就需要強大的RxJava了。

這里我們就看看如何在不修改setFollowersView的前提下,實現對用戶名從小到大的排序:

    private void RxRetrofitList() {
        GithubService service = GenServiceUtil.createService(GithubService.class);
        Observable<List<UserFollowerBean>> myObserve = service.followers(name);
        myObserve
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .map(new Func1<List<UserFollowerBean>, List<UserFollowerBean>>() {
                    @Override
                    public List<UserFollowerBean> call(List<UserFollowerBean> userFollowerBeen) {
                        for (UserFollowerBean bean : userFollowerBeen) {
                            String name = "";
                            name = bean.getLogin().substring(0, 1).toUpperCase() + bean.getLogin().substring(1, bean.getLogin().length());
                            bean.setLogin(name);
                        }
                        return userFollowerBeen;
                    }
                })
                .map(new Func1<List<UserFollowerBean>, List<UserFollowerBean>>() {
                    @Override
                    public List<UserFollowerBean> call(List<UserFollowerBean> userFollowerBean) {
                        Collections.sort(userFollowerBean, new Comparator<UserFollowerBean>() {
                            @Override
                            public int compare(UserFollowerBean o1, UserFollowerBean o2) {
                                return o1.getLogin().compareTo(o2.getLogin());
                            }
                        });
                        return userFollowerBean;
                    }
                })
                .subscribe(new Subscriber<List<UserFollowerBean>>() {
                    @Override
                    public void onCompleted() {
                        loading.dismiss();
                    }

                    @Override
                    public void onError(Throwable e) {
                        loading.dismiss();
                        e.printStackTrace();
                    }

                    @Override
                    public void onNext(List<UserFollowerBean> userFollowerBeen) {
                        setFollowersView(userFollowerBeen);
                    }
                });

    }

在代碼中我們使用RxJava的map 操作符,對返回數據做了兩次處理,首先將所有用戶名的首字母轉換為大寫字母;然后對整個list按照用戶名從小到大排序。因為用戶名中同時包含以大小寫字母打頭的內容,所以為了方便,我們進行了一次轉換大寫的操作。

同樣是隨著需求變更,修改代碼;但是你會發現,使用RxJava的方式,會降低出現bug的概率,而且就算是不同的人去改,也會比較方便維護。

看到了吧,這就是RxJava的優點,當然這個例子也只是冰山一角。這里提到的map操作符只是RxJava龐大操作符集合中的一員,更特別的是,RxJava的操作符還是可以自定義的,這樣讓我們的代碼有了無限的可能;RxJava的存在不僅僅在于網絡請求,可以用在別的方面;RxJava其實是體現了一種思路,所有對數據的操作都在流上完成,將最終的結果返回給觀察者。同時,如果返回的followers 列表有任何異常,RxJava的onError 方法會執行,這就方便我們去處理異常數據了。

總結##

通篇通過對Android 網絡請求各種實現的總結,可以看到 相對于Volley,AsyncHttpClient 等庫,RxJava+Retrofit 的優勢并非特別顯著;在執行效率及功能上并無大的亮點;對Volley進行良好的封裝同樣可以實現類似Retrofit自動轉Gson的功能;RxJava+Retrofit 結合會讓我們寫代碼的方式更加有條理,雖然代碼量會增多,但邏輯的清晰才是最重要的不是嗎?所以,RxJava+Retrofit 組合不失為一種好的選擇。

所以,RxJava+Retrofit 真的是我們需要的東西呀!


文中所有源碼地址github

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

推薦閱讀更多精彩內容