Bài viết này tôi sẽ giới thiệu về phương pháp mở rộng lint và tạo custom rule.
Lint và cách custom rule của lint như thế nào?
Lint là một bộ phân tích tĩnh, có mục tiêu tìm lỗi trên mã nguồn của bạn mà không cần phải biên dịch hoặc chạy nó.
Để tạo custom rule của lint thì cần phải tạo Detector, Issue, Registry.
Creating your rules module
Chúng ta bắt đầu bằng cách định nghĩa một module Java / Kotlin riêng biệt. Sau đó chúng ta sẽ thêm vào build.gradle như sau:
Detector là một lớp có thể tìm thấy một hay nhiều vấn đề cụ thể hơn. Tùy thuộc vào vấn đề, chúng ta có thể sử dụng các loại phát hiện khác nhau: SourceCodeScanner – một trình phát hiện chuyên về các tệp nguồn Java / Kotlin. XmlScanner – một trình phát hiện chuyên về các tệp XML. GradleScanner – một trình phát hiện chuyên về các tệp Gradle. ResourceFolderScanner – một trình phát hiện chuyên về các thư mục tài nguyên (không phải các tệp mà nó chứa).
Bạn có thể tham khảo tạo Detector dưới đây:
package techover.rules
import com.android.tools.lint.detector.api.*
import com.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
/**
* This detector will report any usages of the android.util.Log.
*/
class AndroidLogDetector : Detector(), SourceCodeScanner {
override fun getApplicableMethodNames(): List<String> =
listOf("tag", "format", "v", "d", "i", "w", "e", "wtf")
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
super.visitMethodCall(context, node, method)
val evaluator = context.evaluator
if (evaluator.isMemberInClass(method, "android.util.Log")) {
reportUsage(context, node)
}
}
private fun reportUsage(context: JavaContext, node: UCallExpression) {
context.report(
issue = ISSUE,
scope = node,
location = context.getCallLocation(
call = node,
includeReceiver = true,
includeArguments = true
),
message = "android.util.Log usage is forbidden."
)
}
companion object {
private val IMPLEMENTATION = Implementation(
AndroidLogDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
val ISSUE: Issue = Issue
.create(
id = "AndroidLogDetector",
briefDescription = "The android Log should not be used",
explanation = """
For amazing showcasing purposes we should not use the Android Log. We should the
AmazingLog instead.
""".trimIndent(),
category = Category.CORRECTNESS,
priority = 9,
severity = Severity.ERROR,
androidSpecific = true,
implementation = IMPLEMENTATION
)
}
}
Nó extends Detector để Android Lint có thể sử dụng để phát hiện sự cố.
Nó extends SourceCodeScanner vì chúng ta cần kiểm tra cả hai tệp Kotlin và Java.
getApplossibleMethodNames – chỉ lọc các chữ ký phương thức tồn tại trong android.util.Log
visitMethodCall – sử dụng trình đánh giá để đảm bảo rằng phương thức này được gọi bởi android.util.Log chứ không phải bởi bất kỳ lớp nào khác. Ví dụ: AmazingLog có cùng phương thức và không nên gắn cờ.
reportUsage – được sử dụng để báo cáo sự cố khi tìm thấy.
Creating Issue
Issue là một lỗi tiềm ẩn trong ứng dụng Android. Đây là cách bạn khai báo lỗi mà quy tắc của bạn sẽ giải quyết. id – để xác định duy nhất vấn đề này. briefDescription– mô tả tóm tắt về vấn đề. explanation – nên là một mô tả sâu hơn về vấn đề và lý tưởng về cách giải quyết vấn đề. category – xác định loại vấn đề. Có rất nhiều danh mục có thể có như CORRECTNESS, USABILITY, I18N, COMPLIANCE, PERFORMANCE, … priority – một số từ 1 đến 10, trong đó số càng lớn thì vấn đề càng nghiêm trọng. severity – nó có thể là một trong những giá trị sau: FATAL, ERROR, WARNING, INFORMATIONAL and IGNORE. Lưu ý: Nếu mức độ nghiêm trọng là FATAL hoặc ERROR thì việc chạy lint sẽ thất bại và bạn sẽ phải giải quyết vấn đề. implementation – lớp chịu trách nhiệm phân tích tệp và phát hiện vấn đề.
Bạn có thể tham khảo tạo Issue dưới đây:
val ISSUE: Issue = Issue
.create(
id = "AndroidLogDetector",
briefDescription = "The android Log should not be used",
explanation = """
For amazing showcasing purposes we should not use the Android Log. We should the
AmazingLog instead.
""".trimIndent(),
category = Category.CORRECTNESS,
priority = 9,
severity = Severity.ERROR,
androidSpecific = true,
implementation = IMPLEMENTATION
)
Creating Registry
Bạn có thể tham khảo Registry dưới đây:
package techover.rules
import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue
class IssueRegistry : IssueRegistry() {
override val api: Int = CURRENT_API
override val issues: List<Issue>
get() = listOf(AndroidLogDetector.ISSUE)
}
Nếu source không có Issue mà chúng ta đã Registry thì sẽ in ra nội dung như sau:
> Task :app:lintDebug
Wrote HTML report to ~/Techover/Lint/app/build/reports/lint-results-debug.html
Wrote XML report to ~/Techover/Lint/app/build/reports/lint-results-debug.xml
BUILD SUCCESSFUL
Ví dụ, trong class MainActivity bạn có log như sau:
Thì chúng ta thấy source có Issue mà chúng ta đã Registry nê sẽ in ra nội dung như sau:
> Task :app:lintDebug FAILED
Wrote HTML report to ~/Techover/Lint/app/build/reports/lint-results-debug.html
Wrote XML report to ~/Techover/Lint/app/build/reports/lint-results-debug.xml
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:lintDebug'.
> Lint found errors in the project; aborting build.
Fix the issues identified by lint, or add the following to your build script to proceed with errors:
...
android {
lintOptions {
abortOnError false
}
}
...
Errors found:
~/Techover/Lint/app/src/main/java/techover/lint/MainActivity.kt:13: Error: android.util.Log usage is forbidden. [AndroidLogDetector]
Log.d("MainActivity", "https://magz.techover.io/")
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED
Xin chào các bạn, lại là tôi đây, bài viết lần này tôi sẽ chỉ cho các bạn cách cải thiện mã nguồn của bạn với Detekt.
Detekt là gì?
Detekt là một công cụ để phân tích code cho lập trình kotlin. Nó hoạt động dựa trên các cú phúp trừu tượng được cung cấp bởi trình biên dịch kotlin.
Detekt có những đặc trưng nào?
Phân tích code smell cho các dự án kotlin của bạn.
Báo cáo độ phức tạp dựa trên các dòng code. Độ phức tạp của McCabe và số lượng code smells.
Cấu hình cao (rule set or rule level)
Loại bỏ các phát hiện với chú thích của kotlin là @Suppress và java là @SuppressWarnings
Xác định ngưỡng code smell sẽ phá vỡ bản build của bạn và in ra cảnh báo.
Code smell baseline và bỏ qua danh sách legacy của dự án.
Gradle Plugin để phân tích code qua Gradle Build.
Grade task sử dụng IntelliJ để định dạng và kiểm tra mã kotlin.
Tuỳ chọn cấu hình của detekt cho mỗi module bằng cách sử dụng profiles (gradle-plugin).
Tích hợp SonarQube.
Có thể mở rộng bằng quy tắc riêng và FileProcessListener’s.
Tích hợp IntelliJ.
Sử dụng detekt như nào?
Trước tiên, bạn hãy cấu hình trong gradle build file.
Chúng ta cùng đi tìm hiểu trong task detekt bên trên có những gì? def input = "$projectDir/src/" là phần nào trong dự án mà bạn muốn được phân tích. def config = "$rootDir/detekt/detekt-config.yml" là phần bạn cài đặt sẽ sử dụng những quy tắc nào để phân tích. def filters = ".*/techover.detekt/.*" là phần loại bỏ không cần phân tích, với nhiều đường dẫn khác nhau thì bạn sử dụng dấu phẩy để phân tách. def output = "$rootDir/detekt/reports" là phần mà khi chạy phân tích sẽ đưa ra tài liệu báo cáo.
Vậy là bạn đã có thể chạy detekt cho dự án của bạn rồi, bằng cách chạy câu lệnh:
./gradlew detekt
Sau khi chạy câu lệnh gradlew bên trên thì sẽ xuất hiện output như dưới đây:
Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details
> Task :app:detekt
Successfully generated XmlOutputReport.
Successfully generated PlainOutputReport.
detekt run within 1019 ms
BUILD SUCCESSFUL in 14s
1 actionable task: 1 executed
Như vậy là source code của dự án bạn đang không có lỗi nào.
Nếu như bạn chạy mà source code có lỗi thì sẽ hiển thị như sau:
> Task :app:detekt
Ruleset: code-smell
Ruleset: comments
Ruleset: complexity
Ruleset: empty-blocks
Ruleset: exceptions
Ruleset: performance
Ruleset: potential-bugs
Ruleset: style
WildcardImport - [ExampleInstrumentedTest.kt] at androidTest/java/techover/detekt/ExampleInstrumentedTest.kt:9:1
WildcardImport - [ExampleUnitTest.kt] at test/java/techover/detekt/ExampleUnitTest.kt:5:1
FunctionNaming - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:13:5
FunctionNaming - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:13:5
MagicNumber - [addition_isCorrect] at test/java/techover/detekt/ExampleUnitTest.kt:15:22
Successfully generated XmlOutputReport.
Successfully generated PlainOutputReport.
detekt run within 1203 ms
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
Để bạn có thể hiểu hơn về tập tin cấu hình mà bạn tham khảo ở link bên trên detekt-config.yml thì dưới đây tôi sẽ nói qua về phần này cho bạn hiểu hơn.
Tập tin cấu hình detekt có những gì?
Detekt sử dụng tệp cấu hình kiểu yaml cho nhiều thứ khác nhau:
Bộ quy tắc và thuộc tính quy tắc.
Build thất bại.
Bộ xử lý tệp kotlin.
Console và định dạng đầu ra.
Bộ quy tắc và quy tắc:
Detekt cho phép dễ dàng chỉ cần chọn các quy tắc bạn muốn và cấu hình chúng theo cách bạn muốn. Ví dụ, nếu bạn muốn cho phép tối đa 20 chức năng trong tệp Kotlin thay vì ngưỡng mặc định là 10.
complexity:
TooManyFunctions:
threshold: 20
Bộ lọc đường dẫn / Không bao gồm / Bao gồm:
Bắt đầu với phiên bản RC15 bộ lọc đường dẫn có thể được xác định cho từng quy tắc hoặc bộ quy tắc:
Ghi chú: Bài viết này chỉ là một góc nhìn chủ quan của tác giả về mảng mobile app. vì vậy có gì không đúng mọi người có thể đóng góp ở phần comment nhé!! Thank.
Mở đầu: Ở thời điểm hiện tại việc xây dựng ứng dụng native không phải là lựa chọn duy nhất để tạo lên một một ứng dụng mobile app. Ngày nay chúng ta có thể dựa vào yêu cầu của khách hàng, các chức năng của sản phẩm để lựa chọn được hướng đi phù hợp hơn. Ta có thể dựa trên vào công nghệ web (HTML5, CSS3 và JavaScript) đang phát triển mạnh mẽ trên mobile. Hoặc tận hưởng những lợi ích của các công cụ phát triển đa nền tảng như React Native hoặc Flutter. Dưới đây, bạn sẽ tìm thấy chìa khóa để giải quyết vấn đề khó khăn này khi chọn phương pháp phát triển ứng dụng di động.
Native App
Native app hay còn được gọi là ứng dụng gốc. Vốn dĩ nó có cái tên này là bởi vì nó được viết bằng chính các ngôn ngữ lập trình gốc thần nhất dành riêng cho từng nền tảng cụ thể. Hai nền tảng di động phổ biến nhất hiện nay là Android và iOS (Windows Phone thì đã bị khai tử vào tháng 10/ 2017 ). Từ đó, các ngôn ngữ lập trình tương ứng được chính các công ty mẹ tạo ra phù hợp với từng nền tảng. Chẳng hạn như Apple đã có Swift, Objecive-C được dành cho lập trình ứng dụng trên nền tảng iOS. Lập trình trên Android thì dùng Java, mặc dù đây không phải ngôn ngữ do Google tạo ra.
Viết Native App nghĩa là lập trình viên sẽ sử dụng IDE, SDK mà nhà sản xuất cung cấp để lập trình ra một ứng dụng, build ứng dụng đó thành file cài và gửi lên App Store để kiểm duyệt. Người dùng sẽ phải tìm ứng dụng trên App Store, tải về máy và chạy.
Với những hệ thống lớn, cần đồng bộ, ta vẫn phải viết phần back-end trên server. Server sẽ đưa ra một số API. Native app lấy dữ liệu về máy, truyền dữ liệu lên server thông qua các API này.
Ưu điểm
Tận dụng được toàn bộ những tính năng của device: Chụp ảnh, nghiêng máy, rung, GPS, notification.
Có thể chạy được offline.
Performance rất nhanh vì code native sẽ được chạy trực tiếp.
UX phù hợp với từng nền tảng
Là lựa chọn duy nhất cho các ứng dụng game, xử lý hình ảnh hay video …
Khuyết điểm
Cần cài đặt nặng nề (Android Studio, XCode, Android SDK, …), khó tiếp cận.
Với mỗi hệ điều hành, ta phải viết một ứng dụng riêng. Khó đảm bảo sự đồng bộ giữa các ứng dụng (1 button trên Android sẽ khác 1 button trên iOS, pop cũng khác).
Cần phải submit app lên App Store, mỗi lần update phải thông báo người dùng.
Code mệt và lâu hơn so với Mobile Web dẫn đến một khuyết điểm là chi phí phát triển cao.
Kĩ năng cần có
Ngôn ngữ lập trình: Java / Kotlin cho Android, Objective-C / Swift cho iOS
Kiến thức chuyên sâu về ứng dụng: View, Action, Adapter trong Android …
Cách xây dựng Web Serivce, Restful API, cách gọi API từ device, …
Hybrid App kết hợp những ưu điểm của Mobile Web và Native App. Ta xây dựng một ứng dụng bằng HTML, CSS, Javascript, chạy trên WebView của mobile. Tuy nhiên, Hybrid App vẫn có thể tận dụng những tính năng của device: chụp hình, GPS, rung, ….
Hybrid App sẽ được viết dựa trên một cross-platform framework: Cordova, Phonegap, Ionic …. Ta sẽ gọi những chức năng của mobile thông qua API mà framework này cung cấp, dưới dạng Javascript. Bạn chỉ cần viết một lần, những framework này sẽ tự động dịch ứng dụng này ra các file cài đặt cho Android, iOS . Một số ứng dụng không quá nặng về xử lý, cần tận dụng chức năng của device sẽ chọn hướng phát triển này.
Ưu điểm
Chỉ cần biết HTML, CSS, JS .
Viết một lần, chạy được trên nhiều hệ điều hành
Tận dụng được các chức năng của device.
Khuyết điểm
Không ổn định, khó debug. Framework sẽ dịch code của bạn thành code native, việc sửa lỗi ứng dụng khá khó vì bạn không biết code sẽ được dịch ra như thế nào.
Performance chậm.
Cần cài đặt nhiều thứ (phải cài đặt SDK này nọ thì mới build ứng dụng được).
Kiến thức cần biết
HTML, CSS, Javscript cơ bản.
Cách dùng một số framework CSS, Javascript: jQuery Mobile, Ionic Framework, AngularJS, Bootstrap, …
Kiến thức về các cross-platform framework: Cordova, Phonegap…
Cách xây dựng Web Serivce, Restful API, cách gọi API từ device, … (Hybrid app cũng sẽ kết nối với server thông qua API như Native App).
Được sinh ra nhằm mục đích để giải quyết bài toán hiệu năng của Hybrid và bài toán chi phí khi mà phải viết nhiều loại ngôn ngữ native cho từng nền tảng di động. Nhưng chúng ta lại hay nhầm lẫn giữa Hybrid App và Cross-Platform App, trên thực tế thì chúng khác hoàn toàn nhau. Có lẽ, đặc điểm chung duy nhất giữa chúng là khả năng chia sẻ source code. Lập trình viên chỉ cần lập trình một lần và biên dịch hoặc phiên dịch ra thành nhiều bản Native App tương ứng với từng nền tảng khác nhau.
Công cụ quan trọng nhất để thực hiện các dự án ứng dụng đa nền tảng (Cross Platform) chính là Frameworks đa nền tảng. Có rất nhiều Framework đa nền tảng. Mỗi loại sẽ có những điểm mạnh và điểm yếu khác nhau. Tùy vào mục tiêu xây dựng App mà lập trình viên sẽ lựa chọn Framework nào cho phù hợp.
Nổi tiếng và phổ biến nhất là Framework Xamarin. Ngôn ngữ lập trình chủ đạo trong Xamarin là C#, ngoài ra còn có Objective-C, Swift và Java. Ngoài ra, còn một số cái tên mà khá hot đó là React-Native (thằng này có ông bô là Facebook ), Flutter (thằng này có ông bác là Google)…
Ưu điểm
Tận dụng được những tính năng của device: Chụp ảnh, nghiêng máy, rung, GPS, notification.
Hiệu năng tương đối ổn định.
Tiết kiệm tiền.
Hiệu quả về mặt thời gian khi mà bạn muốn phát triển một ứng dụng nhanh chóng.
Trải nghiệm người dùng tốt hơn là hybrid app.
Nhược điểm
Hiệu năng sẽ thấp hơn với app native code.
Khó học vẫn đòi hỏi kiến thức native code.
Vẫn còn có hạn chế từ framework
Kĩ năng cần có
Kiến thức C# (đối với Xamarin ), JS (đối với React-Native ), Dart(đối với Flutter) Objective-C, Swift và Java cơ bản.
Kiến thức về một số framework React-Native, Xamarin …
Hướng Mobile Web thường được áp dụng khi các bạn đã có sẵn một website đang hoạt động. Ta sẽ tạo thêm 1 trang web riêng cho mobile, sử dụng HTML, CSS, một số framework hỗ trợ mobile và responsive (Bootstrap, jQuery Mobile, Materialize). Người dùng sẽ trang web dành cho mobile để dùng ứng dụng.
Các xử lý khác liên quan đến backend như database sẽ được thực hiện phía trên server. Với một số framework như Angular, VueJS … một trang web có thể giống y hệt một ứng dụng di động thật sự.
Ưu điểm
Chỉ cần có kiến thức về web là viết được
Viết một lần, chạy được trên mọi hệ điều hành
Người dùng không cần phải cài app, có thể vào thẳng trang web
Không cần phải thông qua App Store, tiết kiệm tiền
Dễ nâng cấp (Chỉ việc nâng cấp web là xong)
Nhược điểm
Với một số máy đời cũ, Web App sẽ bị bể giao diện, hiển thị sai, hoặc javascript không chạy.
Performance chậm
Không thể tận dụng được các tính năng của di động: Push notification, chụp hình, nghiêng máy, định vị GPS…
Kĩ năng cần có
Kiến thức HTML, CSS, Javascript cơ bản.
Kiến thức về một số framework responsive/mobile như: jQuery Mobile, Bootstrap, …
Một số framework javascript để viết Single Page Application: AngularJS, VueJS, …
Kết Bài
Sorry các bạn bài viết hơi dài, sau khi nhìn tổng quan về mobile app thì các bạn đã chọn cho mình hướng đi nào chưa? còn mình thì sẽ tiếp tục theo hướng Cross-Platform app. Cảm ơn các bạn đã đọc đến đây nhé.
Điểm mạnh của IJKPlayer là low latency, nó có độ trễ khá thấp khi streaming, nhưng giả sử phát sinh tình huống cần record một đoạn video khi đang streaming thì phải làm thế nào? IJKPlayer không support sẵn.
Sau khi tham khảo 1 số blog của các bạn…..Trung Quốc và build được thành công thì mình share lại thông tin cho ai cần(vâng, ko hiểu tại sao cứ liên quan đến video, camera,Streaming thì tài liệu chỉ có thể tìm thấy bên….Trung Quốc. Không phủ nhận, các bạn ý giỏi thật )
Trước hết, các bạn cần build được IJKPlayer cho Android đã nhé, cụ thể có thể xem link bên dưới
OK, Sau khi các bạn biết cách chuẩn bị môi trường và build thành công, chúng ta cần tiến hành chỉnh sửa một chút trong thư viện IJKplayer(Mình sẽ không đề cập đến vấn đề pháp lý, licence ở đây)
Chúng ta cần check out source code và build thử nhé
git clone https://github.com/baka3k/IjkPlayerRecorder.git
cd IjkPlayerRecorder
// Khởi tạo project build cho Android
./init-android.sh
//IJKPlayer có sử dụng ffmpeg - nên việc build ffmpeg là bắt buộc
cd android/contrib
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all
//quay lại thư mục IJK và build IJKPlayer
cd ..
./compile-ijk.sh all
Nếu thành công – tức là bạn đã setup đầy đủ, có thể build IJKPlayer được, sẵn sàng cho việc thêm code để thêm chức năng recorder cho IJKPlayer.
Tiếp tục nhé
1.Trong thư mục ijkmedia/ijkplayer bạn tìm đến file ff_ffplay.h và khai báo các hàm sau:
Có nhiều cách để play RTSP trên Android, ứng cử viên hàng đầu là VideoView sẵn có trên Android SDK. Tuy nhiên Video View có rất nhiều hạn chế khi chúng ta cần custom lại, ví dụ chỉnh sửa thêm thắt vào protocol, add các hiệu ứng hình ảnh vào video khi đang play, record, chuyển đổi các track..etc. Khi đó chúng ta phải lựa chọn một giải pháp khác, cụ thể ở đây mình đang nói đến
Exoplayer
IJKPlayer
Exoplayer (https://github.com/google/ExoPlayer) open source, apache licence, java core, rất dễ dàng để anh em lập trình android/java tiếp xúc nhưng điểm trừ của nó là độ trễ – low latency
Khi test với 1 số server RTSP, ExoPlayer cho ra độ trễ vào khoảng 1.2s đến 2.5s tùy chất lượng video out put. Còn IJKPlayer(https://github.com/bilibili/ijkplayer) thì có thể đạt 0.6s đến 0.8s. Điểm trừ của IJKPlayer là licence, IJKPlayer sử dụng FFMPEG, một thư viện rất nổi tiếng, nên nếu ko muốn dính dáng đến pháp lý khi bán sản phẩm, bạn nên cẩn thận khi lựa chọn nó cho khách hàng hoặc nhúng vào sản phẩm.
Để build được IJKPlayer cũng khá đơn giản(nếu bạn chuẩn bị đủ môi trường), hướng dẫn này mình viết chạy trên môi trường MAC OS nhé, Window hoặc Ubuntu cách làm tương tự, tuy nhiên môi trường chuẩn bị sẽ khác đi 1 chút
Check out source code
Các bạn có thể lấy source IJK từ nhánh chính ở trên hoặc từ repo bên dưới, repo này mình đã add thêm một vài chỉnh sửa liên quan đến RTSP và Recorder
Cài đặt NDK cho Mac: mình test thực tế thì thấy build trên bản NDK 10, NDK16(khá cũ) thì ok nhất, ko cần sửa lỗi và chỉnh lại config build, tất nhiên các bạn pro hơn có thể thử với version NDK khác
git clone https://github.com/baka3k/IjkPlayerRecorder.git
cd IjkPlayerRecorder
./init-android.sh
cd android/contrib
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all
cd ..
./compile-ijk.sh all
Sau khi build xong các bạn có thể tìm thấy file .so trong thư mục
Tình hình là đợt vừa rồi mình có ngó Kiaplog profile của anh Huy Trần, lướt lướt thấy có chủ đề Phức tạp hoá vấn đề: Phân tích và mô phỏng nút cảm xúc của Facebook có lượng kipalog khiếp quá nên nhảy vào xem luôn. Đọc xong mà thấy mở mang đầu óc, nhưng tiếc là lâu chưa xem lại web + cũng gà nữa nên chắc chả code theo được :disappointed_relieved: , đành ngậm ngùi hấp thu phân tích của anh + phân tích thêm để giống với app Facebook và nung nấu chuyển hóa nó sang Android :sunglasses:
Biểu tượng cảm xúc mới của Facebook trên Android
Đầu tiên khi mình nhìn vào reaction box (hộp biểu tượng) trên web và tư duy theo cách thiết kế trên Android thì tặc lưỡi “không khó lắm nhỉ, chắc dùng mấy view con trong layout rồi mông má thêm tí animation là oke”. Nói thế chứ cũng phải kiểm chứng lại trong app, không lại “treo đầu dê bán thịt chó”.
Đầu tiên là install app :sweat_smile: (mình gỡ khá lâu rồi vì nó nofity liên tọi). Long click thử vào nút Like nào… Má ơi! :scream: Các chuyển động + kích thước khá là khác với web, bỗng nhiên nghi ngờ xem thằng facebook nó làm gì với view đó nên liền bật ngay bounds lên để xem (Settings > Developer options > Show layout bounds) thì thôi xong, đây là kết quả: :sob:
Không có 1 cái viền nào xung quanh cái reaction box > Nó vẽ lên view chứ mếu phải dùng layout (Đường viền ngoài là viền của cả cái view reaction) :sob:. Rồi luôn, vẽ thì vẽ, hồi bé thích vẽ lắm, cứ tưởng là lớn lên làm kiến trúc sư cơ đấy :joy:
Phân tích hiệu ứng Reaction
(Phần này sẽ có những phần lấy từ bài anh Huy Trần, chỉ nhằm mục đích tiện cho mọi người theo dõi)
Những phần dưới đây là thiết kế cho web, những phần khác so với mobile mình sẽ chỉ rõ sau. Đầu tiên là một bản tin trên newfeed mà chúng ta thường thấy:
Tiếp theo là khi chúng ta nhấn lâu (long click) vào nút like, reactions box sẽ xuất hiện theo hướng từ dưới lên + từ mờ thành rõ dần:
Tiếp theo ngay sau đó là các emotion xuất hiện, chúng liên tiếp xuất hiện theo hướng từ dưới lên + từ mờ thành rõ dần (alpha tăng) + từ bé thành lớn dần (size tăng):
Chúng ta có thể giả sử rằng tất cả các thành phần như reactions box + emotion đều thực hiện chuyển động của chúng trong 0.3s, nhưng thời điểm bắt đầu của chúng sẽ khác nhau như: reactions box (xuất phát lúc 0.0s), emotion 1 (xuất phát lúc 0.1s), emotion 2 (xuất phát lúc 0.2s), …tương tự với các emotion tiếp theo.
Nếu phân tích kĩ hơn hiệu ứng di chuyển từ dưới lên trên của các emotion thì các emotion sẽ di chuyển như sau:
Chú ý: Hình vẽ trên chỉ thể hiện trạng thái di chuyển theo chiều dọc (tức trục Oy) và trục Ox chính là thời gian thực hiện.
Ở vị trí đầu tiên xuất hiện, emotion sẽ mờ + cách xa reactions box, chúng di chuyển dần dần đến vị trí của chúng ở reactions box, nhưng chúng sẽ đi quá thêm 1 đoạn nhỏ sau đó quay trở lại vị trí của chúng ở reactions box (đừng quá lo lắng về cách xử lý, nó đơn giản chỉ là 1 phương trình xy thui :relaxed:)
Sau khi hoàn thành hiệu ứng, chúng ở trạng thái “bình thường” như hình dưới đây:
Đến đây có lẽ chúng ta cần dừng lại một chút để phân tích thêm việc khi di chuyển các thành phần đối với ứng dụng Facebook trên Android. Khi ta di tay vào emotion:
Chiều cao của reactions box nhỏ lại, tuy nhiên độ rộng vẫn giữ nguyên.
Các emotion không được select sẽ nhỏ lại.
Emotion được select sẽ to ra (gấp khoảng 2.5 đến 3 lần gì đó).
Title của emotion xuất hiện phía trên emotion + hiệu ứng bé thành lớn dần + mờ thành rõ dần.
Khi ngón tay di chuyển ra khỏi cả view reaction thì các thành phần trở lại trạng thái “bình thường”.
Móe, cứ tưởng được làm như web ==’ ai ngờ lại thêm mấy thứ này, khó nhằn phết nhưng thui cứ chiến nhỉ?
À một tí quan điểm trước khi code 😀 :
Những ý tưởng + logic dưới đây hoàn toàn là ý kiến cá nhân của mình, có thể chưa hợp lý > mong mọi người đóng góp.
Code nhắm đến mục đích mô phỏng chứ không nhắm đến viết thư viện > Đừng quở trách “thằng này code đụt, chả flexible gì cả” tội em :'(
Mình đã cố gắng để cho em nó “mượt” đến mức có thể, do thời gian có hạn và chắc hẳn là cũng khó để mượt như Facebook :sweat:
Trạng thái
Theo mình phân tích thì để diễn tả tất cả các hành động của reaction thì gồm có 4 trạng thái:
Trạng thái “BEGIN” – là trạng thái các thành phần lúc bắt xuất hiện.
Trạng thái “NORMAL” – là trạng thái các emotion kích thước như nhau, nằm ngay ngắn trong box.
Trạng thái “CHOOSING” – là trạng thái emotion được chọn phóng to, emotion còn lại + box thu nhỏ lại.
Trạng thái “CHOOSED” (từ này không có trong TA thì phải :joy:) – là trạng thái emotion đc chọn sẽ bắn vút lên, các emotion còn lại sẽ sụp xuống và biến mất hoàn toàn.
Trong phạm vi bài viết này mình sẽ trình bày 3 trạng thái đầu, trạng thái thứ 4 anh em tự chém thêm nhé :kissing_closed_eyes:
Độ rộng (width): 275dp (6 emotion 40dp + khoảng cách giữa emotion vs nhau và với cạnh trái phải board là 5dp => 7*5 = 35dp)
Baseline là đường thẳng cố định, không thay đổi để giúp các emotion được thẳng hàng. Công thức: tọa độ BASE_LINE = BOARD_Y + Emotion.NORMAL_SIZE + DIVIDE
Đầu tiên ta tạo vài lớp dùng chung đã nhé:
Lớp Util:
public class Util {
public static int dpToPx(int dp) {
return (int) (dp * Resources.getSystem().getDisplayMetrics().density);
}
}
Lớp CommonDimen:
public class CommonDimen {
public static int DIVIDE = Util.dpToPx(5);
public static int HEIGHT_VIEW_REACTION = Util.dpToPx(250);
public static int WIDHT_VIEW_REACTION = Util.dpToPx(300);
public static final int MAX_ALPHA = 255;
public static final int MIN_ALPHA = 150;
}
Oke, bây giờ ta tạo lớp cho các đối tượng ta cần vẽ. Class Emotion:
public class Emotion {
private Context context;
public static final int MINIMAL_SIZE = Util.dpToPx(28);
public static final int NORMAL_SIZE = Util.dpToPx(40);
public static final int CHOOSE_SIZE = Util.dpToPx(100);
public static final int DISTANCE = Util.dpToPx(15);
public static final int MAX_WIDTH_TITLE = Util.dpToPx(70);
public int currentSize = NORMAL_SIZE;
public int beginSize;
public int endSize;
public float currentX;
public float currentY;
public float beginY;
public float endY;
public Bitmap imageOrigin;
public Bitmap imageTitle;
public Paint emotionPaint;
public Paint titlePaint;
private float ratioWH;
public Emotion(Context context, String title, int imageResource) {
this.context = context;
imageOrigin = BitmapFactory.decodeResource(context.getResources(), imageResource);
emotionPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
emotionPaint.setAntiAlias(true);
titlePaint = new Paint(Paint.FILTER_BITMAP_FLAG);
titlePaint.setAntiAlias(true);
generateTitleView(title);
}
private void generateTitleView(String title) {
}
public void setAlphaTitle(int alpha) {
titlePaint.setAlpha(alpha);
}
public void drawEmotion(Canvas canvas) {
canvas.drawBitmap(imageOrigin, null, new RectF(currentX, currentY, currentX + currentSize, currentY + currentSize), emotionPaint);
drawTitle(canvas);
}
public void drawTitle(Canvas canvas) {
}
}
Class Board:
public class Board {
public static final int BOARD_WIDTH = 6 * Emotion.NORMAL_SIZE + 7 * CommonDimen.DIVIDE; //DIVIDE = 5dp, Emotion.NORMAL_SIZE = 40dp
public static final int BOARD_HEIGHT_NORMAL = Util.dpToPx(50);
public static final int BOARD_HEIGHT_MINIMAL = Util.dpToPx(38);
public static final float BOARD_X = 10;
public static final float BOARD_BOTTOM = CommonDimen.HEIGHT_VIEW_REACTION - 200;
public static final float BOARD_Y = BOARD_BOTTOM - BOARD_HEIGHT_NORMAL;
public static final float BASE_LINE = BOARD_Y + Emotion.NORMAL_SIZE + CommonDimen.DIVIDE;
public Paint boardPaint;
public float currentHeight = BOARD_HEIGHT_NORMAL;
public float currentY = BOARD_Y;
public float beginHeight;
public float endHeight;
public float beginY;
public float endY;
public Board(Context context) {
initPaint(context);
}
private void initPaint(Context context) {
boardPaint = new Paint();
boardPaint.setAntiAlias(true);
boardPaint.setStyle(Paint.Style.FILL);
boardPaint.setColor(context.getResources().getColor(R.color.board));
boardPaint.setShadowLayer(5.0f, 0.0f, 2.0f, 0xFF000000);
}
public void setCurrentHeight(float newHeight) {
currentHeight = newHeight;
currentY = BOARD_BOTTOM - currentHeight;
}
public float getCurrentHeight() {
return currentHeight;
}
public void drawBoard(Canvas canvas) {
float radius = currentHeight / 2;
RectF board = new RectF(BOARD_X, currentY, BOARD_X + BOARD_WIDTH, currentY + currentHeight);
canvas.drawRoundRect(board, radius, radius, boardPaint);
}
}
Giờ thì quay lại ReactionView để vẽ thử board và các emotion lên xem thế nào nhé. Trước tiên ta khởi tạo đối tượng cho các thành phần:
Method init():
private void init() {
board = new Board(getContext());
setLayerType(LAYER_TYPE_SOFTWARE, board.boardPaint);
emotions[0] = new Emotion(getContext(), "Like", R.drawable.like);
emotions[1] = new Emotion(getContext(), "Love", R.drawable.love);
emotions[2] = new Emotion(getContext(), "Haha", R.drawable.haha);
emotions[3] = new Emotion(getContext(), "Wow", R.drawable.wow);
emotions[4] = new Emotion(getContext(), "Cry", R.drawable.cry);
emotions[5] = new Emotion(getContext(), "Angry", R.drawable.angry);
//BEGIN: Đoạn này để đặt các thành phần vào vị trí ban đầu để xem kết quả thui,
//chứ các thành phần ban đầu sẽ bị ẩn đi, vì chưa click like mà :D
for (int i = 0; i < emotions.length; i++) {
emotions[i].currentY = Board.BASE_LINE - Emotion.NORMAL_SIZE;
emotions[i].currentX = i == 0 ? Board.BOARD_X + DIVIDE : emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
}
//END
initElement();
}
Cùng xem lại hình này nhé:
Trường hợp này tọa độ Y của tất cả các emotion sẽ bằng nhau, tọa độ Y sẽ nằm ở góc trái phía trên các emotion => currentY = Board.BASE_LINE - Emotion.NORMAL_SIZE
Còn tọa độ X của các emotion sẽ được tính dựa trên 2 trường hợp: Nếu nó là emotion đầu tiên, thì nó luôn bằng tọa độ X của bảng + thêm khoảng cách nhỏ. Còn các emotion còn lại sẽ bằng tọa độ X thằng đứng trước + kích thước hiện tại của thằng đứng trước (size) + thêm khoảng cách nhỏ.
Ở method onDraw này sẽ thực hiện vẽ các đối tượng đã được tính sẵn kích thước và tọa độ ở hàm bên trên lên view. Oke, run cái nào :smiling_imp:
Đây là kết quả hiện tại của chúng ta:
:boom: Vậy là đã vẽ thành công các thành phần lên view, bây giờ để thực hiện các chuyển động khác thì ta chỉ cần tính toán lại kích thước + tọa độ rồi gọi method onDraw qua method invalidate() là các thành phần sẽ được cập nhật theo kích thước + tọa độ mới.
Trạng thái “CHOOSING”
Ở trạng thái này chúng ta phải thực hiện 3 công việc sau:
Xử lý độ cao của reaction box giảm dần.
Xử lý kích thước + tọa độ của các emotion.
Xử lý kích thước + tọa độ của title emotion được chọn.
Ta Override lại phương thức onTouchEvent để xác định được emotion nào đang được chọn:
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
handled = true;
break;
case MotionEvent.ACTION_MOVE:
for (int i = 0; i < emotions.length; i++) {
if (event.getX() > emotions[i].currentX && event.getX() < emotions[i].currentX + emotions[i].currentSize) {
selected(i);
break;
}
}
handled = true;
break;
case MotionEvent.ACTION_UP:
backToNormal();
handled = true;
break;
}
return handled;
}
Khi ngón tay di chuyển trên màn hình, thì sẽ xác định xem tọa độ X của ngón tay đang nằm trong khoảng giá trị tọa độ X của emotion nào thì gọi method selected để thực hiện chuyển động phóng to emotion đó.
Khi ngon tay nhấc lên thì gọi method backToNormal để trở về trạng thái NORMAL.
Method selected:
private void selected(int position) {
if (currentPosition == position && state == StateDraw.CHOOSING) return;
state = StateDraw.CHOOSING;
currentPosition = position;
startAnimation(new ChooseEmotionAnimation());
}
Method backToNormal:
public void backToNormal() {
state = StateDraw.NORMAL;
startAnimation(new ChooseEmotionAnimation());
}
Ta cần một chút animation để cho các chuyển động “nuột” hơn. Ở class ReactionView có khởi tạo class ChooseEmotionAnimation:
class ChooseEmotionAnimation extends Animation {
public ChooseEmotionAnimation() {
if (state == StateDraw.CHOOSING) {
beforeAnimateChoosing();
} else if (state == StateDraw.NORMAL) {
beforeAnimateNormalBack();
}
setDuration(DURATION_ANIMATION);
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
calculateInSessionChoosingAndEnding(interpolatedTime);
}
}
Vì ở trạng thái được chọn CHOOSING và trạng thái NORMAL (chuyển về từ trạng thái CHOOSING) có chung một cách xử lý nên sẽ dùng chung Animation và phương thức tính toán.
Method beforeAnimateChoosing:
private void beforeAnimateChoosing() {
board.beginHeight = board.getCurrentHeight();
board.endHeight = Board.BOARD_HEIGHT_MINIMAL;
for (int i = 0; i < emotions.length; i++) {
emotions[i].beginSize = emotions[i].currentSize;
if (i == currentPosition) {
emotions[i].endSize = Emotion.CHOOSE_SIZE;
} else {
emotions[i].endSize = Emotion.MINIMAL_SIZE;
}
}
}
Method beforeAnimateNormalBack:
private void beforeAnimateNormalBack() {
board.beginHeight = board.getCurrentHeight();
board.endHeight = Board.BOARD_HEIGHT_NORMAL;
for (int i = 0; i < emotions.length; i++) {
emotions[i].beginSize = emotions[i].currentSize;
emotions[i].endSize = Emotion.NORMAL_SIZE;
}
}
Phương thức này dùng để xác định trạng thái hiện tại trước khi bắt đầu di chuyển như board.beginHeight và board.endHeight sẽ là di chuyển độ cao của bảng từ beginHeight đến endHeight trong khoảng thời gian DURATION_ANIMATION. Tương tự với các emotion cũng vậy.
Phương thức này sẽ được gọi liên tục trong lúc thực hiện animation để cập nhật view. Thường thì Animation sẽ thực hiện 60fms(frame/s), mỗi lần gọi đến phương thức này sẽ coi là 1 frame, việc của chúng ta là phải tính toán xem các thành phần đó đang ở kích thước + tọa độ nào trong thời điểm interpolatedTime đó (giá trị interpolatedTime là [0, 1] trong khoảng DURATION_ANIMATION).
Method này thực hiện tính tọa độ X cho các emotion, tọa độ Y ở phương thức calculateInSessionChoosingAndEnding đã tính rùi. Như mình demo khi vẽ các emotion ở trạng thái ban đầu. Mình để sự rằng buộc tọa độ X như sau:
Ví dụ: Ta có 6 emotions 1 2 3 4 5 6. Tọa độ X1 luôn luôn cố định, vì nó nằm bên cạnh của bảng. Tọa độ X2 phụ thuộc vào Tọa độ X1 + Size 1, Tọa độ X3 phụ thuộc vào Tọa độ X2 + Size 2, …Như vậy nếu vẽ tĩnh như lúc khởi tạo thì không vấn đề gì, nhưng khi di chuyển cùng với Animation, mọi thứ cập nhật liên tục khiến cho sự phụ thuộc về Tọa độ X + Size của các emotion cuối như 4 5 6 tăng lên làm các emotion di chuyển sai số + không mượt.
=> Giải pháp của mình được thể hiện ở đoạn code trên, nhằm giảm bớt sự phụ thuộc. Mình nhận thấy emotion 1 và 6 có tọa độ ổn định và không bị phụ thuộc nên mình sẽ lấy 2 emotion này làm chốt, từ đó emotion 2 3 sẽ phụ thuộc và 1, emotion 4 5 sẽ phụ thuộc vào 6. Kết quả là các emotion di chuyển khá mượt + chính xác.
Oki, nói nhiều quá, nếu anh em đã implement xong các đoạn code bên trên thì run nào, đây là kết quả sẽ đạt được:
Hề hế, gần xong phase này rùi đó, còn mỗi đồng chí title nữa thui. Giờ thì quay lại class Emotion một chút nào. Đầu tiên mình lại định dùng canvas vẽ tiếp text vs background của nó, nhưng thui thấy nhọc quá. Thế là làm 1 cái layout xong decode nó sang bitmap vẽ cho lẹ:
private void generateTitleView(String title) {
LayoutInflater inflater = LayoutInflater.from(context);
View titleView = inflater.inflate(R.layout.title_view, null);
((TextView) titleView).setText(title);
int w = (int) context.getResources().getDimension(R.dimen.width_title);
int h = (int) context.getResources().getDimension(R.dimen.height_title);
ratioWH = (w * 1.0f) / (h * 1.0f);
imageTitle = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas c = new Canvas(imageTitle);
titleView.layout(0, 0, w, h);
((TextView) titleView).getPaint().setAntiAlias(true);
titleView.draw(c);
}
Dimen width_title : 60dp
Dimen height_title : 25dp
Method này mình tạo bitmap của titleView với string title tương ứng.
Method drawTitle:
public void drawTitle(Canvas canvas) {
int width = (currentSize - NORMAL_SIZE) * 7 / 6;
int height = (int) (width / ratioWH);
setAlphaTitle(Math.min(CommonDimen.MAX_ALPHA * width / MAX_WIDTH_TITLE, CommonDimen.MAX_ALPHA));
if (width <= 0 || height <= 0) return;
float x = currentX + (currentSize - width) / 2;
float y = currentY - DISTANCE - height;
canvas.drawBitmap(imageTitle, null, new RectF(x, y, x + width, y + height), titlePaint);
}
Method này tính kích thước của titleView tương ứng với size của emotion, anh em thấy chỉ là một số phép toán tỷ lệ thui, đặt giấy bút ra là hiểu ngay ý mà.
Hì hí, run nào:
Trạng thái “BEGIN”
Ngược đời vỡi, cuối bài rùi mới đến BEGIN. Chúng ta cùng xem lại quá trình chuyển động của các emotion nhé:
Đồ thị biểu diễn chuyển động của emotion như sau:
Hình bên trái là đồ thị minh hoạ đường đi của emo icon, và hình bên phải là mô phỏng chi tiết vị trí ứng với từng mốc thời gian của emo icon. Vậy việc chúng ta cần làm là điều khiển cho các emo icon di chuyển theo đồ thị trên.
Đồ thị này được thể hiện bằng một phương trình có tên là EaseOutBack, có khá nhiều đồ thị hay ho mà mình quên xừ mất link rùi, bao giờ mình tìm lại được mình sẽ update lại cho mọi người nhé.
Giờ ta tạo một class EaseOutBack:
public class EaseOutBack {
private final float s = 1.70158f;
private final long duration;
private final float begin;
private final float change;
public EaseOutBack(long duration, float begin, float end) {
this.duration = duration;
this.begin = begin;
this.change = end - begin;
}
public static EaseOutBack newInstance(long duration, float beginValue, float endValue) {
return new EaseOutBack(duration, beginValue, endValue);
}
public float getCoordinateYFromTime(float currentTime) {
return change * ((currentTime = currentTime / duration - 1) * currentTime * ((s + 1) * currentTime + s) + 1) + begin;
}
}
Ta thêm một đối tượng EaseOutBack vào trong class ReactionView để nó thực hiện tính toán Y cho các emotion:
public class ReactionView extends View {
...
private EaseOutBack easeOutBack;
...
}
Giờ thì ta xóa đoạn code tạm để xác định tọa độ ban đầu các thành phần đi nhé:
XÓA ở method init:
//BEGIN: Đoạn này để đặt các thành phần vào vị trí ban đầu để xem kết quả thui,
//chứ các thành phần ban đầu sẽ bị ẩn đi, vì chưa click like mà :D
for (int i = 0; i < emotions.length; i++) {
emotions[i].currentY = Board.BASE_LINE - Emotion.NORMAL_SIZE;
emotions[i].currentX = i == 0 ? Board.BOARD_X + DIVIDE : emotions[i - 1].currentX + emotions[i - 1].currentSize + DIVIDE;
}
//END
Mục đích bây giờ là khi ta ấn nút like, các thành phần di chuyển lên, nên suy ra đầu tiên ta phải gán tọa độ cho các thành phần ở vị trí không nhìn thấy bằng cách là cho nó ở ngoài khoảng cách của view cha:
Ở đây anh em sẽ thấy DURATION_BEGINNING_ANIMATION = 900 của Animation BeginningAnimation có tí lạ. Mình sẽ giải thích thế này, view của ta có 7 thành phần là 1 board + 6 emotion. Mình muốn các thành phần lần lượt thực hiện di chuyển chứ không muốn cả lũ xuất phát cùng lúc nên mình đặt thế này, board xuất phát đầu tiên, emotion 1 xuất phát lúc 100, emotion 2 xuất phát lúc 200, …và thằng cuối cùng xuất phát lúc 600. Các ông thần này đều thực hiện quãng đường của mình trong 0.3s => Tổng thời gian 7 ông thần kia thực hiện mất 600 (0.6s) + 0.3s cho ông cuối thực hiện nốt là 0.9s như ta thấy.
Flutter là một mobile UI technology & SDK from Google, nó cho phép bạn build native apps đa nền tảng (Android & iOS) với hiệu suất và độ trung thực cao trong 1 thời gian ngắn, tiết kiệm chi phí.
Flutter hoạt động với những code có sẵn được sử dụng bởi các lập trình viên và tổ chức trên thế giới.
Tại sao lại là Flutter ?
Ưu điểm:
Dễ dàng cài đặt và sử dụng (cái này thì ăn đứt React-Native , sure win), bộ doc khá ngon và đầy đủ. Chỉ cần tải flutter từ git về, chạy command line “flutter doctor” nó sẽ báo cho bạn tất cả những vấn đề đang tồn tại trong hệ thống của bạn.
Phát triển cho cả iOS và Android trên cùng 1 codebase nên sẽ tiết kiệm được chi phí và thời gian.
Tuy là 1 cross-platform nhưng hiệu năng và giao diện đạt tới mức gần như native app.
Flutter hoàn toàn free và là open-source.
Ngôn ngữ Dart: Dart — OOP, những dev quen làm việc với Java và C# sẽ bắt nhịp rất nhanh với Dart.
Animation và trải nghiệm cài đặt của Flutter thực sự tốt và mượt mà như 1 native app.
Nhược điểm:
Animation: Rất khó để tạo ra 1 animation riêng biệt của mình.
Giới hạn về thư viện.
Framework còn khá “trẻ”, cần thêm thời gian để phát triển.
Đặc điểm nổi bật của Flutter
Fast Development
Hot reload: Mỗi khi update source, thay vì phải build lại app như native, thì chúng ta chỉ cần 1 phím “r” thần thánh (nếu dùng command line để build) là app sẽ được update ngay lập tức trong vòng 1–2s.
Flutter có cầu nối là Dart, kích thước ứng dụng lớn hơn, nhưng nó hoạt động nhanh hơn nhiều. Không giống như React Native với cầu nối là Javascript.
Hỗ trợ tốt cho các IDE (Android Studio ,IntelliJ ,VS Code).
Trình điều hướng được tích hợp sẵn.
Expressive & Flexible UI: Flutter có thể làm thoả mãn những người dùng khó tính nhất với các widget built-in đẹp mắt theo Material Design và Cupertino (iOS — flavor), scroll mượt mà, dễ dàng thao tác với các UI component.
Truy cập các tính năng và SDK native: Flutter cho phép bạn sử dụng lại code Java, Swift, Objective-C hiện tại của mình và truy cập các tính năng và SDK native trên iOS và Android.
Native performance.
Flutter có các công cụ và thư viện để giúp bạn dễ dàng đưa ý tưởng của mình vào cuộc sống trên iOS và Android. Nếu bạn chưa có kinh nghiệm phát triển trên thiết bị di động, thì Flutter là một cách dễ dàng và nhanh chóng để xây dựng các ứng dụng di động tuyệt đẹp. Nếu bạn là một nhà phát triển iOS hoặc Android có kinh nghiệm, bạn có thể sử dụng Flutter cho các View của bạn và tận dụng nhiều code Java / Kotlin / ObjC / Swift hiện có của bạn.