原文一开始是写在csdn上的,复制过来
以下做法纯属个人习惯,欢迎讨论:D
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()
方法,而这仅仅是通知,获取数据需要自己手动去拉取一次。这样我们有统一的时机可以获取最新的数据。
以上做法纯属个人习惯,欢迎讨论:D