Android Studio Plugin 插件開發(fā)教程(三) —— 制作一個自動生成數據庫代碼的插件

項目源碼

https://github.com/boredream/AndroidDatabaseGeneratorPlugin

系列教程

Android Studio Plugin 插件開發(fā)教程(一) —— 開發(fā)你的第一個插件

Android Studio Plugin 插件開發(fā)教程(二) —— 插件SDK中的常用對象介紹

Android Studio Plugin 插件開發(fā)教程(三) —— 制作一個自動生成數據庫代碼的插件

Android Studio Plugin 插件開發(fā)教程(四) —— 為自動生成數據庫代碼的插件添加UI


插件介紹

本篇實戰(zhàn)擼個自動生成安卓Sqlite數據庫代碼的插件,先演示下最終效果
db文件夾下的都是插件自動生成的,而MainActivity里面的代碼是我提前寫好的,用于實驗插件生成的代碼效果

DatabaseGenerator.gif

簡單解釋下插件功能
給定一個數據類,比如User璃氢。希望插件能根據數據類自動生成對應的表結構,存在一個Column類里。然后再生成對應的Dao類其中包含CRUD方法如迟。

和網上常見的一些數據庫框架類似,只不過這里是用插件直接生成Android Sqlite原生代碼

優(yōu)點:

  • 無需額外依賴
  • 無學習成本
  • 便于自定義

缺點:

  • 原生代碼量較多
  • 需要對安卓Sqlite原生代碼有一定了解

開擼~
需要處理這么幾個模塊
SqliteOpenHelper類攻走,其中包含create table的sql語句殷勘;
Columns字段類,統(tǒng)一存在一個DataContract類中昔搂;
數據Dao類玲销,包含CRUD的sql語句

幾個模塊的處理步驟和邏輯都類似,這里拿Columns類生成舉例摘符。
其他可以下載源碼參考 贤斜,源碼地址:
https://github.com/boredream/AndroidDatabaseGeneratorPlugin
歡迎star和follow~
下載源碼后參考教程一先搭建環(huán)境,然后導入項目


處理步驟如下:

一逛裤、定位到需要創(chuàng)建文件的目錄

這里希望把生成的類都存在包名目錄下的db包中(com.packagename.db)

首先要獲取到包名目錄路徑...app/src/main/java/包名瘩绒,然后才能在它下面獲取或新建db文件夾。而獲取包名目錄又要先獲取Android項目的包名别凹,想獲取這個又得先找到AndroidManifest文件~

AndroidManifest.png

因為AndroidManifest文件路徑是固定的草讶,所以可以用上一篇教程中的LocalFileSystem.getInstance().findFileByPath(path);方法獲取文件

public static PsiFile getManifestFile(Project project) {
    String path = project.getBasePath() + File.separator +
            "app" + File.separator +
            "src" + File.separator +
            "main" + File.separator +
            "AndroidManifest.xml";
    VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(path);
    if(virtualFile == null) return null;
    return PsiManager.getInstance(project).findFile(virtualFile);
}

project可以在Action中通過event.getData()獲取,參考上一篇教程
獲取到VirtualFile后再轉換成PsiFile炉菲,大部分操作都是針對Psi體系的

然后解析AndroidManifest文件堕战,獲取package屬性里的包名
因為是Xml文件坤溃,所以和Dom啥的解析差不多,獲取代碼如下

public static String getAppPackageName(Project project) {
    PsiFile manifestFile = getManifestFile(project);
    XmlDocument xml = (XmlDocument) manifestFile.getFirstChild();
    return xml.getRootTag().getAttribute("package").getValue();
}

然后就可以根據包名獲取到包名目錄了

public static VirtualFile getAppPackageBaseDir(Project project) {
    String path = project.getBasePath() + File.separator +
            "app" + File.separator +
            "src" + File.separator +
            "main" + File.separator +
            "java" + File.separator +
            getAppPackageName(project).replace(".", File.separator);
    return LocalFileSystem.getInstance().findFileByPath(path);
}

project.getBasePath()是項目的根目錄嘱丢,在其基礎上拼接后續(xù)路徑
然后薪介,包名目錄下斷有沒有db文件夾,沒有就創(chuàng)建一個

// app包名根目錄 ...\app\src\main\java\PACKAGE_NAME\
VirtualFile baseDir = AndroidUtils.getAppPackageBaseDir(project);

// 判斷根目錄下是否有db文件夾
VirtualFile dbDir = baseDir.findChild("db");
if(dbDir == null) {
    // 沒有就創(chuàng)建一個
    try {
        dbDir = baseDir.createChildDirectory(null, "db");
    } catch (IOException e) {
        e.printStackTrace();
    }
}

這次我們用了 VirtualFile.FindChild(filename) 方法越驻,獲取文件子一級路徑中尋找文件或文件夾
(LocalFileSystem.getInstance().findFileByPath(path); 只能獲取文件不能獲取目錄汁政,所以不用)
沒有db文件夾的話,就 VirtualFile.createChildDirectory(requestor, name) 創(chuàng)建一個
這個方法第一個參數是指定誰調用了它缀旁,一般傳null不做特殊處理

db目錄定位到了记劈,然后就是在里面創(chuàng)建DataContract類了,再在其中存放Columns類并巍。
DataContact.java文件其實也可以通過類似上面的 VirtualFile.createChildData 直接創(chuàng)建文件目木,
但創(chuàng)建的是空的文件,而我們需要的是有代碼內容的java文件懊渡,所以下面我們介紹另一個方法~


二刽射、創(chuàng)建一個包含代碼的文件

按照我們的插件需求,要創(chuàng)建一個DataContract類剃执,然后把Columns類都存進去誓禁。
首先就是要生成這個作為殼子的類~ 我們先拼接出來類文件的字符串,代碼如下

public static String genDataContractInitCode(VirtualFile dir) {
    return StringUtils.formatSingleLine(0, "package " + AndroidUtils.getFilePackagePath(dir) + ";") +
            "\n" +
            StringUtils.formatSingleLine(0, "import android.provider.BaseColumns;") +
            "\n" +
            StringUtils.formatSingleLine(0, "public final class DataContract {") +
            "\n" +
            StringUtils.formatSingleLine(1, "private DataContract() {") +
            StringUtils.formatSingleLine(2, "http:// private") +
            StringUtils.formatSingleLine(1, "}") +
            "\n" +
            "}";
}

其中getFilePackagePath是獲取當前文件/文件夾對應包名 com.xxx.xxx 的肾档,
邏輯是把當前文件路徑的 / 替換成 . 然后截取com.xxx.xxx以后的部分即可

public static String getFilePackageName(VirtualFile dir) {
    if(!dir.isDirectory()) {
        // 非目錄的取所在文件夾路徑
        dir = dir.getParent();
    }
    String path = dir.getPath().replace("/", ".");
    String preText = "src.main.java";
    int preIndex = path.indexOf(preText) + preText.length() + 1;
    path = path.substring(preIndex);
    return path;
}

獲取到代碼字符串以后摹恰,可以用createFileFromText創(chuàng)建有內容的文件,如下

String name = "DataContract.java";
VirtualFile virtualFile = dbDir.findChild(name);
if(virtualFile == null) {
    // 沒有就創(chuàng)建一個阁最,第一次使用代碼字符串創(chuàng)建個類
    PsiFile initFile = PsiFileFactory.getInstance(project).createFileFromText(
            name, JavaFileType.INSTANCE, CodeFactory.genDataContractInitCode(dbDir));
    // 加到db目錄下
    PsiManager.getInstance(project).findDirectory(dbDir).add(initFile);
    virtualFile = dbDir.findChild(name);
}

dbDir是步驟一中獲取到的db文件夾
genDataContractInitCode是上面拼接代碼的方法戒祠,返回代碼字符串

注意,createFileFromText創(chuàng)建的文件是一個無目錄的文件速种,需要手動add到需要位置
這個add操作就會把文件加到指定目錄下姜盈,新建一個文件~


三、解析數據生成對應Columns類

上一篇介紹過配阵,我們可以用action中的event獲取當前正在編輯的文件馏颂,然后在file中獲取到PsiClass元素,最后遍歷Class獲取全部成員變量Field棋傍。PsiClass和Java中的Class相似救拉,有一點反射姿勢的可以很快上手

下面就是根據數據類信息,拼接代碼字符串的方法

public static String genBeanColumnsCode(PsiClass clazz) {
    StringBuilder sb = new StringBuilder();
    sb.append(StringUtils.formatSingleLine(0, "public interface " + clazz.getName() + " extends BaseColumns {"));
    sb.append(StringUtils.formatSingleLine(1, "String TABLE_NAME = \"" + StringUtils.camel2underline(clazz.getName()) + "\";"));
    for (PsiField field : clazz.getFields()) {
        String name = StringUtils.camel2underline(field.getName()).toUpperCase();
        String value = name.toLowerCase();
        sb.append(StringUtils.formatSingleLine(1, "String " + name + " = \"" + value + "\";"));
    }
    sb.append("}");
    return sb.toString().trim();
}

clazz.getField獲取到類的所有成員變量瘫拣,然后拼接成需要的代碼

其中的StringUtils是自己封裝的工具類
camel2underline是將駝峰命名轉換成下劃線風格的字符串
formatStringLine是在前面加縮進符亿絮,后面加換行符
拼好代碼后,就可以用它去生成類、文件派昧、方法等等

和之前生成文件類似黔姜,也是 createXXXFromText一類的方法,
可以用代碼生成類蒂萎、方法秆吵、語句、變量等等五慈。
這里我們就要根據代碼去生成Columns類纳寂,也就是PsiClass對象

上一步我們已經獲取到了DataContract類了,新建的Columns要保存在它里面

PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
// 用拼接的代碼生成Columns Class
String beanColumnsCode = CodeFactory.genBeanColumnsCode(clazz);
PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory();
PsiClass beanColumnsClass = factory.createClassFromText(beanColumnsCode, psiFile);
// 將創(chuàng)建的class添加到DataContract Class中
PsiClass fileClass = PluginUtils.getFileClass(psiFile);
fileClass.add(beanColumnsClass.getInnerClasses()[0]);

這次用到了 PsiElementFactory 類泻拦,然后用它去 **createClassFromText **創(chuàng)建類
方法的第二個參數是Context毙芜,傳入所在File或所在Class都可以

然后將這個生成的類添加到DataContract文件類中
注意不能是添加到DataContract文件上,而是添加到文件里的類上
獲取方法如下(應該有更好的方法吧聪轿,暫時沒找到)

public static PsiClass getFileClass(PsiFile file) {
    for (PsiElement psiElement : file.getChildren()) {
        if (psiElement instanceof PsiClass) {
            return (PsiClass) psiElement;
        }
    }
    return null;
}

class.getInnerClasses()[0] 這里要單獨說明下

createFileFromText的時候我們拼接的字符串是完整的代碼爷肝,
但是在createClassFromText的時候比較特殊,codeText是作為類主體部分的

String classCode = "public class MyClass {\n" +
                                "\tprivate String a;\n" +
                            "}";
PsiClass newClass = factory.createClassFromText(classCode, null);

如果你這樣去生成陆错,那么最終代碼會是

class _Dummy_ {
    public class MyClass {
        private String a;
    }
}

這不是我們想要的!=鹕狻音瓷!
所以一般做法是只用類 { } 里面的代碼去生成,比如"private String a;"
而類的public等信息需要額外設置夹抗,如下

newClass.setName("User"); // 設置類名绳慎,默認名為_Dummy_
newClass.getModifierList().add(factory.createKeyword(PsiKeyword.PUBLIC)); // 定義列表里添加關鍵字public
newClass.getImplementsList().add(factory.createReferenceElementByFQClassName(
                "android.provider.BaseColumns", clazz.getResolveScope())); // 實現接口列表里添加BaseColumns類

這就比較麻煩了,所以介紹個良心小技巧漠烧!
還是用全部代碼生成杏愤,然后再獲取這個類的innerClasses內部類里面的第一個就行了!
所以才有了上面的 class.getInnerClasses()[0] 的處理


四已脓、整合代碼珊楼,運行

將之前的代碼封裝到DatabaseGenerator類中的genCode方法中,然后在action里調用
action的相關介紹參考教程一

public class DatabaseGenerateAction extends AnAction {
    @Override
    public void actionPerformed(AnActionEvent e) {
        Project project = e.getData(PlatformDataKeys.PROJECT);
        PsiFile file = e.getData(PlatformDataKeys.PSI_FILE);
        PsiClass clazz = PluginUtils.getFileClass(file);
        WriteCommandAction.runWriteCommandAction(project, () -> {
            DatabaseGenerator.genCode(file, clazz);
        });
    }
}

注意度液,這里有個特殊的處理 WriteCommandAction.runWriteCommandAction
在插件中厕宗,如果是新建File等操作是可以直接進行的。
但在DataContract文件類中添加個內部類堕担,這種寫入文件內容的操作是需要特殊處理的已慢,需要放在 WriteCommandAction.runWriteCommandAction 第二個參數的runnable中運行

搞定,效果圖見文章開始動態(tài)圖霹购。
源碼部分見
https://github.com/boredream/AndroidDatabaseGeneratorPlugin
歡迎star和follow~
下載源碼后參考教程一先搭建環(huán)境佑惠,然后導入項目

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子膜楷,更是在濱河造成了極大的恐慌旭咽,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件把将,死亡現場離奇詭異轻专,居然都是意外死亡,警方通過查閱死者的電腦和手機察蹲,發(fā)現死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門请垛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洽议,你說我怎么就攤上這事宗收。” “怎么了亚兄?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵混稽,是天一觀的道長。 經常有香客問我审胚,道長匈勋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任膳叨,我火速辦了婚禮洽洁,結果婚禮上,老公的妹妹穿的比我還像新娘菲嘴。我一直安慰自己饿自,他們只是感情好,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布龄坪。 她就那樣靜靜地躺著昭雌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪健田。 梳的紋絲不亂的頭發(fā)上烛卧,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機與錄音抄课,去河邊找鬼唱星。 笑死,一個胖子當著我的面吹牛跟磨,可吹牛的內容都是我干的间聊。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼抵拘,長吁一口氣:“原來是場噩夢啊……” “哼哎榴!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤尚蝌,失蹤者是張志新(化名)和其女友劉穎迎变,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體飘言,經...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡衣形,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了姿鸿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谆吴。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖苛预,靈堂內的尸體忽然破棺而出句狼,到底是詐尸還是另有隱情,我是刑警寧澤热某,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布腻菇,位于F島的核電站,受9級特大地震影響昔馋,放射性物質發(fā)生泄漏筹吐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一秘遏、第九天 我趴在偏房一處隱蔽的房頂上張望骏令。 院中可真熱鬧,春花似錦垄提、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至妥粟,卻和暖如春审丘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背勾给。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工滩报, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捞高,地道東北人钙皮。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓踱讨,卻偏偏與公主長得像沛膳,于是被迫代替她去往敵國和親裂七。 傳聞我的和親對象是個殘疾皇子屏箍,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內容