Android Model正確使用姿勢——AutoValue
[TOC]
最近看到幾篇博客是關于AutoValue的,然后自己十分喜歡,一下子覺的這樣寫代碼很優(yōu)雅,所以決定自己也寫一篇文章介紹下AutoValue。
本文最先發(fā)表于Github,如有轉載,請注明轉載出處。
前言
首先說Android Model,在開發(fā)中網絡請求,以及數據庫操作等,我們都會定義一個Model,不同人對這個的說法不一樣,比如有Entry,Bean,Pojo。
然后開發(fā)的過程中會遇到下面問題:
構成方法:自定義構造方法,如果實體比較復雜,可能會用到工廠模式或者是建造者模式
序列化:比如實現Serializable接口,Parcelable接口。
Json解析:有時候直接使用的是json數據,比如@SerializedName注解。
自定義方法:對Model的字段有setter,getter方法,toString的實現,在處理hash的時候,需要實現equals和hashcode方法。
以上這么問題,其實在Eclipse和Android Studio中都是有快捷功能幫我們自動生成,后面的代碼示例,就是我用Android Studio自動生成的。
比如下面一個User類是我們的本次示例的一個Model,如果按照正常的寫法,是這樣的。
public abstract class User implements Serializable {
@SerializedName("id")
private int id;
@SerializedName("name")
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
if (id != user.id) return false;
return name != null ? name.equals(user.name) : user.name == null;
}
@Override
public int hashCode() {
int result = id;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
簡介
官方文檔給出的解釋是這樣的,大致意思是說是一個生成Java不可變的值類型工具,仔細研讀源代碼后,使用的技術是Java Apt,這個后面再做詳細解釋。
AutoValue - Immutable value-type code generation for Java 1.6+.
簡單使用
按照上面的例子,如果是AutoValue,代碼是這樣的。
首先需要在Android項目里面引入apt功能,在項目根目錄的gradle中添加,
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
// 引入apt插件
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
其次在module(一般是app目錄)中gradle使用apt插件。
apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'
最后加入AutoValue依賴。
dependencies {
provided 'com.google.auto.value:auto-value:1.3'
apt 'com.google.auto.value:auto-value:1.3'
}
修改User類,如下所示,User已經變成了一個抽象類,類似于使用Retrofit一樣,申明已經變成了一個接口,然后實現類是由AutoValue生成的代碼。
import com.google.auto.value.AutoValue;
@AutoValue
public abstract class User {
public abstract int id();
public abstract String name();
public static User newInstance(int id, String name) {
return new AutoValue_User(id, name);
}
}
我們可以看看AutoValue到底干了什么?
AutoValue會自動生成一個AutoValue_User,這個類是繼承了上面申明的User類,這個是默認default的訪問權限,那么在其他package中是無法訪問的,這樣在其他代碼里面也不會看到這么奇怪的名字。
同時所有的字段都是final類型,如果字段是對象類型的,那么還不能為空,這個問題先保留,后面再做詳解。因為申明的是final類型,那么所有的字段都是沒有setter方法的。
代碼里同時也實現了equals、hashcode、toString方法。
final class AutoValue_User extends User {
private final int id;
private final String name;
AutoValue_User(
int id,
String name) {
this.id = id;
if (name == null) {
throw new NullPointerException("Null name");
}
this.name = name;
}
@Override
public int id() {
return id;
}
@Override
public String name() {
return name;
}
@Override
public String toString() {
return "User{"
+ "id=" + id + ", "
+ "name=" + name
+ "}";
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof User) {
User that = (User) o;
return (this.id == that.id())
&& (this.name.equals(that.name()));
}
return false;
}
@Override
public int hashCode() {
int h = 1;
h *= 1000003;
h ^= this.id;
h *= 1000003;
h ^= this.name.hashCode();
return h;
}
}
Immutable/Value types
剛剛上面說到,所有的字段都是final類型,那么而且實現類也是final的,有個專業(yè)術語叫Immutable。
Immutable/Value types 這個概念對有些朋友來說可能還比較陌生,簡單來說就是一個數據對象一旦構造完成,就再也無法修改了。
這樣有什么好處呢?最大的好處就是多線程訪問可以省去很多同步控制,因為它們是不可變的,一旦構造完成,就不會存在多線程競爭訪問問題了。多線程最麻煩的處理就是控制好讀寫問題,如果大家都是讀,那么就不存控制了,所以省去了很多同步操作。
更多關于Immutable 的介紹,可以參閱 wiki 。
舉個Java中的例子:String和StringBuilder,String是immutable的,每次對于String對象的修改都將產生一個新的String對象,而原來的對象保持不變,而StringBuilder是mutable,因為每次對于它的對象的修改都作用于該對象本身,并沒有產生新的對象。
Immutable objects 比傳統(tǒng)的mutable對象在多線程應用中更具有優(yōu)勢,它不僅能夠保證對象的狀態(tài)不被改變,而且還可以不使用鎖機制就能被其他線程共享。
總結下Immutable對象的優(yōu)缺點:
優(yōu)點
- Immutable對象是線程安全的,可以不用被synchronize就在并發(fā)環(huán)境中共享
- Immutable對象簡化了程序開發(fā),因為它無需使用額外的鎖機制就可以在線程間共享
- Immutable對象提高了程序的性能,因為它減少了synchroinzed的使用
- Immutable對象是可以被重復使用的,你可以將它們緩存起來重復使用,就像字符串字面量和整型數字一樣。你可以使用靜態(tài)工廠方法來提供類似于valueOf()這樣的方法,它可以從緩存中返回一個已經存在的Immutable對象,而不是重新創(chuàng)建一個。
缺點
Immutable也有一個缺點就是會制造大量垃圾,由于他們不能被重用而且對于它們的使用就是”用“然后”扔“,字符串就是一個典型的例子,它會創(chuàng)造很多的垃圾,給垃圾收集帶來很大的麻煩。當然這只是個極端的例子,合理的使用immutable對象會創(chuàng)造很大的價值。
高級使用
Nullable
上面說過如果類中有對象類型的成員變量,那么是為非空的,但是在實際情況下,有的字段的是值就是為null,所以在申明時候可申明為Nullable就可以了。
import android.support.annotation.Nullable;
import com.google.auto.value.AutoValue;
import com.google.gson.annotations.SerializedName;
@AutoValue
public abstract class NullableUser {
@SerializedName("id")
public abstract int id();
@Nullable
@SerializedName("name")
public abstract String name();
public static NullableUser newInstance(int id, String name) {
return new AutoValue_NullableUser(id, name);
}
}
生成代碼:
final class AutoValue_NullableUser extends NullableUser {
private final int id;
private final String name;
AutoValue_NullableUser(
int id,
@Nullable String name) {
this.id = id;
this.name = name;
}
}
測試用例
@Test(expected = NullPointerException.class)
public void testUserNullPointException() throws Exception {
User.newInstance(100, null);
}
@Test
public void testUserNullable() {
NullableUser user = NullableUser.newInstance(100, "test");
System.out.println("user = " + user);
Assert.assertEquals(user.id(), 100);
Assert.assertEquals(user.name(), "test");
}
Gson序列化
Gson 使用比較麻煩,在普通的Model中,只需要在字段上面添加 @SerializedName注解即可。但是使用AutoValue,稍微有點繁瑣。
首先需要引入一個依賴包,這里是Auto value gson Github。
provided 'com.ryanharter.auto.value:auto-value-gson:0.4.4'
apt 'com.ryanharter.auto.value:auto-value-gson:0.4.4'
其次申明的抽象類中,每個方法上面添加對應的注解,然后再添加一個typeAdapter方法,申明這個方法,Gson就會根據這個找到對應的adapter,如下所示。
@AutoValue
public abstract class User {
@SerializedName("id")
public abstract int id();
@SerializedName("name")
public abstract String name();
public static User newInstance(int id, String name) {
return new AutoValue_User(id, name);
}
public static TypeAdapter<User> typeAdapter(Gson gson) {
return new AutoValue_User.GsonTypeAdapter(gson);
}
}
typeAdapter方法模板如下,T就是你當前Model的名字,寫完以后會出現錯誤,沒事重新編譯下就好了,這樣就會重新生成了代碼。
public static TypeAdapter<T> typeAdapter(Gson gson) {
return new AutoValue_T.GsonTypeAdapter(gson);
}
第三申明一個TypeAdapterFactory的一個實現類,這個類是abstract的,AutoValue也會自動生成其實現類。
@GsonTypeAdapterFactory
public abstract class MyAdapterFactory implements TypeAdapterFactory {
public static TypeAdapterFactory create() {
return new AutoValueGson_MyAdapterFactory();
}
}
最后是單元測試,在json字符串轉Model的時候,會使用一個Gson對象,這個對象不是平常使用的對象,需要自定義配置一些東西,然后這里就用到了上面所申明的MyAdapterFactory。
@Test
public void testUserToJson() {
User user = User.newInstance(100, "test");
String json = new Gson().toJson(user);
System.out.println(json);
Assert.assertEquals("{\"id\":100,\"name\":\"test\"}", json);
}
@Test
public void testUserParseFromJson() {
String json = "{\"id\":100,\"name\":\"test\"}";
// 自定義的Gson對象,需要配置 MyAdapterFactory
Gson gson = new GsonBuilder().registerTypeAdapterFactory(MyAdapterFactory.create()).create();
User user = gson.fromJson(json, User.class);
System.out.println(user);
Assert.assertNotNull(user);
Assert.assertEquals(user.name(), "test");
Assert.assertEquals(user.id(), 100);
NullableUser nullableUser = gson.fromJson(json, NullableUser.class);
System.out.println(nullableUser);
Assert.assertNotNull(nullableUser);
Assert.assertEquals(nullableUser.name(), "test");
Assert.assertEquals(nullableUser.id(), 100);
}
Serializable & Parcelable
Serializable是Java自帶的序列化方式,和AutoValue結合不影響原先使用,只需要在申明的Model中實現Serializable接口即可。
Parcelable是Android提供的序列化方式,如果需要和AutoValue結合使用,和Serializable基本差不多,實現相關接口,然后在Gradle文件引入相關apt依賴即可。
apt 'com.ryanharter.auto.value:auto-value-parcel:0.2.5'
// Optionally for TypeAdapter support
// compile 'com.ryanharter.auto.value:auto-value-parcel-adapter:0.2.5'
上面的auto-value-parcel-adapter是可選項,是auto-value-parcel提供自定義類型轉化,相關使用可以參見Github地址。
檢查下Autovalue自動給我們實現的代碼,果然不出所料,全部自動生成了。
final class AutoValue_User extends $AutoValue_User {
public static final Parcelable.Creator<AutoValue_User> CREATOR = new Parcelable.Creator<AutoValue_User>() {
@Override
public AutoValue_User createFromParcel(Parcel in) {
return new AutoValue_User(
in.readInt(),
in.readString()
);
}
@Override
public AutoValue_User[] newArray(int size) {
return new AutoValue_User[size];
}
};
AutoValue_User(int id, String name) {
super(id, name);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id());
dest.writeString(name());
}
@Override
public int describeContents() {
return 0;
}
}
Retrofit和Rxjava結合使用
Android 開發(fā)的時候,很多開發(fā)者使用Retrofit這個網絡庫,以及RxJava異步工具。下面舉例如何結合使用AutoValue,Retrofit,Rxjava。
這里有個獲取天氣的接口,返回的結果是json,我們用這個來測試下Retrofit。
// https://api.thinkpage.cn/v3/weather/now.json?key=x4qjfuniyu97mt9y&location=beijing&language=zh-Hans&unit=c
{
"results": [
{
"location": {
"id": "WX4FBXXFKE4F",
"name": "北京",
"country": "CN",
"path": "北京,北京,中國",
"timezone": "Asia/Shanghai",
"timezone_offset": "+08:00"
},
"now": {
"text": "霾",
"code": "31",
"temperature": "10"
},
"last_update": "2016-12-02T14:45:00+08:00"
}
]
}
申明Retrofit Api接口,一個普通的調用,一個是RxJava的方式。
public interface IWeatherApi {
@GET("/v3/weather/now.json?key=x4qjfuniyu97mt9y&location=beijing&language=zh-Hans&unit=c")
Call<Weather> getWeather();
@GET("/v3/weather/now.json?key=x4qjfuniyu97mt9y&location=beijing&language=zh-Hans&unit=c")
Observable<Weather> getWeatherWithRx();
}
Retrofit 接口創(chuàng)建
public class RetrofitUtil {
public static <T> T createApi(@NonNull Class<T> tClass, Gson gson) {
return new Retrofit.Builder()
.baseUrl("https://api.thinkpage.cn")
.client(new OkHttpClient.Builder().build())
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(tClass);
}
}
Weather Model申明
public abstract class Weather {
@SerializedName("results")
public abstract List<ResultsItem> results();
public static TypeAdapter<Weather> typeAdapter(Gson gson) {
return new AutoValue_Weather.GsonTypeAdapter(gson);
}
}
測試用例,注意:Retrofit使用Gson和前面使用Gson使用方式一樣,需要自己自定義,不然無法解決json解析問題。
@Test
public void testRetrofitWithAutoValue() {
Gson gson = new GsonBuilder().registerTypeAdapterFactory(MyAdapterFactory.create()).create();
IWeatherApi weatherApi = RetrofitUtil.createApi(IWeatherApi.class, gson);
try {
// 同步調用
Weather weather = weatherApi.getWeather().execute().body();
Assert.assertNotNull(weather);
System.out.println(weather);
// Rxjava 使用
weatherApi.getWeatherWithRx().subscribe(new Action1<Weather>() {
@Override
public void call(Weather weather) {
System.out.println(weather);
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
運行結果,正常的返回天氣信息。
Weather{results=[ResultsItem{now=Now{code=31, temperature=9, text=霾}, lastUpdate=2016-12-02T14:15:00+08:00, location=Location{country=CN, path=北京,北京,中國, timezone=Asia/Shanghai, timezoneOffset=+08:00, name=北京, id=WX4FBXXFKE4F}}]}
Weather{results=[ResultsItem{now=Now{code=31, temperature=9, text=霾}, lastUpdate=2016-12-02T14:15:00+08:00, location=Location{country=CN, path=北京,北京,中國, timezone=Asia/Shanghai, timezoneOffset=+08:00, name=北京, id=WX4FBXXFKE4F}}]}
相關插件
RoboPOJOGenerator
GsonFormat是一款Android Studio的插件,它可以把json字符串,轉變成Model對象,很多人都喜歡用它。
但是如果使用了AutoValue,那么原先的插件就不能使用了,沒有關系,本來打算自己高仿GsonFormat重新寫了一個插件,以實現我們的需求,后面又發(fā)現有一款插件可以實現——RoboPOJOGenerator。
RoboPOJOGenerator使用, RoboPOJOGenerator Github地址
AutoValue plugin
上面我們發(fā)現有了json字符串,有時候還要寫factory和buildder方法,那么問題來了,沒有插件能幫我們實現這個步驟,然代碼更加的優(yōu)雅,開發(fā)更加高效?
答案是肯定的,Autovalue plugin就是干這個事的。
我們用剛剛上面的Weather做演示,相關演示:
原理介紹
本文重點介紹的AutoValue只是 Google Auto 中的一小部分,Auto中還有其他好玩的。
AutoFactory
AutoFactory和AutoValue類似,可以自動幫助代碼生成工廠類,兼容Java 依賴注入標準(JSR-330)。
代碼示例
@AutoFactory
public class FactoryUser {
private final int id;
private final String name;
public FactoryUser(int id, String name) {
this.id = id;
this.name = name;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "FactoryUser{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
生成后的代碼
public final class FactoryUserFactory {
@Inject
public FactoryUserFactory() {
}
public FactoryUser create(int id, String name) {
return new FactoryUser(id, name);
}
}
測試代碼
@Test
public void testFactoryUser() {
FactoryUser user = new FactoryUserFactory().create(100, "test");
System.out.println(user);
Assert.assertNotNull(user);
Assert.assertEquals(100, user.getId());
Assert.assertEquals("test", user.getName());
}
AutoService
AutoService比較簡單,就是在使用Java APT的時候,使用AutoService注解,可以自動生成meta信息。
AutoCommon
這個是Google對Java Apt的一個擴展,一般的在自己寫Apt的時候,都需要繼承AbstractProcessor,但是google對它進行了擴展,BasicAnnotationProcessor,如果你想自己寫個工具,那么就可以使用這個了。
給大家舉個栗子,Dagger當初是Square公司受到Guice的啟發(fā),然后自己開發(fā)出一套依賴注入框架,當時Dagger使用的是Java反射,大家知道Java反射的效率其實并不高。
再后來都到了AutoValue的啟發(fā),在Dagger的分支上切個新分支,開發(fā)出Dagger2,然后這個Dagger2是由Google維護的,我們可以在Dagger2的Github上面找到證據。
Auto相關使用
IntentBuilder
有時候幾個Activity之間相互跳轉的時候需要傳遞一些參數,這些參數可以是基本類型,也有可能是復雜的類型,如果是負責的類型,必須要實現Serializable 或 Parcelable接口,上面也有介紹。
下面推IntentBuilder,IntentBuilder也是利用代碼生成的方法實現的。
Activity傳參
@IntentBuilder
class DetailActivity extends Activity {
@Extra
String id;
@Extra @Nullable
String title;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DetailActivityIntentBuilder.inject(getIntent(), this);
// TODO use id and title
}
}
// 調用方式
startActivity(
new DetailActivityIntentBuilder("12345")
.title("MyTitle")
.build(context)
)
Service傳參
@IntentBuilder
class DownloadService extends IntentService {
@Extra
String downloadUrl;
@Override
protected void onHandleIntent(Intent intent) {
MyServiceIntentBuilder.inject(intent, this);
}
}
startService(new DownloadServiceIntentBuilder("http://google.com").build(context))
FragmentArgs
上面介紹了Activity、Service的傳參,但Fragment的傳參方式是不一樣的,還有需要提醒一句一般通過setter方法給Fragment傳參是不是正確的方式,必須通過setArgs的方式。
相關代碼示例:
@FragmentWithArgs
public class MyFragment extends Fragment {
@Arg
int id;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentArgs.inject(this);
// inject 之后,就可以使用 id 了
}
}
MyFragment fragment = MyFragmentBuilder.newMyFragment(101);
其他相關
Kotlin Data Class
Kotlin 是一個基于 JVM 的新的編程語言,由 JetBrains 開發(fā)。有機會可以向大家介紹這種語言。
Kotlin 中提供一種類似于AutoValue中的功能,Data Class表示這個類似是一個數據類型。
比如下面是kotlin中對Model的寫法,就是這么的簡單、明了、優(yōu)雅。
data class KotlinUser(val id: Int,
val name: String)
Kotlin與Java是可以相互調用的。下面是Java的測試用例。
public class UserTest {
@Test
public void testUser() {
KotlinUser user = new KotlinUser(100, "test");
System.out.println(user);
Assert.assertEquals(100, user.getId());
Assert.assertEquals("test", user.getName());
}
}
我們可以反編譯Kotlin生成的class字節(jié)碼,看看這個中間到底發(fā)生了什么,很明顯Kotlin做了很多的語法糖,這里編譯器生成的代碼和上面Autovalue生成的代碼很像。
Object-C
Object-C中可以過直接申明@property方式,然后就可以自動實現setter和getter方法,如果要實現Immutable type方式,需要注明readonly。
hash、equals、description如果使用APPCode,代碼是可以自動生成的。
@interface OcUser : NSObject
@property(readonly) int id;
@property(retain, readonly) NSString *name;
- (instancetype)initWithId:(int)id name:(NSString *)name;
- (NSString *)description;
- (BOOL)isEqual:(id)other;
- (BOOL)isEqualToUser:(OcUser *)user;
- (NSUInteger)hash;
@end
// ==========================
#import "OcUser.h"
@implementation OcUser {
}
- (instancetype)initWithId:(int)id name:(NSString *)name {
self = [super init];
if (self) {
_id = id;
_name = name;
}
return self;
}
- (BOOL)isEqual:(id)other {
if (other == self)
return YES;
if (!other || ![[other class] isEqual:[self class]])
return NO;
return [self isEqualToUser:other];
}
- (BOOL)isEqualToUser:(OcUser *)user {
if (self == user)
return YES;
if (user == nil)
return NO;
if (self.id != user.id)
return NO;
return !(self.name != user.name && ![self.name isEqualToString:user.name]);
}
- (NSUInteger)hash {
NSUInteger hash = (NSUInteger) self.id;
hash = hash * 31u + [self.name hash];
return hash;
}
- (NSString *)description {
NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: ", NSStringFromClass([self class])];
[description appendFormat:@"self.id=%i", self.id];
[description appendFormat:@", self.name=%@", self.name];
[description appendString:@">"];
return description;
}
@end
測試用例
#import <Foundation/Foundation.h>
#import "OcUser.h"
int main(int argc, const char *argv[]) {
@autoreleasepool {
OcUser *user = [[OcUser alloc] initWithId:100 name:@"test"];
NSLog(@"user = %@", user);
}
return 0;
}
// 運行結果
// user = <OcUser: self.id=100, self.name=test>
總結
本文主要介紹了Autovalue的主要用法,以及AutoValu周邊只是,可能說的比較多,比較雜,而且有的地方也不夠深入,但是個人覺的這是一種思路,一種解決方案,后面如果自己需要造輪子的時候,我們是可以借鑒的。
本示例代碼地址 AutoValueDemo