IT行业,一直讲一句话,拼到最后都拼的是“内功”,而内功往往就是指我们处理问题的思路、经验、想法,而对于开发者来说,甚至对于产品也一样,都离不开一个“宝典”,就是设计模式。今天我们一起借助Android源码去探索一下建造者模式的优缺点,以及它所想要去解决的问题。同时结合我工作经验中的一个小例子,来总结实践一下。

1.背景&定义

理解:
建造者模式是创建性设计模式的一种。是我们最常见、也可能是开发者肯定会使用的一种设计模式。
先从建造者这个词来理解,应该是用于建造一个东西而存在的设计模式。现实生活中对应的人或物或者事情,在代码的世界中,都可以通过行为和属性抽象为一个对象,而往往对象越复杂,new一个对象时,我们需要不断的set去创建、而且,复杂对象的组装过程,也是需要特定的顺序,这时,为了解耦构建过程和组装过程,建造者模式应运而生
**我的理解:**该模式是为了将复杂对象的构建过程和组装过程相分离,对外不可见。我们知道设计模式离不开一个词解耦,建造者模式,为了解耦 构建过程和组装过程,使构建过程可动态扩展,对组装过程进行封装

定义:
将一个复杂对象的构建与表示相分离,使得同样的构建过程可以创建不同的表示。

2.UML类图设计

android基于源码的开发 android源码与设计模式_android基于源码的开发

3.源码中的建造者模式

在Android源码中,最常使用的Builder模式就是AlertDialog.Builder。使用该Builder创建不同的复杂的AlertDialog对象。我们接下来分析一下AlertDialog源码。I看一下android源码如何实现的?是否和我们上面想的UML一样。

首先看到AlertDialog内部有一个内部类,Builder类,不出所料的话,这应该是一个静态内部类(为什么是静态内部类?记得【Android进阶】篇章里面我们说到的内部类引起的内存泄露了吗?如果忘记了,快去复习一下吧!!!)

android基于源码的开发 android源码与设计模式_设计模式_02


AlertDialog.Builder

//看到了没有,这里是静态内部类哦
public static class Builder {
        @UnsupportedAppUsage
        //的确是使用了builder方式,但是AlertDialog这里使用了一个AlertParams
        private final AlertController.AlertParams P;

        /**
         * Creates a builder for an alert dialog that uses the default alert
         * dialog theme.
         * <p>
         * The default alert dialog theme is defined by
         * {@link android.R.attr#alertDialogTheme} within the parent
         * {@code context}'s theme.
         *
         * @param context the parent context
         */
        public Builder(Context context) {
            this(context, resolveDialogTheme(context, Resources.ID_NULL));
        }
        //先省略很多代码
        //....
}

从这里源码分析,的确是使用了builder方式,但是AlertDialog这里使用了一个AlertParams,从字面意思理解,是Dialog的参数的一个类,我们点进去看一下,果不其然,AlertParams是封装所有Dialog属性参数的一个类而已。
AlertController.AlertParams.java

public static class AlertParams {
        public final Context mContext;
        public final LayoutInflater mInflater;
        //省略很多代码,从这里已经能看到,这里包含了很多属性参数

        public int mIconId = 0;
        public Drawable mIcon;
        public int mIconAttrId = 0;
        public CharSequence mTitle;
        public View mCustomTitleView;
        public CharSequence mMessage;
        public CharSequence mPositiveButtonText;
        public Drawable mPositiveButtonIcon;
        public DialogInterface.OnClickListener mPositiveButtonListener;
        public CharSequence mNegativeButtonText;
        public Drawable mNegativeButtonIcon;
        public DialogInterface.OnClickListener mNegativeButtonListener;
        public CharSequence mNeutralButtonText;
        public Drawable mNeutralButtonIcon;
        public DialogInterface.OnClickListener mNeutralButtonListener;
        public boolean mCancelable;
        public DialogInterface.OnCancelListener mOnCancelListener;
        public DialogInterface.OnDismissListener mOnDismissListener;
        public DialogInterface.OnKeyListener mOnKeyListener;
        public CharSequence[] mItems;
        public ListAdapter mAdapter;
        public DialogInterface.OnClickListener mOnClickListener;
        public int mViewLayoutResId;
        public View mView;
        public int mViewSpacingLeft;
        public int mViewSpacingTop;
        public int mViewSpacingRight;
        public int mViewSpacingBottom;
        public boolean mViewSpacingSpecified = false;
        public boolean[] mCheckedItems;
        public boolean mIsMultiChoice;
        public boolean mIsSingleChoice;
        public int mCheckedItem = -1;
        public DialogInterface.OnMultiChoiceClickListener mOnCheckboxClickListener;
        public Cursor mCursor;
        public String mLabelColumn;
        public String mIsCheckedColumn;
        public boolean mForceInverseBackground;
        public AdapterView.OnItemSelectedListener mOnItemSelectedListener;
        public OnPrepareListViewListener mOnPrepareListViewListener;
        public boolean mRecycleOnMeasure = true;
}

从这里已经能看到,这里包含了很多属性参数,这也是AlertDialog源码比较巧妙的一点,以后大家自己实现复杂对象的Builder模式时,我们也可以不要把所有的代码写在一个类中,可以把这部分参数属性分离(单一原则 & 其他模块也可复用)。

好了,我们接着看,AlertDialog.Builder里面的具体方法

public static class Builder {
		public Builder(Context context, int themeResId) {
            P = new AlertController.AlertParams(new ContextThemeWrapper(
                    context, resolveDialogTheme(context, themeResId)));
        }

        public Context getContext() {
            return P.mContext;
        }

        public Builder setTitle(@StringRes int titleId) {
            P.mTitle = P.mContext.getText(titleId);
            return this;
        }

        public Builder setTitle(CharSequence title) {
            P.mTitle = title;
            return this;
        }
        public Builder setCustomTitle(View customTitleView) {
            P.mCustomTitleView = customTitleView;
            return this;
        }

 
        public Builder setMessage(@StringRes int messageId) {
            P.mMessage = P.mContext.getText(messageId);
            return this;
        }
		//构建过程中,一直填充的是AlertParams属性
        public Builder setMessage(CharSequence message) {
            P.mMessage = message;
            return this;
        }
        //真正调用create时,才去创建了AlertDialog 对象,并且把AlertParams的参数,一一对应赋值给了AlertDialog 
         public AlertDialog create() {
            // Context has already been wrapped with the appropriate theme.
            final AlertDialog dialog = new AlertDialog(P.mContext, 0, false);
            //赋值在这里
            P.apply(dialog.mAlert);
            dialog.setCancelable(P.mCancelable);
            if (P.mCancelable) {
                dialog.setCanceledOnTouchOutside(true);
            }
            dialog.setOnCancelListener(P.mOnCancelListener);
            dialog.setOnDismissListener(P.mOnDismissListener);
            if (P.mOnKeyListener != null) {
                dialog.setOnKeyListener(P.mOnKeyListener);
            }
            return dialog;
        }

我们看一下 P.apply(dialog.mAlert); ``AlertParams.apply

public void apply(AlertController dialog) {
            if (mCustomTitleView != null) {
                dialog.setCustomTitle(mCustomTitleView);
            } else {
                if (mTitle != null) {
                    dialog.setTitle(mTitle);
                }
                if (mIcon != null) {
                    dialog.setIcon(mIcon);
                }
                if (mIconId != 0) {
                    dialog.setIcon(mIconId);
                }
                if (mIconAttrId != 0) {
                    dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
                }
            }
            if (mMessage != null) {
                dialog.setMessage(mMessage);
            }
            if (mPositiveButtonText != null || mPositiveButtonIcon != null) {
                dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
                        mPositiveButtonListener, null, mPositiveButtonIcon);
            }
            if (mNegativeButtonText != null || mNegativeButtonIcon != null) {
                dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
                        mNegativeButtonListener, null, mNegativeButtonIcon);
            }
            if (mNeutralButtonText != null || mNeutralButtonIcon != null) {
                dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
                        mNeutralButtonListener, null, mNeutralButtonIcon);
            }
            //......
  }

可以看到这里,的确是把AlertParams的参数,一一对应赋值给了AlertDialog 。看到这里,这时完成了AlertDialog的创建过程。但是最重要的show还没有看,我们继续扒一扒。

知识点引申扩展:Dialog show的源码分析

public void show() {
		//当前是否正在显示
        if (mShowing) {
        	//当前view不为null,并且有菜单标题栏,则去创建并显示
            if (mDecor != null) {
                if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                    mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
                }
                mDecor.setVisibility(View.VISIBLE);
            }
            //如果正在显示中,则return
            return;
        }

        mCanceled = false;

        if (!mCreated) {
        	//如果没有创建完成,代码view的contentview还未创建完成,则执行view的创建过程,就是onCreate方法
            dispatchOnCreate(null);
        } else {
            // Fill the DecorView in on any configuration changes that
            // may have occured while it was removed from the WindowManager.
            final Configuration config = mContext.getResources().getConfiguration();
            mWindow.getDecorView().dispatchConfigurationChanged(config);
        }
		//执行Dialog的onStart方法
        onStart();
        //获取当前的视图
        mDecor = mWindow.getDecorView();

        if (mActionBar == null && mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
            final ApplicationInfo info = mContext.getApplicationInfo();
            mWindow.setDefaultIcon(info.icon);
            mWindow.setDefaultLogo(info.logo);
            mActionBar = new WindowDecorActionBar(this);
        }

        WindowManager.LayoutParams l = mWindow.getAttributes();
        boolean restoreSoftInputMode = false;
        if ((l.softInputMode
                & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
            l.softInputMode |=
                    WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
            restoreSoftInputMode = true;
        }
		//将当前视图按照布局参数,添加到当前activity所处window的视图中
        mWindowManager.addView(mDecor, l);
        if (restoreSoftInputMode) {
            l.softInputMode &=
                    ~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
        }

        mShowing = true;

        sendShowMessage();
    }

Dialog.show函数关键做了以下三步:
1)dispatchOnCreate,调用Dialog的onCreate,创建视图view
2)执行Dialog的onStart方法
3)将当前视图按照布局参数,添加到当前dialog所处window的视图中

Dialog.dispatchOnCreate

很明显,其实Dialog的创建,也是一系列自定义生命周期函数的调用过程,我们接下来看一下AlertDialog的onCreate

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //mAlert在上面参数构建分析的时候,我们知道是一个封装类AlertController
        mAlert.installContent();
    }

AlertController.installContent

AlertController#installContent

public void installContent() {
		//选择指定的视图布局
        final int contentView = selectContentView();
        //window设置内容视图布局
        mDialog.setContentView(contentView);
        //初始化视图内容
        setupView();
    }

默认视图结构

android基于源码的开发 android源码与设计模式_okhttp_03

Dialog.setContentView

这里获取到layoutID之后,调用了Dialog.setContentView方法

/**
     * Set the screen content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the screen.
     * 
     * @param layoutResID Resource ID to be inflated.
     */
    public void setContentView(@LayoutRes int layoutResID) {
    	//这里的window是个啥,我们在Dialog的源码中找一找
        mWindow.setContentView(layoutResID);
    }

我们跟踪源码,可以看到是在Dialog的构造函数里面创建的
Dialog#构造函数·

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        if (createContextThemeWrapper) {
            if (themeResId == Resources.ID_NULL) {
                final TypedValue outValue = new TypedValue();
                context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
                themeResId = outValue.resourceId;
            }
            mContext = new ContextThemeWrapper(context, themeResId);
        } else {
            mContext = context;
        }

        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
		//可以看到创建了一个phonwwindow,这里很关键,我们知道单独的activity本身就是一个PhoneWindow,里面有DecorView,从这里我们可以看出,Dialog的创建,实际是单独的一个PhoneWindow对象
        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        w.setWindowManager(mWindowManager, null, null);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }

小结
调用AlertDialog的show函数之后,其实就是调用了AlertDialog的一系列生命函数,完成PhoneWindow的创建、视图的创建、视图的内容设置,然后通过WiindowManager,将创建的view add进去,最终用户就可以看到这个Dialog。
引入一个小的课后作用知识点,大家通过上面代码,知道了DIalog其实是创建了一个新的PhoneWindow,我们之前【Android进阶】中讲到过,activity实际上在Acitviy中,也会创建PhoneWindow对象, 这两者有什么区别吗?

4.使用场景&优缺点总结

Builder模式在Android开发中较为常用,通常作为配置类的构建器将配置的构建和表示分离开来,同时也是将配置从目标类中隔离出来,避免过多的setter方法。Builder模式比较常见的实现形式是通过调用链实现,这样的代码更简洁、易懂。

4.1 使用场景

(1)相同的方法,不同的执行顺序,产生不同的事件结果时,可以采用建造者模式
(2)多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式。
(3)产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适
(4)在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到时,也可以采用建造者模式封装该对象的创建过程

4.2 优点

(1)良好的封装性,使用构建者模式可以使客户端不必知道内部的组成细节。
(2)建造者独立,容易扩展。

4.3 缺点

会产生多于的Builder对象以及Director对象,消耗内存。

5.实践经验总结

5.1 自定义NavigationBar的设计

android开发中,顶部导航栏是常用的一个控件,如下

android基于源码的开发 android源码与设计模式_设计模式_04

其实导航栏无非以下几个步骤:
1)加载导航栏布局文件
2)构建视图元素
3)设置视图元素的文本、事件
4)添加到父视图,最后展示
5)这里不妨,我们增加一条,写一个固定的view很简单,如果支持扩展,需要考虑一下

package com.itbird.design.builder.navigationbar;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

/**
 * 自定义NavigationBar
 * Created by itbird on 2022/5/26
 */
public class NavigationBar {

    protected NavigationBar() {
    }

    public static class Builder {
        /**
         * 视图布局id
         */
        int layoutID;
        View mCurrentView;
        ViewGroup parentView;

        /**
         * 初始化view
         *
         * @param context
         * @param layoutID
         * @param rootView
         */
        public Builder(Context context, int layoutID, ViewGroup rootView) {
            this.layoutID = layoutID;
            this.parentView = rootView;
            mCurrentView = LayoutInflater.from(context).inflate(layoutID, rootView, false);
        }

        public Builder setBackColor(int color) {
            mCurrentView.setBackgroundColor(color);
            return this;
        }


        /**
         * 设置textview文本
         *
         * @param viewId
         * @param text
         * @return
         */
        public Builder setTextToTextView(int viewId, String text) {
            TextView textView = findViewByID(viewId);
            textView.setText(text);
            return this;
        }

        /**
         * 设置button文本
         *
         * @param viewId
         * @param text
         * @return
         */
        public Builder setTextToButtonView(int viewId, String text) {
            Button button = findViewByID(viewId);
            button.setText(text);
            return this;
        }

        /**
         * 设置button事件
         *
         * @param viewId
         * @param onClickListener
         * @return
         */
        public Builder setOnClickListenerToButtonView(int viewId, View.OnClickListener onClickListener) {
            Button button = findViewByID(viewId);
            button.setOnClickListener(onClickListener);
            return this;
        }

        /**
         * 展示view
         *
         * @return
         */
        public ViewGroup show() {
            parentView.addView(mCurrentView, 0);
            return parentView;
        }

        public <T extends View> T findViewByID(int viewID) {
            return mCurrentView.findViewById(viewID);
        }
    }

}

使用一下

private void testBuilderPatterm() {
        new NavigationBar.Builder(MainActivity.this, R.layout.navigation_layout, (ViewGroup) getWindow().getDecorView())
                .setBackColor(com.google.android.material.R.color.design_default_color_on_secondary)
                .setTextToButtonView(R.id.back_button, "返回")
                .setTextToTextView(R.id.title_textview, "我是标题")
                .setOnClickListenerToButtonView(R.id.back_button, new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        finish();
                    }
                }).show();
    }

5.2 通用Dialog框架设计

业务开发中,经常会开发各种各样的弹窗样式,例如:倒计时弹窗、进度条弹窗、等待弹窗、通知弹窗、两个button的弹窗、单个button的弹窗等等,在一个app或者一个系统中,往往弹窗风格肯定是统一的,所以大家为了方便使用,一般都会封装各种框架,这里小编一起和大家封装一个Dialog框架,满足以下需求
1)弹窗可以根据是否设置了哪些信息,去自动选择不同的dialog布局,例如:如果只设置了一个button的文本和事件,那么选择只有一个button的layout;如果设置了消息,则有通知区域,反之没有通知区域;
2)弹窗框架,需要做一些异常规避,而不是导致调用者的app崩溃,例如用户调用没有设置title,那么抛出相应异常或者错误给到调用者;例如没有找到相应layoutID,则通知调用者,而不是应用崩溃;
3)弹窗框架,需要包含多种样式,例如进度弹窗、倒计时弹窗

效果如下:

android基于源码的开发 android源码与设计模式_设计模式_05


话不多说,我们直接上代码。

5.2.1 公共弹窗控件封装

package com.itbird.design.builder.dialog;

import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Handler;
import android.os.Message;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.StyleRes;

import com.itbird.design.R;

import java.lang.ref.WeakReference;

/**
 * 公共弹窗框架封装
 * Created by xfkang on 2020/5/23.
 */


public class CommonDialog extends Dialog implements DialogInterface {

    private static final int DIALOG_STYLE_SMALL = 1;
    private static final int DIALOG_STYLE_NORMAL = 2;
    private static final int DIALOG_STYLE_HIGH = 3;

    private ButtonHandler handler;

    private View rootView;
    private int dialogStyle;

    private TextView titleTextView;
    private TextView messageTextView;
    private Button positiveButton;
    private Button negativeButton;

    private Message positiveMessage;
    private Message negativeMessage;


    public CommonDialog(@NonNull Context context) {
        super(context);
    }

    public CommonDialog(@NonNull Context context, @StyleRes int themeResId) {
        super(context, themeResId);
    }

    protected CommonDialog(@NonNull Context context, boolean cancelable, @Nullable OnCancelListener cancelListener) {
        super(context, cancelable, cancelListener);
    }

    CommonDialog(Builder builder) {
        super(builder.context, R.style.common_dialog_style);
        rootView = LayoutInflater.from(builder.context).inflate(getInflateLayout(builder), null);
        setContentView(rootView);
        setupView();
        handler = new ButtonHandler(this);
        setWindowStyle();
    }

    private void setWindowStyle() {
        Window window = getWindow();
        WindowManager.LayoutParams layoutParams = window.getAttributes();
        int width = getContext().getResources().getDimensionPixelOffset(R.dimen.dialog_width);
        int height = 0;
        switch (dialogStyle) {
            case DIALOG_STYLE_SMALL:
                height = getContext().getResources().getDimensionPixelOffset(R.dimen.dialog_small_height);
                break;
            case DIALOG_STYLE_NORMAL:
                height = getContext().getResources().getDimensionPixelOffset(R.dimen.dialog_normal_height);
                break;
            default:
                height = getContext().getResources().getDimensionPixelOffset(R.dimen.dialog_small_height);
                break;
        }
        layoutParams.width = width;
        layoutParams.height = height;
        window.setAttributes(layoutParams);
    }

    private void setupView() {
        titleTextView = (TextView) findViewById(R.id.title);
        messageTextView = (TextView) findViewById(R.id.message);
        positiveButton = (Button) findViewById(R.id.positive_button);
        if (positiveButton != null) {
            positiveButton.setOnClickListener(mButtonHandler);
        }
        negativeButton = (Button) findViewById(R.id.negative_button);
        if (negativeButton != null) {
            negativeButton.setOnClickListener(mButtonHandler);
        }
    }

    public void setTitle(String title) {
        if (titleTextView != null) {
            titleTextView.setText(title);
        }
    }

    public void setMessage(String message) {
        if (messageTextView != null) {
            messageTextView.setText(message);
        }
    }

    public void setPositiveButton(String text, final OnClickListener onClickListener) {
        if (positiveButton != null) {
            positiveButton.setText(text);

            if (onClickListener != null) {
                positiveMessage = handler.obtainMessage(DialogInterface.BUTTON_POSITIVE, onClickListener);
            }
        }
    }

    public void setNegativeButton(String text, final OnClickListener onClickListener) {
        if (negativeButton != null) {
            negativeButton.setText(text);

            if (onClickListener != null) {
                negativeMessage = handler.obtainMessage(DialogInterface.BUTTON_NEGATIVE, onClickListener);
            }
        }
    }

    private int getInflateLayout(Builder builder) {
        if (TextUtils.isEmpty(builder.title)) {
            throw new IllegalStateException("No title for dialog");
        }
        int layoutResID = 0;
        if (!TextUtils.isEmpty(builder.message)
                && TextUtils.isEmpty(builder.positiveText)
                && TextUtils.isEmpty(builder.negativeText)) {
            layoutResID = R.layout.common_no_button_dialog;
            dialogStyle = 1;
        }

        if (TextUtils.isEmpty(builder.message)
                && !TextUtils.isEmpty(builder.positiveText)
                && TextUtils.isEmpty(builder.negativeText)) {
            layoutResID = R.layout.common_no_message_one_button_dialog;
            dialogStyle = 1;
        }

        if (TextUtils.isEmpty(builder.message)
                && !TextUtils.isEmpty(builder.positiveText)
                && !TextUtils.isEmpty(builder.negativeText)) {
            layoutResID = R.layout.common_no_message_two_button_dialog;
            dialogStyle = 1;
        }

        if (!TextUtils.isEmpty(builder.message)
                && !TextUtils.isEmpty(builder.positiveText)
                && !TextUtils.isEmpty(builder.negativeText)) {
            layoutResID = R.layout.common_message_two_button_dialog;
            dialogStyle = 2;
        }

        if (!TextUtils.isEmpty(builder.message)
                && !TextUtils.isEmpty(builder.positiveText)
                && TextUtils.isEmpty(builder.negativeText)) {
            layoutResID = R.layout.common_message_one_button_dialog;
            dialogStyle = 2;
        }

        if (layoutResID == 0) {
            throw new IllegalStateException("Not have this dialog");
        }

        return layoutResID;
    }

    private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            final Message m;
            if (v == positiveButton && positiveMessage != null) {
                m = Message.obtain(positiveMessage);
            } else if (v == negativeButton && negativeMessage != null) {
                m = Message.obtain(negativeMessage);
            } else {
                m = null;
            }

            if (m != null) {
                m.sendToTarget();
            }

            handler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, CommonDialog.this)
                    .sendToTarget();
        }
    };


    public static class Builder {
        private String title;
        private String message;
        private String positiveText;
        private OnClickListener positiveOnClickListener;
        private String negativeText;
        private OnClickListener negativeOnCLickListener;
        private boolean cancelable = true;
        private OnCancelListener onCancelListener;
        private OnDismissListener onDismissListener;
        private OnKeyListener onKeyListener;

        private final Context context;

        public Builder(Context context) {
            this.context = context;
        }

        public Builder setTitle(@StringRes int resID) {
            this.title = context.getResources().getString(resID);
            return this;
        }

        public Builder setTitle(String title) {
            this.title = title;
            return this;
        }

        public Builder setMessage(@StringRes int resID) {
            this.message = context.getResources().getString(resID);
            return this;
        }

        public Builder setMessage(String message) {
            this.message = message;
            return this;
        }

        public Builder setPositiveButton(@StringRes int resID, OnClickListener onClickListener) {
            this.positiveText = context.getResources().getString(resID);
            this.positiveOnClickListener = onClickListener;
            return this;
        }

        public Builder setPositiveButton(String text, OnClickListener onClickListener) {
            this.positiveText = text;
            this.positiveOnClickListener = onClickListener;
            return this;
        }

        public Builder setNegativeButton(@StringRes int resID, OnClickListener onClickListener) {
            this.negativeText = context.getResources().getString(resID);
            this.negativeOnCLickListener = onClickListener;
            return this;
        }

        public Builder setNegativeButton(String text, OnClickListener onClickListener) {
            this.negativeText = text;
            this.negativeOnCLickListener = onClickListener;
            return this;
        }

        public Builder setCancelable(boolean cancelable) {
            this.cancelable = cancelable;
            return this;
        }

        public Builder setOnCancelListener(OnCancelListener onCancelListener) {
            this.onCancelListener = onCancelListener;
            return this;
        }

        public Builder setOnDismissListener(OnDismissListener onDismissListener) {
            this.onDismissListener = onDismissListener;
            return this;
        }

        public Builder setOnKeyListener(OnKeyListener onKeyListener) {
            this.onKeyListener = onKeyListener;
            return this;
        }

        public CommonDialog create() {
            CommonDialog commonDialog = new CommonDialog(this);
            apply(commonDialog);
            commonDialog.setCancelable(cancelable);
            if (cancelable) {
                commonDialog.setCanceledOnTouchOutside(true);
            }
            commonDialog.setOnCancelListener(onCancelListener);
            commonDialog.setOnDismissListener(onDismissListener);
            if (onKeyListener != null) {
                commonDialog.setOnKeyListener(onKeyListener);
            }
            return commonDialog;
        }

        public CommonDialog show() {
            CommonDialog commonDialog = create();
            commonDialog.show();
            return commonDialog;
        }

        private void apply(CommonDialog commonDialog) {
            commonDialog.setTitle(title);
            commonDialog.setMessage(message);
            commonDialog.setPositiveButton(positiveText, positiveOnClickListener);
            commonDialog.setNegativeButton(negativeText, negativeOnCLickListener);
        }

    }

    /**
     * 使用handler进行事件转发
     * 为了防止内存泄露,使用弱引用
     */
    private static final class ButtonHandler extends Handler {
        private static final int MSG_DISMISS_DIALOG = 1;

        private WeakReference<DialogInterface> mDialog;

        public ButtonHandler(DialogInterface dialog) {
            mDialog = new WeakReference<>(dialog);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case DialogInterface.BUTTON_POSITIVE:
                case DialogInterface.BUTTON_NEGATIVE:
                case DialogInterface.BUTTON_NEUTRAL:
                    ((OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
                    break;
                case MSG_DISMISS_DIALOG:
                    ((DialogInterface) msg.obj).dismiss();
            }
        }
    }
}

5.2.2 倒计时控件封装

倒计时实现方式有很多种,例如Rxjava、TimerTask等,但是我们是为了去封装一个控件,所以肯定不会去在框架中引用各种第三方框架的,应该去研究他们内部怎么去实现。不过我想应该逃脱不了handler、thread这些关键词吧。

实现方式
1)基于android.os.CountDownTimer的源码来设计实现,我们知道android原生这个控件是通过handler.postDelay来实现的,而且里面有进度回调,但是没有暂停和恢复,所以我们需要添加onPause、onRestart自定义方法,
2)基于 android.widget.TextClock的源码来设计实现,们知道android原生这个控件是通过handler.postAtTime + thread来实现的,我们之前是通过postDelay来触发消息事件的,但这里系统使用了postAtTime,这样就是设置了在某一个时间点抛出handler消息,前面的
long now = SystemClock.uptimeMillis();
long next = now + (1000 - now % 1000);
精确控制了1秒的整点时间,因此系统可以在每一个整秒的时间点发出消息。

CustomCountDownTimer.java

package com.itbird.design.builder.dialog;

import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;

/**
 * 使用android.os.CountDownTimer的源码
 * 添加了onPause、onRestart自定义方法
 * Created by xfkang on 16/3/18.
 */

public abstract class CustomCountDownTimer {
    private static final int MSG = 1;
    /**
     * 总倒计时时间
     * Millis since epoch when alarm should stop.
     */
    private final long mMillisInFuture;
    /**
     * 倒计时间隔时间
     * The interval in millis that the user receives callbacks
     */
    private final long mCountdownInterval;
    /**
     * 记录开始之后,应该停止的时间节点
     */
    private long mStopTimeInFuture;
    /**
     * 记录暂停的时间节点
     */
    private long mPauseTimeInFuture;
    /**
     * 对应于源码中的cancle,即计时停止时
     * boolean representing if the timer was cancelled
     */
    private boolean isStop = false;
    private boolean isPause = false;

    /**
     * @param millisInFuture    总倒计时时间
     * @param countDownInterval 倒计时间隔时间
     */
    public CustomCountDownTimer(long millisInFuture, long countDownInterval) {
        // 解决秒数有时会一开始就减去了2秒问题(如10秒总数的,刚开始就8999,然后没有不会显示9秒,直接到8秒)
        if (countDownInterval > 1000) {
            millisInFuture += 15;
        }
        mMillisInFuture = millisInFuture;
        mCountdownInterval = countDownInterval;
    }

    private synchronized CustomCountDownTimer start(long millisInFuture) {
        isStop = false;
        if (millisInFuture <= 0) {
            onFinish();
            return this;
        }
        mStopTimeInFuture = SystemClock.elapsedRealtime() + millisInFuture;
        mHandler.sendMessage(mHandler.obtainMessage(MSG));
        return this;
    }

    /**
     * 开始倒计时
     */
    public synchronized final void start() {
        start(mMillisInFuture);
    }

    /**
     * 停止倒计时
     */
    public synchronized final void stop() {
        isStop = true;
        mHandler.removeMessages(MSG);
    }

    /**
     * 暂时倒计时
     * 调用{@link #restart()}方法重新开始
     */
    public synchronized final void pause() {
        if (isStop) return;

        isPause = true;
        mPauseTimeInFuture = mStopTimeInFuture - SystemClock.elapsedRealtime();
        mHandler.removeMessages(MSG);
    }

    /**
     * 重新开始
     */
    public synchronized final void restart() {
        if (isStop || !isPause) return;

        isPause = false;
        start(mPauseTimeInFuture);
    }

    /**
     * 倒计时间隔回调
     *
     * @param millisUntilFinished 剩余毫秒数
     */
    public abstract void onTick(long millisUntilFinished);

    /**
     * 倒计时结束回调
     */
    public abstract void onFinish();


    private Handler mHandler = new Handler() {

        @Override
        public void handleMessage(Message msg) {

            synchronized (CustomCountDownTimer.this) {
                if (isStop || isPause) {
                    return;
                }

                final long millisLeft = mStopTimeInFuture - SystemClock.elapsedRealtime();
                if (millisLeft <= 0) {
                    onFinish();
                } else if (millisLeft < mCountdownInterval) {
                    // no tick, just delay until done
                    sendMessageDelayed(obtainMessage(MSG), millisLeft);
                } else {
                    long lastTickStart = SystemClock.elapsedRealtime();
                    onTick(millisLeft);

                    // take into account user's onTick taking time to execute
                    long delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();

                    // special case: user's onTick took more than interval to
                    // complete, skip to next interval
                    while (delay < 0) delay += mCountdownInterval;

                    sendMessageDelayed(obtainMessage(MSG), delay);
                }
            }
        }
    };
}

小结
不管使用哪种方式实现倒计时,一般绕不过去handler和thread,使用这两者需要解决两个问题,一个是handler持有引用导致内存泄露问题,一个是handler postDealy会有消息处理第一次的跳变问题(如果使用handler.postDealyed(……, 1000)方式来进行每秒的计时,是不准确的,是的,有很大误差,误差的原因在于在你收到消息,到你重新发出handler.postDealyed的时间,并不是瞬间完成的,这里面有很多逻辑处理的时间,即使没有逻辑处理的时间,handler本身也是耗损性能的,所以消息并不可能按照理想的1000延迟来进行发送,这就导致了误差的累积)
1)内存泄露的问题解决方法:弱引用
2)跳变问题的解决方法:通过时间校准(实现方式也有多种,例如TextClock的postAttime(now+1000-now%1000),或者CountDownTimer去加一定的时间,但是这个不太好控制,不建议使用这种),来确保消息是在整数节点发出

6.第三方框架中的建造者模式(Glide、Retrofit)

Retrofit相信大家不陌生了,我们也知道里面依赖了okhttp,OkHttpClient,这个是整个OkHttp的核心管理类,内部包含了请求调度器(Dispatcher),请求拦截器(interceptors),代理,读写超时时间等各种需要配置的对象。
我们反过来结合之前所总结的建造者设计模式适用的场景,来理解一下。这里不就符合第一条吗?

多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时,则可以使用该模式。

这些配置的对象就是构建OkhttpClient的多个部件/零件。配置不同,导致的运行结果也不同。就像读写超时配置的超时时间不同,导致请求结果允许超时时间也不同。

接下来,我们看一下OkHttpClient中如何创建对象的。(由于okttp这个类代码比较多,我们直接截图说明重点)

android基于源码的开发 android源码与设计模式_源码设计_06


我们看到OkHttpClient构造器里面传入了Builder,OkHttpClient的所有属性其实都依赖获取于Builder,这个Builder是个啥?

android基于源码的开发 android源码与设计模式_android基于源码的开发_07


其实就是OkHttpClient的一个内部类而已,这不正符合了使用场景中的3吗?

(3)产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适

还有一点,其实在这个过程中也用到了,不知道大家感觉到了没有,就是第4点

(4)在对象创建过程中会使用到系统中的一些其他对象,这些对象在产品对象的创建过程中不易得到时,也可以采用建造者模式封装该对象的创建过程

我们通过Builder去设置各种参数属性时,对于使用者来说,只需要关注这些需要设置的属性,而不需要关心,OkHttpClient内部用这些属性,去组合构造了不同的对象。这就是上面说的这点了。其实这也是符合我们之前所讲的,面向对象六大基本原则中的迪米特原则(最少知道原则)
其实说白了,就是您封装一个框架(通过各种设计模式),肯定要做到一点,对于调用者来说,不用关心内部细节,例如用哪些属性组合什么对象,而且可以尽量规避各种由于调用者输入异常、输入丢失导致的框架异常等问题。

封装一个框架需要做到的:
1)对于调用者来说,集成简单、使用简单,只需关注需要设置的参数即可
2)框架内部,对于异常参数输入、内部执行异常,有回调、规避手段
3)最重要的一点,设计一个框架,不可能完美覆盖所有的用户需求,所以要通过接口和抽象,去支持扩展,而非去修改您的框架去达到目的

整体设计模式Demo代码