13 thg 4, 2025·8 phút đọc

Kotlin Coroutines vs RxJava cho mạng và tác vụ nền

Kotlin Coroutines vs RxJava: so sánh hủy, xử lý lỗi và mẫu kiểm thử cho networking và công việc nền trong ứng dụng Android thực tế.

Kotlin Coroutines vs RxJava cho mạng và tác vụ nền

Tại sao lựa chọn này lại quan trọng cho networking production

Networking và công việc nền trong một ứng dụng Android thực tế không chỉ là một lần gọi API. Nó bao gồm đăng nhập và làm mới token, màn hình có thể xoay ngang giữa chừng khi đang tải, đồng bộ khi người dùng rời màn hình, tải ảnh, và công việc định kỳ không được tiêu tốn pin.

Các bug gây hại nhất thường không phải lỗi cú pháp. Chúng xuất hiện khi công việc bất đồng bộ sống lâu hơn UI (rò bộ nhớ), khi hủy chỉ dừng UI nhưng không dừng request thực tế (lãng phí băng thông và spinner bị kẹt), khi retry nhân các request (giới hạn tỉ lệ, bị cấm), hoặc khi các tầng khác nhau xử lý lỗi khác nhau khiến không ai dự đoán được người dùng sẽ thấy gì.

Quyết định giữa Kotlin Coroutines vs RxJava ảnh hưởng đến độ tin cậy hàng ngày:

  • Cách bạn mô hình hóa công việc (một lần vs luồng)
  • Cách hủy được truyền lan
  • Cách lỗi được biểu diễn và hiện ra UI
  • Cách bạn kiểm soát luồng cho mạng, đĩa và UI
  • Khả năng test timing, retry và các trường hợp cạnh

Các mẫu dưới đây tập trung vào thứ thường phá vỡ khi tải cao hoặc mạng chậm: hủy, xử lý lỗi, retry và timeout, và thói quen test giúp tránh regressions. Ví dụ ngắn và thực dụng.

Mô hình tư duy chính: suspend calls, streams và Flow

Sự khác biệt chính giữa Kotlin Coroutines vs RxJava là hình dạng công việc bạn đang mô tả.

Một hàm suspend đại diện cho một thao tác một lần. Nó trả về một giá trị hoặc ném một lỗi. Điều đó phù hợp với hầu hết các lời gọi mạng: lấy profile, cập nhật cài đặt, tải ảnh. Mã gọi đọc tuần tự từ trên xuống, dễ đọc ngay cả khi bạn thêm logging, cache, và nhánh.

RxJava bắt đầu bằng câu hỏi bạn xử lý một giá trị hay nhiều giá trị theo thời gian. Single là kết quả một lần (thành công hoặc lỗi). Observable (hoặc Flowable) là một luồng có thể phát nhiều giá trị, rồi hoàn thành hoặc lỗi. Điều này phù hợp với các tính năng có tính sự kiện thật sự: thay đổi text, tin nhắn websocket, hoặc polling.

Flow là cách thân thiện với coroutine để biểu diễn luồng. Hãy nghĩ nó như phiên bản luồng của coroutines, với hủy có cấu trúc và phù hợp trực tiếp với API suspend.

Quy tắc nhanh:

  • Dùng suspend cho một request trả về một lần.
  • Dùng Flow cho các giá trị thay đổi theo thời gian.
  • Dùng RxJava khi app đã phụ thuộc nặng vào các operator và ghép luồng phức tạp.

Khi tính năng mở rộng, độ đọc thường hỏng đầu tiên nếu bạn áp mô hình luồng lên một request một lần, hoặc cố biến sự kiện liên tục thành một giá trị trả về. Hãy chọn abstraction phù hợp với thực tế trước, rồi xây quy ước xung quanh nó.

Hủy trong thực tế (kèm ví dụ code ngắn)

Hủy là nơi mã bất đồng bộ hoặc cho cảm giác an toàn, hoặc biến thành crash ngẫu nhiên và request lãng phí. Mục tiêu đơn giản: khi người dùng rời màn hình, mọi công việc bắt đầu cho màn hình đó nên dừng.

Với Kotlin Coroutines, hủy được tích hợp trong mô hình. Một Job đại diện cho công việc, và với structured concurrency bạn thường không truyền Job ra ngoài. Bạn bắt đầu công việc trong một scope (như ViewModel scope). Khi scope đó bị hủy, mọi thứ bên trong cũng bị hủy.

class ProfileViewModel(
    private val api: Api
) : ViewModel() {

    fun loadProfile() = viewModelScope.launch {
        // If the ViewModel is cleared, this coroutine is cancelled,
        // and so is the in-flight network call (if the client supports it).
        val profile = api.getProfile() // suspend
        // update UI state here
    }
}

Hai chi tiết production quan trọng:

  • Gọi network suspend thông qua client có thể hủy. Nếu không, coroutine dừng nhưng cuộc gọi HTTP có thể vẫn chạy.
  • Dùng withTimeout (hoặc withTimeoutOrNull) cho các request không được treo.

RxJava dùng disposal rõ ràng. Bạn giữ một Disposable cho mỗi subscription, hoặc gom chúng vào CompositeDisposable. Khi màn hình biến mất, bạn dispose, và chuỗi sẽ dừng nếu upstream hỗ trợ hủy.

class ProfilePresenter(private val api: ApiRx) {
    private val bag = CompositeDisposable()

    fun attach() {
        bag += api.getProfile()
            .subscribe(
                { profile -> /* render */ },
                { error -> /* show error */ }
            )
    }

    fun detach() {
        bag.clear() // cancels in-flight work if upstream supports cancellation
    }
}

Một quy tắc thực tế khi rời màn hình: nếu bạn không chỉ rõ được nơi hủy xảy ra (hủy scope hoặc dispose()), hãy cho rằng công việc sẽ tiếp tục chạy và sửa trước khi phát hành.

Xử lý lỗi sao cho dễ hiểu

Một khác biệt lớn giữa Kotlin Coroutines vs RxJava là cách lỗi được truyền. Coroutines khiến thất bại trông giống code bình thường: một suspend call ném, và người gọi quyết định xử lý. Rx đẩy lỗi qua luồng, rất mạnh, nhưng dễ che lấp vấn đề nếu không cẩn thận.

Dùng exception cho lỗi bất ngờ (timeout, 500, lỗi parse). Mô hình hóa lỗi như dữ liệu khi UI cần phản hồi cụ thể (mật khẩu sai, “email đã dùng”) và bạn muốn đó là một phần của domain model.

Một pattern coroutine đơn giản giữ stack trace và dễ đọc:

suspend fun loadProfile(): Profile = try {
    api.getProfile() // may throw
} catch (e: IOException) {
    throw NetworkException("No connection", e)
}

runCatchingResult hữu ích khi bạn thực sự muốn trả về thành công hoặc thất bại mà không ném:

suspend fun loadProfileResult(): Result<Profile> =
    runCatching { api.getProfile() }

Cẩn thận với getOrNull() nếu bạn không xử lý thất bại. Điều đó có thể âm thầm biến bug thành màn hình “rỗng”.

Trong RxJava, giữ đường đi lỗi rõ ràng. Dùng onErrorReturn chỉ cho fallback an toàn. Ưu tiên onErrorResumeNext khi bạn cần chuyển nguồn (ví dụ, trả về dữ liệu cache). Với retry, giữ quy tắc hẹp bằng retryWhen để không retry trên lỗi kiểu “mật khẩu sai”.

Một tập thói quen ngăn lỗi bị nuốt:

  • Log hoặc báo lỗi một lần, gần nơi bạn có ngữ cảnh.
  • Giữ exception gốc làm cause khi bạn bọc lỗi.
  • Tránh fallback bắt tất cả biến mọi lỗi thành giá trị mặc định.
  • Làm lỗi hiển thị cho người dùng thành model có kiểu, không phải chuỗi.

Cơ bản về luồng: Dispatchers vs Schedulers

Làm cho logic đoán trước được
Tạo logic nghiệp vụ bằng workflow kéo-thả khớp với cách ứng dụng của bạn hoạt động.
Xây dựng không code

Nhiều bug async liên quan đến luồng: làm công việc nặng trên main, hoặc chạm UI từ thread nền. Kotlin Coroutines vs RxJava khác nhau chủ yếu ở cách bạn diễn đạt chuyển luồng.

Với coroutines, bạn thường bắt đầu trên main cho công việc UI, rồi chuyển sang dispatcher nền cho phần nặng. Các lựa chọn phổ biến:

  • Dispatchers.Main cho cập nhật UI
  • Dispatchers.IO cho I/O blocking như mạng và đĩa
  • Dispatchers.Default cho công việc CPU như parse JSON, sort, mã hóa

Một pattern thẳng thắn: fetch dữ liệu, parse ngoài main, rồi render.

viewModelScope.launch(Dispatchers.Main) {
    val json = withContext(Dispatchers.IO) { api.fetchProfileJson() }
    val profile = withContext(Dispatchers.Default) { parseProfile(json) }
    _uiState.value = UiState.Content(profile)
}

RxJava diễn đạt “nơi công việc chạy” bằng subscribeOn và “nơi kết quả được quan sát” bằng observeOn. Một bất ngờ thường gặp là mong observeOn ảnh hưởng upstream; nó không làm vậy. subscribeOn đặt thread cho nguồn và operator phía trên nó, và mỗi observeOn chuyển luồng từ điểm đó trở đi.

api.fetchProfileJson()
    .subscribeOn(Schedulers.io())
    .map { json -> parseProfile(json) } // still on io unless you change it
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
        { profile -> render(profile) },
        { error -> showError(error) }
    )

Một quy tắc tránh bất ngờ: giữ công việc UI ở một chỗ. Trong coroutines, gán hoặc collect UI state trên Dispatchers.Main. Trong RxJava, đặt một observeOn(main) cuối cùng ngay trước render và đừng rải observeOn khắp nơi nếu không cần.

Nếu màn hình bị giật, chuyển parsing và map ra khỏi main trước tiên. Thay đổi đó sửa nhiều vấn đề thực tế.

Retry, timeout và công việc song song cho các cuộc gọi mạng

Giao hàng nhanh các tính năng chung
Thêm xác thực và thanh toán với các module tích hợp sẵn khi cần.
Khám phá AppMaster

Đường dẫn thuận rarely là vấn đề. Vấn đề đến từ call treo, retry làm tệ hơn, hoặc công việc “song song” không thực sự song song. Những pattern này thường quyết định đội thích Kotlin Coroutines hay RxJava.

Timeout để thất bại nhanh

Với coroutines, bạn đặt giới hạn cứng quanh bất kỳ suspend call nào. Giữ timeout gần nơi gọi để có thể hiển thị message UI phù hợp.

val user = withTimeout(5_000) {
    api.getUser() // suspend
}

Trong RxJava, bạn gắn operator timeout vào stream. Điều này hữu ích khi hành vi timeout là một phần của pipeline chia sẻ.

Retry mà không gây hại

Chỉ retry khi an toàn. Quy tắc đơn giản: retry các request idempotent (như GET) nhiều hơn các request gây side-effect (như “tạo đơn”). Ngay cả khi vậy, giới hạn số lần và thêm delay hoặc jitter.

Hàng rào mặc định tốt:

  • Retry trên timeouts mạng và lỗi server tạm thời.
  • Không retry trên lỗi validation (400) hoặc lỗi auth.
  • Giới hạn retry (thường 2–3) và log thất bại cuối cùng.
  • Dùng backoff delay để không spam server.

Trong RxJava, retryWhen cho bạn biểu đạt “chỉ retry cho những lỗi này, với delay này.” Trong coroutines, Flow có retryretryWhen, trong khi các suspend function thuần thường dùng vòng lặp nhỏ với delay.

Gọi song song mà code không rối

Coroutines làm song song trực tiếp: bắt hai request, chờ cả hai.

coroutineScope {
    val profile = async { api.getProfile() }
    val feed = async { api.getFeed() }
    profile.await() to feed.await()
}

RxJava nổi bật khi ghép nhiều nguồn là mục đích chính của chuỗi. zip là công cụ “chờ cả hai”, và merge hữu ích khi bạn muốn kết quả tới ngay khi có. Với luồng lớn hoặc nhanh, backpressure vẫn quan trọng. Flowable của RxJava có công cụ backpressure trưởng thành. Flow của coroutines xử lý nhiều trường hợp tốt, nhưng bạn có thể cần buffering hoặc chính sách drop nếu sự kiện nhanh hơn UI hoặc ghi vào DB.

Interop và pattern di trú (codebase hỗn hợp)

Hầu hết đội không chuyển đổi trong một đêm. Chiến lược Kotlin Coroutines vs RxJava thực dụng giữ app ổn định khi bạn chuyển module từng phần.

Wrap API Rx vào suspend

Nếu bạn có Single<T> hoặc Completable, wrap nó với hỗ trợ hủy để coroutine bị hủy sẽ dispose subscription Rx.

suspend fun <T : Any> Single<T>.awaitCancellable(): T =
  suspendCancellableCoroutine { cont ->
    val d = subscribe(
      { value -> cont.resume(value) {} },
      { error -> cont.resumeWithException(error) }
    )
    cont.invokeOnCancellation { d.dispose() }
  }

Điều này tránh một chế độ lỗi phổ biến: người dùng rời màn hình, coroutine bị hủy, nhưng cuộc gọi mạng vẫn chạy và cập nhật state chia sẻ sau đó.

Expose code coroutine cho caller Rx

Trong quá trình di cư, một số tầng vẫn mong đợi kiểu Rx. Wrap suspend work trong Single.fromCallable và block chỉ trên background thread.

fun loadProfileRx(api: Api): Single<Profile> =
  Single.fromCallable {
    runBlocking { api.loadProfile() } // ensure subscribeOn(Schedulers.io())
  }

Giữ biên giới này nhỏ và có tài liệu. Với mã mới, ưu tiên gọi suspend API trực tiếp từ scope coroutine.

Flow phù hợp ở đâu, và không phù hợp ở đâu

Flow có thể thay thế nhiều use case của Observable: state UI, update DB, và luồng kiểu paging. Nó có thể kém trực tiếp nếu bạn dựa nhiều vào hot streams, subjects, tuning backpressure nâng cao, hoặc một tập operator tùy chỉnh lớn mà đội bạn đã quen.

Chiến lược di cư giảm nhầm lẫn:

  • Chuyển modules lá trước (network, storage) sang suspend API.
  • Thêm adapter nhỏ ở ranh giới module (Rx -> suspend, suspend -> Rx).
  • Thay Rx streams bằng Flow chỉ khi bạn kiểm soát cả consumer.
  • Giữ một kiểu async cho mỗi khu vực tính năng.
  • Xóa adapter ngay khi caller cuối cùng đã di cư.

Mẫu test bạn thực sự dùng

Tránh nợ kỹ thuật sớm
Nhận mã nguồn thực tế bạn có thể xem lại, mở rộng và duy trì khi yêu cầu thay đổi.
Tạo mã

Timing và hủy là nơi bug ẩn. Test async tốt làm thời gian trở nên xác định và kết quả dễ assert. Đây là khu vực khác biệt cảm nhận giữa Kotlin Coroutines vs RxJava, mặc dù cả hai đều test tốt.

Coroutines: runTest, TestDispatcher và kiểm soát thời gian

Với coroutine, ưu tiên runTest với test dispatcher để test không phụ thuộc thread thật hoặc delay thật. Thời gian ảo cho phép kích hoạt timeout, retry và debounce mà không sleep.

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `emits Loading then Success`() = runTest {
  val dispatcher = StandardTestDispatcher(testScheduler)
  val repo = Repo(api = fakeApi, io = dispatcher)

  val states = mutableListOf<UiState>()
  val job = launch(dispatcher) { repo.loadProfile().toList(states) }

  testScheduler.runCurrent() // run queued work
  assert(states.first() is UiState.Loading)

  testScheduler.advanceTimeBy(1_000) // trigger delay/retry windows
  testScheduler.runCurrent()
  assert(states.last() is UiState.Success)

  job.cancel()
}

Để test hủy, hủy Job thu thập (hoặc parent scope) và assert fake API dừng hoặc không có state nào nữa được phát.

RxJava: TestScheduler, TestObserver, thời gian xác định

Test Rx thường kết hợp TestScheduler cho thời gian và TestObserver cho assert.

@Test
fun `disposes on cancel and stops emissions`() {
  val scheduler = TestScheduler()
  val observer = TestObserver<UiState>()

  val d = repo.loadProfileRx(scheduler)
    .subscribeWith(observer)

  scheduler.triggerActions()
  observer.assertValueAt(0) { it is UiState.Loading }

  d.dispose()
  scheduler.advanceTimeBy(1, TimeUnit.SECONDS)
  observer.assertValueCount(1) // no more events after dispose
}

Khi test đường đi lỗi, tập trung vào mapping chứ không phải kiểu exception. Assert trạng thái UI mong đợi sau một 401, một timeout, hoặc response hỏng.

Một bộ kiểm tra nhỏ phủ hầu hết regressions:

  • Trạng thái Loading và trạng thái cuối (Success, Empty, Error)
  • Dọn dẹp khi hủy (job bị hủy, disposable bị dispose)
  • Mapping lỗi (mã server -> thông báo người dùng)
  • Không phát trùng sau retry
  • Logic theo thời gian dùng thời gian ảo, không sleep thật

Sai lầm phổ biến gây bug production

Hầu hết vấn đề production không phải do chọn Kotlin Coroutines vs RxJava. Chúng đến từ vài thói quen khiến công việc chạy lâu hơn bạn nghĩ, chạy hai lần, hoặc chạm UI ở thời điểm sai.

Một leak thường gặp là khởi chạy công việc trong scope sai. Nếu bạn bắt cuộc gọi mạng từ scope sống lâu hơn màn hình (hoặc tạo scope riêng và không bao giờ hủy), request có thể hoàn thành sau khi người dùng rời và cố cập nhật state. Trong coroutines, thường do dùng scope sống lâu theo mặc định. Trong RxJava, thường do quên dispose.

Một lỗi kinh điển khác là “fire and forget.” Global scopes và Disposable quên cảm thấy ổn cho tới khi công việc chất đống. Màn chat refresh mỗi khi resume có thể dẫn đến nhiều job refresh chạy sau vài lần chuyển màn hình, mỗi job giữ bộ nhớ và cạnh tranh mạng.

Retry cũng dễ sai. Retry vô hạn hoặc không delay có thể spam backend và cạn pin. Nguy hiểm nhất khi lỗi là vĩnh viễn, như 401 sau logout. Hãy làm retry có điều kiện, thêm backoff, và dừng khi lỗi không thể khôi phục.

Lỗi threading gây crash khó tái hiện. Bạn có thể parse JSON trên main hoặc cập nhật UI từ thread nền tùy nơi bạn đặt dispatcher hoặc scheduler.

Kiểm tra nhanh bắt hầu hết vấn đề:

  • Gắn công việc vào lifecycle owner và hủy khi owner kết thúc.
  • Làm cleanup rõ ràng: hủy Jobs hoặc clear Disposables ở một chỗ.
  • Đặt giới hạn nghiêm ngặt cho retry (số lần, delay, và lỗi nào đủ điều kiện).
  • Đặt một quy tắc cho cập nhật UI (chỉ main thread) trong review code.
  • Xử lý sync nền như một hệ thống có ràng buộc, không phải hàm gọi ngẫu nhiên.

Nếu bạn triển khai app Android từ mã Kotlin sinh tự động (ví dụ từ AppMaster), các cạm bẫy vẫn áp dụng. Bạn vẫn cần quy ước rõ ràng cho scopes, hủy, giới hạn retry và quy tắc luồng.

Checklist nhanh để chọn Coroutines, RxJava, hoặc cả hai

Tạo backend nhanh
Mô hình hóa dữ liệu, thêm xác thực và triển khai endpoint mà không phải viết boilerplate server thủ công.
Bắt đầu xây dựng

Bắt đầu với hình dạng công việc. Hầu hết các cuộc gọi mạng là một lần, nhưng app cũng có tín hiệu liên tục như connectivity, auth state, hoặc cập nhật trực tiếp. Chọn abstraction sai ban đầu thường bộc lộ sau này bằng hủy lộn xộn và đường đi lỗi khó đọc.

Cách đơn giản quyết định (và giải thích cho đội):

  • Yêu cầu một lần (login, lấy profile): ưu tiên suspend.
  • Luồng liên tục (sự kiện, cập nhật DB): ưu tiên Flow hoặc Rx Observable.
  • Hủy theo lifecycle UI: coroutines trong viewModelScope hoặc lifecycleScope thường đơn giản hơn disposables thủ công.
  • Dùng nhiều operator luồng nâng cao và backpressure: RxJava có thể hợp lý hơn, đặc biệt ở codebase cũ.
  • Retry phức tạp và mapping lỗi: chọn cách mà đội giữ cho code dễ đọc.

Quy tắc thực tế: nếu một màn hình thực hiện một request và render một kết quả, coroutines giữ code gần với hàm bình thường. Nếu bạn xây pipeline nhiều sự kiện (gõ, debounce, cancel previous, combine filters), RxJava hoặc Flow cảm thấy tự nhiên hơn.

Tính nhất quán quan trọng hơn hoàn hảo. Hai pattern tốt dùng ở khắp nơi dễ hỗ trợ hơn năm pattern “tốt nhất” dùng không đồng nhất.

Ví dụ thực tế: đăng nhập, lấy profile, và sync nền

Một stack cho UI và API
Xây dựng client web và mobile cùng backend để giữ xử lý lỗi nhất quán.
Tạo ứng dụng

Luồng phổ biến: người dùng bấm Login, gọi endpoint auth, rồi lấy profile cho home, và bắt đầu sync nền. Đây là nơi Kotlin Coroutines vs RxJava khác nhau trong bảo trì hàng ngày.

Phiên bản Coroutines (tuần tự + cancellable)

Với coroutines, hình dạng “làm cái này rồi làm cái kia” rất tự nhiên. Nếu người dùng đóng màn hình, hủy scope dừng công việc đang chạy.

suspend fun loginAndLoadProfile(): Result<Profile> = runCatching {
  val token = api.login(email, password) // suspend
  val profile = api.profile("Bearer $token")
  syncManager.startSyncInBackground(token) // fire-and-forget
  profile
}.recoverCatching { e ->
  throw when (e) {
    is HttpException -> when (e.code()) {
      401 -> AuthExpiredException()
      in 500..599 -> ServerDownException()
      else -> e
    }
    is IOException -> NoNetworkException()
    else -> e
  }
}

// UI layer
val job = viewModelScope.launch { loginAndLoadProfile() }
override fun onCleared() { job.cancel() }

Phiên bản RxJava (chuỗi + disposal)

Trong RxJava, luồng tương đương là một chuỗi. Hủy nghĩa là dispose, thường với CompositeDisposable.

val d = api.login(email, password)
  .flatMap { token -> api.profile("Bearer $token").map { it to token } }
  .doOnSuccess { (_, token) -> syncManager.startSyncInBackground(token) }
  .onErrorResumeNext { e: Throwable ->
    Single.error(
      when (e) {
        is HttpException -> if (e.code() == 401) AuthExpiredException() else e
        is IOException -> NoNetworkException()
        else -> e
      }
    )
  }
  .subscribe({ (profile, _) -> show(profile) }, { showError(it) })

compositeDisposable.add(d)
override fun onCleared() { compositeDisposable.clear() }

Bộ test tối thiểu nên bao phủ: thành công, lỗi được map (401, 500s, không mạng), và hủy/dispose.

Bước tiếp theo: chọn quy ước và duy trì nhất quán

Đội thường gặp rắc rối vì pattern khác nhau giữa các tính năng, không phải vì Kotlin Coroutines vs RxJava sai. Một ghi chú quyết định ngắn (dù chỉ một trang) tiết kiệm thời gian review và làm hành vi dễ dự đoán.

Bắt đầu với phân tách rõ: công việc một lần (một cuộc gọi mạng trả về một lần) vs luồng (cập nhật theo thời gian, như websocket, location, hoặc thay đổi DB). Quyết định mặc định cho mỗi loại và định nghĩa khi nào cho phép ngoại lệ.

Rồi thêm vài helper chung để mọi tính năng cư xử giống nhau khi mạng trục trặc:

  • Một nơi để map lỗi (HTTP codes, timeout, offline) thành failure ở mức app mà UI hiểu
  • Giá trị timeout mặc định cho các cuộc gọi mạng, với cách rõ ràng để ghi đè cho tác vụ dài
  • Chính sách retry tuyên bố cái gì an toàn để retry (ví dụ GET vs POST)
  • Quy tắc hủy: cái gì dừng khi người dùng rời màn hình, và cái gì được phép tiếp tục
  • Quy tắc logging giúp support mà không lộ dữ liệu nhạy cảm

Quy ước test cũng quan trọng. Thống nhất approach để tests không phụ thuộc thời gian thật hay thread thật. Với coroutines, thường là test dispatcher và scope có cấu trúc. Với RxJava, thường là test schedulers và disposal rõ ràng. Dù cách nào, hướng tới tests nhanh, xác định với không sleep.

Nếu bạn muốn đi nhanh hơn tổng thể, AppMaster (appmaster.io) là một lựa chọn để sinh API backend và ứng dụng Kotlin-based mà không viết mọi thứ từ đầu. Ngay cả với code sinh, cùng các quy ước production về hủy, lỗi, retry và test vẫn là thứ giữ hành vi mạng ổn định.

Câu hỏi thường gặp

Khi nào nên dùng suspend function so với stream cho networking?

Ưu tiên dùng suspend cho một yêu cầu trả về một lần, như đăng nhập hoặc lấy hồ sơ. Dùng Flow (hoặc stream của Rx) khi giá trị thay đổi theo thời gian, như tin nhắn websocket, trạng thái kết nối, hoặc cập nhật cơ sở dữ liệu.

Hủy coroutine có thực sự dừng một request mạng đang chạy không?

Có — nhưng chỉ khi client HTTP của bạn hỗ trợ hủy. Coroutines hủy coroutine khi scope bị hủy, nhưng cuộc gọi HTTP bên dưới cũng phải hỗ trợ hủy để request dừng hoàn toàn; nếu không, request có thể tiếp tục chạy nền.

Cách an toàn nhất để tránh leak khi người dùng rời màn hình là gì?

Gắn công việc vào lifecycle scope, ví dụ viewModelScope, để nó bị hủy khi logic màn hình kết thúc. Tránh khởi chạy trong scope sống lâu hoặc global trừ khi công việc thực sự áp dụng cho toàn app.

Xử lý lỗi nên khác nhau thế nào giữa Coroutines và RxJava?

Với coroutines, lỗi thường ném ra và bạn xử lý bằng try/catch gần nơi có thể map sang trạng thái UI. Với RxJava, lỗi chạy theo luồng, nên giữ đường đi lỗi rõ ràng và tránh các operator biến im lặng mọi lỗi thành giá trị mặc định.

Nên mô hình hóa lỗi bằng exception hay dữ liệu?

Dùng exception cho lỗi bất ngờ như timeout, 500, hoặc lỗi parse. Dùng mô hình lỗi kiểu dữ liệu khi UI cần phản hồi cụ thể như “mật khẩu sai” hoặc “email đã được dùng”, để không phải so sánh chuỗi mong manh.

Cách đơn giản thêm timeout mà không làm code lộn xộn là gì?

Áp timeout gần nơi gọi để bạn có thể hiện đúng thông báo UI. Với coroutines, withTimeout đơn giản cho suspend calls; với RxJava, operator timeout giúp biến timeout thành phần của pipeline.

Làm sao để implement retry mà không gây request trùng hoặc bị chặn?

Chỉ retry khi an toàn — thường cho các request idempotent như GET — và giới hạn số lần (thường 2–3). Không retry cho lỗi xác thực hay lỗi validation, và dùng delay/jitter để tránh spam server và cạn pin.

Cạm bẫy threading chính với Dispatchers vs Schedulers là gì?

Rủi ro lớn là chạy công việc nặng trên main hoặc cập nhật UI từ background. Coroutines dùng Dispatchers (Main/IO/Default); RxJava dùng subscribeOn cho upstream và observeOn cho nơi nhận kết quả. Đặt một lần observeOn(main) ngay trước render để tránh bất ngờ.

Tôi có thể mix RxJava và Coroutines khi di chuyển mã không?

Có, nhưng giữ biên giới nhỏ và có hỗ trợ hủy. Wrap Rx thành suspend với adapter cancellable để hủy subscription khi coroutine bị hủy; và chỉ expose suspend sang Rx trong những cầu nối nhỏ, ghi rõ trong tài liệu.

Làm sao test hủy, retry và logic theo thời gian một cách đáng tin cậy?

Dùng thời gian ảo để test không phụ thuộc vào sleep hay thread thật. Với coroutines, runTest và test dispatcher cho phép kiểm soát delay và hủy; với RxJava, dùng TestScheduler và assert không có emission sau dispose().

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu