原文地址 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ì):
沒有拖慢程序速度惨撇。
改善view查找。
改善監(jiān)聽函數(shù)注冊(cè)府寒。
-
改善資源查找魁衙。
<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)包括:
- 進(jìn)行HTTP請(qǐng)求.
- 緩存圖片.
- 簡(jiǎn)單的"resize/裁剪/居中/放大"操作.
- 負(fù)責(zé)在主線程之外進(jìn)行http請(qǐng)求.
- 對(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ì)圖片的操作,
例如:
- placeholder(R.mipmap.loading). 占位圖片, 可以是一個(gè)資源或者drawable
- error(R.drawable.sad_taco) . 如果加載失敗顯示的圖片
- fit(). 將圖片大小縮減到imageView的大小.
- resize(imgWidth, imgHeight). 縮減到指定圖片大小. 單位是px
- centerCrop(). 居中裁剪.
- 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):
- (可以)不需要在類中使用注解.
- 性能好.
- 使用廣泛.
- 默認(rèn)包含類(包括父類)的所有域.
- 支持多維數(shù)組.
- 當(dāng)序列化時(shí), 類的值為null的變量會(huì)被跳過.
- 反序列化時(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>
-
如果變量名和json的域名不同, 可以使用
@SerializeName()
注解修飾.public class Taco { @SerializedName("serialized_labels") private String tag; }
-
通過Gson的API客制化輸出.
//如果變量值為null,則輸出中也輸出null,而不是忽略. Gson gson = new GsonBuilder().serializeNulls().create(); //保留空格 Gson gson = new GsonBuilder().setPrettyPrinting().create();
-
設(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):
- 類型安全.
- 支持認(rèn)證.
- 支持json的序列化和反序列化.
- 支持RxJava
- 支持同步和異步請(qǐng)求.
典型使用<a id="orgheadline16"></a>
下面是Retrofit的一個(gè)典型應(yīng)用, (更多介紹可以看這里):
-
定義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); }
-
使用api進(jìn)行請(qǐng)求
-
同步請(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();
-
異步請(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>
-
通過注解修改請(qǐng)求的url
@POST("http://taco-randomizer.herokuapp.com/v2/taco") private Call<Taco> getFromNewAPI();
-
添加請(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):
- 為手機(jī)而生.
- 可以快到使用同步.
- 支持一個(gè)應(yīng)用包含多個(gè)Realm數(shù)據(jù)庫.(Sqlite只有一個(gè)).
下面是Realm在App中的應(yīng)用實(shí)例:
-
需要持久化的類需要繼承RealmObject:
public class Taco extends RealmObject { private String description; private String tag; private String imageUrl; private boolean favorite; //getters and setters }
-
配置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();
-
持久化. 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();
-
獲取數(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
-
刪除操作:
// 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), 如下代碼:
-
同步寫:
//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"); } });
異步寫, 需要傳入兩個(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) {}
});-
跟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();
-
為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"); } });
-
為防止內(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>
-
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 ... } }
-
使用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);