5.9.0總結

分享人:沈永輝
時    間:2020.5.15

問題:

xml 佈局層級(儘量減少佈局層級、儘量使用RelativeLayout)

修改前

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 
    android:orientation="vertical">

    <LinearLayout 
        android:orientation="vertical" >

        <com.threegene.module.health.ui.widget.HealthModuleTitleLinearLayout  />

        <LinearLayout 
            android:orientation="vertical">

            <TextView />

            <LinearLayout 
                android:orientation="vertical" />

        </LinearLayout>

    </LinearLayout>

修改後:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical">

    <com.threegene.module.health.ui.widget.HealthModuleTitleView  />

    <LinearLayout 
        android:orientation="vertical">

        <TextView  />

        <LinearLayout 
            android:orientation="vertical" />

    </LinearLayout>
</LinearLayout>

開發分享

組件複用

需求描述: 列表展示內容,

實現:

  1. RecycleView
  2. LinearLayout+Item

案例(主要講述第二種實現):

第一版:

private void bindGoodView(LinearLayout container,  List<Goods> goodsList) {
        if (goodsList != null && !goodsList.isEmpty()) {
            if (container.getVisibility() != View.VISIBLE) {
                container.setVisibility(View.VISIBLE);
            }
            
            // 步驟 1: 清空容器內部子View 
            container.removeAllViews();
            
            int size = goodsList.size();
            for (int i = 0; i < size; i++) {
                
                // 步驟 2: 創建子 View
                View v = inflate(R.layout.item_order_good, holder.goodContainer);

                
                // 步驟 3: 找到具體控件
                RemoteImageView goodsImage = v.findViewById(R.id.goods_image);
                TextView goodsName = v.findViewById(R.id.goods_name);
                TextView goodsDesc = v.findViewById(R.id.goods_desc);
                TextView goodsPriceDesc = v.findViewById(R.id.goods_price_desc);
                
                // 步驟 4: 把數據設置到子View中
                Goods goods = goodsList.get(i);
                goodsImage.setCircleImageUri(goods.imgUrl, -1);
                goodsName.setText(goods.productName);
                goodsDesc.setText(goods.productDesc);
                goodsPriceDesc.setText(String.format(Locale.CANADA, "%s 元", goods.unitAmount));
                
                // 步驟 5:設置監聽
                v.setTag(goods);
                v.setOnClickListener(this);
                
                // 步驟 6:把子View添加到父容器中 
                container.addView(v);
            }
        } else {
            holder.goodContainer.setVisibility(View.GONE);
        }
    }

第二版

子View複用:每次設置數據時 不清空容器內子View,而是取出複用.

private void bindGoodView(LinearLayout container,  List<Goods> goodsList) {
        if (goodsList != null && !goodsList.isEmpty()) {
            if (container.getVisibility() != View.VISIBLE) {
                container.setVisibility(View.VISIBLE);
            }
            int size = goodsList.size();
            for (int i = 0; i < size; i++) {
                
                // 步驟 1: 獲取子View(創建新的View or 複用已有子View)
                View v;
                if (i < container.getChildCount()) {
                    // 複用已有子View
                    v = container.getChildAt(i);
                    if (v.getVisibility() != View.VISIBLE) {
                        v.setVisibility(View.VISIBLE);
                    }
                } else {
                    // 創建新的View
                    v = inflate(R.layout.item_order_good, holder.goodContainer);
                    
                    // 步驟 2: 把子View添加到父容器中  
                    container.addView(v);
                    
                    // 步驟 6:在創建新的View時 設置監聽
                    v.setOnClickListener(this);
                }
                
                // 步驟 3: 找到具體控件
                RemoteImageView goodsImage = v.findViewById(R.id.goods_image);
                TextView goodsName = v.findViewById(R.id.goods_name);
                TextView goodsDesc = v.findViewById(R.id.goods_desc);
                TextView goodsPriceDesc = v.findViewById(R.id.goods_price_desc);
                
                // 步驟 4: 把數據設置到子View中
                Goods goods = goodsList.get(i);
                goodsImage.setCircleImageUri(goods.imgUrl, -1);
                goodsName.setText(goods.productName);
                goodsDesc.setText(goods.productDesc);
                goodsPriceDesc.setText(String.format(Locale.CANADA, "%s 元", goods.unitAmount));
                
                // 步驟 5: 將數據與子View綁定
                v.setTag(goods);
            }
            
            // 步驟 7: 將父容器中多餘子View進行隱藏
            for (int i = size; i < holder.goodContainer.getChildCount(); i++) {
                holder.goodContainer.getChildAt(i).setVisibility(View.GONE);
            }
        } else {
            holder.goodContainer.setVisibility(View.GONE);
        }
    }

第三版

思考: 第二版只是複用了一下子View 但是每次向容器中加子Item時還是需要爲每個子View的每個控件findViewById(),這一步也很耗時,如何在複用子View時做到子View內部控件不再查找。
參考ListView的ViewHolder實現,把每個子View做成一個自定義的組件,組件內部處理。

自定義商品View :GoodsView.class

/**
* 商品View
* created by shenyonghui on 2020/5/15
*/
public class GoodsView extends LinearLayout {

   private RemoteImageView goodsImage;
   private TextView goodsName;
   private TextView goodsDesc;
   private TextView goodsPriceDesc;

   public GoodsView(Context context) {
       this(context, null);

   }

   public GoodsView(Context context, AttributeSet attrs) {
       this(context, attrs, 0);
   }

   public GoodsView(Context context, AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
       init();
   }

   private void init() {
       inflate(getContext(), R.layout.item_order_good, this);
       goodsImage = findViewById(R.id.goods_image);
       goodsName = findViewById(R.id.goods_name);
       goodsDesc = findViewById(R.id.goods_desc);
       goodsPriceDesc = findViewById(R.id.goods_price_desc);

   }

   public void setImage(String url, @DrawableRes int defaultResource) {
       goodsImage.setCircleImageUri(url, defaultResource);
   }

   public void setImage(String url) {
       goodsImage.setCircleImageUri(url, -1);
   }

   public void setName(CharSequence str) {
       if (str != null) {
           goodsName.setText(str);
       }
   }

   public void setDesc(CharSequence str) {
       if (str != null) {
           goodsDesc.setText(str);
       }
   }

   public void setPrice(CharSequence str) {
       if (str != null) {
           goodsPriceDesc.setText(str);
       }
   }
}
    private void bindGoodView(LinearLayout container, List<Goods> goodsList) {
        if (order.orderItemInfoVo != null && !order.orderItemInfoVo.isEmpty()) {
            if (holder.goodContainer.getVisibility() != View.VISIBLE) {
                container.setVisibility(View.VISIBLE);
            }
            int goodsSize = goodsList.size();
            for (int i = 0; i < goodsSize; i++) {
                GoodsView goodsView;
                if (i < container.getChildCount()) {
                    goodsView = (GoodsView) container.getChildAt(i);
                    if (goodsView.getVisibility() != View.VISIBLE) {
                       goodsView.setVisibility(View.VISIBLE);
                    }
                } else {
                    goodsView = new GoodsView(container.getContext());
                    container.addView(goodsView);
                }
                
                BaseOrder.GoodItem good = goodsList.get(i);
                goodsView.setImage(good.imgUrl);
                goodsView.setName(good.productName);
                goodsView.setDesc(good.productDesc);
                goodsView.setPrice(String.format(Locale.CANADA, "%s 元", good.unitAmount));
                
            }
            for (int i = order.orderItemInfoVo.size(); i < holder.goodContainer.getChildCount(); i++) {
                holder.goodContainer.getChildAt(i).setVisibility(View.GONE);
            }
        } else {
            holder.goodContainer.setVisibility(View.GONE);
        }
    }

性能對比(時間)

一共69訂單 每個訂單共4個商品

最大時長 (ms) 最小時長(ms) 平均時長(ms)
第一版 20 0 10
第三版 20 0 4

注:在內存維度的提升應該比時間維度更明顯

登錄流程代碼優化

需求:
在這裏插入圖片描述

第一版

每次請求都是分開的 用不同的Callback
Login.class


// 判斷用戶狀態
LoginService.getDefault().checkRegister(...,new Callback<Void> {
        @Override
        public void onSuccess(int type, Void data, boolean immediately) {
            dismissLoadingDialog();
            // 不需要隱私協議,使用手機號+驗證碼登錄
            registerBySMS();
        }

        @Override
        public void onFail(int type, String errorMsg) {
            dismissLoadingDialog();
            if (type == UserService.NEED_ACCEPT_AGREEMENT_ERROR) {
                // 顯示用戶隱私協議
                showAgreementPrivacyPolicyDialog();
            } else {
                ToastMaster.shortToast(errorMsg);
            }
        }
    });

    /**
     * 驗證碼登錄(獲取token)
     */
    private void registerBySMS() {
        UserService.getDefault().loginBySMS(..., new Callback<ResultMultipleAccount>() {
            @Override
            public void onSuccess(int type, ResultMultipleAccount data, boolean immediately) {
                if (data.loginResultList.size() == 1) {
                    // 該手機號只註冊一次 不需要用戶選擇賬號
                    ResultMultipleAccount.LoginResult loginResult = data.loginResultList.get(0);
                    loginSuccessBySMS(loginResult.token);
                } else {
                    List<SelectAccountDialog.Account> accounts = new ArrayList<>();
                    for (ResultMultipleAccount.LoginResult account : data.loginResultList) {
                        accounts.add(new SelectAccountDialog.Account(account.token, account.loginType, account.childNameList));
                    }
                    // 有手機號有多個賬號綁定 需用戶進行選擇
                    showSelectAccountDialog(accounts);
                     
                     
                }
            }

            @Override
            public void onFail(int type, String errorMsg) {
                ToastMaster.shortToast(errorMsg);
            }
        });
    }

      //選擇賬號彈窗
      private void showSelectAccountDialog(List<SelectAccountDialog.Account> accounts){
        //.....
         //用戶選擇一個賬號
         loginSuccessBySMS(loginResult.token);
      }

      // 獲取token成功後的操作
      private void loginSuccessBySMS(String token) {
        // 保存token 拉取用戶信息
        UserService.getDefault().loadLoginInfo(new new Callback<Void> {
        @Override
        public void onSuccess(int type, Void data, boolean immediately) {
             //跳轉到首頁
        }

        @Override
        public void onFail(int type, String errorMsg) {
            // 提示用戶登錄失敗
        }
    })
    }
    

LoginService.class


  public void checkRegister(...,CallBack callback){
      UserAPI.checkRegister(..., new JsonResponseListener<Boolean>() {
            @Override
            public void onSuccess(JsonResponse<Boolean> response) {
                if (!response.getData()) {
                    // 需要用戶隱私協議
                    callback.onFail(LOGIN_BY_NEED_ACCEPT_AGREEMENT, ""); 
                }else{
                    // 不需要
                    callback.onSuccess(Callback.DEFAULT, null, true);
                }
            }
            
            @Override
            public void onError(HError error) {
                // 請求失敗
                callback.onFail(LOGIN_BY_SMS_ERROR, error.getErrorMsg());
            }
        });
   }

     /**
     * 通過手機號登錄/註冊
     */
    public void loginBySMS(...., final Callback<ResultMultipleAccount> callback) {
        UserAPI.loginByCode(..., new JsonResponseListener<ResultMultipleAccount>() {
            @Override
            public void onSuccess(JsonResponse<ResultMultipleAccount> response) {
                if (response.getData() != null) {
                    if (response.getData().loginResultList == null || response.getData().loginResultList.isEmpty()) {
                        callback.onFail(LOGIN_BY_SMS_ERROR, "未獲取到賬號信息");
                    } else {
                        // 請求成功
                        callback.onSuccess(Callback.DEFAULT, response.getData(), true);
                    }
                }
            }

            @Override
            public void onError(HError error) {
                 callback.onFail(LOGIN_BY_SMS_ERROR, error.getErrorMsg());
            }
        });
    }

存在的問題: 登錄主流程被各種中斷 邏輯顯得混亂

第二版

  1. 修改checkRegister()的Callback 增加一個 void onIntercept(int type, Chain chain);回調 ,該回調用於執行被中斷時回調。
  2. 新建Chain接口,用於從中斷操作時從LoginService 獲取數據通知LoginService繼續操作
    public interface Chain<T> {
          Object getData();
    
          void process(T s);
    }
    
  3. LoginActivity 與Service交互
    LoginActivity
     UserService.getDefault().loginBySMS(... , new SMSLoginCallback (){
         
         @Override
         public void onSuccess(int type, Void data, boolean immediately) {
               //登錄成功 進入首頁
         }
         @Override
         public void onFail(int type, String errorMsg) {
            // 登錄失敗
             ToastMaster.shortToast(errorMsg);
         }
    
         @Override
         public void onIntercept(int type, Chain chain) {
             if (chain instanceof LoginAcceptAgreementChain) {
                 dismissLoadingDialog();
                 LoginAcceptAgreementChain acceptAgreementChain = (LoginAcceptAgreementChain) chain;
                 showAgreementPrivacyPolicyDialog(acceptAgreementChain);
             } else if (chain instanceof LoginMultipleAccountChain) {
                 LoginMultipleAccountChain multipleAccountChain = (LoginMultipleAccountChain) chain;
                 dismissLoadingDialog();
                 List<SelectAccountDialog.Account> accounts = new ArrayList<>();
                 for (ResultMultipleAccount.LoginResult account : multipleAccountChain.getData().loginResultList) {
                     accounts.add(new SelectAccountDialog.Account(account.token, account.loginTypeStr, account.childNameList));
                 }
                 showSelectAccountDialog(multipleAccountChain, accounts);
             }
         }
     });
    
       /**
        * 用戶協議彈窗
        */
       private void showAgreementPrivacyPolicyDialog(LoginAcceptAgreementChain chain) {
          // 彈窗顯示用戶協議
          // 當用戶點擊同意時   回調到Service 通知繼續執行
           chain.process(null);
       }
    
      //選擇賬號彈窗
       private void showSelectAccountDialog(LoginMultipleAccountChain multipleAccountChain){
         //.....
          //用戶選擇一個賬號token   回調到Service 通知繼續執行
          multipleAccountChain.process(token);
       }   
    
    
    Service
    /**
      * 手機號 + 驗證碼 登錄
      * 第一步:判斷手機號是否註冊過
      * 第二步:未註冊,提示:同步用戶協議,已註冊,直接登錄成過
      */
     public void loginBySMS(..., final LoginCallback<Void> callback) {
         UserAPI.checkRegister(pnum, new JsonResponseListener<Boolean>() {
             @Override
             public void onSuccess(JsonResponse<Boolean> response) {
                     if (!response.getData()) {
                         if (callback != null) {
                             //中斷,同步用戶協議
                             LoginAcceptAgreementChain chain = new LoginAcceptAgreementChain() {
                                 @Override
                                 public Object getData() {
                                     return null;
                                 }
    
                                 @Override
                                 public void process(String s) {
                                     innerLoginBySMS(pnum, vcode, vCodeToken, callback);
                                 }
                             };
                             callback.onIntercept(LOGIN_INTERCEPT_NEED_ACCEPT_AGREEMENT, chain);
                         }
                     } else {
                         //已註冊,直接登錄成過
                         innerLoginBySMS(pnum, vcode, vCodeToken, callback);
                     }
             }
    
             @Override
             public void onError(HError error) {
                if (callback != null) {
                     callback.onFail(LOGIN_BY_SMS_ERROR, error.getErrorMsg());
                }
             }
         });
    
    
        /**
        * 通過手機號登錄/註冊
        */
        private void innerLoginBySMS(final String pnum, String vcode, String vCodeToken, final LoginCallback<Void> callback) {
           UserAPI.loginByCode(pnum, vcode, vCodeToken, new JsonResponseListener<ResultMultipleAccount>() {
             @Override
               public void onSuccess(JsonResponse<ResultMultipleAccount> response) {
                  final ResultMultipleAccount result = response.getData();
                  if (result != null) {
                     if (result.loginResultList == null || result.loginResultList.isEmpty()) {
                         callback.onFail(LOGIN_BY_SMS_ERROR, "未獲取到賬號信息");
                     } else if (result.loginResultList.size() == 1) {
                         // 獲取只有一個賬號 不需要選賬號 直接調回去用戶接口
                         loadLoginInfo(callback);
                     } else {
                         LoginMultipleAccountChain chain = new LoginMultipleAccountChain() {
                             @Override
                             public ResultMultipleAccount getData() {
                                 return result;
                             }
    
                             @Override
                             public void process(String token) {
                                 // 用戶選擇完賬號後 回調
                                 loadLoginInfo(callback);
                             }
                         };
                         callback.onIntercept(LOGIN_INTERCEPT_NEED_MULTIPLE_ACCOUNT, chain);
                     }
                 } else {
                     callback.onFail(LOGIN_BY_SMS_ERROR, "未獲取到賬號信息");
                 }
             }
    
             @Override
             public void onError(HError error) { 
                  if (callback != null) {
                      callback.onFail(LOGIN_BY_SMS_ERROR, error.getErrorMsg());
                 } 
             }
         });
      }
    

在Service中 登錄的幾個接口是直線的調用 當被中斷時把中斷回調到Activity,當Activity處理完中斷 再回調到Service 保證Service邏輯集中

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章