【翻譯】當(dāng)我開始Android開發(fā)時(shí),希望能早點(diǎn)認(rèn)識(shí)的一些開源庫

原文地址 https://speakerdeck.com/chrisguzman/android-libraries-i-wish-i-knew-when-i-started

介紹:當(dāng)從事App開發(fā)時(shí),有時(shí)候沒必要重復(fù)造輪子,尤其是對(duì)新手而言。 這個(gè)演講涵蓋了一些可以在App開發(fā)時(shí)使用的庫,這些庫可以解決你正在遇到的問題赐劣。 無論是要從Web接口獲取數(shù)據(jù), 或者顯示哩都、緩存圖片魁兼, 或者存儲(chǔ)、同步數(shù)據(jù)漠嵌, 這些庫都可以幫助你咐汞。

本文是翻譯的Groupon工程師Chris Guzman的一個(gè)演講PPT。作者以一個(gè)45分鐘“hackathon”的方式儒鹿, 從零開始構(gòu)造一個(gè)App "TAaSKY", 這個(gè)App用于展示做"土豆"的食譜. 作者逐步介紹在開發(fā)這個(gè)app過程中使用到的開源庫碉考。

第一步:構(gòu)造、使用view - Butter Knife<a id="orgheadline7"></a>

這一步是用來構(gòu)造TAaSKY應(yīng)用的UI界面.

基本使用<a id="orgheadline1"></a>

下面的內(nèi)容是activity的一個(gè)簡(jiǎn)略的layout文件挺身,這是開發(fā)App過程中 必不可少的東西侯谁,當(dāng)寫完layout之后,大部分情況下都需要在代碼中 引用相關(guān)的組件章钾。這里就可以使用在Android領(lǐng)域舉世聞名的開源庫: Butter Knife墙贱。

<LinearLayout ... android:orientation="vertical">
<ImageView android:id="@+id/taco_img" .../>
<TextView android:id="@+id/description" .../>
<LinearLayout android:orientation="horizontal" .../>
<Button android:id="@+id/reject" .../>
<Button android:id="@+id/info" .../>
<Button android:id="@+id/save" .../>
</LinearLayout>
<EditText android:id="@+id/tag" .../>
</LinearLayout>

Butter Knife通過注解的方式將代碼和xml文件綁定到一起,無需在 重復(fù)寫大量的 findViewById() 這種代碼贱傀。該庫有以下幾個(gè)優(yōu)勢(shì):

  1. 沒有拖慢程序速度惨撇。

  2. 改善view查找。

  3. 改善監(jiān)聽函數(shù)注冊(cè)府寒。

  4. 改善資源查找魁衙。

      <TextView android:id="@+id/description"
         ...
       />
     public class MainActivity extends Activity {
          @BindView(R.id.description) TextView description;
    
          @Override
          protected void onCreate(Bundle bundle) {
               ...
               ButterKnife.bind(this);
              description.setText("Tofu with Cheese on a tortilla");
       }
     }
    

下面是一個(gè)通常的Butter Knife用法, ButterKnife.bind(this) 函數(shù) 會(huì)自動(dòng)生成代碼尋找相關(guān)的view、資源并把它們保存到activity代碼中株搔。 類似這樣:

public void bind(MainActivity activity) {
   activity.description = (android.widget.TextView) activity.findViewById(2130968577);
}

下面是一些更高級(jí)的用法:

綁定剖淀、解綁fragment中的view<a id="orgheadline2"></a>

public class TacoFragment extends Fragment {
 @BindView(R.id.tag) EditText tag;
 private Unbinder unbinder;

 @Override
 public View onCreateView(LayoutInflater inflater, ViewGroup group, Bundle bundle) {
      ...
      //Important!
      unbinder = ButterKnife.bind(this, parentView);
      tag.setHint("Add tag. Eg: Tasty!, Want to try")
      return view;
 }

 @Override
 public void onDestroyView() {
      super.onDestroyView();
      //sets the views to null
      unbinder.unbind();
 }
}

注冊(cè)監(jiān)聽函數(shù)<a id="orgheadline3"></a>

ButterKnife支持大部分常用的監(jiān)聽函數(shù)。

@OnClick(R.id.save)
public void saveTaco(Button button) {
 button.setText("Saved!");
}

綁定資源<a id="orgheadline4"></a>

class MainActivity extends Activity {
 @BindString(R.string.title) String title;
 @BindDrawable(R.drawable.star) Drawable star;
 // int or ColorStateList
 @BindColor(R.color.guac_green) int guacGreen;
 // int (in pixels) or float (for exact value)
 @BindDimen(R.dimen.spacer) Float spacer;
}

給多個(gè)組件設(shè)置同一個(gè)監(jiān)聽函數(shù)<a id="orgheadline5"></a>

@OnClick({ R.id.save, R.id.reject})
public void actOnTaco(View view) {
 if (view.getId() == R.reject) {
    Toast.makeText(this, "Ew Gross!", LENGTH_SHORT).show();
 }
 else {
    Toast.makeText(this, "Yummy :)", LENGTH_SHORT).show();
 }
 //TODO: implement
 getNextTaco();
}

操作view的屬性<a id="orgheadline6"></a>

//下面的代碼將兩個(gè)button綁定到一個(gè)list中纤房, 并通過操作這個(gè)list來
//操作這些按鈕的屬性纵隔。
@BindViews({R.id.save, R.id.reject})
List<Button> actionButtons;
ButterKnife.apply(actionButtons, View.ALPHA, 0.0f);

ButterKnife.apply(actionButtons, DISABLE);
ButterKnife.apply(actionButtons, ENABLED, false);
static final ButterKnife.Action<View> DISABLE = new ButterKnife.Action<View>() {
 @Override public void apply(View view, int index) {
    view.setEnabled(false);
 }
};
static final ButterKnife.Setter<View, Boolean> ENABLED = new ButterKnife.Setter<View, Boolean>() {
 @Override public void set(View view, Boolean value, int index) {
    view.setEnabled(value);
 }
};

第二步:加載網(wǎng)絡(luò)圖片 - Picasso<a id="orgheadline11"></a>

這一步用于在應(yīng)用顯示土豆的照片, 照片可能是網(wǎng)絡(luò)或本地圖片.

通過第一步的代碼,UI部分基本已經(jīng)寫完了。然后接下來要實(shí)現(xiàn)APP的一個(gè)功能, 從網(wǎng)絡(luò)下載圖片并顯示. 這里用到了一個(gè)同樣有名的開源庫: Picasso.

基本介紹<a id="orgheadline8"></a>

該庫的一些特點(diǎn)包括:

  1. 進(jìn)行HTTP請(qǐng)求.
  2. 緩存圖片.
  3. 簡(jiǎn)單的"resize/裁剪/居中/放大"操作.
  4. 負(fù)責(zé)在主線程之外進(jìn)行http請(qǐng)求.
  5. 對(duì)RecyclerView的view進(jìn)行合理回收.

在介紹Picasso之前, 先看一下比較通用的"自己寫"的下載圖片代碼: 這段代碼通過http請(qǐng)求獲取圖片的stream, 然后再調(diào)用Android的BitmapFactory 類來將stream轉(zhuǎn)化成bitmap. 其中 OpenHttpGETConnection()函數(shù)還要考慮在 子線程中進(jìn)行http請(qǐng)求操作.

private Bitmap DownloadImage(String url)
{
 Bitmap bitmap = null;
 InputStream in = null;
 try {
    in = OpenHttpGETConnection(url);
    bitmap = BitmapFactory.decodeStream(in); in.close();
 } catch (Exception e) {
    Log.d("DownloadImage", e.getLocalizedMessage());
 }
 return bitmap;
}

如果使用Picasso,則上面的代碼就變?yōu)?

Picasso.with(context)
 .load("http://placekitten.com/200/300")
 .into(imageView);

更多特性<a id="orgheadline9"></a>

上面展示了Picasso的一個(gè)典型使用方式, 該庫還包含其他的對(duì)圖片的操作,
例如:

  1. placeholder(R.mipmap.loading). 占位圖片, 可以是一個(gè)資源或者drawable
  2. error(R.drawable.sad_taco) . 如果加載失敗顯示的圖片
  3. fit(). 將圖片大小縮減到imageView的大小.
  4. resize(imgWidth, imgHeight). 縮減到指定圖片大小. 單位是px
  5. centerCrop(). 居中裁剪.
  6. rotate(90f). 旋轉(zhuǎn)圖片. 或者也可以使用函數(shù) rotate(degrees, pivotX, pivotY)

除了網(wǎng)絡(luò)下載圖片, Picasso也支持加載本地圖片. 例如下面的代碼:

Picasso.with(context).load(R.drawable.salsa).into(imageView1);
Picasso.with(context).load("file:///asset/salsa.png").into(imageView2);
Picasso.with(context).load(new File(...)).into(imageView3);

一個(gè)完整的代碼片段<a id="orgheadline10"></a>

下面是Picasso和ButterKnife一起用的場(chǎng)景, 在通過Picasso下載圖片時(shí), 使用 ButterKnife的apply函數(shù)來使按鈕不可用.

//Butter Knife!
@BindView(R.id.taco_img) ImageView tacoImg;
private void setTacoImage() {
 Picasso.with(context)
 .load("http://tacoimages.com/random.jpg")
 .into(tacoImg);
}
private void getNextTaco() {
 ButterKnife.apply(actionButtons, DISABLE);
 setTacoImage();
 //TODO: implement
 loadTacoDescription();
}

第三步: json轉(zhuǎn)換 - Gson<a id="orgheadline14"></a>

這一步用于對(duì)服務(wù)器返回的json格式數(shù)據(jù)轉(zhuǎn)化成類對(duì)象, 或者反過來.

基本介紹<a id="orgheadline12"></a>

Gson的一些特點(diǎn):

  1. (可以)不需要在類中使用注解.
  2. 性能好.
  3. 使用廣泛.
  4. 默認(rèn)包含類(包括父類)的所有域.
  5. 支持多維數(shù)組.
  6. 當(dāng)序列化時(shí), 類的值為null的變量會(huì)被跳過.
  7. 反序列化時(shí), json中沒有的域會(huì)在對(duì)象中生成一個(gè)null值.

例如下面的例子對(duì)類Taco使用Gson進(jìn)行Json的序列化和反序列化.

class Taco {
 private String description;
 private String imageUrl;
 private String tag;
 //not included in JSON serialization or deserialization
 private transient boolean favorite;
 Taco(String description, String imageUrl, String tag, boolean favorite) {
 ....
 }
}

// Serialize to JSON
Taco breakfastTaco = new Taco("Eggs with syrup on pancake", "imgur.com/123", "breakfast", true);
Gson gson = new Gson();
String json = gson.toJson(breakfastTaco);
// ==> json is {description:"Eggs with syrup on pancake", imageUrl:"imgur.com/123", tag:"breakfast"}
// Deserialize to POJO
Taco yummyTaco = gson.fromJson(json, Taco.class);
// ==> yummyTaco is just like breakfastTaco except for the favorite boolean

高級(jí)用法<a id="orgheadline13"></a>

  1. 如果變量名和json的域名不同, 可以使用 @SerializeName() 注解修飾.

    public class Taco {
        @SerializedName("serialized_labels")
        private String tag;
    }
    
  2. 通過Gson的API客制化輸出.

       //如果變量值為null,則輸出中也輸出null,而不是忽略.
    Gson gson = new GsonBuilder().serializeNulls().create();
    //保留空格
    Gson gson = new GsonBuilder().setPrettyPrinting().create();
    
  3. 設(shè)置日期格式

    public String DATE_FORMAT = "yyyy-MM-dd";
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.setDateFormat(DATE_FORMAT);
    Gson gson = gsonBuilder.create();
    

第四步: 請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù) - Retrofit

這一步用于從服務(wù)器上獲取數(shù)據(jù).
(作者說: 請(qǐng)不要再使用AsyncTask了, 真的, 停下來吧)<a id="orgheadline18"></a>

基本介紹<a id="orgheadline15"></a>

Retrofit的一些特點(diǎn):

  1. 類型安全.
  2. 支持認(rèn)證.
  3. 支持json的序列化和反序列化.
  4. 支持RxJava
  5. 支持同步和異步請(qǐng)求.

典型使用<a id="orgheadline16"></a>

下面是Retrofit的一個(gè)典型應(yīng)用, (更多介紹可以看這里):

  1. 定義API

    public interface TacoApi {
       // Request method and URL specified in the annotation
       // Callback for the parsed response is the last parameter
       @GET("random/")
       Call<Taco> randomTaco(@Query("full-taco") boolean full);
       @GET("contributions/")
       Call<List<Contributor>> getContributors();
       @GET("contributions/{name}")
       Call<Contributor> getContributors(@Path("name") String username));
       @POST("recipe/new")
       Call<Recipe> createRecipe(@Body Recipe recipe);
     }
    
  2. 使用api進(jìn)行請(qǐng)求

    1. 同步請(qǐng)求:

      Retrofit retrofit = new Retrofit.Builder()
          .baseUrl("http://taco-randomizer.herokuapp.com/")
          .addConverterFactory(GsonConverterFactory.create())
          .build();
      
          // 創(chuàng)建api實(shí)例
          TacoApi tacoApi = retrofit.create(TacoApi.class);
          // 創(chuàng)建請(qǐng)求
          Call<Taco> call = tacoApi.randomTaco(true);
          // 執(zhí)行請(qǐng)求
          Taco taco = call.execute().body();
      
    2. 異步請(qǐng)求

      Recipe recipe = new Recipe();
      Call<Recipe> call = tacoApi.createRecipe(recipe);
      call.enqueue(new Callback<Recipe>() {
          @Override
          public void onResponse(Call<Recipe> call, Response<Recipe> response) {}
          @Override
          public void onFailure(Call<Recipe> call, Throwable t) {}
      

小技巧<a id="orgheadline17"></a>

  1. 通過注解修改請(qǐng)求的url

    @POST("http://taco-randomizer.herokuapp.com/v2/taco")
    private Call<Taco> getFromNewAPI();
    
  2. 添加請(qǐng)求頭部

    @Headers({"User-Agent: tacobot"})
    @GET("contributions/")
    private Call<List<Contributor>> getContributors();
    

第五步: 存儲(chǔ)數(shù)據(jù) - Realm (sqlite的替代品)<a id="orgheadline21"></a>

這一步用于將服務(wù)器返回的數(shù)據(jù)(如食譜)存儲(chǔ)起來.

基本介紹<a id="orgheadline19"></a>

Realm的一些特點(diǎn):

  1. 為手機(jī)而生.
  2. 可以快到使用同步.
  3. 支持一個(gè)應(yīng)用包含多個(gè)Realm數(shù)據(jù)庫.(Sqlite只有一個(gè)).

下面是Realm在App中的應(yīng)用實(shí)例:

  1. 需要持久化的類需要繼承RealmObject:

    public class Taco extends RealmObject {
     private String description;
     private String tag;
     private String imageUrl;
     private boolean favorite;
     //getters and setters
    }
    
  2. 配置Realm, 一般是創(chuàng)建一個(gè)RealmConfiguration對(duì)象, 將Realm文件存儲(chǔ)到
    App的"file"目錄下.

    RealmConfiguration realmConfig =
     new RealmConfiguration.Builder(context).build();
    Realm.setDefaultConfiguration(realmConfig);
    // Get a Realm instance for this thread
    Realm realm = Realm.getDefaultInstance();
    
  3. 持久化. Realm支持存儲(chǔ)一個(gè)已存在的類實(shí)例, 或者通過傳入class文件直接存儲(chǔ)一個(gè)
    新的類實(shí)例.

    realm.beginTransaction(); // Persist your data in a transaction
    final Taco managedTaco = realm.copyToRealm(unmanagedTaco); // Persist unmanaged objects
    Taco taco = realm.createObject(Taco.class); // Create managed objects directly
    realm.commitTransaction();
    
  4. 獲取數(shù)據(jù).

    Realm realm = Realm.getDefaultInstance(); // Get a Realm instance for this thread
    final RealmResults<Taco> likedTacos =
    realm.where(Taco.class).equalTo("favorite", true).findAll(); //find all favorite tacos
    
  5. 刪除操作:

    // All changes to data must happen in a transaction
    realm.executeTransaction(new Realm.Transaction() {
     @Override
     public void execute(Realm realm) {
     // remove single match
     limeTacos.deleteFirstFromRealm();
     //or limeTacos.deleteLastFromRealm();
     // remove a single object
     Taco fishTaco = limeTacos.get(1);
     fishTaco.deleteFromRealm();
     // Delete all matches
     limeTacos.deleteAllFromRealm();
     }
    });
    

一些特性<a id="orgheadline20"></a>

Realm同樣支持同步和異步的"寫數(shù)據(jù)"操作, 通過調(diào)用不同的Api實(shí)現(xiàn), 如下代碼:

  1. 同步寫:

        //Transaction block
        realm.executeTransaction(new Realm.Transaction() {
         @Override
         public void execute(Realm realm) {
            Taco taco = realm.createObject(Taco.class);
            taco.setDescription("Spaghetti Squash on Fresh Corn Tortillas");
            user.setImageUrl("http://tacoimages.com/1.jpg");
         }
        });
    
  2. 異步寫, 需要傳入兩個(gè)個(gè)回調(diào)類對(duì)象參數(shù), 分別是成功和失敗的回調(diào).
    realm.executeTransactionAsync(new Realm.Transaction() {
    @Override
    public void execute(Realm bgRealm) {
    Taco taco = bgRealm.createObject(Taco.class);
    taco.setDescription("Spaghetti Squash on Fresh Corn Tortillas");
    user.setImageUrl("http://tacoimages.com/1.jpg");
    }}, new Realm.Transaction.OnSuccess() {
    @Override
    public void onSuccess() {}},
    new Realm.Transaction.OnError() {
    @Override
    public void onError(Throwable error) {}
    });

  3. 跟Gson一樣, Realm也支持類成員變量的解析. 例如:

     public class Taco extends RealmObject {
      ...
      private List<Ingredient>
      ...
     }
     public class Ingredient extends RealmObject {
      private String name;
      private URL url;
     }
     
     RealmResults<Taco> limeTacos = realm.where(Taco.class)
      .equalTo("ingredients.name", "Lime")
      .findAll();
    
  4. 為RealmObject和RealmResults增加數(shù)據(jù)變化的listener.

    limeTacos.addChangeListener(
     new RealmChangeListener<RealmResults<Taco>>() {
     @Override
     public void onChange(RealmResults<Taco> tacosConLimon) {
     //tacosConLimon.size() == limeTacos.size()
     // Query results are updated in real time
         Log.d("LimeTacos", "Now we have" + limeTacos.size() + " tacos");
     }
    });
    
  5. 為防止內(nèi)存泄漏, 需要在onDestroy中關(guān)閉Realm.

    @Override
    protected void onDestroy() {
     realm.removeChangeListener(realmListener); // Remove the listener.
     realm.close(); //or realm.removeAllChangeListeners(); Close the Realm instance.
    }
    

番外篇: 簡(jiǎn)便啟動(dòng)activity - Dart + Henson<a id="orgheadline23"></a>

這兩個(gè)類是受到ButterKnife啟發(fā)實(shí)現(xiàn)的, 提供了一個(gè)更簡(jiǎn)便的啟動(dòng)Activity的方法, 作者說, 不要再浪費(fèi)時(shí)間寫這樣的代碼啦.

intent.putExtra(EXTRA_TACO_DESCRIPTION, "Seasoned Lentils with Green Chile on Naan");
tacoDescription = getIntent().getExtras().getString(EXTRA_TACO_DESCRIPTION);

基本用法<a id="orgheadline22"></a>

  1. Dart定義intent使用到的參數(shù).

    public class TacoDetailActivity extends Activity {
     //Required. Exception thrown if missing
     @InjectExtra boolean favorite;
     @InjectExtra String description
     //default value if left null
     @Nullable @InjectExtra String tag = "taco";
     //Ingredient implements Parcelable
     @Nullable @InjectExtra Ingredient withIngredient;
     @Override
     public void onCreate(Bundle bundle) {
     super.onCreate(bundle);
     Dart.inject(this);
     //TODO use member variables
     ...
     }
    }
    
  2. 使用Henson生成intent.

    //Start intent for TacoDetailActivity
    Intent intent = Henson.with(context)
     .gotoTacoDetailActivity()
     .favorite(true)
     .description("Seasoned Lentils with Green Chile on Naan")
     .ingredient(new Ingredient())
     .build();
    // tag is null or defaults to "taco"
    startActivity(intent);
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子正蛙,更是在濱河造成了極大的恐慌,老刑警劉巖芦圾,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異俄认,居然都是意外死亡堕扶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門梭依,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人典尾,你說我怎么就攤上這事役拴。” “怎么了钾埂?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵河闰,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我褥紫,道長(zhǎng)姜性,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任髓考,我火速辦了婚禮部念,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘氨菇。我一直安慰自己儡炼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布查蓉。 她就那樣靜靜地躺著乌询,像睡著了一般。 火紅的嫁衣襯著肌膚如雪豌研。 梳的紋絲不亂的頭發(fā)上妹田,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音鹃共,去河邊找鬼鬼佣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛霜浴,可吹牛的內(nèi)容都是我干的沮趣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼坷随,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼房铭!你這毒婦竟也來了驻龟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤缸匪,失蹤者是張志新(化名)和其女友劉穎翁狐,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凌蔬,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡露懒,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了砂心。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片懈词。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖辩诞,靈堂內(nèi)的尸體忽然破棺而出坎弯,到底是詐尸還是另有隱情,我是刑警寧澤译暂,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布抠忘,位于F島的核電站,受9級(jí)特大地震影響外永,放射性物質(zhì)發(fā)生泄漏崎脉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一伯顶、第九天 我趴在偏房一處隱蔽的房頂上張望囚灼。 院中可真熱鬧,春花似錦祭衩、人聲如沸啦撮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽赃春。三九已至,卻和暖如春劫乱,著一層夾襖步出監(jiān)牢的瞬間织中,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工衷戈, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狭吼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓殖妇,卻偏偏與公主長(zhǎng)得像刁笙,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

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