Swift51.com
麦子学院 头像
麦子学院  2016-10-13 23:16

Android开发中的一些注意事项

回复:0  查看:2242  

开发中,我们很有必要掌握一些套路和技巧,提升代码开发效率,今天和大家分享的就是android开发中的一些套路,一起来看看吧。

 

initView()与updateView()

 

通常,我会添加一个 initView() 方法来初始化所有的 View 对象,在这个方法的具体实现中,可能会有两种不同的细微差别。第一种是仅仅做 findViewById() 就好了,也就是仅仅是去找到每一个 View 对象,而不去给它们设置属性,比如 setText() 之类的。另一种则是在 findViewById() 后,顺便给它们设置初始值。

 

我更倾向于第一种做法,因为如果你在 initView ()方法中给 View 设置一些属性,那么当一些数据变更时,你可能也需要去变更 View 的一些属性,你必然会有一个 updateView() 这样的方法。 updateView() 方法中,需要根据当前页面的状态和数据去给 View 设值,问题就在于,当需求发生变化的时候,你可能需要改两个地方, initView() 和 updateView() 。考虑到这一点。最佳的做法就是你需要一个 initView() 方法和一个 updateView() 方法。

 

initView() 方法只做初始化操作,也就是仅仅只会发生一次的操作,比如 findViewById() , setListener() 之类的。而 updateView() 方法中,则是去做一些根据某些成员变量,flagboolean值之类的去变更 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。也就是说,这个单例通常情况下会一直在内存中的,也会一直监听remoteprofile变化,并且会去拉去最新的数据,请注意这里的 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