Android开发中的一些注意事项
在开发中,我们很有必要掌握一些套路和技巧,提升代码开发效率,今天和大家分享的就是android开发中的一些套路,一起来看看吧。
initView()与updateView()
通常,我会添加一个 initView() 方法来初始化所有的 View 对象,在这个方法的具体实现中,可能会有两种不同的细微差别。第一种是仅仅做 findViewById() 就好了,也就是仅仅是去找到每一个 View 对象,而不去给它们设置属性,比如 setText() 之类的。另一种则是在 findViewById() 后,顺便给它们设置初始值。
我更倾向于第一种做法,因为如果你在 initView ()方法中给 View 设置一些属性,那么当一些数据变更时,你可能也需要去变更 View 的一些属性,你必然会有一个 updateView() 这样的方法。 updateView() 方法中,需要根据当前页面的状态和数据去给 View 设值,问题就在于,当需求发生变化的时候,你可能需要改两个地方, initView() 和 updateView() 。考虑到这一点。最佳的做法就是你需要一个 initView() 方法和一个 updateView() 方法。
initView() 方法只做初始化操作,也就是仅仅只会发生一次的操作,比如 findViewById() , setListener() 之类的。而 updateView() 方法中,则是去做一些根据某些成员变量,flag,boolean值之类的去变更 View 的属性,会被反复调用的操作。
关于 updateView() 方法,我又有两种不同的思路,在此之前,先具体的说明一下 updateView() 中要干的工作。比如我们有一些成员变量 dataA , dataB ,有一些会随之变化的 View , ViewA1 , ViewA2 , ViewB1 , ViewB2 ……然后当数据 dataA 改变时,我们需要更改 ViewA1 , ViewA2 的属性,当数据 dataB 改变时,我们要更改 ViewB* 的属性,于是,我们通常写的 updateView() 方法是这样的。
private void updateView() {
...
viewA1.setText(dataA.getContent());
viewA2.setTextColor(dataA.getTextColor());
viewB1.setImage(dataB.getImage());
viewB2.setText(dataB.getTitle());
...
}
在我们的 Activity / Fragment 比较简单的时候,这样写应该没有什么问题,但是当页面的逻辑因需求的变更而变得越来越复杂,我们可能需要维持很多很多的成员变量(数据)和 View 。那么 updateView() 方法可能里面做了很多很多的工作,这样调用一次必然是效率低下的。因此,我认为另一种比较好的方式是将数据A所关联的Views都封装成一个方法,数据B所关联的 Views 都封装成另一个方法,像这样。
private void updateAViews() {
viewA1.setText(dataA.getContent());
viewA2.setTextColor(dataA.getTextColor());
...
}
private void updateBViews() {
viewB1.setImage(dataB.getImage());
viewB2.setText(dataB.getTitle());
...
}
private void updateAllViews() {
updateAViews();
updateBViews();
...
}
显然,第二种方式是效率最好的一种方式,也是维护起来最麻烦的一种方式,但我个人还是比较倾向于第二种写法。因为有一些 View 它的 onDraw() 方法本身真的会消耗比较长的时间,如果简单粗暴的更新所有的 View ,可能会让UI的流畅度大打折扣。
使用boolean值来避免updateView()中的空指针异常
当我们使用 initView() 和 updateView() 两个方法来变更 View 的时候,要注意空指针的情况,因为调用 updateView 的时机不是自己能控制的, updateView 可能是在网络数据返回时调用,那么如果 onCreate 的时候先请求数据,数据马上返回了并调用 updateView 方法,这个时候, initView 还没有执行,那么 updateView 中对 View 的操作就会报空指针异常。
我们可以使用一个 boolean 值来解决这个问题。
提前考虑Activity和Fragment的复用
当我们写 Activity 或 Fragment 的时候需要考虑到这个页面可能会从哪些地方调过来。比如说,我们要完成一个需求,这个需求是显示一个列表,列表里面有特定的数据,这个页面必须要自己全新写一个 Activity 或 Fragment来完成,入口也只有一个,那么我们几乎是可以“为所欲为”的实现这个页面,想怎么写就怎么写。
但是当需求发生了变化,比如其他地方也可以点击进入你这个页面,并且还显示了不一样的数据,考虑到页面复用这一点,我们应该通过传入不同的参数,来改变这个页面的行为(应该显示怎么样的数据,或者UI上有哪些其他的变化)。
所以,在我们全新写这个页面的时候,就应该有所收敛,要主动思考一下,因为这个页面如果是被复用的,那么一般来说,是这个页面的样式,行为会被复用。不一样的地方往往是数据,页面的复用,就要考虑到在 onCreate的时候可以传入不同的参数,完成不同的要求和显示。
我们应该在 Activity 或 Fragment 中添加几个成员变量,用来标记状态,比如:
public class DataListActivity extends Activity {
public static final int DATA_TYPE_ALL = 1;
public static final int DATA_TYPE_PART = 2;
private int mDataType = DATA_TYPE_ALL;
...
}
这样,我们内部获取数据的时候就根据这个 mDataType 来做具体的处理就好了。考虑到复用这一点,后面扩展的时候就会更游刃有余。并且这个 mDataType 也许会影响到UI上的一些表现, updateView 系列方法可能也需要关心这个(些)变量的情况。
通过封装好的静态方法启动Activity
初学的时候,我们总是是用下面类似的代码启动 Activity 。
Intent i = new Intent();
i.setClass(context, TargetActivity.class);
context.startActivity(i);
但是,根据上一个小主题上面所说的,往往我们需要告诉要启动的 Activity 一些特定的信息,然后展示出不同的行为,一般有两种常见的写法。
方式A:
public class TargetActivity extends Activity {
public static final String INTENT_KEY_DATA_TYPE = "INTENT_KEY_DATA_TYPE";
public static final int DATA_TYPE_ALL = 1;
public static final int DATA_TYPE_PART = 2;
public static void start(Context c, int dataType) {
Intent i = new Intent();
i.setClass(c, TargetActivity.class);
i.putExtras(INTENT_KEY_DATA_TYPE, dataType);
c.startActivity(i);
}
}
//in other Activity
TargetActivity.start(context, TargetActivity.DATA_TYPE_ALL);
方式B:
public class TargetActivity extends Activity {
public static final String INTENT_KEY_DATA_TYPE = "INTENT_KEY_DATA_TYPE";
public static final int DATA_TYPE_ALL = 1;
public static final int DATA_TYPE_PART = 2;
public static Intent obtainIntent(Context, int dataType) {
Intent i = new Intent();
i.setClass(c, TargetActivity.class);
i.putExtras(INTENT_KEY_DATA_TYPE, dataType);
return i;
}
}
//in other Activity.
startActivity(TargetActivity.obtainIntent(this, TargetActivity.DATA_TYPE_ALL));
方式A更简洁,方式B更繁琐一些,但是方式B更好,因为有时候我们需要启动的 Activity 结束时返回一些东西,那么我们需要调用到 startActivityForResult() 方法来启动,在当前的 Activity 调用这个方法,必须要获取到 Intent 对象,所以,方式B的 obtainIntent 使用情况就更广泛了。
但在编写 obtianIntent 方法的时候,建议让它带上你需要传递的参数,当前的demo是只有一个 int 型的 dataType ,也许你还有很多其他的参数,但都请在 obtainIntent 方法中就给 Intent 填上,这样外面(其他)的 Activity就不需要去填写这些额外的信息了,你的 INTENT_KEY 可以完全的定义在要用它的内部,这样做真是又干净又漂亮。
父类应该减轻子类的负担,而不是给子类添加约束
上面几个话题,我们讲了几个常见的套路做法,这样可以使代码更加清晰,更加易于维护。
但是我们习惯的套路中那些 initView , updateView , obtainIntent 等方法,并不适合移动到父类去,因为这不是逻辑,如果你挪到父类中写成抽象方法,方法就是限定死了,所有的子类都要有这个 initView 方法,这样是不合适的,不同的人也许有不同的代码习惯,因此将多余的流程挪到父类,就会形成对子类的约束。子类中如果有重复的逻辑,才是应该移动到父类的。
监听器,观察者模式,回调
其实监听器和观察者模式,回调都是一样的东西,表面上看,它们就是一群叫 OnXxxxx 的一群方法或者接口。
它们负责告诉你一些事件发生了,比如系统给你的 onClick , onTouch , onSrcoll ……还可以是在新的线程发起一个网络请求,当请求结果返回时,告诉你,像 onResult , onPush ……这样的形式。
总之,当你理解了这个东西,你就可以熟练的使用,当你想写一个控件,这个控件要完成一个功能或者一些特性,你需要提供一些回调接口来供客户程序员使用。比如我之前写过一个底部有loading的控件,滚动到底部的时候,会出现一个loading(转菊花),然后给你一个“时机”来让你请求数据,然后让 adapter 更新数据。这里有是具体的代码: BottomLoadListView.java in github
通常,我们可以把这个回调接口都让 Activity 或者 Fragment 来实现,像这样:
public class MyActivity extends Activity implement OnClickListener, OnNetworkChangeListener, IOnRequestCallback{
...
}
这样,这个 Activity 内部的一些对象需要回调接口的时候,直接给它 this 即可,就不需要那么多匿名内部类了,而这些回调方法都放在 Activity 中,当它们被调用的时候,也能很好的控制整个 Activity 的行为,是很方便的。
多个页面共用数据与回调
通常,我们某一个页面( Activity / Fragment )需要显示一些数据,这些数据的引用都是让 Activity 自己持有的,如果仅仅是一个页面需要这些数据,这么做没有什么问题,当我们有两个页面需要对同一份数据进行操作的时候,这样做就不太方便了。通常可以写一个名为 XxxxEngine 的东西,xxx具体是什么跟所关联的业务逻辑有关,比如说是消息列表,那么就叫 MessageEngine 好了。
这个 Engine 一般会写成单例模式,然后让它来持有数据的引用,而两个或多个页面需要对这份消息列表(message list)进行操作的时候,就通过这个 Engine 来获取就行了。
使用 Engine 还有另一个场景,就是两个页面都需要监听某一个网络push,比如说在多终端的情况下,我们有一个个人信息页面,个人信息是可以在别的终端被修改的,那么我们的页面就会收到一个通知,有时候,通知回调是不带数据的,我们需要手动去拉去数据,就算带上了数据,如果两个页面都监听这个网络回调,也会有问题,因为这样就有两份数据,或者说有两个地方会对数据进行操作。我用来代码来演示。
public class ProfileActivity extends Activity implement OnProfileChangedListener, OnResultForProfileRequest {
private Profile mProfile = null;
//当别的终端更新了个人信息后调用这里
@override
public void onProfileChanged() {
ProfileManager.getInstance().requestProfile(this); //传入OnResultForProfileRequest接口
}
//当requestProfile()请求结果返回时调用
@override
public void onResult(Profile profile) {
mProfile = profile;
updateView();
}
}
上面代码展示了一个页面收到数据变更的通知以及请求数据的情况,那么当我们有两个页面都需要关心数据发生变化的时候,如果两个页面都像上面这样写,那么我们就有两处来请求数据,这样是不好的,因为两个地方用的是同一份数据,这样根据上面说的,我们需要一个 ProfileEngine 来维持这份数据的引用,另一方面,我们可以把 profile changed 的监听,放在 ProfileEngine 上,这样就只有它一个地方收到变化的通知,一个地方来拉取最新数据,更新好了之后,再通知两个(多个)页面通过单例来获取最新的数据。这种情形下,我们需要定义一个本地的接口。
public class ProfileEngine implement OnRemoteProfileChangedListener, OnResultForProfileRequest {
public interface OnLocalProfileChangedListener {
void onLocalProfileChanged(Profile newProfile);
}
private Profile mProfile = null;
//监听列表
private ArrayList<OnLocalProfileChangedListener> mListeners = new ArrayList<>();
//当别的终端更新了个人信息后调用这里
@override
public void onProfileChanged() {
ProfileManager.getInstance().requestProfile(this); //传入OnResultForProfileRequest接口
}
//当requestProfile()请求结果返回时调用
@override
public void onResult(Profile profile) {
mProfile = profile;
}
//通知所有的页面,profile发生了变更,并且已经取好了最新的数据了,拿过去更新UI就好了
private void notifyListener() {
for (OnLocalProfileChangedListener l : mListeners) {
l.onLocalProfileChanged(mProfile);
}
}
}
这个套路感觉真的很简洁干练,但我们需要注意一个问题就是本地的监听的注册与反注册。
单例一旦被创建就不会被销毁了,除非进程被干掉,或者我们主动置空( null )并且GC。也就是说,这个单例通常情况下会一直在内存中的,也会一直监听remote的profile变化,并且会去拉去最新的数据,请注意这里的 mListeners ,里面存放的两个页面( Activity / Fragment ),如果我们没有在页面销毁( onDestory )的时候将自己从监听列表中移除,那么 mListeners 就会一直持有Activity的引用,但是页面却已经是消失了,这样就造成了内存泄露。因此一定要严格的在 onCreate 和 onDestory 中调用注册与反注册方法。
一种网络请求套路
这种网络请求套路也是最近才学习到的,感觉非常的简单巧妙。
//发起一个请求检查一下数据是否有变更,如果有变更,会通过通知onChanged()告诉客户端,无参数无返回值
void check();
//通知,告知客户端数据有变更,要拉取最新数据需要另一个接口,无参数,无返回值
void onChanged();
//通过网络拉取数据,无返回值,传入回调接口,因为是异步返回数据
void request(onRequestResult);
//请求数据的回调接口,参数中是最新的数据
void onRequestResult(Data)
//通过网络更新数据,无返回值,通过参数传入新数据和回调接口
void set(Data, OnSetResult);
//更新数据的回调接口,参数表示有没有成功,以及最新的数据,同时也会调用onChanged()方法
void onSetResult(int, Data);
可以发现,数据变化的时候,总是会调用 onChanged() 方法,而这仅仅是通知,获取数据需要自己手动去拉取一次。这样我们有统一的时机可以获取最新的数据。
文章来源:SegmentFault