Có bao giờ bạn bị comment chỉ vì code format của bạn khác với những người còn lại? bạn phải hì hục chỉnh sửa thay đổi code setting trong Android Studio? import setting của người khác vào AS và làm lại điều này nhiều lần trên nhiều project khác nhau? Thực sự hơi phiền toái !
Trong bài viết này, mình sẽ giới thiệu cho các bạn 2 công cụ mình thường hay sử dụng trong code android là ktlint
và detekt
để kiểm tra code style, format code và code smell. Việc config được thực hiện trực tiếp trên code base và kết quả hoàn toàn giống nhau trên các máy khác nhau nên rất thuận tiện. Bài viết chủ yếu share code, các bước config nếu có chỗ nào thắc mắc mọi người hãy comment hoặc contact, mình sẽ hỗ trợ.
Chuẩn bị
Những thứ cần lưu ý khi bắt đầu.
- Android gradle project sử dụng Gradle’s Kotlin DSL . Nếu bạn dùng
Groovy
, hãy nghiên cứu thêm và tự convert, mình nghĩ cũng không quá phức tạp. - Nếu project của bạn có nhiều
module
(nhớ phân biệt vớipackage
) thì bạn cần thực hiện trên tất cả module.
Cài đặt ktlint
Ktlint
là Kotlin linter
opensource để check và format Kotlin code style, convention. Mục đích mình sử dụng ktlint trong dự án là để giúp anh em đỡ phải config android studio code style setting lằng nhằng như trước nữa, và để đơn giản nhất thì mình gần như dùng format chuẩn của pinterest
đưa ra, mọi người có thể custom và nó không quá khó. Quan trọng là code style rule nên được thực hiện ngay lúc init hoặc start dự án (có thể là maintain source hay source mới).
Không dông dài nữa, hãy bắt đầu:
1. Vào github của ktlint và chọn version mới nhất
Khai báo trong file dependency của bạn (trong project mình là Configs.kt
), nếu bạn không có file này, tạm thời bỏ qua step 1 này.
1 2 3 4 5 6 7 8 9 | <span class="token keyword">object</span> Versions <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token keyword">val</span> ktlint <span class="token operator">=</span> <span class="token string">"0.36.0"</span> <span class="token comment">// other libs versions ...</span> <span class="token punctuation">}</span> <span class="token keyword">object</span> Deps <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token keyword">val</span> ktlint <span class="token operator">=</span> <span class="token string">"com.pinterest:ktlint:<span class="token interpolation"><span class="token delimiter variable">${</span>Versions<span class="token punctuation">.</span>ktlint<span class="token delimiter variable">}</span></span>"</span> <span class="token comment">// other dependencies ...</span> <span class="token punctuation">}</span> |
2. Tạo 1 file quản lý source ktlint
, tạm gọi là ktlint.gradle.kts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | repositories <span class="token punctuation">{</span> <span class="token function">jcenter</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">val</span> ktlint <span class="token keyword">by</span> configurations<span class="token punctuation">.</span>creating <span class="token keyword">val</span> ktlintCheck <span class="token keyword">by</span> tasks<span class="token punctuation">.</span><span class="token function">creating</span><span class="token punctuation">(</span>JavaExec<span class="token operator">::</span><span class="token keyword">class</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> group <span class="token operator">=</span> <span class="token string">"verification"</span> description <span class="token operator">=</span> <span class="token string">"Check Kotlin code style."</span> classpath <span class="token operator">=</span> configurations<span class="token punctuation">.</span><span class="token function">getByName</span><span class="token punctuation">(</span><span class="token string">"ktlint"</span><span class="token punctuation">)</span> main <span class="token operator">=</span> <span class="token string">"com.pinterest.ktlint.Main"</span> args <span class="token operator">=</span> <span class="token function">listOf</span><span class="token punctuation">(</span><span class="token string">"src/**/*.kt"</span><span class="token punctuation">,</span> <span class="token string">"--reporter=html,output=<span class="token interpolation variable">$buildDir</span>/reports/ktlint/ktlint.html"</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token keyword">val</span> ktlintFormat <span class="token keyword">by</span> tasks<span class="token punctuation">.</span><span class="token function">creating</span><span class="token punctuation">(</span>JavaExec<span class="token operator">::</span><span class="token keyword">class</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> group <span class="token operator">=</span> <span class="token string">"formatting"</span> description <span class="token operator">=</span> <span class="token string">"Fix Kotlin code style deviations."</span> classpath <span class="token operator">=</span> configurations<span class="token punctuation">.</span><span class="token function">getByName</span><span class="token punctuation">(</span><span class="token string">"ktlint"</span><span class="token punctuation">)</span> main <span class="token operator">=</span> <span class="token string">"com.pinterest.ktlint.Main"</span> args <span class="token operator">=</span> <span class="token function">listOf</span><span class="token punctuation">(</span><span class="token string">"-F"</span><span class="token punctuation">,</span> <span class="token string">"src/**/*.kt"</span><span class="token punctuation">)</span> <span class="token punctuation">}</span> dependencies <span class="token punctuation">{</span> <span class="token function">ktlint</span><span class="token punctuation">(</span>Deps<span class="token punctuation">.</span>ktlint<span class="token punctuation">)</span> <span class="token punctuation">}</span> |
Một vài điểm lưu ý trong đống code này:
- Mình tạo 1 configurations để khai báo dependency tên là
ktlint
(giống nhưimplementation
,testImplementation
haykapt
,…) - Tạo 2 gradle task:
ktlintCheck
: dùng để check Kotlin code style và convention. Ở đây mình chỉ làm đơn giản là check và tạo report dạnghtml
ở đường dẫnapp/build/reports/ktlint/ktlint.html
ktlintFormat
: dùng để format code tự động.
- Nếu bạn skip bước 1 thì có thể thay thế chỗ dependencies như sau:1234dependencies <span class="token punctuation">{</span><span class="token function">ktlint</span><span class="token punctuation">(</span><span class="token string">"com.pinterest:ktlint:0.36.0"</span><span class="token punctuation">)</span><span class="token punctuation">}</span>
3. Vào file app/build.gradle.kts
hoàn thành các bước cuối cùng
- Thêm references đến file
ktlint.gradle.kts
, lưu ý chỗfrom
, ở đây mình đang để cùng cấp với filebuild.gradle.kts
nhé.
1 2 3 4 5 6 7 8 9 10 11 | plugins <span class="token punctuation">{</span> <span class="token function">id</span><span class="token punctuation">(</span>Plugins<span class="token punctuation">.</span>androidApp<span class="token punctuation">)</span> <span class="token function">kotlin</span><span class="token punctuation">(</span>Plugins<span class="token punctuation">.</span>kotlinAndroid<span class="token punctuation">)</span> <span class="token operator">..</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> <span class="token comment">// Add this references</span> buildscript <span class="token punctuation">{</span> <span class="token function">apply</span><span class="token punctuation">(</span>from <span class="token operator">=</span> <span class="token string">"ktlint.gradle.kts"</span><span class="token punctuation">)</span> <span class="token comment">// path to ktlint.gradle.kts file</span> <span class="token punctuation">}</span> android <span class="token punctuation">{</span><span class="token operator">..</span><span class="token punctuation">.</span><span class="token punctuation">}</span> |
4. Custom một vài rule
Nếu bạn thấy cần thiết để phù hợp dự án hoặc code style của bạn và anh em trong team.
- Tạo 1 file
.editorconfig
đặt ở thư mục ngoài cùng (rootDir
) dự án. Chỉnh sửa config cần thiết, xem hướng dẫn đầy đủ ở đây. Với dự án của mình, hiện tại mình chỉ setup 1 tý thôi:
1 2 3 4 5 6 | <span class="token comment"># this use for ktlint config</span> <span class="token comment"># see more at https://github.com/pinterest/ktlint#editorconfig</span> <span class="token punctuation">[</span><span class="token operator">*</span><span class="token punctuation">.</span><span class="token punctuation">{</span>kt<span class="token punctuation">,</span> kts<span class="token punctuation">}</span><span class="token punctuation">]</span> <span class="token comment"># Comma-separated list of rules to disable</span> disabled_rules<span class="token operator">=</span>no<span class="token operator">-</span>wildcard<span class="token operator">-</span>imports |
5. Chạy thử kết quả
- Chạy lệnh
./gradlew ktlintFormat
sau khi bạn code xong để tự động format code. - Sau khi chạy lệnh, thường thì tự động format sẽ thành công. Nếu bị lỗi thì có thể vào thư mục
app/build/reports/ktlint
để xem report và fix bằng tay. Theo kinh nghiệm mình làm thì chắc phải 99.69% là thành công, phần trăm lỗi còn lại là do thuật toán tự động format còn 1 vài lỗi nhỏ và cần fix bằng cơm (yaoming).
Report html format:
Cài đặt detekt
Meet detekt, a static code analysis tool for the Kotlin programming language. It operates on the abstract syntax tree provided by the Kotlin compiler.
Về cơ bản đây là một linter, analyzer dùng để check code smell, tạo một report về độ phức tạp của code như LOCs, độ phức tạp chu kỳ (cyclomatic complexity
) và số lượng code smell…
detekt cũng là một wrapper dựa trên ktlint nên nó có thể formatting code, nhưng mình đã dùng ktlint riêng (luôn mới nhất) nên không muốn dùng detekt cho việc auto format.
Và tương tự như ktlint, mình cũng chỉ apply ở mức nhanh, gọn nhất có thể, hạn chế thay đổi qúa nhiều config có sẵn của detekt. Bạn có thể thay đổi cho phù hợp dự án mình nhé.
Nào bắt đầu thôi !
1. Setup dependency
Khai báo dependency
1 2 3 4 5 6 7 | <span class="token keyword">object</span> Versions <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token keyword">val</span> detekt <span class="token operator">=</span> <span class="token string">"1.9.1"</span> <span class="token punctuation">}</span> <span class="token keyword">object</span> Plugins <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token keyword">val</span> detekt <span class="token operator">=</span> <span class="token string">"io.gitlab.arturbosch.detekt"</span> <span class="token punctuation">}</span> |
Trong file app/build.gradle.kts
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | plugins <span class="token punctuation">{</span> <span class="token function">id</span><span class="token punctuation">(</span>Plugins<span class="token punctuation">.</span>androidApp<span class="token punctuation">)</span> <span class="token function">kotlin</span><span class="token punctuation">(</span>Plugins<span class="token punctuation">.</span>kotlinAndroid<span class="token punctuation">)</span> <span class="token function">id</span><span class="token punctuation">(</span>Plugins<span class="token punctuation">.</span>detekt<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">version</span><span class="token punctuation">(</span>Versions<span class="token punctuation">.</span>detekt<span class="token punctuation">)</span> <span class="token comment">// or id("io.gitlab.arturbosch.detekt").version("1.9.1")</span> <span class="token operator">..</span><span class="token punctuation">.</span> <span class="token punctuation">}</span> detekt <span class="token punctuation">{</span> config <span class="token operator">=</span> <span class="token function">files</span><span class="token punctuation">(</span><span class="token string">"<span class="token interpolation variable">$rootDir</span>/config/detekt/detekt.yml"</span><span class="token punctuation">)</span> <span class="token comment">// config rules file</span> input <span class="token operator">=</span> <span class="token function">files</span><span class="token punctuation">(</span><span class="token string">"src/main/java"</span><span class="token punctuation">)</span> <span class="token comment">// note: java or kotlin???</span> <span class="token punctuation">}</span> tasks <span class="token punctuation">{</span> withType<span class="token operator"><</span>Detekt<span class="token operator">></span> <span class="token punctuation">{</span> <span class="token comment">// Target version of the generated JVM bytecode. It is used for type resolution.</span> <span class="token keyword">this</span><span class="token punctuation">.</span>jvmTarget <span class="token operator">=</span> <span class="token string">"1.8"</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> |
Note:
Detekt
chạy cầnjvmTarget 1.8
- Lưu ý thư mục input source của dự án mình đang là
src/main/java
, của bạn có thể làsrc/main/kotlin
hoặc gì đó.
2. Generate default detekt config
Chạy lệnh ./gradlew detektGenerateConfig
, task này sẽ tự động sinh ra file detekt.yml
chứa toàn bộ default config rules.
3. Chạy thử kết quả
- Chạy lệnh
./gradlew detekt
. - Mình nghĩ với dự án đã implement lâu (maintain) thì sẽ fail kha khá. Mình khuyên ngay tại lúc này, hãy làm các bước sau theo đúng trình tự:
- Vào thư mục report để xem toàn bộ lỗi.
- Fix các lỗi nặng về code smell, càng nhiều càng tốt vì bản chất nó không tốt cho dự án.
- Chạy lại và check report để fix tiếp nếu còn nhiều lỗi nặng.
- Sau khi fix toàn bộ các lỗi nên fix, nếu không thể pass, hãy thử chỉnh sửa vài rule cho phù hợp dự án – bước 4.
4. Custom config rules
Important Note: Vì đây là những config đã chuẩn và phù hợp cho hầu hết các dự án kotlin, nên hãy suy nghĩ trước khi thay đổi.
Ok, sự thật là nếu bạn apply detekt vào một dự án đã quá lâu thì thực sự nó rất khó để làm điều note trên, và một sự thật khác là một vài config trong android khác so với kotlin thuần, dù là do vậy nhưng việc custom vẫn nên hạn chế thấp nhất có thể. Rule để thay đổi là:
Kiểm tra những lỗi fail trong report và chắc chắn rằng nó cần được dùng trong dự án.
Mở file config/detekt.yml
và thay đổi. Dưới đây là những phần bên mình có modify, bạn có thể tham khảo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <span class="token comment"># this is some of sample rules can be updated or not</span> <span class="token key atrule">maxIssues</span><span class="token punctuation">:</span> <span class="token number">3 </span><span class="token comment"># default is 0, we should consider on this :D </span> <span class="token key atrule">TooGenericExceptionCaught</span><span class="token punctuation">:</span> <span class="token key atrule">active</span><span class="token punctuation">:</span> <span class="token boolean important">true</span> <span class="token key atrule">excludes</span><span class="token punctuation">:</span> <span class="token string">"**/test/**,**/androidTest/**,**/*.Test.kt,**/*.Spec.kt,**/*.Spek.kt"</span> <span class="token key atrule">exceptionNames</span><span class="token punctuation">:</span> <span class="token punctuation">-</span> ArrayIndexOutOfBoundsException <span class="token punctuation">-</span> Error <span class="token comment"># - Exception My project needs to catch Exception... :(</span> <span class="token punctuation">-</span> IllegalMonitorStateException <span class="token punctuation">-</span> NullPointerException <span class="token punctuation">-</span> IndexOutOfBoundsException <span class="token punctuation">-</span> RuntimeException <span class="token comment"># - Throwable My project needs to catch Throwable... :(</span> <span class="token key atrule">ForbiddenComment</span><span class="token punctuation">:</span> <span class="token key atrule">active</span><span class="token punctuation">:</span> <span class="token boolean important">false </span><span class="token comment">#true, I think while implementation phase, TODO, FIXME comments should be kept.</span> <span class="token key atrule">ReturnCount</span><span class="token punctuation">:</span> <span class="token key atrule">active</span><span class="token punctuation">:</span> <span class="token boolean important">false </span><span class="token comment">#true</span> <span class="token punctuation">...</span> <span class="token key atrule">WildcardImport</span><span class="token punctuation">:</span> <span class="token key atrule">active</span><span class="token punctuation">:</span> <span class="token boolean important">false </span><span class="token comment">#default is true</span> |
Conclustion
Trên đây là toàn bộ những bước cơ bản để giúp dự án android (kotlin) tăng một chút chất lượng ở giai đoạn implementation code về convention, code style, quality… Hy vọng mọi người apply thành công !
Source code tham khảo:
Happy Coding !