DEV Community

Hoang Son
Hoang Son

Posted on • Edited on

Từ Design System đến Disaster System: Một câu chuyện cười về Breaking Change

Chào mọi người,

Hôm nay, tôi sẽ kể cho bạn nghe một câu chuyện có thật về việc tôi xây dựng một thư viện Design System bằng Jetpack Compose, rồi tự tay biến nó thành một "cơn ác mộng" cho các feature team trong công ty. Đây không chỉ là câu chuyện về code, mà còn là về stress, những đêm dài debug, và một bài học đắt giá pha chút hài hước để tôi không khóc thành tiếng. Nếu bạn từng làm việc với Jetpack Compose hoặc từng gây ra breaking change, hãy ngồi xuống, uống một ngụm cà phê, và cùng tôi cười vào nỗi đau này nhé!

Bắt đầu từ một giấc mơ đẹp với Jetpack Compose

Mọi chuyện bắt đầu khi tôi quyết định xây dựng một Design System bằng Jetpack Compose – công nghệ UI hiện đại, declarative, và đầy hứa hẹn của Android. Ý tưởng là tạo ra một thư viện chứa các component như button, text field, modal… để các team trong công ty có thể tái sử dụng một cách dễ dàng. Tôi tưởng tượng mình là một anh hùng: "Các bạn cứ việc gọi Composable, phần còn lại để tôi lo!" Các designer cũng rất hào hứng, liên tục gửi variant mới để tôi cập nhật thư viện. Nghe có vẻ tuyệt, đúng không? Nhưng rồi, tôi nhanh chóng nhận ra mình không phải siêu anh hùng, mà giống như một kẻ vụng về cầm cưa cắt nhầm chân mình.

Sai lầm số 1: Data Class – Tưởng bạn thân, hóa ra phản bội

Tôi dùng data class để định nghĩa các style trong Design System. Ví dụ:

data class ButtonStyle(
    val textColor: Color,
    val backgroundColor: Color
)
Enter fullscreen mode Exit fullscreen mode

Rồi một ngày, designer bảo: "Thêm padding đi cho nó sang chảnh hơn!" Thế là tôi cập nhật:

data class ButtonStyle(
    val textColor: Color,
    val backgroundColor: Color,
    val padding: Dp
)
Enter fullscreen mode Exit fullscreen mode

Kết quả? Crash tan nát ở phía feature team vì binary incompatibility. Tôi không biết rằng thay đổi cấu trúc data class là một "tội ác" trong API Guidelines của JetBrains. Lúc này, tôi chỉ muốn hét lên: "Padding à, mày có đáng để tao chịu cảnh này không?!"

Sai lầm số 2: "Chỉ đổi thứ tự param thôi mà, có sao đâu!"

Một lần khác, tôi "tối ưu" một hàm trong thư viện:

@Composable
fun PrimaryButton(text: String, style: ButtonStyle) { ... }
Enter fullscreen mode Exit fullscreen mode

Tôi nghĩ: "Cho style lên trước cho nó quan trọng hơn đi!" Và thế là:

@Composable
fun PrimaryButton(style: ButtonStyle, text: String) { ... }
Enter fullscreen mode Exit fullscreen mode

Hậu quả? JVM function signature thay đổi, các team dùng phiên bản cũ crash runtime. Tôi tưởng tượng họ nhìn lỗi mà thầm chửi: "Thằng nào làm cái hàm này vậy?!" Đúng rồi, chính tôi đây.

Sai lầm số 3: Optional Param và sự ngây thơ của tôi

Tôi từng nghĩ mình thông minh khi cố gắng giữ backward compatibility. Tôi dùng annotation @Deprecated với message rõ ràng để thông báo cho các team về những thay đổi sắp tới. Ví dụ:

@Deprecated("Use PrimaryButtonV2 instead", ReplaceWith("PrimaryButtonV2(text, style)"))
@Composable
fun PrimaryButton(text: String, style: ButtonStyle) { ... }
Enter fullscreen mode Exit fullscreen mode

Tự hào lắm! Nhưng rồi tôi lại phạm một sai lầm chết người khi xử lý các optional parameter (nullable param). Theo best practice về parameter order từ Google cho Jetpack Compose:

  1. Required parameters (bắt buộc).
  2. Single Modifier = Modifier.
  3. Optional parameters (tùy chọn).
  4. (Optional) Trailing @Composable lambda. Tôi đã cố gắng tuân thủ thứ tự này. Ví dụ ban đầu tôi có:
@Composable
fun PrimaryButton(
    text: String,
    modifier: Modifier = Modifier,
    onClick: () -> Unit
) { ... }
Enter fullscreen mode Exit fullscreen mode

Sau đó, designer gửi thêm variant, tôi thêm một optional param:

@Composable
fun MyButton(
    text: String,
    modifier: Modifier = Modifier,
    enabled: Boolean = true, // Optional param mới
    onClick: () -> Unit
) { ... }
Enter fullscreen mode Exit fullscreen mode

Nghe thì hợp lý, vì tôi chen enabled (optional) vào trước onClick (trailing lambda), đúng theo thứ tự Google khuyến cáo. Nhưng tôi quên mất một điều: việc chen optional param vào giữa làm thay đổi JVM binary signature! Các team dùng phiên bản cũ – vốn gọi hàm theo thứ tự param cũ – crash ngay lập tức. Lẽ ra tôi nên để nguyên thứ tự cũ và thêm optional param ở cuối với giá trị default, như thế này:

@Composable
fun MyButton(
    text: String,
    modifier: Modifier = Modifier,
    onClick: () -> Unit,
    enabled: Boolean = true // Thêm ở cuối
) { ... }
Enter fullscreen mode Exit fullscreen mode

Nhưng không, tôi đã quá tự tin và không dùng Kotlin Binary Compatibility Validator (BCV) để kiểm tra. Kết quả là hàng loạt ticket bug bay đến, kèm theo những ánh mắt nghi ngờ từ feature team.

Hậu quả: Stress và những đêm dài tự vấn

Các team bắt đầu gọi tôi là "kẻ phá hoại Design System". Mỗi lần tôi push code mới, họ lại thấp thỏm: "Liệu lần này có crash nữa không?" Tôi thì stress đến mức tối nào cũng ngồi debug, vừa code vừa tự nhủ: "Mình viết thư viện để giúp người ta mà, sao giờ mình thành nhân vật phản diện vậy trời?" Designer thì cứ gửi variant mới, còn tôi thì vừa muốn cập nhật, vừa sợ phá thêm thứ gì đó.

Và nếu tôi được làm lại hả?

Sau tất cả drama này, tôi quyết định viết bài blog để xả stress và chia sẻ kinh nghiệm xương máu. Đây là những gì tôi học được:

  1. Đọc tài liệu chính thức: API Guidelines của JetBrains và best practice của Google không phải để trưng bày, mà để cứu bạn khỏi những sai lầm ngớ ngẩn.
  2. Cẩn thận với data class: Nếu cần thêm property, hãy tạo class mới hoặc dùng default param thay vì sửa trực tiếp.
  3. Không chen optional param lung tung: Thêm param mới ở cuối hàm để tránh phá binary signature, đặc biệt với Jetpack Compose.
  4. Dùng Kotlin BCV: Công cụ này là cứu tinh để phát hiện breaking change trước khi bạn đẩy code lên production.
  5. Giao tiếp là chìa khóa: Báo trước cho các team khi có thay đổi lớn, đừng để họ bất ngờ với crash.
  6. Cười để sống: Khi mọi thứ rối tung, hãy tự cười vào mình một chút – ít nhất bạn sẽ không khóc.

Hiện tại, tôi đang refactor lại thư viện, áp dụng các best practice tử tế hơn, và cố gắng lấy lại niềm tin từ feature team. Nếu bạn cũng từng gây ra breaking change với Jetpack Compose (hoặc bất kỳ thứ gì khác), hãy kể tôi nghe câu chuyện của bạn nhé!

Kết luận

Design System bằng Jetpack Compose là một giấc mơ đẹp, nhưng nếu không cẩn thận, nó có thể biến thành "Disaster System". Hy vọng câu chuyện của tôi giúp bạn tránh được những cái hố tôi đã ngã (và ngã khá đau). Còn giờ thì tôi đi uống cà phê đây, vì sau tất cả drama này, tôi xứng đáng được thư giãn một chút.

Happy coding, và đừng quên: Breaking change không phải tận thế – chỉ là một bài kiểm tra xem bạn có đủ kiên nhẫn để debug hay không thôi!

Prefference

  1. API Guidelines for components in Jetpack Compose
  2. Backward compatibility guidelines for library authors
  3. Binary Compatibility Validator: Managing API Breaking Changes in Your Android Library

Top comments (0)