مقالات

شکار که شکست خورد: تله جریان کاتلین


چرا یک catch بعد از flowOn ممکن است برنامه بازگشتی شما را رد کند یا برنامه را خراب کند و به جای آن کجا قرار دهید.

TL; دکتر

  • چه چیزی قطع می کند: یک catch بعد از flowOn، یا بعد از هر عبارت دیگری که یک کانال ایجاد می‌کند، می‌تواند به استثنای منبع اجازه دهد آن را از دست بدهد، که برنامه اندروید را خراب می‌کند، یا مقداری را که هنوز در صف است قبل از اینکه صفحه نمایش آن را دریافت کند، رها کند. بازی در kotlinx.coroutines 1.11.0، در JVM و در دستگاه اندروید، با مدلی که راهنمای رسمی اندروید آموزش می دهد.
  • چه کسانی تحت تأثیر قرار می گیرند: جمع کننده ای که برای یک مقدار (تماس شبکه یا نوار اسنک به روز رسانی) یا توقف اولیه مانند first() متوقف می شود. مجموعه‌ای که فقط وضعیت رابط کاربری را ثبت می‌کند، مانند stateIn’s، از خراب شدن در امان است.
  • رفع: خطاها را قبل از اولین عبارتی که یک کانال ایجاد می کند، در لایه ای که منبع را در اختیار دارد، کنترل می کند. catch را قبل از flowOn (و قبل از stateIn یا shareIn) حرکت دهید و خطا را در آنجا یک مقدار قرار دهید، یا یک فراخوانی را بپیچید که ممکن است در یک try/catch در سازنده جریان شکست بخورد. اگر هر تماس در حال حاضر اساساً ایمن است، flowOn را اجرا کنید.
  • نمی توانید آن را حرکت دهید؟ اگر کلکتور شما روی یک مقدار متوقف شد، عملیات آن را به oneEach قبل از catch منتقل کنید یا از conflate() بعد از catch یا collectLatest { } زمانی که فقط آخرین مقدار مهم است استفاده کنید. به عنوان آخرین راه حل، تماس ترمینال را در یک try/catch بپیچید و از صاحب موضوع بخواهید تا catch را جابجا کند. هیچ کدام از اینها مقداری را نمی نویسند که از قبل در صف است. به CoroutineExceptionHandler تکیه نکنید: برنامه زنده می ماند، اما بلوک catch شما تقریباً هرگز اجرا نمی شود.
  • نحوه تشخیص آن: خرابی به کد شما در بالای catch اشاره می کند که باید آن را کنترل می کرد، و ردیابی آن Suppressed: cancellation را نشان می دهد.

بقیه مقاله، سقوط، چرایی وقوع آن و راه‌های خروج را نشان می‌دهد، همراه با اخراج در پشت هر ادعا.

افشا: توسط من تهیه شده و با ابزارهای هوش مصنوعی که همچنین به ایجاد تست ها کمک می کند، اصلاح شده است.

برای جلوگیری از آن تصادف کنید

یک برنامه چت دریافت کنید که برای هر پیام جدید یک اعلان نشان می دهد. مشاهده‌گر در این مثال همان شکل Wire را دارد: اعلان‌ها را منتشر می‌کند، سپس پیام‌ها را به‌عنوان اطلاع‌رسانی علامت‌گذاری می‌کند، در محدوده کل برنامه بدون کنترل‌کننده استثنا. مخزن و catch مختص این نمونه هستند.

مخزن هر ردیف را از پایگاه داده به مدل دامنه نگاشت می کند و کار را از رشته اصلی خارج می کند:

class MessageRepository(
private val dao: MessageDao,
private val io: CoroutineDispatcher,
) {
fun messagesToNotify(): Flow<List<Message>> =
dao.observeNotNotified()
.map { rows -> rows.map { it.toDomain() } } // throws on a message type it doesn't know
.filter { it.isNotEmpty() }
.flowOn(io)
suspend fun markNotified(ids: List<String>) =
dao.markNotified(ids) // Room emits again, without these messages
}

ناظر یک گیره در انتهای زنجیره اضافه می کند، فقط در مورد:

class MessageNotifier(
private val repository: MessageRepository,
private val notifications: ChatNotifications,
) {
suspend fun observe() {
repository.messagesToNotify()
.catch { log(it) }
.collect { messages ->
notifications.show(messages) // posts, then waits
repository.markNotified(messages.map { it.id })
}
}
}
// Once for the whole app, as Wire does
val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
appScope.launch { messageNotifier.observe() }

show() اعلان‌ها را پست می‌کند، سپس 100 میلی‌ثانیه منتظر می‌ماند، مانند Wire، تا به سیستم اجازه دهد تا لیست اعلان‌های فعال خود را به‌روزرسانی کند. هر کاری که در آنجا متوقف شود همان اثر را دارد: تایپ کردن در اتاق، تماس با سرور، نشان دادن یک اسنک بار.

نسخه جدیدتر برنامه ممکن است نوع پیامی را ارسال کند که این نسخه آن را تشخیص ندهد، مانند نظرسنجی. هنگامی که Room یک لیست حاوی یک را منتشر می کند، toDomain() پرتاب می کند. در یک دستگاه Android، آنچه بعدا اتفاق می افتد بستگی به زمان رسیدن نظرسنجی دارد:

  • ناظر غیرفعال است: catch هر بار همانطور که در نظر گرفته شده بود خطا را ثبت کرد.
  • نگهبان هنوز داخل بود show()، پس از ارسال منتظر بمانید: هر بار برنامه خراب می شود. استثنایی که باعث خرابی آن شد همان موردی بود که toDomain() پرتاب کرد، تنها catch برای ورود به سیستم بود و بلوک catch هرگز اجرا نشد.

زمان بندی همه تفاوت است و چرا گرفتن شروع نمی شود آن را توضیح می دهد.

درست قبل از flowOn، گیره را از ناظر به محل ذخیره منتقل کنید و هیچ چیز خراب نشد: هر بار که نظرسنجی می رسید، خطا ثبت می شد.

خطای آرام تر: مقداری که هرگز نمایش داده نمی شود

نه هر کلکسیونری میخ. اگر یک مخزن اندروید نوشته اید، این را نوشته اید. Flow مقدار کش شده را منتشر می کند، سپس مقدار جدید را از شبکه، و flowOn از موضوع اصلی خارج می شود:

class ProfileRepository(
private val dao: ProfileDao,
private val api: ProfileApi,
private val io: CoroutineDispatcher,
) {
private fun cachedThenFresh(): Flow<Profile> = flow {
emit(dao.cachedProfile())
emit(api.fetchProfile()) // throws when the device is offline
}
fun profile(): Flow<Profile> = cachedThenFresh().flowOn(io)
}

ViewModel یک catch اضافه می کند و جریان را با stateIn به حالت UI تبدیل می کند. صفحه آن را با collectAsStateWithLifecycle():

val uiState: StateFlow<ProfileUiState> = repository.profile()
.map<Profile, ProfileUiState> { ProfileUiState.Content(it) }
.catch { emit(ProfileUiState.Error(it.message ?: "Unknown error")) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ProfileUiState.Loading,
)

این فرمتی است که راهنمای رسمی اندروید با یک فید خبری به جای نمایه آموزش می‌دهد: در انتهای زنجیره، در ViewModel و در مخزن، جایی که انتظار می‌رود همه چیز را در بالادست مدیریت کند، می‌رود. این کار تا زمانی کار می‌کند که یک اپراتور بالادستی یک کانال ایجاد کند، و بخش بعدی راهنما، “اجرا در یک متن متفاوت CoroutineContext” یکی را اضافه می‌کند: NewsRepository آن یک flowOn.(defaultDispatcher) را درست قبل از گرفتن آن، در جریان .flowOn(ioDispatcher) خود منبع داده دریافت می‌کند. نمایه بالا به همین شکل است: flowOn در مخزن، پس از آن در ViewModel بگیرید.

تماس شبکه فقط یک مثال است. منبع همچنین ممکن است از یک نقشه نگار، یک پایگاه داده خوانده شده یا یک خطای ساده پرتاب کند. چیزی که پرتاب می‌کند مهم نیست: برای هر چیزی که به ارزش تبدیل نکرده‌اید، گیرایی وجود دارد.

رفتار مورد انتظار در حالت آفلاین دستگاه، تشخیص آن آسان است: صفحه نمایه ذخیره شده در حافظه پنهان و سپس وضعیت خطا را نشان می دهد. مرجع catch API به همان اندازه وعده می دهد: “اگر upstream با یک استثنا تمام شود، این اپراتور آن استثنا را می گیرد و اقدام مشخص شده را با آن فراخوانی می کند.”

در واقع چه اتفاقی می افتد بستگی به جمع کننده پس از گرفتن دارد. کلکتور stateIn فقط مقدار را ذخیره می کند، بنابراین برنامه هرگز خراب نمی شود. اما نمایه حافظه پنهان می‌تواند ناپدید شود: زمانی که رشته اصلی مشغول کشیدن یک فریم تا رسیدن خطا بود، به ندرت به uiState می‌رسید و صفحه بدون آن حالت خطا را نشان می‌داد.

با از کار افتادن اعلان کننده، این دو خطا برای توضیح ایجاد می کند: یک مقدار گم شده و یک خطایی که نتیجه را از دست می دهد.

چرا صید شکست خورد؟

کانال

تله هر عملگری است که به منبع اجازه می دهد قبل از جمع کننده کار کند.

در یک جریان معمولی، emit() فراخوانی یک تابع در جمع کننده است. تنها زمانی برمی‌گردد که جمع‌کننده با مقدار تمام شود، بنابراین منبع نمی‌تواند در زمانی که جمع‌کننده مشغول است از کار بیفتد. این دو به نوبت در یک کار روتین.

برخی از اپراتورها عمداً این تناوب را می شکنند. آنها یک کوروتین دوم را برای منبع شروع می کنند و مقادیر را از طریق کانالی که یک صف است عبور می دهند. emit() اکنون بدون انتظار برای تکمیل جمع کننده برمی گردد.

جایی که شکار در ارتباط با کانال است: سه ترتیب (با استفاده از هوش مصنوعی ارائه شده است)

سبز نشانه گرفتنی است که هر بار رد می شود. قرمز ترفندی را نشان می دهد که می تواند دو باگ را به یکی تبدیل کند.

از این نقطه، هر دو طرف می توانند به طور مستقل از کار بیفتند، و اپراتور که اکنون دو کوروتین را اجرا می کند، باید دو خطا را در یک استثنا ادغام کند. زمانی که دارای هر دو استثنای تهی و واقعی باشد، واقعی را گزارش می کند. این استثنا ادغام شده چیزی است که catch می گیرد.

صید چگونه تصمیم می گیرد

catch نمی تواند از یک try/catch ساده استفاده کند زیرا نباید استثناهایی را که جمع کننده زیر آن پرتاب می کند، بگیرد. بنابراین یک دفترچه یادداشت، متغیری با نام fromDownstream نگهداری می‌کند: هر استثنایی که توسط جمع‌آورنده پرتاب می‌شود در مسیر بالادستی روی آن نوشته می‌شود. وقتی مجموعه با استثنا e پایان می یابد، سه قانون به ترتیب اعمال می شود:

  1. این استثنا یا نادیده گرفتن خود این برنامه است. e همانی است که در دفترچه یادداشت است، یا دلیل لغو این برنامه است. آن را دست نخورده به عقب پرتاب کنید.
  2. دفترچه خالی است. فقط بالادست شکست خورد. e را برگردانید و catch بلوک شما را با آن شروع می کند.
  3. چیز دیگری در دفترچه وجود دارد. هر دو طرف شکست خوردند. استثنا را در بالادست بازگردانید و مورد پایین دست را به صورت سرکوب شده متصل کنید. اگر استثناء بالادست خود یک لغو باشد، این دو نقش تغییر می کنند.

قانون 3 عمدی است. نظر منبع می‌گوید که اگر پایین‌دست «قبل یا همزمان با بالادست» از کار بیفتد، استثناء بالادستی مجدداً برگردانده می‌شود تا از بین نرود. نمونه آن در نهایت {resource.close() } داخل جریانی است که پرتاب می‌کند در حالی که استثنای جمع‌آوری قبلاً در آن پیشروی می‌کند. در کوروتین، این یک قانون خوب است، زیرا یک استثنای پایین‌دستی از طریق فراخوانی emit() خود منبع بازگردانده می‌شود. این دو شکست یک پشته تماس مشترک دارند و یکی پایین دستی در واقع اولین بود.

این خود کد است: catch و catchImpl که فراخوانی می‌کند، در 1.11.0، با کامنت‌های طولانی‌تر و یک حاشیه Suppress@ قطع شده است. در این فایل، جریان، ناامن داخلی کتابخانه با نامی دیگر است، نه سازنده جریان عمومی { }:

public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> =
flow {
val exception = catchImpl(this)
if (exception != null) action(exception)
}
internal suspend fun <T> Flow<T>.catchImpl(
collector: FlowCollector<T>
): Throwable? {
var fromDownstream: Throwable? = null
try {
collect {
try {
collector.emit(it)
} catch (e: Throwable) {
fromDownstream = e
throw e
}
}
} catch (e: Throwable) {
val fromDownstream = fromDownstream
if (e.isSameExceptionAs(fromDownstream) ||
e.isCancellationCause(currentCoroutineContext())
) {
throw e // Rethrow exceptions from downstream and cancellation causes
} else {
if (fromDownstream == null) {
return e
}
if (e is CancellationException) {
fromDownstream.addSuppressed(e)
throw fromDownstream
} else {
e.addSuppressed(fromDownstream)
throw e
}
}
}
return null
}

خرابی اعلان، مرحله به مرحله

با یک کانال بین آنها، قانون 3 جرقه را می شکند. نوت‌بوک نمی‌تواند بین مجموعه‌ای که واقعاً شکست خورده و مجموعه‌ای که به دلیل خرابی بالادستی لغو شده است، تمایز قائل شود:

دو خط به موقع کد منبع: 1، اتاق منتشر می‌کند، نقشه‌بردار اجرا می‌کند، و emit() لیست را در صف قرار می‌دهد و برمی‌گرداند. 3، نظرسنجی می رسد، نقشه نگار می اندازد و منبع از کار می افتد. coroutine گردآورنده، با catch: 2، show() 100 ms پس از ارسال منتظر می ماند. 4، خطای منبع آن را لغو می کند، show() یک override می اندازد و catch آن را ثبت می کند. 5، قانون 3: نوت بوک خالی نیست، بنابراین بلوک شما حذف می شود و برنامه از کار می افتد.
خرابی اعلان · دو برنامه، پنج مرحله (ساخته شده با هوش مصنوعی)

در مرحله 5، هیچ چیز در بالا این خطا را دریافت نمی کند: برنامه ناظر با شکست مواجه می شود، و بدون یک کنترل کننده استثنایی در محدوده برنامه، برنامه از کار می افتد. با catch before flowOn، مرحله 3 در کنار catch، در کد منبع اتفاق می افتد. نوت بوک خالی است، بلوک شما اجرا می شود و انتظار در show() کامل می شود.

مراحل 1 تا 5 مختص اندروید نیست. catchImpl یک کد عمومی است و همان مراحل در JVM معمولی بدون اندروید تکرار می شود. همچنین بلوک catch شما هرگز اجرا نمی شود.

کلکسیونر هیچ اشتباهی نکرد. خطای upstream آن را لغو می کند، همراه با انتظار در show() و catch آن لغو را به عنوان یک خطای دوم و مستقل می خواند.

خطای 1: مقدار از دست رفته

مقداری که هنوز در صف قرار دارد، در لحظه ای که منبع از کار می افتد، کنار گذاشته می شود.

هنگامی که منبع پرتاب می کند، کوروتین آن از کار می افتد و این شکست بلافاصله جشن جمع آوری را لغو می کند. منتظر تخلیه صف نمی ماند. هر چیزی که کلکسیونر قبلاً نگرفته از بین رفته است.

کلکسیونر هرگز پرتاب نکرد، بنابراین دفترچه خالی است و قانون 2 اعمال می شود. catch بلوک شما را اجرا می کند، صفحه وضعیت خطا را نشان می دهد و هیچ چیز خراب نمی شود. نمایه ذخیره شده هرگز نشان داده نمی شود.

اینکه چند وقت یکبار این اتفاق می افتد بیشتر به زمان بندی بستگی دارد: آیا خطا سریعتر از مقداری که جمع کننده بتواند مقدار را دریافت کند، دنبال می شود یا خیر. نمرات مانند این از اجرا به اجرا و از ماشینی به ماشین دیگر تغییر می کند، بنابراین مقاله به جای دادن درصد، آنها را مسابقه می نامد.

  • flowOn(IO)، رشته اصلی برای یک فریم 16 میلی‌ثانیه مشغول است: نمایه کش تقریباً هرگز به uiState نمی‌رسد.
  • flowOn(IO)، رشته اصلی بیکار، منبع بلافاصله پس از انتشار از کار می افتد: در برخی از اجراها گم می شود. این یک مسابقه است و تعداد دفعات آن بستگی به دستگاه دارد: اغلب در دستگاه 16 هسته ای نسبت به دستگاه 2 هسته ای.
  • buffer() در یک رشته، منبع بلافاصله پس از پخش از کار می افتد: هر بار از دست می رود.

خطای 2: خطایی که catch را از دست می دهد

هنگامی که طرف جمع‌آوری کننده نیز یک استثنا در حین پرواز داشته باشد، زمانی که منبع از کار بیفتد، خطا از دست می‌دهد. نوت بوک خالی نیست، بنابراین قانون 3 خطا را در بالادست منتشر می کند. خرابی اعلان اولین راه از چهار راه برای رسیدن به آن است:

  1. هنگامی که منبع از کار بیفتد جمع کننده متوقف می شود. خرابی منبع، طرف جمع‌آوری‌کننده را باطل می‌کند. تماس لغو شده در داخل جمع آوری {} یک CancellationException می اندازد و آن به دفترچه یادداشت می رود. با flowOn(IO) اعلان کننده و نمایه هر بار در JVM و دستگاه اندروید خراب می شوند. به رشته دوم نیازی نیست: بافر() با هر چیزی که در رشته اصلی است نیز هر بار دوباره پرتاب می شود. فقط پس از شمارش صید کار کنید. نوشتن توقف در ذخیره‌سازی، قبل از flowOn یا حتی بین flowOn و catch، قبل از catch قرار می‌گیرد و هر بار خطا رسیدگی می‌شود.
  2. چیزی زود جریان را متوقف می کند. first()، firstOrNull()، any()، all()، none()، take()، و takeWhile() یک جریان را با پرتاب یک AbortFlowException داخلی متوقف می کنند. اینجا بعد از گرفتن می نشینند. بین منبع و catch، take و takeWhile نقشه امن هستند.
    اگر منبع قبل از رسیدن وقفه از کار بیفتد، هر دو طرف شکست خورده اند. با first()، یک تماس مسدود کننده تقریباً در هر اجرا برنامه را خراب می کند، و یک تماس توقف هرگز انجام نمی شود، زیرا در آنجا لغو ابتدا به منبع می رسد.
    هنگامی که منبع بلافاصله پس از پخش از کار می افتد، یک مسابقه است: با first() و سایر توقف های اولیه، خطای از دست رفته در برخی از اجراها و نه برخی دیگر، و نسبت به طور گسترده ای از ماشینی به ماشین دیگر متفاوت است.
  3. کاربر در حالی که کلکتور در حال اجرا است صفحه را ترک می کند. نادیده گرفتن viewModelScope یک استثنای پایین دستی است. تماس شبکه مسدود کننده همچنان پس از این کار انجام نمی شود و برنامه پس از ناپدید شدن صفحه تقریباً در هر بار اجرا از کار می افتد. برنامه لغو شده در مورد CancellationException بی‌صدا می‌ماند، اما در مورد هیچ استثنای دیگری بی‌صدا می‌ماند: قانون 3 خطای شبکه را با لغو پیوست‌شده به‌عنوان سرکوب‌شده بازمی‌گرداند، کوروتین با یک شکست واقعی پایان می‌یابد، و viewModelScope هیچ کنترل‌کننده‌ای برای آن ندارد.
  4. گردآورنده استثنای خود را می اندازد. در عوض، تماس گیرنده ممکن است خطای شبکه را دریافت کند، با خطای واقعی پنهان شده در خطای سرکوب شده. بستگی به این دارد که کدام کشور اول شکست بخورد. هنگامی که خطا برای اولین بار رخ می دهد، تماس گیرنده هر بار خود خطا را دریافت می کند. هنگامی که منبع تقریباً در همان زمان ناموفق بود، باگ در برخی از اجراها پنهان بود و نه در برخی دیگر: مسابقه، بنابراین انتظار داشته باشید که بر اساس ماشین و زمان متفاوت باشد.

retry و retryWhen بر روی یک catchImpl ساخته می شوند، بنابراین در این موارد بدون تلاش مجدد دوباره پرتاب می شوند. flowOn(IO).retry(3) با یک جمع کننده توقف هر بار پرتاب می شود و منبع تقریباً همیشه فقط یک بار شروع می شود: تلاش های مجدد انجام نشد.

یک سوال منصفانه این است که چرا لغو ابتدا منبع را متوقف نمی کند. لغو مشارکتی است: فقط در یک نقطه شکست متوجه می شود و نمی تواند آسیبی را که قبلاً رخ داده است خنثی کند. در مورد 1، هیچ کس به هیچ وجه منبع را لغو نمی کند. در موارد 2 و 3، خلأ در راه است، اما تماس مسدود یا پرتاب بلافاصله پس از پخش هرگز آن را بررسی نمی کند.

راه حل: قبل از کانال بگیرید

خطا را در جایی که خطا وجود دارد قرار دهید: قبل از هر چیزی که یک کانال ایجاد می کند، در همان برنامه اصلی منبع.

برای اعلان ها، راه حل این است که یک ردیف را به مخزن منتقل کنید. ناظر شکار خود را رها می کند و هیچ چیز دیگری تغییر نمی کند:

fun messagesToNotify(): Flow<List<Message>> =
dao.observeNotNotified()
.map { rows -> rows.map { it.toDomain() } }
.filter { it.isNotEmpty() }
// in the source's coroutine, before the channel
.catch { log(it) }
.flowOn(io)

گیر شبکه ایمنی است: به جای برنامه، جریان را خاتمه می دهد. برای اعلان‌ها، این به معنای عدم خرابی است، اما همچنین عدم اطلاع از چیزی که پس از نظرسنجی می‌رسد، زیرا پس از بازگشت بلوک شما، پخش جریانی انجام می‌شود. برای خطایی که ممکن است انتظار داشته باشید، مانند نوع پیامی که این نسخه نمی‌داند، آن را برای هر پیام مدیریت کنید تا اعلان‌ها همچنان ادامه داشته باشند، و برای هر چیزی که انتظارش را نداشتید، پیش از جریان فعال نگه دارید:

fun messagesToNotify(): Flow<List<Message>> =
dao.observeNotNotified()
.map { rows ->
rows.mapNotNull { row ->
try { row.toDomain() } catch (e: IllegalStateException) { log(e); null }
}
}
.filter { it.isNotEmpty() }
.catch { log(it) }
.flowOn(io)

پس از آن، نظرسنجی اطلاع رسانی نمی شود و هر زمان که لیست پیام های اطلاع رسانی تغییر کند، دوباره ثبت نام می کند.

منبع بومی Wire خطاها را به همین ترتیب مدیریت می کند: پایگاه داده خوانده شده و نقشه برداری را در یک wrapStorageRequest می پیچد که یک استثنا را به مقدار خطا تبدیل می کند، و onlyRight() آن را حذف می کند، بنابراین خطای نگاشت به جای شکست جریان، تبدیل به یک مقدار می شود.

برای نمایه، خطا باید به مقداری تبدیل شود که صفحه نمایش بتواند نمایش دهد:

fun profileResult(): Flow<Result<Profile>> =
cachedThenFresh()
.map { Result.success(it) }
.catch { emit(Result.failure(it)) }
.flowOn(io)

ViewModel دیگر اصلاً نیازی به گیرایی ندارد. ()toUiState نتیجه را به محتوا یا خطا تبدیل می کند:

val uiState: StateFlow<ProfileUiState> = repository.profileResult()
.map { it.toUiState() }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = ProfileUiState.Loading,
)

این به دو دلیل کار می کند:

  • نوت بوک دوباره دقیق است. catch و source یک پشته تماس را به اشتراک می گذارند. اگر قرار دادن یک مقدار در صف پرتاب شود، این استثنا از طریق emit() خود منبع برگشت داده می شود و آن را متوقف می کند.
  • برنامه تولیدکننده مشترک هرگز شکست نمی خورد. منبع همچنان پرتاب می‌کند، اما catch خطا را به مقداری در آن coroutine تبدیل می‌کند، که سپس به طور معمول خارج می‌شود. هیچ چیز بر گردآورنده غلبه نمی کند و هیچ چیز رد نمی شود. این تا زمانی درست است که گیره درست جلوی کانال بایستد و بلوک آن پرتاب نشود: استثنایی که از دست می‌دهد، یا یکی از اپراتور بین آن و کانال، دوباره تولیدکننده را از کار می‌اندازد.

با استفاده از این مخزن، صفحه هرگز خراب نمی شود و نمایه کش همیشه قبل از حالت خطا در زمانی که کاربر روی صفحه می ماند به uiState می رسد. StateFlow فقط آخرین مقدار را نگه می دارد، بنابراین هنگامی که یک خطا در همان فریم می آید، صفحه فقط می تواند وضعیت خطا را نمایش دهد. این است که چگونه StateFlow با یا بدون تله کار می کند.

تعمیر هزینه دارد:

  • خطا باید تبدیل به مقدار شود. این به معنای یک نتیجه، نوع مهر و موم شده یا مقدار بازگشتی است. با یک نتیجه یا نوع مهر و موم شده، نوع برگشتی مخزن تغییر می کند.
  • از تاپیک اصلی منقضی می شود. بلوک catch روی هندلری اجرا می‌شود که پس از آن به flowOn ارسال می‌شود، در اینجا io، بنابراین فقط باید یک مقدار تولید کند، نه نماها یا هر چیز دیگری که باید در رشته اصلی باقی بماند.
  • یک بلوک زمانی می تواند کار کند که هیچکس به ارزش آن نیاز نداشته باشد، و first() منتظر آن است. در یک فراخوانی مسدود کننده، first() از قبل دارای مقدار کش شده است، اما نمی تواند تا زمانی که فراخوانی کامل شود، برگردد، زیرا flowOn منتظر تکمیل تولید کننده خود است. وقتی تماس ناموفق بود، catch به کار خود ادامه می‌دهد: سیگنال بازگشتی که منتشر می‌کند کنار گذاشته می‌شود، اما هر چیز دیگری در بلوک، مانند ثبت خطا، همچنان اتفاق می‌افتد. فقط پس از آن first() مقدار کش شده را برمی گرداند، بنابراین به اندازه تماس شبکه کند است. در یک تماس پایان‌دهنده، لغو () first تماس را قطع می‌کند، بلوک اجرا نمی‌شود، و first() بلافاصله برمی‌گردد.

تلاش مجدد از همین قانون پیروی می کند. با همان جمع کننده توقف، مجدداً (3) که قبل از flowOn قرار داده شد، هر چهار تلاش را انجام داد. با فاصله بین retry(3) و flowOn، کلکتور برگشتی را دریافت کرد و چیزی پرتاب نکرد. بدون این ترفند، خطای چهارمین تلاش همچنان به کدی می‌رسد که جمع‌آوری نامیده می‌شود.

آیا کد شما تحت تأثیر قرار گرفته است؟

برای معمولی ترین کلکسیونر، نه. برای دو فرم خاص، بله.

کلکتور stateIn فقط یک مقدار را تعیین می کند و هرگز متوقف نمی شود، بنابراین چیزی روی نوت بوک نوشته نمی شود مگر اینکه پرتاب کند. در سه نوع خرابی شبکه، خطا هرگز با چنین کلکتوری به دام نیفتاد. در بدترین حالت، یک مقدار کش وجود دارد که هرگز به uiState نمی رسد، معمولاً زمانی که خطا در یک فریم دنبال می شود. نوار اسنک مدیریت شده توسط این حالت، LaunchedEffect(state.message) { showSnackbar(…) }، به همین دلیل ایمن است: جمع‌آورنده جریان فقط وضعیت را ثبت می‌کند و انتظار در برنامه‌ای دیگر اتفاق می‌افتد.

فرم هایی که خراب می شوند به شرح زیر است:

  • مجموعه‌ای که برای یک مقدار پس از یک کانال مکث می‌کند: به‌روزرسانی تماس شبکه، به‌روزرسانی نوار اسنک، پیام یک‌بار ارسال به رابط کاربری، انیمیشن.
  • first()، take()، یا any() بعد از یک کانال، در منبعی که می تواند بدون توقف شکست بخورد.

تلاش مجدد بعد از کانال شما را نجات نمی دهد: با جمع آوری کننده ای که متوقف می شود، خطا همچنان نتیجه را از دست می دهد، معمولاً بدون یک بار امتحان مجدد.

جمع‌آوری که برای یک مقدار متوقف می‌شود، نباید در UI باشد. هنگامی که ViewModel جریان را جمع آوری می کند و هر به روز رسانی را به UI ارسال می کند، ارسال آن آخرین مرحله جمع کننده است. اگر این تماس منتظر بماند، جمع‌آورنده ViewModel هنگامی که منبع خراب می‌شود متوقف می‌شود و این مورد 1 است.

اینکه آیا انتقال منتظر می ماند بستگی به اتاق دارد. send() در یک کانال منتظر می ماند تا کانال پر شود و emit() در SharedFlow منتظر می ماند تا مشترک مشغول شود و بافر پر شود. Channel() و MutableSharedFlow() فضای خالی ندارند، بنابراین منتظر می مانند تا UI پیام را دریافت کند. کانال (BUFFERED) فضایی برای 64 پیام دارد، اندازه بافر پیش‌فرض کتابخانه، و shareIn نیز به‌طور پیش‌فرض دارای همین اندازه است. SharedFlow بدون مشترک، مانند حالت پس‌زمینه، به جای انتظار، پیام‌ها را رها می‌کند. تنظیم مقدار StateFlow هرگز منتظر نمی ماند زیرا فقط آخرین مقدار را نگه می دارد.

این نتایج با catch after flowOn، در دستگاه Android هستند. «در پس‌زمینه» به این معنی است که پس از قرار گرفتن برنامه در پس‌زمینه، هیچ چیز مانند repeatOnLifecycle یا collectAsStateWithLifecycle()، پیام‌ها را جمع‌آوری نمی‌کند:

Channel(): هر بار با اسنکبار نمایش داده شده و در پس‌زمینه خراب می‌شود. کانال (BUFFERED): تا زمانی که 64 پیام منتظر بمانند، هرگز خراب نمی شود، سپس در هر دو خراب می شود. MutableSharedFlow(): هر بار که نوار اسنک نشان داده می شود از کار می افتد. در پس زمینه پیام ها حذف شدند. shareIn(…, Eagerly): هرگز خراب نشود تا زمانی که 64 پیام در حال انتظار با نمایش نوار اسنک باشد. در پس زمینه پیام ها حذف شدند. MutableStateFlow و stateIn(...، Eagerly): هرگز خراب نشد، حتی با 100 به روز رسانی همزمان.
چگونه ViewModel به‌روزرسانی‌های رابط کاربری را با catch after flowOn به دستگاه Android ارسال می‌کند (با استفاده از هوش مصنوعی ارائه شده است)

فقط StateFlow هرگز خراب نمی شود زیرا تنظیم مقدار آن هرگز منتظر نمی ماند. هزینه این است که فقط آخرین مقدار را حفظ می کند، بنابراین رابط کاربری می تواند به روز رسانی بین آنها را رد کند. برای پیامی مانند نوار اسنک، آن را در حالت رابط کاربری نگه دارید و اجازه دهید رابط کاربری پس از نمایش آن را پاک کند تا پس از چرخش دوباره نمایش داده نشود. این الگوی راهنمای Android برای رویدادهای UI است: رویدادهای ViewModel را به حالت UI تبدیل کنید.

کدام اپراتورها یک کانال ایجاد می کنند

هر flowOn یک کانال ایجاد نمی کند، اما بسیاری از اپراتورهای غیرflowOn این کار را انجام می دهند.

منبع کتابخانه نشان می‌دهد که کدام اپراتورها از یک کانال استفاده می‌کنند و برخی از اسناد آن نیز مانند flowOn چنین می‌گویند. چیزی که هیچ کس به شما نمی گوید این است که چه اتفاقی برای صید می افتد. بنابراین من هر عبارت میانی را در kotlinx.coroutines 1.11.0 بین منبع ناموفق و catch قرار دادم و در یک رشته اصلی جمع آوری کردم. هر ستون یک بررسی جداگانه است:

  • منبع ادامه می دهد: بدون کانال، emit() منبع منتظر می ماند تا جمع کننده با مقدار به پایان برسد. آیا منبع قبل از این ادامه داشت؟ اگر چنین بود، یک کانال بین آنها وجود داشت.
  • ارزش از دست رفته با toList(): منبع یک مقدار واحد منتشر می کند و بلافاصله از کار می افتد. آیا مقدار از لیست گم شده بود؟
  • خطا از دست رفته است گرفتن: منبع منتشر می شود، سپس 2 میلی ثانیه بعد از کار می افتد در حالی که جمع کننده هنوز با مقدار 10 میلی ثانیه مشغول است. آیا خطا از قلم افتاده است؟

retry در بالای retryWhen ساخته شده است، بنابراین retryWhen هر دو را پوشش می دهد. نتایج به چهار گروه تقسیم می شوند:

چهار مجموعه گزاره بین منبع و گرفتن. هیچ یا عبارتی که در یک برنامه معمولی باقی می‌ماند: منبع هرگز به جلو اجرا نمی‌شود، ارزش هرگز از بین نمی‌رود، خطا هرگز از دست نمی‌دهد. flowOn(Dispatchers.IO) در رشته اصلی: بیشتر اوقات به جلو کار می کند، گاهی اوقات ارزش از بین می رود (مسابقه)، خطاها تقریباً هر بار پیدا نمی شوند. تبدیل ترکیب و ترکیب: هرگز هدر ندهید، هر بار از دست ندهید. هر اپراتور کانال دیگر: هر بار ضرر می کند، تقریباً هر بار یک مورد را از دست می دهد.
هر عبارت میانی در kotlinx.coroutines 1.11.0 بین منبع ناموفق و catch، در چهار گروه (ساخته شده با استفاده از هوش مصنوعی)

“تقریبا” به معنای چند اجرا، با flowOn(Dispatchers.IO)، debounce، نمونه، یا flatMapLatest است که زمان بندی مطابقت ندارد. معمولاً سورس قبل از اینکه جمع‌کننده مقدار را دریافت کند، از کار می‌افتد، بنابراین مانند خطای 1، مقدار از بین می‌رفت. یک بار، با flatMapLatest، منبع فقط پس از پایان جمع‌آور و اجرای catch از کار افتاد.

آنها در یک رول می مانند، بنابراین الف گرفتن آنها بی خطر است: map, mapNotNull, filter, filterNot, filterNotNull, filterIsInstance, onEach, onStart, onCompletion, onEmpty, transform, transformWhile, take, takeWhile, drop, dropWhile, distinctUntilChanged, distinctUntilChangedBy, withIndexReducedW, در حال اجرا, باIndexReduced, cancell,hen, یک catch دیگر، که قبل از آن قرار می گیرد، flatMapConcat و flattenConcat (آنها جریان های داخلی را یکی یکی جمع آوری می کنند)، flatMapMerge (concurrency = 1) (که به flattenConcat برمی گردد) و zip زمانی که رشته شکست خورده همان چیزی است که روی آن فراخوانی شده است (failing.zip(دیگر)).

آنها یک کانال ایجاد می کنند تا یک گرفتن بعد از آنها ممکن است شکست بخورد: flowOn با توزیع کننده متفاوت، بافر، ترکیب، mapLatest، transformLatest، flatMapLatest، flatMapMerge، و flattenMerge با هر همزمانی بیشتر از 1، ادغام، ترکیب، ترکیب تبدیل، zip هنگامی که جریان ناموفق آرگومان آن است (other.zip(failing)، نمونه بازخوانی کانالF و بازخوانی، زمان بازخوانیF.

دو جزئیات در عمل مهم هستند و هر دو به یک نقطه خلاصه می‌شوند: تله از یک کانال می‌آید، نه از یک رشته دوم.

  • جریان روشن تنها زمانی یک کانال ایجاد می کند که توزیع کننده تغییر کند، و این به معنای یک شی توزیع کننده متفاوت است، نه یک رشته متفاوت. با توزیع کننده خود کلکسیونر یا فقط CoroutineName، emit() همچنان روی کلکتور منتظر می ماند و آثار را می گیرد. اما viewModelScope روی Dispatchers.Main.immediate اجرا می‌شود، بنابراین flowOn(Dispatchers.Main) داخل آن یک کانال ایجاد می‌کند، حتی اگر هر دو در رشته اصلی در حال اجرا باشند. با کلکسیونری که هنوز در هنگام خرابی منبع مشغول بود، هر بار از جمع‌آوری صرفنظر می‌شود.
  • سایر اپراتورهای کانال نیز نیازی به نخ دوم ندارند. کل توده روی یک نخ اصلی جمع شد. با buffer، conflate، mapLatest، ادغام یا ترکیب، هر دو طرف در این رشته اجرا می شوند و خطا هر بار نتیجه را از دست می دهد.

هنگامی که طعمه جلوی اپراتور حرکت می کند، دوباره چیزی پرتاب نمی شود. مقدار ذخیره شده در حافظه پنهان هنوز در بررسی مقدار از دست رفته وجود ندارد، جایی که بک گراند بلافاصله بعد از آن می آید، با عملگرهایی که مقادیر را بر اساس طراحی حذف می کنند:

  • debounce و نمونه هر بار آن را از دست می دهد، در نخ اصلی و در یک Thread Pool.
  • combination و combinationTransform آن را در برخی از اجرای Thread Pool از دست داد.
  • conflate آن را در چندین بار استخر نخ، روی یک ماشین 2 هسته ای از دست داد.

وقتی یک مقدار جدیدتر بلافاصله بعد از آن می رسد، فقط مقدار جدیدتر را نگه می دارند. اپراتور کار را انجام می دهد، نه تله.

shareIn و stateIn

shareIn و stateIn یک مورد متفاوت هستند: خطای upstream به حوزه اشتراک می رود، نه جمع کننده ها.

با آنها را بگیر هرگز اجرا نشد و دو چیز دنبال شد:

  • به مشترکین هرگز در مورد شکست گفته نشد. به انتظار ادامه دادند.
  • SharingStarted.WhileSubscribed مانع آن نشد. هنگامی که صفحه نمایش را ترک می کند، جمع آوری آن متوقف می شود، اما نمی تواند تماس مسدودکننده ای را که از قبل در حال اجرا است، قطع کند. هنگامی که این تماس پس از ناپدید شدن صفحه با شکست مواجه می‌شود، خطا همچنان به محدوده اشتراک‌گذاری می‌رود و یک محدوده بدون دستگیره برای آن، مانند viewModelScope، برنامه را خراب می‌کند.

با جلوی آنها را بگیر خرابی از بین می رود، حتی پس از flowOn: جمع کننده آنها فقط مقدار را ذخیره می کند، بنابراین هیچ چیز بعد از catch منتظر نمی ماند. مقداری که توسط catch به عنوان یک بازگشت منتشر می شود تنها راهی است که مشترکین از شکست یاد می گیرند. دو محدودیت باقی می ماند:

  • مقداری که هنوز در صف است ممکن است مانند خطای 1 از بین برود.
  • shareIn همچنان ممکن است خراب شود. دارای حداکثر 64 مقدار برای مشترکین کند. پس از معلق بودن 64، جمع آوری کننده پس از گرفتن آن باید منتظر بماند و خرابی دوباره ظاهر می شود زیرا جدول موجود در “آیا کد شما تحت تاثیر قرار گرفته است؟” نشان می دهد.

نحوه تشخیص آن در گزارش خرابی

خرابی به کد خود شما اشاره می‌کند، بالای کدی که باید آن را مدیریت می‌کرد. در ردیابی کامل logcat، استثناء Suppressed: cancellation: یک JobCancellationException یا AbortFlowException را پس از first() یا دیگر خاتمه زودهنگام می اندازد. هر خرابی کوروتین همچنین دارای یک خط Suppressed: برای DiagnosticCoroutineContextException است. این نشانه نیست وقتی اعلان خراب می شود:

java.lang.IllegalStateException: …
...
Suppressed: kotlinx.coroutines.JobCancellationException: ScopeCoroutine is cancelling
...
Caused by: [CIRCULAR REFERENCE: java.lang.IllegalStateException: …]

مرجع دایره ای به معنای “دوباره همان استثنا” است: جمع آوری کننده توسط همان استثنایی که برنامه را خراب کرده است لغو شده است.

در اندروید، یک کیس همه اینها را پنهان می کند. هنگامی که استثنا UnknownHostException است، همانطور که دستگاه آفلاین است، گزارش Android تمام ردیابی ها را به صورت عمدی حذف می کند و logcat فقط دو خط هدر، FATAL EXCEPTION و Process را نشان می دهد. برای مشاهده آن، ()throughable.stackTraceToString را از کنترل کننده استثنای خود ثبت کنید.

دیگری خارج می شود

جابجایی گیره همیشه امکان پذیر نیست. یک کتابخانه ممکن است جریانی را که لوله از قبل در داخل آن است به شما منتقل کند، یا ممکن است نوع برگشتی برای تغییر مال شما نباشد. پچ می‌تواند در سه مکان برود: قبل از جریان، بعد از آن، یا در تماس ترمینال که جریان را جمع‌آوری می‌کند. دو مورد اول خود جریان را تغییر می دهند، بنابراین برای هر راهی برای جمع آوری آن نگه می دارند.

گزینه ها سنگین شد

جدول نحوه انجام اصلاحات در جریان را در سه موقعیت نشان می‌دهد: کار بر روی استاپ‌های جمع‌آوری (مورد 1)، first() با یک فراخوانی مسدودکننده (مورد 2)، و یک مقدار همچنان در صف زمانی که منبع خراب می‌شود (مورد 1).

رفع جریان، سه وضعیت: توقف کار، () first با مسدود کردن تماس، مقدار در صف. catch after streamOn: rethrown، rethrown، ممکن است گم شود. قبل از کانال (catch before flowOn، try/catch in constructor، no channel): پردازش شده، پردازش شده، ذخیره شده است. After channel conflate() after catch: پردازش شده، پردازش شده، ممکن است گم شود. catchAware: دسته، پرتاب مجدد، می تواند گم شود.
هر وصله در جریان در سه موقعیت: کار در جمع کننده متوقف شد، اول () با یک تماس مسدود کننده، و یک مقدار هنوز در صف (با استفاده از هوش مصنوعی ارائه شد)

آخرین ستون رقابت است: مقدار در برخی از سری ها از بین می رود و در برخی دیگر حفظ می شود. در خط اول، پیاده‌سازی نادری که دوباره پرتاب نشده است، معمولاً ارزش را از دست می‌دهد: منبع قبل از برداشتن جمع‌کننده از کار می‌افتد، بنابراین catch اجرا می‌شود، اما شکست مقدار را از بین می‌برد، مانند شکست 1.

سه خط تمیز یک ایده مشترک دارند: خطا در روال خود منبع، قبل از هر صف بررسی می شود. اولین مورد از این موارد رفع مشکل بالا است. این دو نفر دیگر هستند.

داخل سازنده جریان. تماس ناموفق را در یک امتحان یا گرفتن و نه چیز دیگری بپیچید:

private fun cachedThenFresh(): Flow<Result<Profile>> = flow {
emit(Result.success(dao.cachedProfile()))
val fresh = try {
Result.success(api.fetchProfile())
} catch (e: IOException) {
Result.failure(e)
}
emit(fresh)
}

هرگز emit() را در این تلاش قرار ندهید. تجربه پیرامون emit استثناهای جمع‌آوری را نیز جلب می‌کند، که این اشکالی است که برای جلوگیری از آن وجود دارد. در مقایسه با گیره متحرک، این دقیقاً مشخص می کند که کدام استثنا را مدیریت می کند و جریان می تواند پس از شکست ادامه یابد. همه چیزهای دیگر همچنان می گذرد: برای مثال، HttpException Retrofit برای پاسخ به خطا، یک IOException نیست. بنابراین قبل از flowOn مراقب خطاهایی باشید که انتظارش را نداشتید، همانطور که پچ برای هر پیام اعلان کننده انجام می دهد.

اصلا کانالی نیست flowOn اغلب از روی عادت وجود دارد. توابع تعلیق DAO اتاق و توابع تعلیق Retrofit اکنون کار خود را به خارج از موضوع اصلی منتقل می کنند. اگر همه چیز در سازنده اساساً ایمن است، flowOn را حذف کنید و catch در ViewModel همانطور که نوشته شده است کار می کند. یک تماس مسدودکننده را می‌توان در داخل باContext(io) { } قرار داد تا زمانی که هیچ پخشی در داخل وجود نداشته باشد. هزینه آن نظم و انضباط است: به محض اینکه شخصی flatMapLatest را اضافه کند، بین منبع و این catch ترکیب یا بافر کند، تله برمی گردد.

بعد از کانال آن‌ها صف را در جای خود رها می‌کنند، بنابراین مقداری که قبلاً در آن وجود دارد ممکن است هنوز وجود نداشته باشد، اما همچنان می‌توانند کار کنند.
conflate() after catch به catch بافری با یک مقدار می دهد که به جای اتاق انتظار، مقدار انتظار را جایگزین می کند، به طوری که وقتی منبع از کار می افتد، catch هرگز مشغول نمی شود. هزینه مقادیر از دست رفته است: در حالی که جمع کننده مشغول است، فقط آخرین مقدار منتظر آن است.
buffer() در همان مکان فقط مشکل را به تاخیر می اندازد: پس از پر شدن 64 اسلات آن، catch دوباره منتظر جمع کننده می ماند و خطا catch را از دست می دهد.
بافر (Channel.UNLIMITED) هرگز پر نمی‌شود، بنابراین هر مقدار را حفظ می‌کند و catch را به هزینه حافظه در حال اجرا نگه می‌دارد، در حالی که کلکتور عقب می‌افتد.
catchAware در قسمت بعدی catch را تغییر می دهد.

هنگام تماس با ترمینال آنها فقط جمع یا first() را که به آن اعمال می شوند تغییر می دهند و هیچ یک مقداری را از قبل در صف ذخیره نمی کنند.

  • کار را به هر قبل گرفتن سپس هیچ چیز بعد از catch منتظر نمی ماند و خطا را دریافت می کند: .onEach { work }.catch { … }.collect(). کد واقعی این کار را انجام می دهد: Firebase AI Logic’s LiveSession صدای خود را از oneEach قبل از گرفتن آن ارسال می کند.
    در JVM و در دستگاه Android، با انتقال کار از جمع به oneEach، catch در هر اجرا اجرا می‌شد و هیچ چیز خراب نمی‌شد، با و بدون flowOn بعد از oneEach. خطاهای خود کار هم اکنون به catch می رسد و catch همچنان جریان را خاتمه می دهد.
    یک هزینه: کار اکنون در سمت مجموعه کانال مخزن اجرا می شود، بنابراین وقتی منبع از کار بیفتد، کار فعلی لغو می شود. در اطلاع‌دهنده، اولین پیام پست می‌شود، اما تقریباً هرگز به‌عنوان اطلاع‌رسانی علامت‌گذاری نمی‌شود. اضافه کردن .flowOn(Dispatchers.IO) بعد از oneEach اجازه داد کار تمام شود، اما فقط به این دلیل که مخزن از Dispatchers.IO نیز استفاده می کرد: flowOn خودش چیزی را تغییر نداد، کانالی ایجاد نکرد و کار در رشته منبع اجرا شد. از آنجایی که مخزن در یک توزیع کننده متفاوت بود، کانال او گیر می کرد و کار هر بار قطع می شد.
  • ارزش خود اشتغالی راه اندازی شد. وقتی کار روی هر مقدار جداگانه است و ترتیب مهم نیست، مانند رویداد تجزیه، رسید خواندن یا آپلود، اجرای آن به هر حال طراحی معقولی است: جمع آوری { value ->start { work(value) } }، در coroutineScope { } یا محدوده ای که متعلق به شماست. بازده ها را به یکباره جمع آوری می کند، بنابراین وقتی منبع از کار می افتد و مورد 1 نمی تواند اتفاق بیفتد، هرگز مشغول نیست.
    دو هزینه باقی می ماند: هیچ چیز تعداد اجراها را در یک زمان محدود نمی کند، و خطاهای خود کار از بین نمی روند، بنابراین هر کار شروع شده نیاز به مدیریت خاص خود دارد. برای محدود کردن تعداد اجرا شده، یک مجوز Semaphore در داخل کار شروع شده دریافت کنید، { permits.withPermit { work(value)} } را اجرا کنید. این به کار خود ادامه داد. به جای دریافت مجوز در کلکتور، مورد 1 برگردانده شد: پس از گرفتن مجوز، جمع آوری منتظر آن بود و خطا تقریباً در هر اجرا نتیجه را از دست داد.
    فقط برای جلوگیری از خرابی به آن دسترسی ندهید: توقف تماسی که بعداً به جمع کننده اضافه می شود، آن را برمی گرداند.
  • درعوض جمع آوری آخرین { } جمع آوری {}. کار را روی هر مقداری در کوروتین فرزند شروع می‌کند و فوراً برمی‌گردد، به‌طوری‌که وقتی منبع از کار می‌افتد، catch waits در صف قرار می‌گیرد، نه در کار شما. خرابی به کار نمی رسد، نوت بوک خالی می ماند، و اجرا می شود: مورد 1 و مورد 3 ناپدید می شوند.
    قیمت: مانند conflate()، از مقادیر می گذرد، اما به روشی متفاوت. conflate() اجازه می دهد تا کار فعلی به پایان برسد و مقادیری که در پشت آن هستند را حذف می کند. collectLatest کار فعلی را برای آخرین مقدار لغو می کند. این مهم زمانی است که کار روی هر مقدار باید تکمیل شود، مانند نوار اسنک به‌روزرسانی، و بازگشتی که توسط catch منتشر می‌شود نیز به‌عنوان مقدار جدیدتر محسوب می‌شود.
    شکاف: اگر کار قدیمی به آرامی متوقف شود، مانند یک تماس مسدود کننده در داخل withContext(io)، و منبع از کار بیفتد در حالی که collectLatest منتظر آن است، خطا دوباره نتیجه را از دست می دهد. conflate() این شکاف را ندارد.
  • سعی کن/تماس ترمینال را دنبال کنید. خطای پرتاب مجدد را می‌گیرد، اما وقتی این اتفاق می‌افتد، بلوک catch حذف می‌شود. با جمع آوری، شرط خطا باید در دو مکان ایجاد شود: بلوک catch برای مسیر عادی و try/catch برای مسیری که از آن عبور می کند. با first()، فراخوان دهنده در جایی که تقریباً در هر اجرا کانتینر را می‌خواست، استثنا را دریافت کرد. آن را به عنوان آخرین راه حل ذخیره کنید.
  • الف CoroutineExceptionHandler در محدوده. خطا در هر دو مورد حقه را از دست می دهد. با viewModelScope یا LaunchedEffect که هیچ کنترل کننده ای ندارد، برنامه از کار می افتد. با یک کنترلر، برنامه زنده می ماند، اما بلوک catch تقریباً هرگز اجرا نمی شود، بنابراین صفحه تقریباً هرگز حالت خطای خود را دریافت نمی کند.

mapLatest، transformLatest و flatMapLatest همگی با همان کد collectLatest کار می کنند. پس از گرفتن، آنها به همان روش کار می کنند. قبل از catch، آنها کانال هایی مانند flowOn هستند و خطا می تواند از catch رد شود.

آیا می توانم بهتر انجام دهد؟

همه چیز در بالا در مورد catchImpl کار می کند. راه دیگر تغییر قانون 3 است. ردیابی خرابی اکنون نشان می دهد که قانون 3 چه چیزی را از دست می دهد: یک سقط سرکوب شده توسط خطای بالادست ایجاد می شود. این یک شکست است که دو بار دیده شده است، نه دو شکست. catchImpl می تواند آن را قبل از قانون 3 بررسی کند:

// From catchImpl in kotlinx.coroutines 1.11.0, with one check added.
if (e.isSameExceptionAs(fromDownstream) || e.isCancellationCause(currentCoroutineContext())) {
throw e // Rethrow exceptions from downstream and cancellation causes
} else {
if (fromDownstream == null) {
return e
}
// New: the downstream did not fail on its own.
// It was cancelled by this upstream error.
if (fromDownstream is CancellationException &&
e !is CancellationException &&
fromDownstream.wasCausedBy(e)
) {
return e
}
// Rule 3, unchanged: rethrow, with the other failure attached as suppressed.
}

wasCausedBy زنجیره دلایل لغو را طی می کند و به دنبال e می گردد. با این چک catchAware تماس بگیرید. CatchAware.kt مخزن یک کپی از catchImpl با این تغییر است. این شامل نسخه هایی از دستیاران شخصی کتابخانه برای قوانین 1 و 2 است.

مورد 1 را رفع می کند. با جمع‌آوری که متوقف می‌شود، catchAware خطا را مدیریت می‌کند و هر بار پس از flowOn(IO)، پس از buffer() در یک رشته، و بعد از flatMapLatest، بازگشتی را ارائه می‌دهد. استثناهای خود گردآورنده همچنان به تماس گیرنده می رسد.

بقیه رو درست نمیکنه first()، firstOrNull()، take(1) و هر { } تقریباً در هر اجرا باز هم ارسال می‌شوند، و کاربر خارج‌شده در طول یک تماس مسدود شده همچنان تقریباً هر بار از کار می‌افتد: این لغو‌ها توسط باگ ایجاد نمی‌شوند. مقدار از دست رفته قبل از اینکه هر چیزی آن را ببیند به کانال رها می شود.

و هزینه واقعی دارد. با تبدیل { } بین catchAware و جمع‌آور، تقریباً هر اجرا با IllegalStateException خاتمه می‌یابد: شفافیت استثنای جریان نقض می‌شود: ارسال یک بازگشت به مجموعه‌ای که به تازگی لغو شده است، قانون کتابخانه را نقض می‌کند.

من catchAware را نمی فرستم. این نشان می‌دهد که اطلاعات در حال حاضر در استثنا هستند، اما کتابخانه به عنوان یک قاعده با “بدون انتشار پس از پرتاب پایین دست” برخورد می‌کند. این یک سوال برای شماره 4159 است، نه یک سوال کد برنامه.

چه چیزی را انتخاب کنم؟

  • شما صاحب منبع هستید. یک نگهدارنده در جلوی کانال قرار دهید. هنگامی که یک تماس نیاز به مدیریت خاص خود دارد، از try/catch در سازنده استفاده کنید: فقط آن تماس را بپیچید، هرگز استثناهای جمع‌آور را نیز منتشر نکنید یا نگیرید. اگر همه چیز در سازنده اساساً ایمن باشد، flowOn را اجرا کنید.
  • شما صاحب جریان نیستید. هیچ تنظیم تمیزی از بیرون وجود ندارد. اگر کلکتور شما روی یک مقدار متوقف شد، قبل از catch، عملیات آن را به oneEach منتقل کنید. اگر فقط به آخرین مقدار نیاز دارد، conflate() بعد از catch باعث می شود که catch در حال اجرا باشد. اگر فقط جدیدترین شغل مهم است، collectLatest { } نیز مهم است.
    به عنوان آخرین راه حل، فراخوانی ترمینال (collect یا first()) را در یک try/catch بپیچید تا خطا به جای خراب کردن برنامه، به حالت خطای شما برسد و از صاحب موضوع بخواهید که catch را قبل از کانال منتقل کند.
    هیچ‌کدام از اینها به خطای 1 کمک نمی‌کند: مقداری که از قبل در صف مالک وجود دارد ممکن است هنوز گم شده باشد.
  • در هر صورت. به مجموعه‌ای که هرگز متوقف نمی‌شود یا CoroutineExceptionHandler تکیه نکنید. اولین تماس به محض اینکه کسی یک تماس توقف اضافه می کند، قطع می شود. دومی برنامه را زنده نگه می‌دارد اما بلوک گرفتن شما را رد می‌کند.

قانون سرانگشتی

قانون سرانگشتی که اکنون دنبال می‌کنم: خطاها را قبل از اولین عبارتی که یک کانال ایجاد می‌کند، در لایه‌ای که منبع دارد رسیدگی کنید. درمان a دنبال کانال به عنوان بهترین تلاش.

هیچ یک از بخش‌ها به خودی خود یک اشکال نیستند: لغو جمع‌آوری کننده زمانی که منبع از کار بیفتد، قانون 3 و شفافیت استثنا عمدی هستند. تله این است که چگونه آنها ترکیب می شوند، و این وعده را در اسناد خود شکار می کند.

در حال حاضر، رفع به کد خود شما تعلق دارد. بدون تغییر: از 20 اکتبر 2026، آخرین commit در شاخه توسعه a7cd867 است و catchImpl در آن نسبت به 1.11.0 بدون تغییر است. در شماره 4159، نگهدارنده kotlinx.coroutines @qwwdfsad تغییر شفافیت استثنا در PR #3017 را به عنوان دلیل اصلی “که مشکل را معرفی کرد (و سایر مشکلات را برطرف کرد)” ذکر کرد. همان نگهدارنده این رفتار را «کمی پیچیده‌تر از صرفاً یک خطای نادیده گرفته یا بی‌اهمیت» نامید و باز گذاشت که آیا می‌توان آن را «بدون شکستن چیزهای بیشتری در این مدت» برطرف کرد.

نکته در مخازن شما کجاست: قبل از flowOn یا بعد از آن؟ در نظرات به من اطلاع دهید!

چگونه این آزمایش شد

هر عبارت اندازه‌گیری شده در این مقاله از اعدام‌ها ناشی می‌شود: در JVM روی دو ماشین، یکی با 16 هسته و دیگری با 2، و در یک دستگاه Android. کد هر پیاده سازی در kotlin-flow-catch-trap است و README آن هر عبارت را به چک پشت آن پیوند می دهد.

منابع

  • Flow.catch استثناهای بالادستی را از دست می دهد، kotlinx.coroutines شماره 4159
  • PR #3017: شفافیت استثنا را بهبود بخشید
  • Errors.kt در kotlinx.coroutines 1.11.0
  • Kotlin بر روی اندروید و توسعه دهندگان اندروید اجرا می شود
  • catch، ارجاع به kotlinx.coroutines API
  • رویدادهای رابط کاربری برنامه نویسان اندروید
  • kotlin-flow-catch-trap، کد هر پیاده سازی در این مقاله


شکار That Didn’t: Kotlin Flow’s Channel Trap در ابتدا در ProAndroidDev در Medium پست شد، جایی که مردم با برجسته کردن و پاسخ دادن به این داستان به گفتگو ادامه می دهند.


Source link

نوشته های مشابه

دیدگاهتان را بنویسید

همچنین ببینید
بستن
دکمه بازگشت به بالا