專案的升級-給RemoveButterKnife外掛增加新功能

u3shadow發表於2018-12-25

前言

經過專案的初步編寫和進一步改造,RemoveButterKnife外掛終於也有模有樣了,但是,功能上僅僅支援Activity/Fragment的BindView註解。

關於編寫和優化的過程可以看下面兩篇文章 專案構造RemoveButterKnife

專案改進-重構RemoveButterKnife

當然,這裡也附上這個專案的github地址

為了讓外掛支援更加徹底,我們還要支援組合自定義view以及viewholder中使用butterknife的情況,當然,我們也要支援OnClick註解以及一些其他的使用場景。

要增加哪些功能?

首先要確定需要增加哪些功能點,功能點的更新如下

  1. 增加對多moudle時的R2.id.xxx的支援
  2. 增加對OnClick註解的支援
  3. 增加對viewholder和自定義組合view的支援

確定增加功能的先後順序

審閱我們的功能,發現1號功能是比較容易的,所以我們把1號定為優先 再次思考發現,2,3號功能之間存在關聯性,即,3號功能提及的支援種類中也要支援Onclick註解的形式。 所以確定開發順序為1->3->2

功能拆分

在這裡我們使用github提供的project功能,具體分解如下 可以看到,我們分為了todo,doing,done三個部分,而且把每個任務都細分為了幾個步驟,這樣我們就可以在開發某一功能時保持專注,而不需要東寫一點西寫一點了

具體功能開發

1.對R2.id.xxxx的支援

由於我們原來的程式碼中尋找匹配使用的是正規表示式,如下

   String pattern = "^@(BindView|InjectView|Bind)\\((R.id.*)|(R2.id.*)\\)$";
   Pattern r = Pattern.compile(pattern);
複製程式碼

可以看到,我們的程式碼已經新增了對R2.id.xxx的支援,只需要給正規表示式增加一個條件。 最後,我們給這個功能新增上unit test,就可以完成對功能1的開發

2.對自定義view和viewholder的支援

首先,我們要判別一個類到底是自定義view還是viewholder,對於activity和fragment很簡單,因為初始函式是不同的,一個是oncreate,一個是oncreateview,但是由於增加了支援種類,老辦法就行不通了,這時我們需要使用idea的sdk來進行判斷,程式碼如下

   GenCodeContext codeContext = new GenCodeContext(mClass, mFactory);
        String type = mClass.getSuperClassType().toString();
            if (type.contains("Activity")){
                codeContext.setStrategy(new ActivityStrategy(code,clickMap));
            }else if (type.contains("Fragment")) {
                codeContext.setStrategy(new FragmentStrategy(code,clickMap));
            }else if (type.contains("ViewHolder")||type.contains("Adapter<ViewHolder>")) {
                codeContext.setStrategy(new AdapterStrategy(code,clickMap));
            }else {
                codeContext.setStrategy(new CustomViewStrategy(code,clickMap));
            }
            codeContext.executeStrategy();
複製程式碼

對於原來的程式碼,我們已經能夠找到activity/fragment的特定位置插入程式碼,但是對於自定義view和viewholder,又該用什麼特徵來定位該在哪裡插入呢? 對於這個問題我們分情況討論

  1. 自定義view 我們這裡討論的自定義view僅僅針對組合view,自繪和擴充套件方式不做討論,因為這兩種方式一般不會使用ButterKnife. 組合自定義view的特徵 對於這種自定view,最大的特徵就是在構造的時候會使用inflate方法將xml檔案進行壓入,那麼,找到inflate或者R.layout.xxx的語句,這裡就是我們插入生成後程式碼的位置 程式碼
   private PsiStatement findInflateStatement(PsiClass mClass){
        PsiStatement result = null;
        PsiMethod[] methods = mClass.getAllMethods();
        for (PsiMethod method:methods) {
            for (PsiStatement statement : method.getBody().getStatements()) {
                String returnValue = statement.getText();
                if (returnValue.contains("R.layout") || returnValue.contains("LayoutInflater.from(context).inflate")) {
                    result = statement;
                    break;
                }
            }
        }
        return result;
    }
複製程式碼
  1. viewholder 這裡說的viewholder特指recyclerview.viewholder. 這種viewholder都有一個建構函式,引數為(View xxx)第一句是super(xxx); 我們可以基於這兩個特徵進行定位。
  private PsiStatement findSuperStatement(PsiMethod method,String viewName){
        PsiStatement result = null;
        for (PsiStatement statement : method.getBody().getStatements()) {
            String returnValue = statement.getText();
            if (returnValue.contains("super(" + viewName + ")")) {
                result = statement;
                break;
            }
        }
        return result;
    }
複製程式碼

那麼,既然能夠識別和找到哪裡插入程式碼了,我們的型別支援也就水到渠成了。 在型別支援的時候,我們使用了策略模式,這樣根據型別不同,設定不同的策略就可以方便的進行處理。 目錄結構如下

3.對onclick註解的支援

我們對onclick的處理分以下幾步

  1. 尋找註解
  2. 分析註解資訊並保持
  3. 根據儲存資訊生成程式碼並插入

1.尋找註解

使用正規表示式很容易找到,這裡不再重複貼程式碼

2.分析註解資訊並儲存

onclick註解有幾種情況

  1. 單id/多id的繫結
  2. 點選函式是否有引數的情況 我們要獲取的資訊有以下幾個
  3. 繫結的id列表
  4. 點選函式的名稱,是否有引數,引數的型別 針對第二點,我們使用一個物件將其封裝起來 儲存獲取資訊我們使用一個Map<>來進行 程式碼:
 @Override
    public void process() {
        String pattern = "^@OnClick\\(\\{*(R.id.*,|R.id.*|R2.id.*|R2.id.*,)+\\}*\\)$";
        Pattern r = Pattern.compile(pattern);
        for (int i = 0;i < currentDoc.length;i++){
            Matcher m = r.matcher(currentDoc[i].trim());
            currentDoc[i] = currentDoc[i].trim();
            if (m.find()) {
                method = detectMethod(currentDoc[i+1]);
                ids = detectID(currentDoc[i], method);
                methodAndIDMap.put(method,ids);
                deleteLineNumbers.add(i);
            }
        }
    }
複製程式碼

3.根據儲存資訊生成程式碼並插入

這步我們需要根據儲存的資訊進行程式碼生成和插入,我們主要討論生成,插入部分和findviewbyid程式碼大同小異 我們已經知道了註解的id和點選對應的方法,那麼我們復原的結果就應該是 findViewById(R.id.xxx).setOnclickListener(new OnclickListener(.... 我們需要注意的地方就是點選函式是否有引數,這會影響到我們生成的程式碼 看具體程式碼:

  protected StringBuilder getMethodInvokeString(ClickMehtod method) {
        StringBuilder methodString = new StringBuilder();
        if (method.isHaveArg()){
            methodString.append(method.getName()+"(("+method.getArgType()+")"+"v);");
        }else{
            methodString.append(method.getName()+"();");
        }
        return methodString;
    }
 protected String getOnClickCode(StringBuilder methodString, String id) {
        return "findViewById("+id+").setOnClickListener(new View.OnClickListener() {\n" +
                " @Override\n" +
                " public void onClick(View v){\n"+
                methodString.toString()+
                "}"+
                "});";
    }
複製程式碼

到了這裡,我們的Onclick註解支援也完成了。

總結

通過對這個小小的外掛的開發和重構以及功能新增,雖然專案很小,但是工程和麵向物件的思想的重要性已經體現了出來,在一個擁有良好專案結構的工程下增加新功能是非常簡答而明快的,如果像最初版本那樣把所有的程式碼寫在一個檔案中而沒有進行邏輯拆分的話,新增功能基本等於重寫專案,這肯定是痛苦的。

還有一點值得一提,在做專案的時候第一步永遠是總體構思,第二部是具體拆分,寫程式碼這件事的優先順序並沒有那麼高,容易犯的一個問題就是一提到某個功能馬上就開始寫具體程式碼,這樣的結果往往費力不討好,有一個明確的功能拆分和行進步驟會極大的增強開發體驗。

至此,RemoveButterKnife系列文章就告一段落了,這幾篇文章的目的不僅僅是記錄開發RemoveButterKnife外掛中的思路和遇到的問題,更重要的是總結了作者我開發軟體專案的一個歷程,而把這些寫下來的過程,也是鞏固這段歷程的重要步驟。

相關文章