首页 > 编程语言 > Android开发 > Android开发套路收集整理与讨论
2016
10-11

Android开发套路收集整理与讨论

以下做法纯属个人习惯,欢迎讨论: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

编程技巧