Android 的日常

這邊主要抽一些日常的小撇步。

不要用 Thread 或 HandlerThread 推薦使用 AsyncTask

如果要背景下載檔案,應該使用 IntentService 而不是自己去操作 Thread。

如果是這個 Activity 內的長時間存取,可以使用 AsyncTask 。如有必要請搭配 LoaderManager 使用。

不要用 Handle Message 推薦使用 post(Runnable)

通常你其實需要的是 post(Runnable)

post(() -> updateProgress());

這樣不用再管理編號了。

static final int MSG_UPDATE_PROGRESS = 0;

// ...

    MSG_UPDATE_PROGRESS:
        updateProgress();
        break;

// ...

推薦使用 postDelayed(milliseconds, Runnable) 來做延遲呼叫

防彈跳推薦 removeCallback(Runnable) + postDelayed(Runnable) 不再量測時間 timeout

removeCallback(updateProgressRunnable);
postDelayed(300, updateProgressRunnable);

不再 findViewById 推薦使用 ButterKnife

After:

@BindView(R.id.username)
TextView usernameView;


// ...

@Override public void onCreate(Bundle savedInstanceState) {
   // ...
   ButterKnife.bind(this);
}

Before:

TextView usernameView;

@Override public void onCreate(Bundle savedInstanceState) {
   // ...
   usernameView = (TextView) findViewById(R.id.username);
}

使用 @Nullable/@NonNull 標注

以及各種 support-annotations

  • @IntDef
  • @Keep
  • etc.

宣告型別應盡可能抽象型別,應 ListArrayList

  • MapHashMap
List<String> getNames(List<User> users) {
    List<String> names = new ArrayList<>();
    for (User user : users) names.add(user.name());
    return names;
}

使用泛型建置技巧 new ArrayList<>()

java7 之後可省略型別,直接推定型別

List<String> names = new ArrayList<>();

java6 沒有內建推定型別,可透過推定型別函式來包裝,常見的 apache common-lang 或者 guava 函式庫也有此範例:

List<String> names = newArrayList();

public static <T> ArrayList<T> newArrayList() {
    return new ArrayList<T>();
}

IDE 推薦使用 Android Studio 而不是 Eclipse/ADT

...

推薦使用 gradle 建置系統而不是 ant

還有一些其他選項:

  • buck
  • maven
  • sbt
  • kobalt

有個基本原則,就是支援 maven 套件中心的建置系統

該傳遞 Context 就不要傳遞 Activity,避免記憶體浪費

ImageView imageView = new ImageView(activity.getApplicationContext());

不該:

ImageView imageView = new ImageView(activity);

同樣的,函式庫的開發者,盡可能不要紀錄 activity:

public class ImageLoader {
    Context context;
    public ImageLoader(Activity activity) { this(activity.getApplicationContext()); }
    public ImageLoader(Context context) { this.context = context; }
}

參考樹可能類似:

applicationContext <- activity

如果你只是需要 getResources() 之類的行為,並不需要 activity 整個實體,你可能只需要最基礎的 context 那麼就不需要握著 activity 不讓 gc 去釋出。

詳細的部份, 我們可以看 ContextWrapper/Impl

常見的取得 LayoutInflater

LayoutInflater.from(Context);

常見的取得 context

view.getContext();

UI 執行緒執行

activity.runOnUiThread(runnable);
view.post(runnable);
new Handler(Looper.getMainLooper()).post(runnable);

使用 Objects 工具類別來簡化 null 檢查

Before:

if (!mBuildConfigFields.equals(that.mBuildConfigFields)) {
    return false;
}
if (!mConsumerProguardFiles.equals(that.mConsumerProguardFiles)) {
    return false;
}
if (!mManifestPlaceholders.equals(that.mManifestPlaceholders)) {
    return false;
}
if (mMultiDexEnabled != null ? !mMultiDexEnabled.equals(that.mMultiDexEnabled) :
        that.mMultiDexEnabled != null) {
    return false;
}
if (mMultiDexKeepFile != null ? !mMultiDexKeepFile.equals(that.mMultiDexKeepFile) :
        that.mMultiDexKeepFile != null) {
    return false;
}
if (mMultiDexKeepProguard != null ? !mMultiDexKeepProguard.equals(that.mMultiDexKeepProguard) :
        that.mMultiDexKeepProguard != null) {
    return false;
}
if (!mProguardFiles.equals(that.mProguardFiles)) {
    return false;
}
if (!mResValues.equals(that.mResValues)) {
    return false;
}
return true;

After:

return Objects.equal(mBuildConfigFields, that.mBuildConfigFields) &&
        Objects.equal(mConsumerProguardFiles, that.mConsumerProguardFiles) &&
        Objects.equal(mManifestPlaceholders, that.mManifestPlaceholders) &&
        Objects.equal(mMultiDexEnabled, that.mMultiDexEnabled) &&
        Objects.equal(mMultiDexKeepFile, that.mMultiDexKeepFile) &&
        Objects.equal(mMultiDexKeepProguard, that.mMultiDexKeepProguard) &&
        Objects.equal(mProguardFiles, that.mProguardFiles) &&
        Objects.equal(mResValues, that.mResValues);

使用 Objects 工具類別來產生 hashcode

Before:

int result = mBuildConfigFields.hashCode();
result = 31 * result + mResValues.hashCode();
result = 31 * result + mProguardFiles.hashCode();
result = 31 * result + mConsumerProguardFiles.hashCode();
result = 31 * result + mManifestPlaceholders.hashCode();
result = 31 * result + (mMultiDexEnabled != null ? mMultiDexEnabled.hashCode() : 0);
result = 31 * result + (mMultiDexKeepFile != null ? mMultiDexKeepFile.hashCode() : 0);
result = 31 * result + (mMultiDexKeepProguard != null ? mMultiDexKeepProguard.hashCode() : 0);
return result;

After:

return java.util.Objects.hashCode(
        mBuildConfigFields,
        mResValues,
        mProguardFiles,
        mConsumerProguardFiles,
        mManifestPlaceholders,
        mMultiDexEnabled,
        mMultiDexKeepFile,
        mMultiDexKeepProguard);

善用 @CallSuper

@CallSuper
@Override
public void onResume() {
    super.onResume();
}

盡可能使用 private final mMember

成員變數名稱開頭,慣用小寫 "m" 開頭:mMember

不要直接 Handler 成員變數,避免記憶體浪費,可改用 WeakReference 包裝

before:

private final Handler mHandler = new Handler();

after:

private final WeakReference<Handler> mHandler = new WeakReference<>(new Handler());

static final 常數慣用大寫

static final float PI = 3.14f;

static 變數慣用小寫 "s" 開頭

private static Runtime sRuntime = new Runtime();

不要輕易使用 static 變數,避免記憶體浪費

static Drawable sBackground;

非得要用了話,請用 WeakReference 裝起來:

static WeakReference<Drawable> sBackground;

不要輕易使用 static 變數在 View 上,避免記憶體浪費

static TextView sView;

因為 view 本身帶有 Context ,非得要用了話,請用 WeakReference 裝起來:

static WeakReference<TextView> sView;

工具類別應不給繼承且不給建構子

public final class Utils {
    private Utils() {
        throw new UnsupportedException();
    }
}

回傳空 List 應該用 return Collections.emptyList(); 而不是 return new ArrayList<>();

List<String> getNames(List<User> users) {
    if (users == null) return Collections.emptyList();

    List<String> names = new ArrayList<>();
    for (User user : users) names.add(user.name());
    return names;
}

回傳空 Map 應該用 return Collections.emptyMap(); 而不是 return new HashMap<>();

Map<String, User> getNames(List<User> users) {
    if (users == null) return Collections.emptyMap();

    Map<String, User> map = new HashMap<>();
    for (User user : users) map.put(user.id(), user);
    return map;
}

大量的字串串接,請用 StringBuilder 來避免不必要的低消

System.out.println("Hello" + ", " + "world" + "!");
new StringBuilder().append("Hello").append(", ").append("world").append("!");

SystemService 取得方法,如 NotificationManager 請改用 context.getSystemService(NotificationManager.class)

Before:

NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

After:

NotificationManager notificationManager = context.getSystemService(NotificationManager.class);

好處是不用強轉型了。

// 另外推薦函式庫 com.github.yongjhih:android-system-services:1.0.0
NotificationManager notificationManager = SystemServices.from(context).getNotificationManager();

Listener/Callback 設計三兩事

就算我們只想要知道 onPageSelected(position) ,但是所有的方法還是都要假實現:

pager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
    @Override
    public void onPageScrollStateChanged(int state) {
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int position) {
       System.out.println(position);
    }
});

public interface ViewPager.OnPageChangeListener() {
    void onPageScrollStateChanged(int state);
    void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
    void onPageSelected(int position);
}

所以一般我們都習慣寫一個空類別來偷懶一下:

pager.setOnPageChangeListener(new SimpleOnPageChangeListener() {
    @Override
    public void onPageSelected(int position) {
       System.out.println(position);
    }
});

public class SimpleOnPageChangeListener implements OnPageChangeListener {
    @Override
    public void onPageScrollStateChanged(int state) {
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
    }

    @Override
    public void onPageSelected(int position) {
    }
}

由於已經有 lambda 的幫助下,我們可以想到這個用法:

pager.setOnPageChangeListener(new SimplerOnPageChangeListener().onPageSelected(position -> {
   System.out.println(position);
}));

public class SimplerOnPageChangeListener implements OnPageChangeListener {
    @Override
    public void onPageScrollStateChanged(int state) {
        if (onPageScrollStateChanged == null) return;
        onPageScrollStateChanged.call(state);
    }

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (onPageScrolled == null) return;
        onPageScrolled.call(position, positionOffset, positionOffsetPixels);
    }

    @Override
    public void onPageSelected(int position) {
        if (onPageSelected == null) return;
        onPageSelected.call(position);
    }

    Action1<Integer> onPageScrollStateChanged;
    Action3<Integer, Float, Integer> onPageScrolled;
    Action1<Integer> onPageSelected;

    public SimplerOnPageChangeListener onPageScrollStateChanged(Action1<Integer> onPageScrollStateChanged) {
        this.onPageScrollStateChanged = onPageScrollStateChanged;
        return this;
    }

    public SimplerOnPageChangeListener onPageScrolled(Action3<Integer, Float, Integer> onPageScrolled) {
        this.onPageScrolled = onPageScrolled;
        return this;
    }

    public SimplerOnPageChangeListener onPageSelected(Action1<Integer> onPageSelected) {
        this.onPageSelected = onPageSelected;
        return this;
    }

    public SimplerOnPageChangeListener onPageChange(Action1<Integer> onPageSelected) {
        return onPageSelected(onPageSelected);
    }

    public SimplerOnPageChangeListener onPageChange(Action1<Integer> onPageSelected, Action1<Integer> onPageScrollStateChanged) {
        return onPageSelected(onPageSelected).onPageScrollStateChanged(onPageScrollStateChanged);
    }

    public SimplerOnPageChangeListener onPageChange(Action1<Integer> onPageSelected, Action1<Integer> onPageScrollStateChanged, Action3<Integer, Float, Integer> onPageScrolled) {
        return onPageSelected(onPageSelected).onPageScrollStateChanged(onPageScrollStateChanged).onPageScrolled(onPageScrolled);
    }

    public interface Action1<T> {
        void call(T t);
    }

    public interface Action3<T, T2, T3> {
        void call(T t, T2 t2, T3 t3);
    }
}

泛型 Generic 應用於 Callback

如果你是設計一個 Github RESTful API 客戶端,可能會是這樣寫:

github.request("yongjhih/rxparse", new RequestListener() {
    @Override public void onComplete(String json) {
        List<Contributor> contributors = Contributors.parse(json);
        // ...
    }
    @Override public void onException(Exception e) {
    }
  });

github.request("yongjhih", new RequestListener() {
    @Override public void onComplete(String json) {
        List<Repository> repositories = Repositories.parse(json);
        // ...
    }
    @Override public void onException(Exception e) {
    }
  });

interface RequestListener {
    void onComplete(String json);
    void onException(Exception e);
}

class GitHub {
    public void request(String endpoint, RequestListener listener) {
        // ...
        // listener.onComplete(json); or listener.onException(e);
    }
}

我們可以看到,每一個 callback 內都要寫一個 parse() 才得以使用,或許你會修改成:

github.contributors("yongjhih/rxparse", new ContributorsRequestListener() {
    @Override public void onComplete(List<Contributor> contributors) {
        // ...
    }
    @Override public void onException(Exception e) {
    }
  });

github.repositories("yongjhih", new RepositoriesRequestListener() {
    @Override public void onComplete(List<Repository> repositories) {
        // ...
    }
    @Override public void onException(Exception e) {
    }
  });

interface ContributorsRequestListener {
    void onComplete(List<Contributor> contributors);
    void onException(Exception e);
}

interface RepositoriesRequestListener {
    void onComplete(List<Repository> repositories);
    void onException(Exception e);
}

class GitHub {
    // ...
    public void contributors(String endpoint, ContributorsRequestListener listener) {
        request(endpoint, new RequestListener() {
            @Override public void onComplete(String json) {
                listener(Contributors.parse(json));
            }
            @Override public void onException(Exception e) {
                listener(e);
            }
        });
    }

    public void repositories(String endpoint, RepositoriesRequestListener listener) {
        request(endpoint, new RequestListener() {
            @Override public void onComplete(String json) {
                listener(Repositories.parse(json));
            }
            @Override public void onException(Exception e) {
                listener(e);
            }
        });
    }
}

這樣使用上確實有簡化了,但是這樣的 Listener 的數量就跟著回傳 model 種類成長,會造成維護上的繁瑣。

我們可以透過泛型來簡化,寫更通用一點介面:

github.contributors("yongjhih/rxparse", new SimpleRequestListener<>() {
    @Override public void onComplete(List<Contributor> contributors) {
        // ...
    }
    @Override public void onException(Exception e) {
    }
  });

github.repositories("yongjhih", new SimpleRequestListener<>() {
    @Override public void onComplete(List<Repository> repositories) {
        // ...
    }
    @Override public void onException(Exception e) {
    }
  });

interface SimpleRequestListener<T> {
    void onComplete(List<T> list);
    void onException(Exception e);
}

class GitHub {
    // ...
    public void contributors(String endpoint, SimpleRequestListener<Contributor> listener) {
        request(endpoint, new RequestListener() {
            @Override public void onComplete(String json) {
                listener(Contributors.parse(json));
            }
            @Override public void onException(Exception e) {
                listener(e);
            }
        });
    }

    public void repositories(String endpoint, SimpleRequestListener<Repository> listener) {
        request(endpoint, new RequestListener() {
            @Override public void onComplete(String json) {
                listener(Repositories.parse(json));
            }
            @Override public void onException(Exception e) {
                listener(e);
            }
        });
    }
}

這樣就可以維持一個 Listener 介面而已。

另外,對內維護上,如果有很多 API 要寫,複製貼上的程式碼片段仍然大了一點,所以再設計一個通用實做:

class GitHub {
    // ...
    public <T extends Parsable> void request(String endpoint, SimpleRequestListener<T> listener) {
        request(endpoint, new RequestListener() {
            @Override public void onComplete(String json) {
                listener(T.parse(json));
            }
            @Override public void onException(Exception e) {
                listener(e);
            }
        });
    }

    public void contributors(String endpoint, SimpleRequestListener<Contributor> listener) {
        request(endpoint, listener);
    }

    public void repositories(String endpoint, SimpleRequestListener<Repository> listener) {
        request(endpoint, listener);
    }
}

interface Parsable<T> {
    T parse(String json);
}

public class Contributor implements Parsable {
    public Contributor() {}
    public static List<Contributor> parse(String json) {
        // ...
        return contributors;
    }
}

留意,泛型目前還沒有重載(Overloading)的能力,所以不能有類似:

class JsonParser {
    String toJson(List<Repository> repositories) {
    }

    String toJson(List<Contributor> Contributors) {
    }
}

需要表明泛型:

String contributorsJson = JsonParser.toJson(contributors);
String repositoriesJson = JsonParser.toJson(repositories);

class JsonParser {
    static <T extends Contributor> String toJson(Collection<T> list) {
        return Contributors.toJson(list);
    }
    static <T extends Repository> String toJson(Collection<T> list) {
        return Repositories.toJson(list);
    }
}

或者改用介面取代重載:

class JsonParser {
    <T extends Jsonable> String toJson(Collection<T> list) {
        return T.toJson(list);
    }
}

interface Jsonable<T> {
    String toJson(Collection<T> list);
}

public class Contributor implements Jsonable {
    public Contributor() {}
    public static String toJson(Collection<T> list) {
        // ...
        return contributors;
    }
}

無參數具回傳值的介面: Callable

interface java.util.concurrent.Callable<V> {
    V call();
}
interface Runnable {
    void run();
}

results matching ""

    No results matching ""