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

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

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

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

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

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

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

下面的內(nèi)容是activity的一個簡略的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() 這種代碼。該庫有以下幾個優(yōu)勢:

  1. 沒有拖慢程序速度。

  2. 改善view查找。

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

  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");
       }
     }
    

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

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

下面是一些更高級的用法:

綁定、解綁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();
 }
}

注冊監(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;
}

給多個組件設(shè)置同一個監(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>

//下面的代碼將兩個button綁定到一個list中, 并通過操作這個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的一個功能, 從網(wǎng)絡(luò)下載圖片并顯示. 這里用到了一個同樣有名的開源庫: Picasso.

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

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

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

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

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的一個典型使用方式, 該庫還包含其他的對圖片的操作,
例如:

  1. placeholder(R.mipmap.loading). 占位圖片, 可以是一個資源或者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);

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

下面是Picasso和ButterKnife一起用的場景, 在通過Picasso下載圖片時, 使用 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>

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

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

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

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

例如下面的例子對類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

高級用法<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();
    

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

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

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

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

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

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

下面是Retrofit的一個典型應(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)行請求

    1. 同步請求:

      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)建請求
          Call<Taco> call = tacoApi.randomTaco(true);
          // 執(zhí)行請求
          Taco taco = call.execute().body();
      
    2. 異步請求

      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. 通過注解修改請求的url

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

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

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

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

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

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

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

下面是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)建一個RealmConfiguration對象, 將Realm文件存儲到
    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支持存儲一個已存在的類實(shí)例, 或者通過傳入class文件直接存儲一個
    新的類實(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. 異步寫, 需要傳入兩個個回調(diào)類對象參數(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.
    }
    

番外篇: 簡便啟動activity - Dart + Henson<a id="orgheadline23"></a>

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

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)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容