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

1.背景&定义

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

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

2.UML类图设计

3.源码中的建造者模式

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

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

AlertDialog.Builder

//看到了没有,这里是静态内部类哦
public static class Builder {@UnsupportedAppUsage//的确是使用了builder方式,但是AlertDialog这里使用了一个AlertParamsprivate 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);}//如果正在显示中,则returnreturn;}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

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

AlertController.installContent

AlertController#installContent

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

默认视图结构

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开发中,顶部导航栏是常用的一个控件,如下

其实导航栏无非以下几个步骤:
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() {@Overridepublic void onClick(View v) {finish();}}).show();}

5.2 通用Dialog框架设计

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

效果如下:

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

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() {@Overridepublic 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);}@Overridepublic 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() {@Overridepublic 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 donesendMessageDelayed(obtainMessage(MSG), millisLeft);} else {long lastTickStart = SystemClock.elapsedRealtime();onTick(millisLeft);// take into account user's onTick taking time to executelong delay = lastTickStart + mCountdownInterval - SystemClock.elapsedRealtime();// special case: user's onTick took more than interval to// complete, skip to next intervalwhile (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这个类代码比较多,我们直接截图说明重点)

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

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

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

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

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

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

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

整体设计模式Demo代码

Android源码设计模式探索与实战【建造者模式】相关推荐

  1. Android源码设计模式探索与实战【外观模式】

    IT行业,一直讲一句话,拼到最后都拼的是"内功",而内功往往就是指我们处理问题的思路.经验.想法,而对于开发者来说,甚至对于产品也一样,都离不开一个"宝典",就 ...

  2. Android源码设计模式探索与实战【原型模式】

    IT行业,一直讲一句话,拼到最后都拼的是"内功",而内功往往就是指我们处理问题的思路.经验.想法,而对于开发者来说,甚至对于产品也一样,都离不开一个"宝典",就 ...

  3. 《Android源码设计模式解析与实战》读书笔记(十三)

    第十三章.备忘录模式 备忘录模式是一种行为模式,该模式用于保存对象当前的状态,并且在之后可以再次恢复到此状态,有点像是我们平常说的"后悔药". 1.定义 在不破坏封闭的前提下,捕获 ...

  4. 《Android源码设计模式解析与实战》读书笔记(十七)

    第十七章.中介者模式 中介者模式也称为调解者模式或调停者模式,是一种行为型模式. 1.定义 中介者模式包装了一系列对象相互作用的方式,使得这些对象不必相互明显作用.从而使它们可以松散耦合.当某些对象之 ...

  5. 《Android源码设计模式解析与实战》读书笔记(十二)

    第十二章.观察者模式 观察者模式是一个使用率非常高的模式,它最常用在GUI系统.订阅–发布系统.因为这个模式的一个重要作用就是解耦,将被观察者和观察者解耦,使得它们之间的依赖性更小,甚至做到毫无依赖. ...

  6. 《Android源码设计模式解析与实战》读书笔记(十四)

    第十四章.迭代器模式 迭代器模式,又叫做游标模式,是行为型设计模式之一.我们知道对容器对象的访问必然会涉及遍历算法,我们可以将遍历的方法封装在容器中,或者不提供遍历方法,让使用容器的人自己去实现去吧. ...

  7. 《Android源码设计模式解析与实战》读书笔记(二十四)

    第二十四章.桥接模式 桥接模式也称为桥梁模式,是结构型设计模式之一.桥接模式中体现了"单一职责原则"."开闭原则"."里氏替换原则".&qu ...

  8. 《Android源码设计模式解析与实战》读书笔记(二十一)

    第二十一章.装饰模式 装饰模式也称为包装模式,是结构型设计模式之一.装饰模式是一种用于替代继承技术的一种方案. 1.定义 动态的给一个对象添加一些额外的职责.就增加功能来说,装饰模式相比生成子类更为灵 ...

  9. 《Android源码设计模式解析与实战》读书笔记(十一)

    第十一章.命令模式 命令模式是行为型模式之一.总体来说并不难理解,只是比较繁琐,他会将简单的调用关系解耦成多个部分,增加类的复杂度,但是即便如此,命令模式的结构依然清晰. 1.定义 将一个请求封装成一 ...

最新文章

  1. anki 插入表格_另一种把思维导图和excel表格放入anki的方式
  2. python中gui有没有电子表格控件_python实现表格控件
  3. openpyxl 绘制饼形图_openpyxl3.0官方文档(13)—— 饼图
  4. py获取前端的参数_微前端 qiankun 项目实践
  5. RAID 01 和RAID 10的区别
  6. windows 2502 2503 错误解决
  7. 引入CSS文件的@import与link的权重分析
  8. android怎么注释代码块,Android.mk 代码注释
  9. xampp如何上传文件到服务器,xampp安装到云服务器
  10. Windows查看端口占用
  11. lopatkin俄大神精简Windows 10 Pro 19042.487 20H2 PreRelease x86-x64 ZH-CN BIZ(2020-09-01)
  12. 【ExtJs】Extjs RowNumberer序号分页递增
  13. 三线表里加小短线_快速搞定学术论文中的三线表
  14. 深度探索C++对象模型复习和学习 第三章 Data 语义学(The Semantics of Data )
  15. 论文数模真的好难?那是你还不会Matlab!
  16. 学会充分利用你的零碎时间
  17. 苹果在中国失掉 iPad 商标
  18. 单片机:红外遥控实验(内含红外遥控介绍+硬件原理+软件编程+配置环境)
  19. vue下载sass依赖
  20. 安卓/苹果手机直播声卡方案,实现一边直播一边充电功能

热门文章

  1. 20135337朱荟潼 Linux第二周学习总结——操作系统是如何工作的
  2. 1到100的和(C)
  3. 通信科技人员职业道德
  4. 使html轮廓颜色不同,css中border颜色不同怎么设置?
  5. 记录卸载h5py出现的问题
  6. 《 种子用户方法论》读书笔记
  7. smarty-wap端
  8. jBox 2.3基于jquery的最新多功能对话框插件 常见使用问题解答
  9. Ubuntu中解压出现:bzip2: (stdin) is not a bzip2 file.
  10. 如何在windows开启UAC(用户账号控制)的情况下优雅的管理程序的权限申请方案(一)