React Native tại Airbnb (Phần cuối): Điều kì diệu tiếp theo là gì?

Tác giả: Gabriel Peal

Một chặng đường thú vị phía trước 

Khi đang còn làm việc với React Native, chúng tôi cũng bỏ không ít nỗ lực vào native. Ngày hôm nay, chúng tôi đã có được một số lượng kha khá các project thú vị trong production hoặc trong pipeline. Một số project được lấy cảm hứng từ những kinh nghiệm và trải nghiệm tốt nhất của chúng tôi với React Native.

Tuyển dụng lập trình React Native lương cao

Server-Driven Render

Mặc dù chúng tôi không còn dùng React Native nữa, chúng tôi vẫn thấy được giá trị lớn lao của việc viết product code chỉ 1 lần. Thật ra chúng tôi vẫn phụ thuộc khá nhiều vào hệ thống universal design language system (DLS) của mình và nhiều screen nhìn cũng khá tương tự nhau trên Android và iOS.

Rất nhiều team đã thí nghiệm thử và bắt đầu tiến hành hợp nhất các framework render chạy bằng server rất mạnh. Bằng những framework này, server sẽ gửi data mô tả component được render về thiết bị, screen configuration, và cá action có thể diễn ra. Mỗi mobile platform sẽ xử lý data này và renders các native screen hoặc hết nguyên flow dùng các component.

Render chạy bằng server cũng kéo theo không ít thử thách. Dưới đây là một số vấn đề điển hình mà chúng tôi phải xử lý:

  • Vừa update các định dạng component an toàn vừa maintain tương thích backward.
  • Share định dạng type cho các component trên các platform.
  • Respond các event vào runtime như các button tap hoặc user input.
  • Chuyển giao giữa các screen đa JSON-driven trong lúc duy trì state.
  • Render hoàn toàn các custom component không có implementation sẵn tại build-time. Chúng tôi đã thử nghiệm format Lona.

Các framework render theo server này đã mang đến rất nhiều giá trị như cho phép chúng ta thử nghiệm và update tính năng ngay trong cái búng tay.

Các Epoxy Component

Trong năm 2016, we đã cho mở nguồn mở Epoxy cho Android. Epoxy là một framework cho phép đa dạng dễ dàng như RecyclerViews, UICollectionViews, và UITableViews. Ngày nay, hầu như mọi screen mới đều dùng Epoxy. Làm vậy cho phép chúng ta tách mỗi screen thành các component riêng biệt và đạt lazy-rendering. Giờ đây, chúng ta có Epoxy trên cả Android và iOS.

Trên iOS nó trông như sau:

BasicRow.epoxyModel(
  content: BasicRow.Content(
    titleText: "Settings",
    subtitleText: "Optional subtitle"),
  style: .standard,
  dataID: "settings",
  selectionHandler: { [weak self] _, _, _ in
    self?.navigate(to: .settings)
  })

Trên Android, chúng ta cũng đã tận dụng khả năng viết các DSL trên Kotlin để áp dụng các component một cách dễ dàng:

basicRow {
 id("settings")
 title(R.string.settings)
 subtitleText(R.string.settings_subtitle)
 onClickListener { navigateTo(SETTINGS) }
}

Epoxy Diffing

Trong React, bạn return một list component từ render. Mấu chốt quan trọng trong performance của React đó là những component này chỉ là một cái gì đó đại diện cho data model của views/HTML thực mà bạn muốn render. Component tree từ đó sẽ được diff và chỉ có những thay đổi mới bị xử lý. Chúng tôi đã build môt concept tương tự cho Epoxy. Trong Epoxy, bạn phải declare các model cho toàn bộ screen trong buildModels. Chuyện này cộng với Kotlin DSL làm nó khá giống với React và sẽ trông như thế này:

override fun EpoxyController.buildModels() {
  header {
    id("marquee")
    title(R.string.edit_profile)
  }
  inputRow {
    id("first name")
    title(R.string.first_name)
    text(firstName)
    onChange { 
      firstName = it 
      requestModelBuild()
    }
  }
  // Put the rest of your models here...
}

Cứ mỗi khi data thay đổi, bạn sẽ call requestModelBuild() và nó sẽ render screen lần nữa bằng các RecyclerView call tối ưa hoá đã gửi đi.

Trên iOS, nó trông như thế này:

override func itemModel(forDataID dataID: DemoDataID) -> EpoxyableModel? {
  switch dataID {
  case .header:
    return DocumentMarquee.epoxyModel(
      content: DocumentMarquee.Content(titleText: "Edit Profile"),
      style: .standard,
      dataID: DemoDataID.header)
  case .inputRow:
    return InputRow.epoxyModel(
      content: InputRow.Content(
        titleText: "First name",
        inputText: firstName)
      style: .standard,
      dataID: DemoDataID.inputRow,
      behaviorSetter: { [weak self] view, content, dataID in
        view.textDidChangeBlock = { _, inputText in
          self?.firstName = inputText
          self?.rebuildItemModel(forDataID: .inputRow)
        }
      })
  }
}

Một Android Product Framework mới (MvRx)

Một trong những phát triển thú vị nhất mới gần đây đó là Framework mới mà chúng tôi đang phát triển nội bộ tên là MvRx, là sự kết hợp tinh hoa của Epoxy, JetpackRxJava, và Kotlin với nhiều bộ quy tắc của React giúp việc build screen mới dễ dàng hơn và trơn tru hơn bao giờ hết. Là một framework có nhiều ý kiến trái chiều nhưng khá linh hoạt ra đời từ những pattern phổ biến mà chúng ta hay thấy và cũng là những phần tinh hoa nhất của React. Nó cũng an toàn cho thread và gần như mọi thứ đều trải đều trên main thread làm cho việc kéo thả và animation trở nên mượt mà và đẹp hơn hẳn.

Đến nay nó đã hiệu quả trên rất nhiều screen và gần như bỏ đi nhu cầu xử lý các lifecycle. Hiện chúng tôi đang cho dùng thử trên mọi product Android và dự tính sẽ làm nó open source nếu nó thành công. Đây là phần code đầy đủ cần để tạo nên một screen tính năng hoàn thiện đưa ra các network request:

data class SimpleDemoState(val listing: Async<Listing> = Uninitialized)

class SimpleDemoViewModel(override val initialState: SimpleDemoState) : MvRxViewModel<SimpleDemoState>() {
    init {
        fetchListing()
    }

    private fun fetchListing() {
        // This automatically fires off a request and maps its response to Async<Listing>
        // which is a sealed class and can be: Unitialized, Loading, Success, and Fail.
        // No need for separate success and failure handlers!
        // This request is also lifecycle-aware. It will survive configuration changes and
        // will never be delivered after onStop.
        ListingRequest.forListingId(12345L).execute { copy(listing = it) }
    }
}

class SimpleDemoFragment : MvRxFragment() {
    // This will automatically subscribe to the ViewModel state and rebuild the epoxy models
    // any time anything changes. Similar to how React's render method runs for every change of
    // props or state.
    private val viewModel by fragmentViewModel(SimpleDemoViewModel::class)

    override fun EpoxyController.buildModels() {
        val (state) = withState(viewModel)
        if (state.listing is Loading) {
            loader()
            return
        }
        // These Epoxy models are not the views themself so calling buildModels is cheap. RecyclerView
        // diffing will be automaticaly done and only the models that changed will re-render.
        documentMarquee {
            title(state.listing().name)
        }
        // Put the rest of your Epoxy models here...
    }

    override fun EpoxyController.buildFooter() = fixedActionFooter {
        val (state) = withState(viewModel)
        buttonLoading(state is Loading)
        buttonText(state.listing().price)
        buttonOnClickListener { _ -> }
    }
}

MvRx có cấu trúc khá đơn giản để xử lý các Fragment arg, savedInstanceState persistence khi quy trình bắt đầu, TTI tracking, và một số feature khác.

Chúng tôi cũng đang xử lý một framework tương tự trên iOS và sẽ sớm được đưa vào testing.

Iteration Speed

Có một điều khá rõ ràng khi chuyển từ React Native về lại native đó là iteration speed. Nếu phải đi từ việc test thay đổi chỉ trong 1 vài giây sang test lên đến 15 phút là điều không thể chấp nhận được. May mắn thay là chúng tôi có thể thay đổi tình thế một chút ở đây.

Chúng tôi đã build infrastructure trên Android và iOS để bạn có thể compile một phần app bao gồm một launcher và có thể phụ thuộc vào một số feature module nhất định.

Trên Android, nó dùng rất nhiều gradle product flavor. Gradle module của chúng tôi trông như thế này:

Loại hình gián tiếp này sẽ giúp kĩ sư build và develop trên một slice nhỏ của app. Cộng với IntelliJ module unloading sẽ cải thiện được đáng kể build và IDE performance trên MacBook Pro.

Chúng tôi đã build nên các script để tạo được các phép testing mới và chỉ trong vòng vài tháng đã có trên 20 cái.

Những build mới sử dụng các phép thử mới này nhanh hơn trung bình gấp 2.5 lần và tỉ lệ các build tốn hơn 5 phút thì giảm xuống còn 15x.

Bạn có thể tham khảo gradle snippet này được dùng để tạo ra các product flavor có root dependency module.

Tương tự, trên iOS, các module như sau:

Hệ thông tương tự cũng cho ra các build nhanh hơn gấp 3 đến 8 lần.

Kết luận

Chúng tôi luôn tự hào khi là một công ty không ngần ngại thử các loại công nghệ mới và không ngừng nỗ lực maintain từ chất lượng, tốc độ đến trải nghiệm cho developer tốt nhất. Suy cho cùng, React Native đã đóng vai trò một tool hết sức quan trọng trong ship các feature và cho chúng ta một cái nhìn mới về phát triển mobile. Nếu cuộc hành trình này làm bạn thích thú và muốn trở thành một phần của nó, đừng ngần ngại đến với chúng tôi!

Bài viết gốc được đăng tải tại Medium

  React Native tại Airbnb (P4): Ngày tàn của React Native

  Thất nghiệp tuổi 35: Khủng hoảng tuổi 30 thực ra được báo trước bởi những cơn buồn ngủ tuổi 25?