<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>نوشته های Android Corner</title><link>https://virgool.ir/feed/@cornerdroid</link><description>اینجا جاییکه در مورد مسائل و اخبار اندرویدی حرف میزنیم.  cornerdroid@gmail.com / کانال: https://t.me/AndroidCorner</description><language>fa</language><pubDate>2026-11-11 12:51:16</pubDate><image><url>https://files.virgool.ir/upload/users/2769830/avatar/C7pfte.jpg</url><title>Android Corner</title><link>https://virgool.ir/@cornerdroid</link></image><item><title>Discovering Kotlin: Concepts</title><link>https://virgool.ir/@cornerdroid/discovering-kotlin-concepts-yp3e0jzxt38f</link><description>تو این پست کُدامون رو با اینا تمیز‌تر میکنیم: * This keyword * Destructuring declaration * Function iteral* The benefits of getterاستفاده از this برای اشاره کردن به کلاس استفاده شدهبه این کد نگاه کنید:private fun isCurrentlyConnected(connectivityManager: ConnectivityManager?): Boolean {&#xA;    if (connectivityManager == null) {&#xA;        return false&#xA;    } else {&#xA;        val network = connectivityManager.activeNetwork&#xA;        if (network != null) {&#xA;            val capabilities = connectivityManager.getNetworkCapabilities(network)&#xA;            return capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false&#xA;        } else {&#xA;            return false&#xA;        }&#xA;    }&#xA;}اگه یکم دقت کنین این ساختار یه extension function تو کاتلینه چون یه کلاس رو بعنوان پارامتر دریافت کرده (اینجوری تو جاوا می‌نویسیم) تو کاتلین اینجوری میشه با extension function.private fun ConnectivityManager?.isCurrentlyConnected(): Boolean {&#xA;    if (this == null) {&#xA;        return false&#xA;    } else {&#xA;        val network = this.activeNetwork&#xA;        if (network != null) {&#xA;            val capabilities = getNetworkCapabilities(network)&#xA;            return capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false&#xA;        } else {&#xA;            return false&#xA;        }&#xA;    }&#xA;}خب یکم ساده‌تر شد. باز میتونه ساده‌تر بشه، اگه بیایم این if-else های تودرتو رو با when condition عوض کنیم (چون همه‌ی شرط ها یک کاندیشن رو درنظر دارن درنتیجه میشه از when استفاده کرد). اینجوری:private fun ConnectivityManager?.isCurrentlyConnected(connectivityManager: ConnectivityManager?) = when (connectivityManager) {&#xA;    null -&gt; false&#xA;    else -&gt;&#xA;        connectivityManager.activeNetwork&#xA;            ?.let(::getNetworkCapabilities)&#xA;            ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)&#xA;            ?: false&#xA;}الان بهتر شد. خب باز میتونیم بهترش کنیم، چون داریم یه extension function برای کلاس ConnectivityManager می‌نویسیم پس تو این block، ما به instance کلاسی که براش extension مینویسم، دسترسی داریم و میتونیم با استاده از کیبورد &#34;this&#34; به این instance دسترسی داشته باشیم.private fun ConnectivityManager?.isCurrentlyConnected() = when (this) {&#xA;    null -&gt; false&#xA;    else -&gt;&#xA;        activeNetwork&#xA;            ?.let(::getNetworkCapabilities)&#xA;            ?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)&#xA;            ?: false&#xA;}دلیل‌اش هم اینه که اگه شما بیاین این کد رو دیکامپایل کنین و بایت‌کدهاش رو نگاه کنید، میبینید که extension function تو کاتلین میاد (مثلا اینجا) ConnectivityManager رو به عنوان پارامتر میگیره و برای همین بهش دسترسی داریم.برای مثال بیشتر:fun &lt;T&gt; CheckboxState.TriState&lt;T&gt;.asToggleableState() = when (this) {&#xA;    is CheckboxState.TriState.Exclude -&gt; ToggleableState.Indeterminate&#xA;    is CheckboxState.TriState.Include -&gt; ToggleableState.On&#xA;    is CheckboxState.TriState.None -&gt; ToggleableState.Off&#xA;}د: destructuring declarationفرض کنید ما این سه تا متغیر رو داریم:val chapters = remember(state) { state.processedChapters }&#xA;val listItem = remember(state) { state.chapterListItems }&#xA;val isAnySelected = remember(state) { state.isAnySelected }اینجا کدمون ساده هستش ولی چون داریم همش با یه state کار میکنیم، اگه این کار رو بکنیم بنظرم ساده‌تر بشه:val (chapters, listItem, isAnySelected) = remember(state) {&#xA;    Triple(&#xA;        first = state.processedChapters,&#xA;        second = state.chapterListItems,&#xA;        third = state.isAnySelected,&#xA;    )&#xA;}نکته: Triple یه کلاسه تو لایبرری استاندارد کاتلین که میتونه سه object رو براتون hold کنه.چ: Function Literalبه این کد نگاه کنید:val isAdult : Boolean = user.age&gt; 18&#xA;val isValidUser : Boolean =  isAdult &amp;&amp; user.isActiveیک کاری رو داره با فراخوانی یک object تکراری انجام میده. که میتونید با استفاده از lambda assignment این‌کار رو ساده تر کنید. اینجوری:val isValidUser: (User) -&gt; Boolean = { user -&gt;&#xA;    user.age &gt; 18 &amp;&amp; user.isActive&#xA;}با این کار، خیلی چیزا رو میتونید  encapsulate هم بکنید.استفاده از فانکشن get(همون getter)این قسمت ربطی به تمیزکاری نداره، ولی قطعا performance رو تحت تاثیر قرار میده. چرا و کِی باید از get استفاده کنیم؟ بیاید با یه مثال اینو بگم:// first way&#xA;val trackers = trackerManager.trackers.filter { it.isLoggedIn }&#xA;// the second way&#xA;val trackers&#xA;    get() = trackerManager.trackers.filter { it.isLoggedIn }توی روش اول ما مستقیما trackers رو Initializ کردیم، ولی تو روش دوم از مقدار رو ریختیم توی getter متغیر.روش دوم چند مزیت داره:ی- Lazy Initialization: شما وقتی از فانکشن get استفاده میکنید، وقتی مقداردهی میشه متغیر، که این متغیر صدا زده بشه- درنتیجه getter اش هم صدا زده میشه: پس همین اول کاری مقدار دهی نمیشه. این میتونه خیلی خوب باشه وقتی مثلا دارید یه کار نسبتا سنگینی می‌کنید.د- Dynamic Calculation: اگه مقدارهایی دارید که هِی درحال تغییر ان، میتونید این تغییرهارو تو بدنه get اعمال کنید.3- میتویند مقدار هارو تغییر بدید یا فیلترشون کنین(Filtering or Transformation) یا Encapsulate شون کنین و ... .</description><category>Android Corner</category><author>Android Corner</author><pubDate>Tue, 26 Mar 2024 22:47:40 +0330</pubDate></item><item><title>Exception Handling</title><link>https://virgool.ir/@cornerdroid/exception-handling-savgyuhx9a4a</link><description>این چند روز مشغول یه پروژه‌‌ی kmp هستم که برای decstop ،ios و android خروجی میده. تو این پروژه‌ها خیلی مهمه که تا حد ممکن کدها رو تو ماژول shared  (commonMain) قرار داد تا هی کد تکراری ننویسیم تو هر ماژول(پلتفرم) یا به‌اصطلاح باید اصل DRY پیروی کنیم.  یه موضوع مهمی که برخوردم بهش بحث exception handling بود. معمولا وقتی که از compose و برای بحث async از kotlin flow استفاده میکنین، میایم برای responseمون یه دیتا کلاس wrapper ایجاد میکنیم و توش error ها و loading و ... رو هندل می‌کنیم. اینجوری: interface HomeRepository {&#xA; fun getPhotos(&#xA;        page: Int?,&#xA; perPage: Int?,&#xA; orderBy: String?&#xA;    ): Flow&lt;DataState&lt;List&lt;Photo&gt;&gt;&gt;&#xA;}اینجا wrapper مون کلاس DataState هستش:data class DataState&lt;T&gt;(&#xA; val message: String? = null,&#xA;    val data: T? = null,&#xA;    val isLoading: Boolean = false,&#xA;    val type: Type? = Type.SIMPLE&#xA;) {&#xA; enum class Type {&#xA; SIMPLE,&#xA; AUTH&#xA; }&#xA;&#xA; companion object {&#xA;&#xA; fun &lt;T&gt; error(&#xA;            message: String,&#xA; type: Type?&#xA;        ): DataState&lt;T&gt; {&#xA; return DataState(&#xA; message = message,&#xA; type = type&#xA;            )&#xA;        }&#xA;&#xA; fun &lt;T&gt; data (&#xA;            data : T?  = null&#xA; ) : DataState&lt;T&gt;{&#xA; return DataState(&#xA; data = data&#xA;            )&#xA;        }&#xA;&#xA; fun &lt;T&gt; loading() = DataState&lt;T&gt;(isLoading = true)&#xA;    }&#xA;&#xA;&#xA;}اینم repository impl:override fun getPhotos(&#xA;    page: Int?,&#xA; perPage: Int?,&#xA; orderBy: String?&#xA;): Flow&lt;DataState&lt;List&lt;Photo&gt;&gt;&gt; = flow {&#xA; emit(DataState.loading())&#xA; try {&#xA; val photos = photoService.getPhotos(&#xA; page = page,&#xA; per_page = perPage,&#xA; order_by = orderBy&#xA;        )&#xA;        emit(DataState.data(photos))&#xA;    } catch (e: Exception) {&#xA;        emit(&#xA;            DataState.error(&#xA; message = e.message ?: &#34;Unknown Error&#34;,&#xA; type = DataState.Type.SIMPLE&#xA; )&#xA;        )&#xA;    }&#xA;}و داخل viewModel:@HiltViewModel&#xA;class HomeViewModel @Inject constructor(&#xA; private val savedStateHandle: SavedStateHandle,&#xA;    private val homeRepository: HomeRepository&#xA;) : FrameViewModel() {&#xA;&#xA; val state: MutableState&lt;HomeState&gt; = mutableStateOf(HomeState())&#xA;&#xA; init {&#xA;&#xA; homeRepository.getPhotos(&#xA; page = state.value.page,&#xA; perPage = 25,&#xA; orderBy = state.value.orderBy,&#xA; )&#xA;            . {&#xA; state.value = state.value.copy(isLoading = true)&#xA; }&#xA; .onCompletion {&#xA; if (it == null) {&#xA; state.value = state.value.copy(isLoading = false)&#xA;                } else {&#xA;                    Log.e(&#34;dlfjask&#34;, &#34;there is a problem&#34;)&#xA;                }&#xA; }&#xA; .onEach { dataState -&gt;&#xA;//                state.value = state.value.copy(isLoading = dataState.isLoading)&#xA; Log.i(TAG, &#34;photos: $dataState.isLoading&#34;)&#xA;&#xA;&#xA;                dataState.data?.let { photos -&gt;&#xA; state.value = state.value.copy(photos = photos)&#xA;                    Log.i(TAG, &#34;photos: $photos&#34;)&#xA; }&#xA;            }.catch {&#xA; Log.e(TAG, &#34;error: $it&#34;)&#xA; }.launchIn(viewModelScope)خب این کد الان کار میکنه ولی چندین جا کدهای تکراری داریم. خب یه روش دیگه که بنظرم بهتره اینه که: اول یه extension function  برای اینترفیس Flow می‌نویسیم تا همه error ها و loading ها رو یکجا تو یه مکان داشته باشیم و بتونیم بعدا هندل کنیم:sealed interface Result&lt;out T&gt; {&#xA; data class Success&lt;T&gt;(val data: T) : Result&lt;T&gt;&#xA; data class Error(val exception: Throwable? = null) : Result&lt;Nothing&gt;&#xA; data object Loading : Result&lt;Nothing&gt;&#xA;}&#xA;&#xA;fun &lt;T&gt; Flow&lt;T&gt;.asResult(): Flow&lt;Result&lt;T&gt;&gt; {&#xA; return this&#xA; .map&lt;T, Result&lt;T&gt;&gt; {&#xA; Result.Success(it)&#xA; }&#xA; . { emit(Result.Loading) }&#xA; .catch { emit(Result.Error(it)) }&#xA;}خب دیگه نمی‌خواد wrap کنیم response مون رو. این میشه repository: interface PhotoRepository  {&#xA; fun getPhotos() : Flow&lt;List&lt;Photo&gt;&gt;&#xA;}این هم repository impl: fun getPhotos(sortBy: PhotoSortField = NONE): Flow&lt;List&lt;Photo&gt;&gt; {&#xA; return combine(&#xA; userDataRepository.userData,&#xA; photoRepository.getPhoto(),&#xA; ) { userData, photos -&gt;&#xA; val followedPhotos = photos&#xA;                .map { pohto -&gt;&#xA; FollowablePhoto(&#xA; photo = pohto,&#xA; isFollowed = pohto.id in userData.followedPhoto,&#xA; )&#xA; }&#xA; when (sortBy) {&#xA;                NAME -&gt; followedPhotos.sortedBy { it.photo.name }&#xA; else -&gt; followedPhotos&#xA;            }&#xA; }&#xA; }&#xA;}خب تا اینجا که کاری به هندل کردن ارور نداشتیم. هندل کردن ارورها و لودینگ‌ها رو تو viewmodel انجام میدیم. خب یه فانکشن می‌نویسیم برای state های اون صفحه‌مون:private fun photoUiState(&#xA;    photoId: String,&#xA; userDataRepository: UserDataRepository,&#xA; photosRepository: photosRepository,&#xA;): Flow&lt;photoUiState&gt; {&#xA; // Observe the followed photos, as they could change over time.&#xA; val followedphotoIds: Flow&lt;Set&lt;String&gt;&gt; =&#xA;        userDataRepository.userData&#xA; .map { it.followedPhoto }&#xA;&#xA; // Observe photo information&#xA; val photoStream: Flow&lt;photo&gt; = photosRepository.getphoto(&#xA; id = photoId,&#xA; )&#xA;&#xA; return combine(&#xA;        followedphotoIds,&#xA; photoStream,&#xA; ::Pair,&#xA; )&#xA;        .asResult()&#xA;        .map { followedphotoToPhotoResult -&gt;&#xA; when (followedPhotoTophotoResult) {&#xA; is Result.Success -&gt; {&#xA; val (followedphotos, photo) = followedphotoTophotoResult.data&#xA; val followed = followedphotos.contains(photoId)&#xA;                    photoUiState.Success(&#xA; followablephoto = Followablephoto(&#xA; photo = photo,&#xA; isFollowed = followed,&#xA; ),&#xA; )&#xA;                }&#xA;&#xA; is Result.Loading -&gt; {&#xA;                    photoUiState.Loading&#xA;                }&#xA;&#xA; is Result.Error -&gt; {&#xA;                    photoUiState.Error&#xA;                }&#xA;            }&#xA; }&#xA;} اینم از viewmodel مون:val photoUiState: StateFlow&lt;photoUiState&gt; = photoUiState(&#xA; photoId = photoArgs.photoId,&#xA; userDataRepository = userDataRepository,&#xA; photosRepository = photosRepository,&#xA;)&#xA;    .stateIn(&#xA; scope = viewModelScope,&#xA; started = SharingStarted.WhileSubscribed(5_000),&#xA; initialValue = photoUiState.Loading,&#xA; ) خب این هم از این. نکته: روش‌های دیگه‌ای هم هستن مثل RxBus یا استفاده از shared flow برای پیاده‌سازی مکانیزمی شبیه Event Bus.خب امیدوارم خوشتون اومده باشه. اگه نظری داشتین درباره کد می‌تونین بپرسین.</description><category>Android Corner</category><author>Android Corner</author><pubDate>Wed, 27 Dec 2023 19:28:11 +0330</pubDate></item><item><title>اخبار هفته</title><link>https://virgool.ir/@cornerdroid/اخبار-هفته-sh6znxvurmvr</link><description>Android Corner ? نسخه‌ی Compose Multiplatform 1.5.0تغییرات این نسخه هیجان‌انگیزه. کلا تغییرات KMP برای من خیلی جذابه(:کامپوز برای دسکتاپ stableه، برای iOS تو حالت alpha و برای وب هم تو حالت experimental قرار داره.این نسخه براساس Jetpack Compose 1.5 هستش که تمرکزاش روی performanceه. همچنین تو این نسخه، از نسخه 1.1 Material Design 3 میتونید استفاده کنید که یه‌سری امکانات جدید داره.پشتیبانی Compose Multiplaform از dialog ها، popup ها و windowInsetاز حالا به بعد دیگه نیاز ندارین برای همه پلتفرم‌ها کد‌های جداگانه بنویسین برای ساخت Dialog و Popup DropdownMenu و AlertDialog. با داشتن یکدیگر تیکه کد برای مثلا AlertDialog، تو همه پلتفرم‌ها ازش استفاده کنید:Composable&#xA;fun CommonDialog() {&#xA;   var isDialogOpen by remember { mutableStateOf(false) }&#xA;   Button( = { isDialogOpen = true }) {&#xA;       Text(&#34;Open&#34;)&#xA;   }&#xA;   if (isDialogOpen) {&#xA;       AlertDialog(&#xA;           onDismissRequest = { },&#xA;           confirmButton = {&#xA;               Button( = { isDialogOpen = false }) {&#xA;                   Text(&#34;OK&#34;)&#xA;               }&#xA;           },&#xA;           title = { Text(&#34;Alert Dialog&#34;) },&#xA;           text = { Text(&#34;Lore ipsum&#34;) },&#xA;       )&#xA;   }&#xA;} ?  ? از طریق windowInset هم میتونین محتواتون رو با system ui یکی کنید یه همچین چیزی: ? پیشرفت های iOS1- پیمایش یا اسکرول‌کردن طبیعی: تو این نسخه اسکرول‌کردن توی iOS مثل اسکرول‌کردن بومی خود iOS شده. مثلا:@Composable&#xA;fun NaturalScrolling() {&#xA;   val items = (1..30).map { &#34;Item $it&#34; }&#xA;   LazyColumn {&#xA;       items(items) {&#xA;           Text(&#xA;               text = it,&#xA;               fontSize = 30.sp,&#xA;               modifier = Modifier.padding(start = 20.dp)&#xA;           )&#xA;       }&#xA;   }&#xA;}اینجوری میشه، مثل اپلیکیشن‌های native: ? 2- پشتیبانی از Dynamic Type: این ویژگی Dynamic Type تو iOS  به کاربر امکان میده تا فونت سایز دلخواه‌اش رو تنظیم کنه. کاربر می‌تونه تو setting و اون text size گوشی رو حالت دلخواه از max تا min تنظیم کنن.@Composable&#xA;fun DynamicType() {&#xA;   Text(&#34;This is some sample text&#34;, fontSize = 30.sp)&#xA;}اینجوری: ? پشتیبانی از high-refresh rate display: توی ورژن‌های قبلی ماکزیمم فریم‌ریت 60 FPS بوده که منجر به کُند و لَگی بودن ui (تو دستگاه‌های با صفحه 120Hz) میشد. ولی تو این نسخه فریم‌ریت تا 120 فریم در ثانیه پشتیبانی می‌شه.3- ساده‌شدن مدیریت resource ها: دیگه لازم نیست asset های پوشه resource توی iOS رو توی یه bundle کپی کنید‌.4- و یه‌سری بهبودی‌هایی توی TextField و صفحه کلید داشتیم.پیشرفت‌های دسکتاپ1- پایدارشدن فریمورک تست برای تست کردن ui بدون محدودیت:@Composable&#xA;fun App() {&#xA;   var searchText by remember { mutableStateOf(&#34;cats&#34;) }&#xA;   val searchHistory = remember { mutableStateListOf&lt;String&gt;() }&#xA; &#xA; &#xA;   Column(modifier = Modifier.padding(30.dp)) {&#xA;       TextField(&#xA;           modifier = Modifier.testTag(&#34;searchText&#34;),&#xA;           value = searchText,&#xA;           onValueChange = {&#xA;               searchText = it&#xA;           }&#xA;       )&#xA;       Button(&#xA;           modifier = Modifier.testTag(&#34;search&#34;),&#xA;            = {&#xA;               searchHistory.add(&#34;You searched for: $searchText&#34;)&#xA;           }&#xA;       ) {&#xA;           Text(&#34;Search&#34;)&#xA;       }&#xA;       LazyColumn {&#xA;           items(searchHistory) {&#xA;               Text(&#xA;                   text = it,&#xA;                   fontSize = 20.sp,&#xA;                   modifier = Modifier.padding(start = 10.dp).testTag(&#34;attempt&#34;)&#xA;               )&#xA;           }&#xA;       }&#xA;   }&#xA;}نتیجه این کد، این میشه: ? خب بیاین با JUnit تست‌اش کنیم:class SearchAppTest {&#xA;   @get:Rule&#xA;   val compose = createComposeRule()&#xA; &#xA; &#xA;   @Test&#xA;   fun `Should display search attempts`() {&#xA;       compose.setContent {&#xA;           App()&#xA;       }&#xA; &#xA; &#xA;       val testSearches = listOf(&#34;cats&#34;, &#34;dogs&#34;, &#34;fish&#34;, &#34;birds&#34;)&#xA; &#xA; &#xA;       for (text in testSearches) {&#xA;           compose.onNodeWithTag(&#34;searchText&#34;).performTextReplacement(text)&#xA;           compose.onNodeWithTag(&#34;search&#34;).performClick()&#xA;       }&#xA; &#xA; &#xA;       val lastAttempt = compose&#xA;           .onAllNodesWithTag(&#34;attempt&#34;)&#xA;           .assertCountEquals(testSearches.size)&#xA;           .onLast()&#xA; &#xA; &#xA;       val expectedText = &#34;You searched for: ${testSearches.last()}&#34;&#xA;       lastAttempt.assert(hasText(expectedText))&#xA;   }&#xA;}2- این نسخه، پشتیبانیِ آزمایشی رو برای بهبود رندرکردن پنل‌های کامپوز توی کامپوننت‌های Swing رو هم ارائه میده. این مانع از مشکلات رندر انتقالی(transition) هنگام نمایش، پنهان یا تغییر اندازه پنل‌ها می شود.fun main() {&#xA;   System.setProperty(&#34;compose.swing.render.on.graphics&#34;, &#34;true&#34;)&#xA;   SwingUtilities.invokeLater {&#xA;       val composePanel = ComposePanel().apply {&#xA;           setContent {&#xA;               Box(modifier = Modifier.background(Color.Black).fillMaxSize())&#xA;           }&#xA;       }&#xA; &#xA; &#xA;       val popup = object : JComponent() { ... }&#xA; &#xA; &#xA;       val rightPanel = JLayeredPane().apply {&#xA;           add(composePanel)&#xA;           add(popup)&#xA;           ...&#xA;       }&#xA; &#xA; &#xA;       val leftPanel = JPanel().apply { background = CYAN }&#xA; &#xA; &#xA;       val splitter = JSplitPane(..., leftPanel,rightPanel)&#xA; &#xA; &#xA;       JFrame().apply {&#xA;           add(splitter)&#xA;           setSize(600, 600)&#xA;           isVisible = true&#xA;       }&#xA;   }&#xA;}لوگوی جدید برای ربات اندروید(Bugdroid)گوگل لوگوی جدیدی رو برای برند اندروید معرفی کرده و میگه که از این به بعد قراره همه جا ببینیدش: ? آخرین ARTwork روی صدها میلیون دستگاهبروزرسانی های سیستم گوگل‌پلی برای android runtime، یه سری بهبودی هایی رو به همراه داشته البته برای اندروید 12 به بالا و گفته‌که android go هم بزودی از این آپدیت بهره‌مند میشه:1- پیشرفت‌های عملکرد: این بروز‌رسانی‌ها شامل بهینه‌سازی در زمان اجرا و کامپایلر هستش که باعث بهبود زمان startup برنامه، سرعت اجرای برنامه و استفاده از حافظه می شود.2- رفع مشکلات امنیتی: همچنین ماژولار بودن ART اجازه می‌ده تا اصلاحات امنیتی به سرعت در دستگاه های اندرویدی قراربگیرن که درنتیجه باعث میشه آسیب پذیری ها را pacth کنند.3- سازگاری سریع‌تر با ویژگی‌های جدید جاوا: این بروزرسانی سرعت پشتیبانی از نسخه‌های جدیدتر Java / OpenJDK رو به دستگاه های Android فراهم می‌کنه و به ما این امکان رو می‌ده تا از جدیدترین ویژگی های زبان استفاده کنیم.برای خوندن جزییات مقاله.ی: Library releasesآخرین نسخه از Dagger and Hilt — v2.48 شامل نسخه alpha از KSP processor هست. الان دیگه با خیال راحت میتونید از kapt به ksp مهاجرت کنید‌‌.یه‌سری تغییرات هم داشته که اینجا میتونین ببینین.برای استفاده از ksp این رو دنبال کنید.برای مهاجرت‌تون این مقاله رو بخونید.اینا رو هم بخونین ضرری نداره:1. بخش article های این لینک رو هم ببینید جالبه.2. این لینک.کانال:Android Corner / ایمیل: cornerdroid@gmail.com</description><category>Android Corner</category><author>Android Corner</author><pubDate>Tue, 19 Sep 2023 08:58:03 +0330</pubDate></item><item><title>YAGNI, DRY and KISS principles</title><link>https://virgool.ir/@cornerdroid/yagni-dry-and-kiss-principles-zghpfij81bj9</link><description>Android Corner ? سناریوهای کمی هستن که در نگاه اول میتونه حل کردنشون سخت باشه. ولی چرا اون سناریوها رو پیچیده‌تر میکنیم؟!استفاده از سه اصل YAGNI, KISS و DRY ما رو به کدی تمیز، ساده و قابل‌فهم‌ می‌رسونه و مهم‌تر از همه اینه‌که نمی‌ذاره درآینده مورد عنایت برنامه‌نویس‌های دیگه قرار‌بگیریم⁦(:ی: DRYمخفف: “Don’t Repeat Yourself”. یادتون باشه هروقت دارید یه کد تکراری می‌نویسید یه جای کار اشتباهه. پس هروقت به‌کد تکراری بَرخوردید اونو به یک موجودیت، entity یا functionality واحد تبدیل کنید. مثلا تو مثال پایین، دوبار یه چیز رو تکرار کردیم:// Without DRY&#xA;&#xA;class LoginActivity : AppCompatActivity() {&#xA;&#xA;  private fun validateInput(username: String, password: String): Boolean {&#xA;    if(username.isEmpty()) {&#xA;      return false&#xA;    }&#xA;&#xA;    if(password.length &lt; 6) {&#xA;      return false&#xA;    }&#xA;&#xA;    return true&#xA;  }&#xA;&#xA;  fun onLoginClick() {&#xA;    val username = usernameInput.text&#xA;    val password = passwordInput.text&#xA;&#xA;    if(!validateInput(username, password)) {&#xA;      showError(&#34;Invalid input&#34;)&#xA;      return&#xA;    } &#xA;&#xA;    // login user&#xA;  }&#xA;&#xA;  fun onSignupClick() {&#xA;    val username = usernameInput.text&#xA;    val password = passwordInput.text&#xA;&#xA;    if(!validateInput(username, password)) {&#xA;      showError(&#34;Invalid input&#34;)&#xA;      return&#xA;    }&#xA;&#xA;    // signup user&#xA;  }پس میایم اون کد رو به یک functionality توی یک مکان واحد تبدیل میکنیم. اینجوری://  With DRY principle&#xA;&#xA;class LoginActivity : AppCompatActivity() {&#xA;&#xA;private fun validateInput(username: String, password: String): Boolean {&#xA;  // validation logic&#xA;}&#xA;&#xA;private fun validateAndLogin(username: String, password: String) {&#xA;  if (!validateInput(username, password)) {&#xA;    showError(&#34;Invalid input&#34;)&#xA;    return&#xA;  }&#xA;&#xA;  // login user&#xA;}&#xA;&#xA;private fun validateAndSignup(username: String, password: String) {&#xA;  if (!validateInput(username, password)) {&#xA;    showError(&#34;Invalid input&#34;)&#xA;    return&#xA;  }&#xA;&#xA;  // signup user&#xA;}&#xA;&#xA;fun onLoginClick() {&#xA;  val username = usernameInput.text&#xA;  val password = passwordInput.text&#xA;  validateAndLogin(username, password)&#xA;}&#xA;&#xA;fun onSignupClick() {&#xA;  val username = usernameInput.text&#xA;  val password = passwordInput.text&#xA;  validateAndSignup(username, password)&#xA;}د: KISSمخفف: “Keep It Simple, Stupid”. این اصل میگه سعی کنید همیشه کدهای ساده و قابل‌فهم بنویسید چون هرچقدر‌ که کدتون ساده‌تر باشه نگهداری‌اش درآینده ساده‌تره. ? س: YAGNIمخفف: “You Aren’t Gonna Need It”. این اصل میگه تمرکزتون رو روی نیازهای فعلی محصول بزارید. هرچیز اضافی تو پروژه، ناقض این اصله. مثلا تو اندروید، نمی‌دونم چه‌کاریه که همه می‌خوان از معماری MVVM پیروی کنن. شما میتونین یه پروژه کوچیک رو با MVP پیاده‌سازی کنین. بعضی پروژه‌ها اصلا معماری نیاز ندارن، فرض کنین یه پروژه‌ای دارید که می‌خواین 4 تا request بزنین به سرور، خب برای این پروژه چرا باید بیایم کارای اضافی انجام بدیم مثل ساختن repository و.... ? امیدوارم این اصل‌ها رو یادگرفته باشید و از این به‌بعد بتونید تو پروژه‌هاتون استفاده کنید.کانال:Android Corner / ایمیل: cornerdroid@gmail.com</description><category>Android Corner</category><author>Android Corner</author><pubDate>Tue, 19 Sep 2023 08:50:29 +0330</pubDate></item><item><title>آموزش Compose(قسمت سوم)</title><link>https://virgool.ir/@cornerdroid/آموزش-composeقسمت-سوم-v6lqrswsfigc</link><description>State and Jetpack Compose AndroidCornerآدرسصفحهوبیکهمیخواهیدنمایشدهیدراPasteکنید به هرمقدار(value) ای که ممکنه باگذشت زمان تو برنامه‌تون تغییر کنه، بهش میگن State. این تعریف خیلی کلی هست و میتونه هرچیزی رو دَربَربگیره از یک دیتابیس room گرفته تا یه متغیر توی یک کلاس.چند نمونه از state تو برنامه های اندرویدی:* یه Snackbar ای که نشون میده: اینترنت برقرار نشد مثلا. مقدار این snackbar با برقرار شدن اینترنت، میتونه تغییر کنه، درنتیجه میگیم: snackbar یه state داره که اگه اینترنت برقرار شد، اون state تغییر میکنه.* یک پست همراه کامنت‌هاش. با تعامل کاربر با کامنت‌ها(حذف، ویرایش یا اضافه‌کردن کامنت)، درنتیجه state کامنت و پست مربوطه‌اش تغییر پیدا میکنه.* انیمیشن Ripple ای که کاربر با زدن رو دکمه به نمایش درمیاد، میتونه نمایانگر تغییر state خود انیمیشن باشه.تو این پست ما ارتباط بین composable و state رو بررسی میکنیم و با API ای که compose دراختیار مون قرار میده میفهمم کجا و چطوری state مون رو ذخیره و استفاده کنیم.نکته: بعضا من اول متن (یک، دو، سه) گذاشتم؛ این فقط برای راست چین کردن متنه و بار معنایی خاصی نداره.یک: State و compositionکامپوز declarative هستش، بنابراین تنها راه برای آپدیت کردنش اینه که یه composable رو با با آرگمان جدید صدا بزنیم. این آرگمان ها نماینده state هستن. هرموقعی که یه state آپدیت بشه، یه recomposition هم صورت میگیره.@Composable&#xA;private fun HelloContent() {&#xA;    Column(modifier = Modifier.padding(16.dp)) {&#xA;        Text(&#xA;            text = &#34;Hello!&#34;,&#xA;            modifier = Modifier.padding(bottom = 8.dp),&#xA;            style = MaterialTheme.typography.bodyMedium&#xA;        )&#xA;        OutlinedTextField(&#xA;            value = &#34;&#34;,&#xA;            onValueChange = { },&#xA;            label = { Text(&#34;Name&#34;) }&#xA;        )&#xA;    }&#xA;}ببینید مقدار OutlinedTextField یه رشته‌ی خالیه، درنتیجه هرچی کاربر می‌نویسه، چیزی نوشته نمیشه چون رشته خالیه و تغییری نکرده درنتیجه recomposition ای هم اتفاق نمی‌افته. پس باید یه راهی پیدا کنیم تا مقدارهایی که کاربر وارد می‌کنه رو بگیریم و بریزیم توی value اون OutlinedTextField. می‌بینید که مثل XML نیست که مثلا EditText خودشو آپدیت می‌کرد‌‌، اینجا باید خودمون state رو آپدیت کنیم.د: State در composable هافاکنشن‌های composable از remember استفاده می‌کنن برای ذخیره‌کردن یه آبجکت تو حافظه. rem‌‌ember یه مقدار رو محاسبه و ذخیره می‌کنه درطول composition اولیه. این مقدارهای ذخیره‌شده توسط remember، درطول recomposition، برگشت داده‌میشن. تو این مثال، کاربر یه‌چیزی رو تو TextField می‌نویسه، همیشه آخرین مقداری که کاربر وارد کرده تو TextField کرده، remember ذخیره می‌کنه‌ درنتیجه چون اینجا از mutableStateOf استفاده میکنیم برای ذخیره شدن این مقادیر، و کامپوز می‌دونه این mutableState پایدار و stable هستش درنتیجه با هر تغییر recomposition اتفاق می‌افته.این mutableStateOf یه آبجکتی می‌سازه که state اش قابل‌مشاهده هست و از نوعMutableState&lt;T&gt;هستش.interface MutableState&lt;T&gt; : State&lt;T&gt; {&#xA;    override var value: T&#xA;}خود MutableState&lt;T&gt; یه observable هست که اومده اینترفیس state که با compose runtime ادغام شده رو implement کرده. درنتیجه هرتغییری در مقدارهای mutableStateOf منجر به شروع به recomposition میشه‌‌ و هر فانکشنی که از اون value استفاده می‌کنه recompose میشه و درنتیجه اون فانکشن ها آپدیت میشن.سه روش برای تعریف آبجکت MutableState:1- val mutableState = remember { mutableStateOf(default) }2- var value by remember { mutableStateOf(default) }3- val (value, setValue) = remember { mutableStateOf(default) }وقتی شما از delegate by استفاده میکنین این پکیج ها import میشه به پروژه‌تون:import androidx.compose.runtime.getValue&#xA;import androidx.compose.runtime.setValueخب حالا این مقادیر ذخیره‌شده، catch شده، بخاطرسپرده‌شده رو می‌تونین بعنوان پارامتر یه composable استفاده کنید، حتی می‌تونین تو statementها بعنوان logic ازش استفاده کنید. برای مثال تو مثال پایین، وقتی نمی‌خوایین greeting رو وقتی name خالی بود نمایش بدید، از این state ذخیره‌شده توی statement if استفاده میکنیم:@Composable&#xA;fun HelloContent() {&#xA;    Column(modifier = Modifier.padding(16.dp)) {&#xA;        var name by remember { mutableStateOf(&#34;&#34;) }&#xA;        if (name.isNotEmpty()) {&#xA;            Text(&#xA;                text = &#34;Hello, $name!&#34;,&#xA;                modifier = Modifier.padding(bottom = 8.dp),&#xA;                style = MaterialTheme.typography.bodyMedium&#xA;            )&#xA;        }&#xA;        OutlinedTextField(&#xA;            value = name,&#xA;            onValueChange = { name = it },&#xA;            label = { Text(&#34;Name&#34;) }&#xA;        )&#xA;    }&#xA;}همونطور که گفتم ما برای این از remember استفاده میکنیم که state حفظ بشه تا در هر recomposition ای که اتفاق می‌افته بتونیم اونو return کنیم تا درنتیجه composable مون آپدیت بشه با state جدید. اما وقتی که configuration change اتفاق می‌افته، نمی‌تونه state رو حفظ کنه. درنتیجه اینجاست که باید از rememberSaveable استفاده کنیم. این بصورت خودکار مقدارها رو تو یه bundle ذخیره می‌کنه درنتیجه هر مقداری که بشه توی bundle ذخیره کرد رو می‌تونید بهش بدید.احتیاط: استفاده از آبجکت های تغییرپذیر(mutable) مثل  ArrayList&lt;T&gt; or mutableListOf() بعنوان state باعث میشه که کاربر داده های نادرست و قدیمی رو ببینه. چون کلا compose آبجکت‌های mutable مثل ArrayList یا data class های mutable رو نمی‌بینه درنتیجه با تغییر مقادیرشون recomposition ای هم اتفاق نمی‌افته چون کلا اینا stable نیستن. بجای استفاده از آبجکت های non-observable mutable، توصیه میشه از یه نگه‌دارنده داده مثل اینState&lt;List&lt;T&gt;&gt; یا listOf()استفاده کنید‌.نوع‌های دیگه‌ای که state ازشون پشتیبانی می‌کنهکامپوز شمارو ملزم نمی‌کنه که حتما باید از MutableState استفاده کنید برای نگه‌داری state. کامپوز از یه‌سری نوع های observable دیگه هم پشتیبانی می‌کنه. این نگه‌دارنده‌های داده باید به State&lt;T&gt;t تبدیل یا ادغام بشن تا کامپوز متوجه تغییرات‌شون بشه. ولی خود کامپوز اومده observable های رایج تو اندروید رو ادغام کرده با state:1- اکستنشن فانکشن collectAsStateWithLifecycle() برای Flowاین فانکشن میاد مقادیر رو به روشی lifecycle-aware از یه Flow جمع می‌کنه که این lifecycle-aware بودنش فایده‌ای زیادی داره. این متد آخرین مقادیر ساطع‌شده رو به‌وسیله state کامپوز برمی‌گردونه.برای ادغام data source های lifecycle-aware ی مثل Flow با compose، این dependency رو به پروژه‌تون اضافه کنید:dependencies {&#xA;      ...&#xA;      implementation(&#34;androidx.compose.runtime:runtime-livedata:1.4.3&#34;)&#xA;}2- متد collectAsState() برای Flowاین فانکشن مشابه collectAsStateWithLifecycle هستش، چون مقادیر Flow رو جمع‌آوری می‌کنه و تبدیل‌اش می‌کنه به state کامپوز. اما تفاتشون اینه‌که collectAsStateWithLifecycle فقط برای پلتفرم اندرویده، از collectAsState می‌تونید برای پلتفرم‌هایی مثل دسکتاپ مثلا استفاده کنید. dependency خاصی هم نیاز نداره.3- متد observeAsState() برای LiveDataاین متد observe کردن LiveData رو شروع میکنه و مقادیر رو به‌وسیله State بهمون میده. برای انجام این کار اول این dependency رو به پروژه‌تون اضافه کنید.dependencies {&#xA;      ...&#xA;      implementation(&#34;androidx.compose.runtime:runtime-livedata:1.4.3&#34;)&#xA;}4- متد subscribeAsState() برای Rxjava2این یه اکستنشن فانکشنه که جریان‌های reactive‌(واکنشیِ) Rxjava2 رو به State کامپوز‌ تبدیل می‌کنه. جریان‌ها میتونن Single، Observable یا مثلا Completable باشن. برای استفاده از این ویژگی باید این dependency رو به پروژه‌تون اضافه کنید:dependencies {&#xA;      ...&#xA;      implementation(&#34;androidx.compose.runtime:runtime-rxjava2:1.4.3&#34;)&#xA;}5- متد subscribeAsState() برای Rxjava3این هم مثل قبلی یه اکستنشن فانکشنه که جریان‌های reactive‌(واکنشیِ) Rxjava2 رو به State کامپوز‌ تبدیل می‌کن. جریان‌ها میتونن Single، Observable یا مثلا Completable باشن. برای استفاده از این ویژگی باید این dependency رو به پروژه‌تون اضافه کنید:dependencies {&#xA;      ...&#xA;      implementation(&#34;androidx.compose.runtime:runtime-rxjava3:1.4.3&#34;)&#xA;}توجه: بغیر از این فانکشن‌هایی که کامپوز براتون آماده کرده، شما میتونین یه اکستنشن فانکشن برای دیگر observable ها هم بنویسید. اگه اپ شما از یه کلاس observable سفارشی یا کاستوم استفاده می‌کنه، باید بااستفاده از produceState تبدیل‌اش کنین به State&lt;T&gt;t کامپوز.س: Stateful درمقابل statelessوقتی یه composable از remember استفاده می‌کنه برای ذخیره آبجکت، ما میگیم این composable یه internal state برای خودش ایجاد کرده و وقتی یه composable دارای state داخلی هستش، به‌اصطلاح میگیم این composable، خودش رو دارای state کرده یعنی stateful هستش‌. اگه به HelloContent نگاه کنید می‌بینید که State name رو داخل‌خودش نگه‌داری می‌کنه درنتیجه میگیم HelloContent یه internal state داره و stateful هستش. توجه داشته‌باشید، composable هایی که internal state دارن، کمتر قابل‌استفاده‌ی‌مجدد هستن و سخت‌تر میشه تست‌شون کرد.درمقابل‌اش، زمانی که یه فانکشن کامپوزبل، state ای رو داخل‌خودش نگه‌داری نداره، میگیم این فانکشن statless هست. یه راه ساده برای دست‌یابی به یک فانکشن stateless، استفاده از state hoisting هستش.شمایی که می‌خواین یه composable قابل‌استفاده‌مجدد(reusable) بنویسید، باید به هردویِ اینا توجه کنید. اگه کسی که composable مون رو صدا می‌کنه، نیازی به stateاش نداشته باشه، اون‌وقت از composable stateful استفاده کنید. اما اگه اون صداکننده composableمون نیاز به state composable داره از کاپوزِبِل stateless استفاده کنید.لغت state hoisting: یعنی بلندکردن state و دادنش به یه کسی که اونو نیاز داره مثلا صداکننده‌اش(caller). همون لغت‌شو یادبگیرین.چ: State hoistingچ: State hoisting یه پترنِ تا state رو به caller پاس بدیم تا درنتیجه‌اش به کامپوزبلی stateless دست‌بیابیم‌. کُل پیاده‌سازی‌اش هم اینه‌که بجای اینکه بیایم متغیر برای state مون تعریف کنیم، بیایم بعنوان پارامتر تعریفش کنیم، با این اسم‌ها:یک: value: T --» نمایانگر مقدار فعلی برای نمایشدو: onValueChange: (T) -&gt; Unit  --»  یه رویداد(event) برای درخواست مقدارهایی که تغییر کردن، T همون مقدار جدیده‌.با این‌حال شما محدود به onValueChange نیستید، اگه event های بیشتری دارین می‌تونین با lambdaها تعریف‌شون کنین، مثلا ExpandingCard دوتا event داره onExpand و onCollapse.اِستیتی که پاس داده شده یا hoiste شده، این ویژگی های مهم رو دارن:یک: Single source of truth --» با حرکت دادن state بجای کپی کردنش، مطمئن میشیم که یک منبع حقیقی برای state مون داریم. این باعث باگ کم‌تر میشه.شاید این اصل رو تو بهتر بنویسم بعدی توضیح بدم.دو: Encapsulated  --» فقط کامپوزبل های stateful میتونن، state سون رو تغییر بدن.سه: Shareable --» میتونین اِستیت Hoiste شده مثل name رو با چندین composable به اشتراک بذارینچهار: Interceptable --» صداکننده‌ی(caller) کامپوزبل stateless می‌تونند قبل از تغییر state، وقایع یا event ها رو نادیده بگیرن یا تغییرشون بدهند.پنج: Decoupled --» اگه کامپوزبل‌تون stateless باشه، میتونین اون state رو هرجایی که دل‌تون خواست ازش استفاده کنین یا ذخیره کنین مثلا توی viewmodel.مثال پایین رو ببینید. اومدیم name و onValueChange رو از HelloContent حرکت‌دادیم یا hoist کردیم به caller اش یعنی HelloScreen:@Composable&#xA;fun HelloScreen() {&#xA;    var name by rememberSaveable { mutableStateOf(&#34;&#34;) }&#xA;&#xA;    HelloContent(name = name, onNameChange = { name = it })&#xA;}&#xA;&#xA;@Composable&#xA;fun HelloContent(name: String, onNameChange: (String) -&gt; Unit) {&#xA;    Column(modifier = Modifier.padding(16.dp)) {&#xA;        Text(&#xA;            text = &#34;Hello, $name&#34;,&#xA;            modifier = Modifier.padding(bottom = 8.dp),&#xA;            style = MaterialTheme.typography.bodyMedium&#xA;        )&#xA;        OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text(&#34;Name&#34;) })&#xA;    }&#xA;}photoبه این الگو که توش state میاد پایین و event ها بالا می‌رن، میگن جریان داده یک‌طرفه یا unidirectional data flow. با این الگو، شما میتونید کامپوزبل هایی که state رو نمایش میدن رو از بخش های که اونا رو تغییر یا ذخیره می‌کنن، جدا کنید.نکات کلیدی: وقتی که state رو hoist می‌کنید، سه قانون هستش که بهتون کمک می‌کنه بفهمید که کجا باید state رو قرار بدید:1- اِستیت باید حداقل به پایین‌ترین والد مشترکی که دارن فرستاده بشه. برای مثال:Parent &#xA;  |- Sibling A  &#xA;     |- Child A1&#xA;  |- Sibling Bفرض sibling A و sibling B، دارن یه state مشترک رو میخونن. حالا اگه ما بیایم برای هردو دوتا یه state جداگانه بسازیم بنظرتون چطوره؟ من میگم یه راه حل بهتری هم هست، اونم الگوی بالایی هستش. اینطوری که ما state رو hoist میکنیم به والدشون. بهتر نیست؟2- اِستیت باید hoist بشه به بالاترین سطحی که ممکنه تغییر پیدا کنه.3- اگه دو state در پاسخ به یک event یا action مشترک تغییر کنند، باید hoist شون کنیم به parent composable مشترک.بازیابی state در composeگفتیم که rememberSaveable شبیه به remember هستش با این تفاوت که rememberSaveable چون از saved instance state استفاده می‌کنه درنتیجه تو هر configuration change، توی هر recomposition، یا اگه activity دوباره صدا زده شد، این داده‌ای که توش قرار دادیم رو حفظ می‌کنه.راه‌های ذخیره stateبالا گفتم که هر مقداری که بشه توی bundle ذخیره کرد رو می‌تونید به rememberSaveable بهش بدید ومشکلی نداره. ولی آبجکت‌هایی که نمیشه ذخیره‌شون کرد چی؟بسته‌بندی یا Parcelizeساده‌ترین راه parcelable کردن‌شونه اینطور که شما باید annotation @Parcelize رو به آبجکت‌تون اضافه کنید. اینجوری:@Parcelize&#xA;data class City(val name: String, val country: String) : Parcelable&#xA;&#xA;@Composable&#xA;fun CityScreen() {&#xA;    var selectedCity = rememberSaveable {&#xA;        mutableStateOf(City(&#34;Madrid&#34;, &#34;Spain&#34;))&#xA;    }&#xA;}استفاده از mapSaverاگه به‌ دلایلی @Parcelize براتون مناسب نبود، می‌تونید از mapSaver استفاده کنید برای تعریف قانون‌تون به‌منظور  تبدیل یه آبجکت به مجموعه‌ای از مقادیر که سیستم می‌تونه ذخیره کنه.data class City(val name: String, val country: String)&#xA;&#xA;val CitySaver = run {&#xA;    val nameKey = &#34;Name&#34;&#xA;    val countryKey = &#34;Country&#34;&#xA;    mapSaver(&#xA;        save = { mapOf(nameKey to it.name, countryKey to it.country) },&#xA;        restore = { City(it[nameKey] as String, it[countryKey] as String) }&#xA;    )&#xA;}&#xA;&#xA;@Composable&#xA;fun CityScreen() {&#xA;    var selectedCity = rememberSaveable(stateSaver = CitySaver) {&#xA;        mutableStateOf(City(&#34;Madrid&#34;, &#34;Spain&#34;))&#xA;    }&#xA;}استفاده از ListSaverبرای برای هر فیلد نیاز به تعریف key نباشه، میتونین از listSaver استفاده کنید‌ اینجوری:data class City(val name: String, val country: String)&#xA;&#xA;val CitySaver = listSaver&lt;City, Any&gt;(&#xA;    save = { listOf(it.name, it.country) },&#xA;    restore = { City(it[0] as String, it[1] as String) }&#xA;)&#xA;&#xA;@Composable&#xA;fun CityScreen() {&#xA;    var selectedCity = rememberSaveable(stateSaver = CitySaver) {&#xA;        mutableStateOf(City(&#34;Madrid&#34;, &#34;Spain&#34;))&#xA;    }&#xA;}نگه‌دارندگان state در composeبلندکردن یا hoist کردنِ اِستیتی که بالا مثال‌اش رو دیدیم ساده بود و توی خود فانکشن کامپوزبل خودمون مدیریت‌اش کردیم. اما وقتی‌که state‌ها زیاد‌شدن و logic برنامه‌تون پیچیده‌ شد درنتیجه مدیریت‌شون براتون سخته، یه راه‌حل خوب اینه‌که بیاید مسئولیت های منطق و state رو بریزید توی کلاس‌هایی بنام state holders.لغت state holder: این کلاس برای مدیریت منطق و state کامپوزبل ها به کار می‌ره.اینجا پیشنهاد شده دوتا صفحه  state hoisting in Compose(اینو فردا کانال میذارم) و State holders and UI State رو بخونید.هنگام تغییر کلیدها، محاسبات را به خاطر بسپاریدخب!! remember اغلب اوقات با نینبن استفاده میشه. اینجوری:var name by remember { mutableStateOf(&#34;&#34;) }اینجا فانکشن remember مقادیر mutableStateOf رو توی هر recomposition حفظ می‌کنه.بطور کلی فانکشن remember یه lambda رو بعنوان پارامتر میگیره که این لمبدا یه‌سری عملیات/محاسبات انجام میده. وقتی این فانکشن برای اولین بار اجرا میشه، لمبدا رو صدا میزنه و عملیات مربوطه اش رو انجام میده، وقتی تموم شد اونو ذخیره می‌کنه.جدا از ذخیره‌کردن state، شما می‌تونید هر آبجکت یا نتیجه‌ای در composition که انجام‌اش زمانبر، پُرمحاسبه یا expensive هستش برای initialize کردن یا محاسبه کردن به remember بدید. با انجام این کار، دیگه اون عملیات expensive هِی توی هر recomposition تکرار نمیشه. برای مثال ساختن ShaderBrush یه عمل expensive هستش:val brush = remember {&#xA;    ShaderBrush(&#xA;        BitmapShader(&#xA;            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),&#xA;            Shader.TileMode.REPEAT,&#xA;            Shader.TileMode.REPEAT&#xA;        )&#xA;    )&#xA;}همونطور که گفتم لمبدا محاسبات رو انجام میده و نتیجه رو remember ذخیره‌ می‌کنه تا زمانی که composable ای که از remember استفاده می‌کنه از ui حذف بشه. با این‌حال، ممکنه بخوایم این مقدار ذخیره شده دوباره ارزیابی بشه و اون lambda دوباره اجرا بشه. اینجاست که key ها وارد می‌شن. remember  بجز این lambda یه پارامتر دیگه هم می‌تونه بگیره بنام key یا keys. اگه مقدار key تغییر کنه بین recomposition ها:1- اول remember نگاه می‌کنه، می‌بینیه که key تغییر کرده.2- بعد remember مقدار ذخیره شده رو نامعتبر(invalid) فرض میکنه3- درنتیجه وقتی که بعدا recompose صورت بگیره، دیگه اون مقدار رو. حفظ نمی‌کنه بلکه lambda دوباره محاسبات رو انجام میده و مقدار جدید رو remember ذخیره می‌کنه.کد پایین رو نگاه کنید. یه ShaderBrush ساخته میشه برای بکگراند box مون. remember یه instance ازش ذخیره می‌کنه. remember این avatarRes رو بعنوان key1 پارامتر میگیره که تصویر پس زمینه انتخاب شده است. اگه avatarRes تغییر کنه، brush با image جدید ریکامپوز میشه و دوباره بکگراند به جعبه اعمال میشه.@Composable&#xA;private fun BackgroundBanner(&#xA;    @DrawableRes avatarRes: Int,&#xA;    modifier: Modifier = Modifier,&#xA;    res: Resources = LocalContext.current.resources&#xA;) {&#xA;    val brush = remember(key1 = avatarRes) {&#xA;        ShaderBrush(&#xA;            BitmapShader(&#xA;                ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),&#xA;                Shader.TileMode.REPEAT,&#xA;                Shader.TileMode.REPEAT&#xA;            )&#xA;        )&#xA;    }&#xA;&#xA;    Box(&#xA;        modifier = modifier.background(brush)&#xA;    ) {&#xA;        /* ... */&#xA;    }&#xA;}تیکه کد بعدی درس فردامونه.ذخیره کردن state با key ها فراتر از recompositionخب. rememberSaveable یه پوشنده() ای روی remember هست. که داده رو توی یه bundle ذخیره می‌کنه. این یه پارامتر input میگیره مثل همون key توی remember عمل می‌کنه. یعنی هروقت input تغییر کنه توی recomposition بعدی lambda هم دوباره اجرا میشه.var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {&#xA;    mutableStateOf(&#xA;        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))&#xA;    )&#xA;}خب اینم از این. امیدوارم یادگرفته باشین، چون تو قسمت های بعدی این مفاهیم پایه ای رو نیاز داریم. اگه جایی رو نفهمیدید تو کامنت‌ها به‌اشتراک بذارین.لطفا پست‌ها رو به اشتراک بذارین تا افراد بیشتری بتونن از اینا استفاده کنن و درنتیجه‌اش الکی سرمایه‌شون رو سَر خریدن دوره تلف نکنن.کانال:Android Corner / ایمیل: cornerdroid@gmail.com</description><category>Android Corner</category><author>Android Corner</author><pubDate>Mon, 04 Sep 2023 20:10:25 +0330</pubDate></item><item><title>اخبار هفته</title><link>https://virgool.ir/@cornerdroid/اخبار-هفته-cexoogbhthww</link><description>اخبار هفته، قسمت سوم:اَندروید 14 بتا 5چند هفته دیگه اندروید 14 بطور رسمی معرفی میشه، ولی الان beta 5 اش در دسترسه روی Pixel Tablet، Pixel Fold و بقیه خانواده pixel ها و همچنین Android emulator ها. پس برید اپلیکیشن تون رو تست کنید روی اندروید 14 بتا 5 تا از عملکرد برنامه تون مطمئن بشید، تا قبل از اینکه نسخه پایدارِش بیاد.توی Beta 5 چخبره؟شما میتونید با استفاده از این نسخه Android 14، تمام رفتار/ویژگی های android 14 رسمی رو میبینید و خیلی فرصت خوبیه تا اپ تون رو با اندروید 14 سازگار کنید و حتی شما میتونید اپ هاتون برای اندروید 14 منتشر کنید بااا target کردن SDK Version 34. یه سری تغییرات هم داشته:1- دسترسی محدود به عکس ها و ویدیوها: توی اندروید 13 اگه شما از READ_MEDIA_IMAGES و READ_MEDIA_VIDEO برای دسترسی به عکس ها و ویدیوها استفاده میکردید، کاربر با موافقت کردن با این دسترسی ها، شما به کل عکس ها و ویدیوها دسترسی داشتید، ولی تو Android 14، کاربر میتونه دسترس محدودی به عکس ها و ویدیوهاش بده. اگه شما از photo picker استفاده میکردید تا الان، نمیخواد خودتون رو با این ویژگی وقف بدید اما شماهایی که از این استفاده نمی کردید باید بگم که از الان باید READ_MEDIA_VISUAL_USER_SELECTED استفاده کنید.2- محدودیت در استفاده از full-screen Intent notifications: اگه شما از این نوع نوتیفیکیش استفاده میکردید تا قبل از اندروید 14، الان دیگه با اومدن اندروید 14، فقط برنامه‌هایی که قابلیت تماس و زنگ هشدار دارن میتونن از این نوتیفیکیشن استفاده کنن. بعلاوه، Google play store از این به بعد هر برنامه ای که معیار های برنامه زنگ هشدار یا تماس رو ندارن و از پرمیشن USE_FULL_SCREEN_INTENT استفاده میکنن رو لغو میکنه.3- برنامه ریزی هشدارها(Schedule exact alarms) نیاز به مجوز صریح داره: اگه target SDK تون رو اندروید 13 به بالا هستش، از به بعد باید از کاربر درخواست کنین تا مجوزِSCHEDULE_EXACT_ALARMرو برای تنظیم هشدارها رو بهتون بده.4- بزرگ نمایی متن های کوچک: یه ویژگی که اندروید 14 داره اینه که اگه یه فونتی خیلی کوچک بود، کاربر میتونه اون رو تا 200% بزرگ کنه، این قابلیت مخصوصا برای کاربرهای کم‌بینا میتونه جذاب باشه.اینجا میتونین کل تغییرات رو ببینید و اپ تون رو با اندروید 14 سازگار کنید.پلی استور برای دستگاه هایِ بزرگ صفحه(large screen)گوگل میگه که ما توی Google IO سال قبل یه سری تغییرات play store رو به اشتراک گذاشتیم برای large screen ها، از اون موقع به بعد افراد بیشتری جذب large screen ها شدن. و حالااا گوگل اومده یه سری چهارتا ویژگی جدید به play store اضافه کرده تاا کاربرایی که از large screen ها استفاده میکنن راحت‌تر اپلیکیشن های باکیفیت(مخصوص large screen ها) پیدا کنن.1. صفحه لیست جزئیات تازهلیست جزئیات یه سری تغییرات داشته: برای بازی ها، ویدیویی بالای صفحه جزئیات قرار میگیره تا کاربرا یه حسی از gameplay بازی داشته باشن. همچنین صفحه جزئیات اپ/بازی هارو چندسِتونه کردن که باعث میشه محتوای بیشتری از اپ تون قرار بگیره.2. رتبه بندی و بهبود کیفیتگوگل برای اینکه بتونه برنامه هایی که از دستگاه های با صفحات بزرگ پشتیبانی میکنن رو زیاد کنه، اومده چندتا تغییر توی نحوه رتبه بندی ایجاد کرده؛ اینطوریه که اگه برنامه شما ریسپانسیوه و تو large screen ها محتوا خوب نمایش داده میشه، وقتی کاربر رفت جستجو کرد، برنامه شما توی جایگاه بالاتری قرار میگیره. این به کاربرای large screen ها کمک میکنه تا برنامه/بازی هایی که با دستگاه شون سازگاری داره رو پیدا کنن.همچنین میگه سال قبل یه سری هشدارهایی برای لیست جزئیات قرار قرار داده بودیم تا کاربر متوجه بشه که این برنامه از نظر کیفیت و سازگاری مناسب دستگاهش نیست. و یه معیاری رو برای تعیین کیفیت برنامه درنظر میگرن که شامل: پشتیبانی از large screen ها و crash برنامه تون برای کاربرا از یه میزانی بالاتر نره(8 درصد). اگه به اینا توجه نکنید، پلی استور هم به شما توجه نمیکنه(توی جایگاه سرچ و ...). پس حواستون باشه!!این تغییرات از اواخر ماه اوت شروع میشه(الان سپتامبر، احتمالا لحاظ کنن دیگه). میتونین با استفاده از Android Vitals در Play Console یا API Reporting برای نظارت و بهبود این معیارها اقدام کنید.تغییر آخر هم اینکه پلی استور asset های برنامه تون رو توی صفحه home نمایش میده تا کاربرا درک بهتری داشته باشن از آنچه که تو اپ شما تجربه میکنن.3. ناوبری ساده فروشگاهااین چیز خاصی نیست برای ما(دولوپرها)، یه سری تغییرات دادن برای navigate کردن ساده تر و تجربه بهتر مثلا تو large screen ها اومدن یه navigation rail اضافه کردن یا تو صفحه سرچ اومدن دو صفحه رو split کردن.خبرهای کامپوزچندتا API جدید برای compose معرفی شده که کارِتون رو خیلی راحتتر میکنن.یک: 1.1 Jetpack  WindowManager پایدار شدجت پک WindowsManager یه کتابخونه مهم برای بهینه سازی برنامه تون برای پشتیبانی از فرم های دستگاه های مختلف هستش.* میتونین با استفاده از این کتابخونه- با کلاس/اینترفیس هایی که داره- برای فرم مختلف دستگاه ها(مثل تاشوها یا همون foldable) اپی سازگار بنویسین.* دوتا activity رو کنار هم نشون بدید. اینو بزارید یکم توضیح بدم.1- اگه برنامه‌تون چندین activity داره، میتونین از activity embedding استفاده کنید، تا اپ تون توی فرم های تاشو خوب به نمایش در بیاد.2- و اگه دارین از معماری single-activity استفاده میکنین، میتونین از یه API بنام SlidingPaneLayout استفاده کنید تا این قابلیت رو به برنامه‌تون اضافه کنید.کامپوز برای کتابخانه های Wear OS و Tiles 1.2 پایدار شدندالان میتونید کامپوز 1.2 رو توی Wear OS و Wear Tiles استفاده کنید، این کار رو خیلی ساده تر میکنه تا شما اپی زیبا و جذاب رو برای ساعت هوشمند بنویسید. جزئیات رو میتونین تو این مقاله ببینید.معرفی Jetpack Emoji Pickerیه راه جدیده برای اضافه کردن ایموجی به برنامه‌تون. برای جزئیات بیشتر، این مقاله چک کنید.این مقالات رو هم بخونین ضرری نداره:* این مقاله* این مقاله* و این مقالهخب اینم از اخبار هفته، اگه براتون مفید بود، لطفا با دوستانتون به اشتراک بذارین چون حمایتتون یه مثل همیشه باعث خوشحالیه.کانال: Android Corner / ایمیل: cornerdroid@gmail.com</description><category>Android Corner</category><author>Android Corner</author><pubDate>Sat, 02 Sep 2023 13:26:13 +0330</pubDate></item><item><title>آموزش compose(قسمت دوم)</title><link>https://virgool.ir/@cornerdroid/آموزش-composeقسمت-دوم-xfrnses9qdzu</link><description>قسمت دوم: Lifecycle of composableتو این پست درباره چرخه عمر فانکشن های composable حرف میزنیم و میبینم که چطور compose تصمیم میگیره تا یه composable رو recompose کنه یا نه.چرخه عمرگفتیم که رابط کاربری مون رو با مجموعه ای از composable ها تعریف میکنیم. وقتی که این composable ها برای اولین بار کشیده میشن رو صفحه، ما میگیم composition اتفاق افتاده. و بعد از composition، اگه state ای تغییر کرد، باید اون composable هایی که state(آرگمان) شون تغییر کرده دوباره اجرا بشن با داده جدید، این دوباره اجرا شدنه بعد از composition رو بهش میگیم recomposition. پس به render شدن اولیه میگیم composition و به update های بعدش میگیم recomposition. همینطور که گفتم، ما دیگه getter و setter نداریم تا internal state ویجت هامون رو تغییر بدیم، بجاش هر وقت که ورودی فانکشن های composable تغییر کنن، کامپوز متوجه میشه و recomposition انجام میشه بصورت خودکار.ببصورت کلی اینجوریه:یه فانکشن composable وارد composition میشه و وقتی composition تموم شد، با تغییر پارامتر میتونه هر چندبار recompose بشه.نکته: قاعدتا دفعه اول فقط composition اتفاق میوفته دیگه و recomposition هنوز اتفاق نیافتاده.اگه یه composable چندین بار صدا زده بشه، چندین instance هم ازش تو composition ساخته میشه و هر فراخوانی ای، چرخه عمر خودش رو داره. مثلا:@Composable&#xA;fun MyComposable() {&#xA;    Column {&#xA;        Text(&#34;Hello&#34;)&#xA;        Text(&#34;World&#34;)&#xA;    }&#xA;}الان میبینید که فانکشن Text دوبار با پارامتر های مختلف صدا زده شده، پس دو instance ازش ساخته میشه(قرار میگیره). تو عکس هم رنگ های متفاوت نمایانگر instance جداگانه هست.آناتومی یک composable تو compositionهر instance ی از یه composable، با call site اش مشخص میشه. call site جاییکه که یه فانکشن composable صدا زده میشه و هر وقت میگیم که composable چندین instance داره، یعنی توی چندین call site صدا زده شده.کامپوز دقیقا تشخیص میده که کدوم composable فراخوانی شده تو composition و اگه فانکنشن فراخوانی شده باشه تو composition و پارامترش هم تغییری نکرده باشه، دیگه تو recomposition، کامپوز اونو نادیده میگیره(skip میکنه).به مثال زیر نگاه کنید:@Composable&#xA;fun LoginScreen(showError: Boolean) {&#xA;    if (showError) {&#xA;        LoginError()&#xA;    }&#xA;    LoginInput() // This call site affects where LoginInput is placed in Composition&#xA;}&#xA;&#xA;@Composable&#xA;fun LoginInput() { /* ... */ }&#xA;&#xA;@Composable&#xA;fun LoginError() { /* ... */ }توی کد بالا، LoginScreen با یک شرط LoginError رو صدا میزنه و همیشه LoginInput هم فراخوانی میشه. هرکدوم از این فانکشن ها call site مربوط به خودشون رو دارن که با استفاده از این compiler کامپوز میتونه اینا رو از هم تشخیص بده.میبینید که با تغییر state(تغییر مقدار showError)، recomposition اتفاق افتاده و LoginError به درخت اضافه شده. نکته مهم اینه که چون state فانکشنِ LoginInput تغییری نکرده، پس compose میاد اونو نادیده میگیره(skip) و همون instance قبلی خودش رو قرار میده که این عمل باعث افزایش performance میشه.اضافه کردن اطلاعات بیشتر برای داشتن recomposition هوشمندانه تراگه یه composable چندین بار فراخوانی کنیم، چندین بار هم به composition اضافه میشه. اما وقتی که  چندین بار یه composable رو از یه call site یکسان فراخوانی کنیم(مثل توی یه loop)، compiler کامپوز نمیتونه اینا رو از هم تشخیص بده، بنابراین کامپوز از یه مکانیسم دیگه برای تشخیص اینا استفاده میکنه بنام ترتیب اجرایی یا execution order که تو بعضی موارد مشکل ساز میشه.به مثال زیر نگاه کنید:@Composable&#xA;fun MoviesScreen(movies: List&lt;Movie&gt;) {&#xA;    Column {&#xA;        for (movie in movies) {&#xA;            // MovieOverview composables are placed in Composition given its&#xA;            // index position in the for loop&#xA;            MovieOverview(movie)&#xA;        }&#xA;    }&#xA;}تو این مثال، چون ما توی یه loop قرار داریم درنتیجه call site توش همه یکسانن پس compose از execution order استفاده میکنه. حالا مثلا اگه یه movie جدید اضافه بشه به پایین لیست، کامپوز میتونه از مابقیِ movie ها دوباره استفاده کنه و دیگه instance جدید برای هرکدوم از اینا نمیسازه:اما اگه لیست تغییر کنه مثلا ترتیب آیتم ها عوض بشه، یا بعضی هاشون حذف بشن یا اضافه بشن به بالا یا وسط لیست، recomposition باید اتفاق بیفته، و چون مکانیسم execution order، ترتیب براش مهمه و توی همه این موارد(حذف، جابجایی) ترتیب آیتم ها عوض شده درنتیجه از هرکدوم یک instance جدید میسازد تو recomposition و جایی مهم میشه که شما یه side-effect داشته باشین مثلا برین یه تصویر رو از سمت از سرور دریافت کنید و درحالی که دارید این کار رو انجام میدید، recomposition اتفاق بیفته و هنوز اون load تصویر انجام نشده باشه، بارگذاری تصویر هم باید cancel بشه و دوباره از سَر گرفته بشه با این recomposition.@Composable&#xA;fun MovieOverview(movie: Movie) {&#xA;    Column {&#xA;        // Side effect explained later in the docs. If MovieOverview&#xA;        // recomposes, while fetching the image is in progress,&#xA;        // it is cancelled and restarted.&#xA;        val image = loadNetworkImage(movie.url)&#xA;        MovieHeader(image)&#xA;&#xA;        /* ... */&#xA;    }&#xA;}فرایند composition صفحه MoviesScreen رو نشون میده که برای هر کدوم از MovieOverview یه instance جدید ایجاد کرده(مثلا بخاطر عوض شدن ترتیب). Side effect هم دوباره restart میشه.ولی ما اینو نمیخوایم که برای هرکدوم یه instance جدید بسازه. ما میخوایم مثلا وقتی ترتیب آیتم ها عوض شد، instance هاشون هم تو درخت composition جاشون عوض بشه نه که instance جدید ساخته بشه. برای همین، compose یه راهی رو فراهم میکنه که به runtime میگه یه سری مقادیر رو از قسمت معینی از درخت composition رو با استفاده از کلید(key composable) شناسایی کن.این key میاد تو یه بلاک کد یه سری مقادیر منحصر به فرد(unique) رو میگیره که هویت هر instance(آیتم) رو مشخص میکنه و وقتی recomposition اتفاق افتاد بدلیل تغییر ترتیب لیست، دیگه نمیاد با استفاده از مکانیسم نین برای هرکدوم یه instance جدید بسازه، بللکه میاد نگاه میکنه میبینه که key ای که تعریف کردیم توش یه سری مشخصات از اینکه قبلا اینا instance هاشون موجود بوده تو composition، فقط ترتیب شون عوض شده، پس نمیاد instance جدید بسازه فقط ترتیب instance هاشون رو تو درخت composition تغییر میده. (امیدوارم فهمیده باشین!) اینجوری:@Composable&#xA;fun MoviesScreenWithKey(movies: List&lt;Movie&gt;) {&#xA;    Column {&#xA;        for (movie in movies) {&#xA;            key(movie.id) { // Unique ID for this movie&#xA;                MovieOverview(movie)&#xA;            }&#xA;        }&#xA;    }&#xA;}میبینید که با مقادیر یونیت این key، کامپوز instance های قبلی رو تشخیص داده و دوباره ازشون استفاده کرده و side effect هم دیگه از سَر گرفته نمیشه و به اجرا شدنش ادامه میده.ببعضی از composable ها، توی خودشون key رو دارن مثل  LazyColumn برای آیتم هاش:@Composable&#xA;fun MoviesScreenLazy(movies: List&lt;Movie&gt;) {&#xA;    LazyColumn {&#xA;        items(movies, key = { movie -&gt; movie.id }) { movie -&gt;&#xA;            MovieOverview(movie)&#xA;        }&#xA;    }&#xA;}اگه ورودی‌ها تغییر نکرده، نادیده بگیر(skipping)اگه یه composable توی composition باشه و ورودی اش هم تغییر نکرده باشه و پایدار باشه(stable) میتونه skip بشه توی recomposition.لغت skip: یعنی compose اونو نادیده بگیره و recompose نشه.لغت stable: یعنی ورودی ها پایداره یعنی تغییر نکرده، درنتیجه recompose نمیشه.یک نوع stable با این قرارداد ها مطابقت داره:1- نتیجه متد equals همیشه یک چیزه برای دو instance، پس compose اینو stable درنظر میگیره.2- همه انواع public property ها stable اند و قاعدتا اگه تغییری کنن composition متوجه اش میشه.کامپایلر کامپوز اینا رو بدون گذاشتن @Stable، پایدار یا stable درنظر میگیره:1- همه primitive تایپ ها: Boolean, Int, Long, Float, Char و غیره.2- رشته ها(Strings)3- لامبدا ها4- هرچی که تو MutableState نگهداری بشه stable هستش با اینکه mutable.اگه یه composable همه ورودی هاش stable باشن و البته تغییری هم نکرده باشن، compose اونو skip میکنه.اگه یه چیزی stable نیست، ولی میخواین compose باهاش مثل stable ها برخورد کنه از annotation @Stable استفاده کنید:// Marking the type as stable to favor skipping and smart recompositions.&#xA;@Stable&#xA;interface UiState&lt;T : Result&lt;T&gt;&gt; {&#xA;    val value: T?&#xA;    val exception: Throwable?&#xA;&#xA;    val hasError: Boolean&#xA;        get() = exception != null&#xA;}خب اینم از این، امیدوارم اینا رو یادگرفته باشین. اشکالات، ابهامات و نظراتتون رو تو کامنت ها به اشتراک بذارین.کانال: Android Corner / ایمیل: cornerdroid@gmail.com</description><category>Android Corner</category><author>Android Corner</author><pubDate>Fri, 01 Sep 2023 07:34:23 +0330</pubDate></item><item><title>آموزش Compose(قسمت اول)</title><link>https://virgool.ir/@cornerdroid/آموزش-composeقسمت-اول-tkhlzxmt5wb1</link><description>قسمت اول:نکته 1: سعی کنید لغت های تخصصی رو که قرار دادم بلد باشید.نکته 2: هرجایی که جمله نامفهوم بود یا کلمه ای اشتباه املایی داشت(پیش میاد!) حتما با من درمیون بذارین تا اصلاحش کنم.کامپوز یه kit توسعه رابط کاربریِ declarative هستش که پیاده سازی رابط کاربری و نگهداری از اون رو برای شما راحت میکنه.پارادایم اعلانی(declarative)توی سیستم UI قبلی اندروید شما یه درختی از view داشتین که وقتی وضعیت(state) برنامه تغییر میکرد بدلیل مثلا تعامل کاربر، این سلسله مراتب UI هم باید update بشه در پاسخ به تعامل کاربر، پروسه update اینجوری بود که میرفت کل درخت UI رو میگشت با استفاده از متد findViewById و node ها رو با صدا زدن اینا عوض میکرد.button.setText(String)container.addChild(View)img.setImageBitmap(Bitmap)این متدها(همونایی که bold کردم) میان وضعیت داخلی(internal state) ویجت رو تغییر میدن و خییلی راحته که شما یادتون بره مثلا یک ویجت setText کنین و متن رو آپدیت کنین یا اصلا یه node رو تنظیم میکنین روی یک ویجت و اون ویجت هم کلا از درخت UI قبلا حذف شده بوده. بطور کلی پیچیدگیِ نگهداریِ نرمافزار وقتی رشد میکنه که تعداد زیادی view نیاز به آپدیت داشته باشن.لغت node: زیرمجموعه یه view که میتونه تغییر کنه، همون text و image و color و ... منظورشه.این پارادایم تا حد زیادی طراحی رابط کاربری رو ساده تر میکنه. اینطوری کار میکنه که بجای اینکه کل سلسله مراتب UI بگرده که کدوم view آپدیت شده، میاد کل صفحه رو از اول render میکنه و فقط اون تغییرات موردنیاز رو اعمال میکنه. این رویکرد نمیاد مثل UI سیستم سنتی بیاد getter/setter تعریف کنه، بلکه با یه سری فانکشن بنام composable میاد UI رو تعریف میکنه و تغییرات یا داده ها رو بعنوان پارامتر به اینا پاس میدیم.فانکشن composableنکته: ما قبلا به عناصر رابط کاربری مون میگفتیم View، ولی الان به هرچی که view میگفتیم، میگیم Widget(ویجت) و این ویجت ها رو هم با composable فانکشن ها تعریف میکنیم.نکته: قواعد نامگذاری اش هم بصورت PascalCase هستش.گفتیم که رابط کاربری مون رو با مجموعه ای از فانکشن های composable تعریف میکنه. اینا data رو بعنوان پارامتر ورودی میگیرن و عناصر رابط کاربری رو از تغییرات باخبر میکنن. یه نگاه به فانکشن Greeting بندازید:میبینید که این فانکشن یه string میگیره و یه ویجت text ساتِع(emit) میکنه.1- همه composable فانکشن ها باید annotation @composable رو داشته باشن؛ این فانکشن به compiler کامپوز میگه که این فانکشن برای دادن داده ها به رابط کاربریه.2- این فانکشن یه متن رو نشون میده تو UI با استفاده از composable فانکشنِ Text(). فانکشن های composable سلسله مراتب UI رو با استفاده از call کردن composable فانکشن های دیگه میسازند؛ مثل اینجا که ما یه composable فانکشن ساختیم و توش از یه composable فانکش دیگه استفاده کردیم.3- این فانکشن ها چیزی رو return نمی کنن بلکه عنصر های UI رو ساتع یا emit میکنند.4- این فانکشن ها سریع، idempotent و  side-effect free(البته اگه شما side-effect ای درست نکنین، تو پست های بعضی دربارش حرف میزنیم) هستن.*  این فانکشن ها اگه صد بار هم با همون آرگومنت صدا زده بشن باید یک رفتار رو داشته باشن مگر اینکه از طریق global variable هایی از خارج فانکشن(side-effect) رفتار تابع رو تغییر بدنن.مهاجرت از رویکرد Imperative به declarativeتوی سیستم UI سنتی، هر ویجتی یه internal state خودش رو داره و با getter و setter میتونیم اون state اش رو عوض کنیم(اصطلاحا بهش میگن stateful)ولی توی رویکرد declarative کامپوز ویجت ها stateless(استیتی ندارن) اند و هیچ getter و setter ندارن و وقتی هم میخواین state شون رو عوض کنین بجای set کردن مثلا text میاین اون داده رو تو آرگمانِ فانکشنِ کامپوز قرار میدین.میبینید که (مثلا) از viewmodel یه سری داده ها اومده و داده ها از composable های بالاتر میرسه به پایینتر، اونایی که داده لازم دارن(البته تو آرگومان هاشون) .وقتی که state تغییر کرد، فانکشن composable دوباره صدا زده میشه با داده جدید که سبب میشه عناصر رابط کاربری دوباره کشیده بشن رو صفحه، به این پروسه میگن recomposition.محتوای پویا(Dynamic)چون فانکشن های composable با کاتلین نوشته شدند، شما میتونین از هر ویژگی ای که کاتلین داره تو پیاده سازی UI و فانکشن های composable تون استفاده کنید. مثلا میتونین از حلقه ها، عبارت های شرطی، عملگرها و .. استفاده کنید:@Composable&#xA;fun Greeting(names: List&lt;String&gt;) {&#xA;    for (name in names) {&#xA;        Text(&#34;Hello $name&#34;)&#xA;    }&#xA;}ریکامپوزیش(Recomposition)فریمورک کامپوز بصورت هوشمندانه فقط اون اجزایی رو recompose میکنه که آرگومانشون تغییر کرده.برای مثال این فانکشن رو با یه button درنظر بگیرین:@Composable&#xA;fun ClickCounter(clicks: Int, : () -&gt; Unit) {&#xA;    Button( = ) {&#xA;        Text(&#34;I&#39;ve been clicked $clicks times&#34;)&#xA;    }&#xA;}هر بار که روی این دکمه کلیک میشه، فرض کنین مقدار clicks هم آپدیت بشه، اون وقت compose میاد این lambda با فانکشن Text که داره دوباره با داده جدید صدا میزنه که گفتیم بهش میگن recomposition  و فانکشن های دیگه که به  مقدار clicks وابسته نیستن رو recompose نمیکنه یعنی اون فانکشن هارو نادیده میگیره(skip میکنه).یه نکته ای رو هم که باید درنظر داشته باشین درباره فانکشن های کامپوز اینه که این فانکشن ها باید side-effect نداشته باشن(تو پست های بعدی درباره این مفصل صحبت میکنیم). Side-effect تعاریف مختلفی داره از افراد مختلف، ولی تو این مورد: به هر تغییراتی که state بیرونِ فانکشن (composable) رو تغییر بِده، میگیم side effect. اگه side effect داشته باشین، ممکنه توی پروسه recomposition، اونی که میخواین skip بشه، نشه(پست های بعدی).فانکشن های composable باید تو هر فریم دوباره اجرا باشن، پس باید عملکردشون سریع باشه تا وقتی که مثلا یه انیمیشن اجرا میشه تو هر فریم لَگ نزنه و سریع اونو render کنه، پس اگه کارهایی دارین که ممکنه زمانبر باشه برید تو background انجامش بدید.فانکشن های composable می تونن به هر ترتیبی اجرا بشناگه به کد زیر نگاهی بندازین، احتمالا فکر کنید این فانکشن ها به ترتیب اجرا میشن، ولی لزوما اینطور نیست و کامپوز ممکنه اینا رو تو هر ترتیبی اجرا کنه. Compose تشخیص میده کدوم فانکشن اولویت بالاتری داره و اونو اول اجرا میکنه. مثلا یه checkbox ای که هنوز visible نیست تو صفحه رو میندازه تو اولویت آخر قاعدتا که خیلی performance رو بهبود میده.@Composable&#xA;fun ButtonRow() {&#xA;    MyFancyNavigation {&#xA;        StartScreen()&#xA;        MiddleScreen()&#xA;        EndScreen()&#xA;    }&#xA;}یه مثال دیگه: فرض کنید compose اول StartScreen اجرا کرد اما وسط اجرا کردنش یهو میبینه که این فانکشن وابسته است(side effect) به MiddleScreen، اون وقت compose قاعدتا میره و MiddleScreen رو اجرا میکنه.فانکشن های composable می تونن بصورت موازی اجرا بشنکامپوز میتونه recomposition رو با اجرا کردن composable ها بصورت موازی بهینه کنه. تو این مورد، compose میاد از core های CPU دستگاه کاربر استفاده میکنه و composable ها رو بصورت موزای با استخری از background thread اجرا میکنه.و برای صدمین بار نباید تو composable فانکشن ها side effect داشته باشیم چون چیزایی که مربوط به UI هستش رو حتما باید توی UI thread اجرا بشه اگه تو background thread اجرا بشه برنامتون crash میکنه.تا اونجایی که ممکنه، recomposition پَرِش میکنهکامپوز فقط اون بخش هایی رو recompose میکنه که state شون(آرگومان هاشون) عوض شده باشه. مثلا اگه یه state یه button تغییر کنه فقط اون recompose میشه، دیگه کاری به قبل و بعدش نداره. برای مثال:/**&#xA; * Display a list of names the user can click with a header&#xA; */&#xA;@Composable&#xA;fun NamePicker(&#xA;    header: String,&#xA;    names: List&lt;String&gt;,&#xA;    onNameClicked: (String) -&gt; Unit&#xA;) {&#xA;    Column {&#xA;        // this will recompose when [header] changes, but not when [names] changes&#xA;        Text(header, style = MaterialTheme.typography.bodyLarge)&#xA;        Divider()&#xA;&#xA;        // LazyColumn is the Compose version of a RecyclerView.&#xA;        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.&#xA;        LazyColumn {&#xA;            items(names) { name -&gt;&#xA;                // When an item&#39;s [name] updates, the adapter for that item&#xA;                // will recompose. This will not recompose when [header] changes&#xA;                NamePickerItem(name, onNameClicked)&#xA;            }&#xA;        }&#xA;    }&#xA;}&#xA;&#xA;/**&#xA; * Display a single name the user can click.&#xA; */&#xA;@Composable&#xA;private fun NamePickerItem(name: String, onClicked: (String) -&gt; Unit) {&#xA;    Text(name, Modifier.clickable( = { onClicked(name) }))&#xA;}هرکدوم از اینا ممکنه skip بشه توسط کامپوز اگه ورودی شون تغییر نکنه.ریکامپوزیشن خوش بینانه استوقتی پارامتر یه composable تغییر کرد، compose اونو recomose میکنه. این خوش بینانه بودنش یعنی compose انتظار داره قبل از اینکه پارامتر دیگه ای تغییر کنه، recomposition رو انجام بده؛ اگه قبل از تموم شدن recomposition یه پارامتر از یه composable تغییر کنه، اونوقت compose میاد recomposition رو متوقف میکنه و recomposition رو دوباره شروع میکنه با پارامتر جدیده.اینجا هم اگه side effect ای داشته باشین، مشکل براتون پیش میاد. مثلا، فرض کنین که تو پروسه recomposition هستیم و یه composable یه side effect داره، حالا اگه compose بیاد recomposition رو متوقف کنه اون side effect هنوز داره اجرا میشه و فاجعه رخ میده.فانکشن های composable می تونن بصورت مکرر اجرا بشنگفتم که مثلا یه فانکشنی برای اجرای هر فریم از  یه انیمیشن دارید، به تعداد فریم ها، همونقدر هم فانکنش composable صدا زده میشه و اگه وظیفه سنگینی مثل خوندن از storage رو به این فانکشن واگذار کنین، احتمالا به خطای not responding بَر بخورید.این قسمت اول بود. امیدوارم خوشتون اومده باشه. اشکالات، ابهامات و نظراتتون رو تو کامنت ها به اشتراک بذارین.کانال: Android Corner ایمیل: cornerdroid@gmail.com</description><category>Android Corner</category><author>Android Corner</author><pubDate>Wed, 30 Aug 2023 18:56:36 +0330</pubDate></item><item><title>آموزش Kotlin Multiplatform(قسمت سوم)</title><link>https://virgool.ir/@cornerdroid/آموزش-kotlin-multiplatformقسمت-سوم-a6q06pihggcm</link><description>فصل دو، قسمت اول:ما قراره تو این فصل رابط کاربری اپ اندرویدی مون رو با jetpack compose توسعه بدیم. کامپوز declarative هستش، کد خیلی کمتری نسبت به UI سیستم قبلی اندروید میخواد برای پیاده سازی رابط کاربری، منعطفه و ...نکته: این کتاب بصورت سطحی درباره jetpack compose حرف زده و چون ما یه بخش مجزا برای compose داریم، خیلی روی اینا مانُور نمیدیم و سریع میریم سراغ پیاده سازی UI.اگه پروژه تون رو آماده کردید، برید MainActivity رو باز کنید، بجای setContentView که توش layout xml مون رو inflate میکردیم، setContent قرار گرفته؛ setContent یه extension فانکشن روی کلاس ComponentActivity. این متد قاعدتا composable هستش چون UI مون رو توش قرار میدیم.اپ Time finderخب ما قراره یه اپی رو توسعه بدیم که کاربرا بتونن چندین منطقه زمانی(time zone) رو انتخاب کنن و یه سری زمان های مناسبی رو برای مثلا جلسه پیدا کنن. این صفحه اول اپ مونه:میبینین که تو هر آیتم ما منطقه زمانی محلی، زمان و تاریخ رو داریم و هر کدوم از کاربر ها location خاصی دارن یکی لندنه یکی دیگه لس آنجلس و.. کاربر سعی میکنه تا ساعت مناسبی برای جلسه پیدا کنه تا همه با اون زمان جور بشن.وقتی کاربر بخواد یه منطقه زمانی دیگه رو اضافه کنه، باید رو floating action button کلیک کنه و یه dialog براش ظاهر میشه برای انتخاب هر منطقه زمانی ای که میخواد:صفحه بعدی صفحه جستجو هستش که کاربر میتونه یه ساعت شروع و پایان بذاره برای جلسشون و...:وقت روی دکمه search کلیک کرد یه همچین چیزی میاد:تِم برنامهشما قبلا برای تعریف theme برنامه تون از فایل نین و نینی استفاده میکردید، ولی توی compose میتونین از کلاس MaterialTheme استفاده کنید برای رنگ، فونت و شکل ها.خب، توی ماژول androidApp کنار MainActivity یه پکیج بنام theme بسازید و توش هم یه فایلی بنام Colors.kt درست کنید و اینا رو توش قرار بدید:import androidx.compose.ui.graphics.Color &#xA;&#xA;val primaryColor = Color(0xFF1e88e5) &#xA;val primaryLightColor = Color(0xFF6ab7ff) &#xA;val primaryDarkColor = Color(0xFF005cb2) &#xA;val secondaryColor = Color(0xFF26a69a) &#xA;val secondaryLightColor = Color(0xFF64d8cb) &#xA;val secondaryDarkColor = Color(0xFF00766c) &#xA;val primaryTextColor = Color(0xFF000000) &#xA;val secondaryTextColor = Color(0xFF000000)اینا رنگ های اولیه(primary) و ثانویه(secondary) برنامه تون هستش.خب یه فایل دیگه توی همین پکیج بنام Typography.kt بسازید و اینا رو توش قرار بدید:import androidx.compose.material.Typography&#xA;import androidx.compose.ui.graphics.Color&#xA;import androidx.compose.ui.text.TextStyle&#xA;import androidx.compose.ui.text.font.FontFamily&#xA;import androidx.compose.ui.text.font.FontWeight&#xA;import androidx.compose.ui.unit.sp&#xA;// 1 &#xA;val typography = Typography(&#xA;  // 2 &#xA;  h1 = TextStyle(&#xA;    // 3 &#xA;    fontFamily = FontFamily.SansSerif,&#xA;    // 4 &#xA;    fontSize = 24. sp,&#xA;    // 5 &#xA;    fontWeight = FontWeight.Bold,&#xA;    // 6 &#xA;    color = Color.White&#xA;  ),&#xA;  h2 = TextStyle(&#xA;    fontFamily = FontFamily.SansSerif,&#xA;    fontSize = 20. sp,&#xA;    color = Color.White&#xA;  ),&#xA;  h3 = TextStyle(&#xA;    fontFamily = FontFamily.SansSerif,&#xA;    fontSize = 12. sp,&#xA;    color = Color.White&#xA;  ),&#xA;  h4 = TextStyle(&#xA;    fontFamily = FontFamily.SansSerif,&#xA;    fontSize = 10. sp,&#xA;    color = Color.White&#xA;  )&#xA;)1- متغیر typography یه instance هستش از کلاس تایپوگرافی کامپوز.2- این h1 از پیش تعریف شده رو override میکنیم3- فونت family رو مشخص میکنیم که اینجا  فونت مون از خانواده SansSerif هستش.4- اندازه فونت5- وزن(weight) فونت6- اینم رنگ فونتهشما میتونین ویژگی های دیگه مثل فاصله بین حروف(letter spacing) و ... رو برای TextStyle تنظیم کنین.خب یه فایل دیگه تو این پکیج ایجاد کنید به اسن AppTheme.kt و اینا رو اضافه کنید:import androidx.compose.foundation.isSystemInDarkTheme&#xA;import androidx.compose.material.MaterialTheme&#xA;import androidx.compose.material.darkColors&#xA;import androidx.compose.material.lightColors&#xA;import androidx.compose.runtime.Composable&#xA;import androidx.compose.ui.graphics.Color&#xA;@Composable&#xA;fun AppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content:&#xA;  @Composable() -&gt; Unit) {&#xA;  // TODO: Add Colors &#xA;}این فانکشن یه پارامتر اختیاری داره برای تنظیم کردن حالت dark و چک میکنه آیا سیستم تو حالت dark هست یا خیر. پارامتر دومی هم یه composable فانکشن برای نشون دادن تم. خب بیاید رنگ هارو برای تم روشن و تیره تنظیم کنیم:val colors =&#xA;  if (darkTheme) {&#xA;    darkColors()&#xA;      .copy(&#xA;        primary = primaryDarkColor,&#xA;        primaryVariant = primaryLightColor,&#xA;        secondary = secondaryDarkColor,&#xA;        secondaryVariant = secondaryLightColor,&#xA;        onPrimary = Color.White,&#xA;      )&#xA;  } else {&#xA;    lightColors()&#xA;      .copy(&#xA;        primary = primaryColor,&#xA;        primaryVariant = primaryLightColor,&#xA;        secondary = secondaryColor,&#xA;        secondaryVariant = secondaryLightColor,&#xA;        onPrimary = Color.Black,&#xA;      )&#xA;  }&#xA;// TODO: Add Themeفانکشن های lightColors و darkColors یه کلاس Colors برگشت میده. این کد ها رو با// TODO: Add Themeعوض کنید:MaterialTheme(&#xA;  colors = colors,&#xA;  typography = typography,&#xA;  content = content&#xA;)این MaterialTheme رو اعمال میکنه به پروژه با رنگ ها و تایپوگرافی ای که خودمون تنظیم کرده بودیمتایپ هاچندتا تایپ لازم داریم که بعدا توی پروژه ازش استفاده میکنیم. برید توی پوشه UI و یه فایل به اسم یتیمی Types.kt و اینا رو قرار بدید:import androidx.compose.runtime.Composable&#xA;// 1 &#xA;typealias OnAddType = (List &lt; String &gt; ) -&gt; Unit&#xA;// 2 &#xA;typealias onDismissType = () -&gt; Unit&#xA;// 3 &#xA;typealias composeFun = @Composable() -&gt; Unit&#xA;// 4 &#xA;typealias topBarFun = @Composable(Int) -&gt; Unit&#xA;// 5 &#xA;@Composable&#xA;fun emptyComposable() {}نکته: توی بهتر بنویسیم بعدی typealias رو توضیح میدم.1- یه نام مستعار به نام onaddType تعریف میکنه که لیستی از رشته ها رو به شما میده و چیزی هم برنمیگردونه.2- نام مستعار برای بستن dialog3- یه composable فاکنشن رو تعریف میکنه4- یه فانکشن که یه integer میگیره5- یه composable خالیصفحه اصلیتوی ماژول androidApp توی پوشه UI یه فایل به اسم MainView.kt بسازید. با ساختن یه سری متغیر و کلاس های helper شروع میکنیم. اول این import ها رو به فایل اضافه کنید(برای ذخیره وقت):import androidx.compose.foundation.layout.padding &#xA;import androidx.compose.material.BottomNavigation &#xA;import androidx.compose.material.BottomNavigationItem &#xA;import androidx.compose.material.FloatingActionButton &#xA;import androidx.compose.material.Icon &#xA;import androidx.compose.material.Scaffold &#xA;import androidx.compose.material.icons.Icons &#xA;import androidx.compose.material.icons.filled.Add &#xA;import androidx.compose.material.icons.filled.Language &#xA;import androidx.compose.material.icons.filled.Place&#xA;import androidx.compose.runtime.Composable &#xA;import androidx.compose.runtime.mutableStateOf &#xA;import androidx.compose.runtime.remember &#xA;import androidx.compose.runtime.snapshots.SnapshotStateList &#xA;import androidx.compose.ui.Modifier &#xA;import androidx.compose.ui.graphics.vector.ImageVector &#xA;import androidx.compose.ui.unit.dp &#xA;import com.raywenderlich.findtime.android.theme.AppThemeبرای پیگیری دو screen تون،یه sealed کلاس بنام Screen ایجاد کنید:sealed class Screen(val title: String) {&#xA;  object TimeZonesScreen: Screen(&#34;Timezones&#34;)&#xA;  object FindTimeScreen: Screen(&#34;Find Time&#34;)&#xA;}توش دوتا صفحه TimeZonesScreen و FindTimeScreen تعریف کردیم بهمراه title شون. قدم بعدی تعریف یه کلاس برای handle کردن آیتم bottom navigation مون:data class BottomNavigationItem(&#xA;  val route: String,&#xA;  val icon: ImageVector,&#xA;  val iconContentDescription: String&#xA;)یه مسیر یا route(برای navigate کردن بین صفحه ها بعدا باهاش آشنا میشید)، یه آیکون و محتوای توضیحات داره. قدم بعدی ساختن یه متغیر با دوتا آیتم هستش برای bottom navigation:val bottomNavigationItems = listOf(&#xA;  BottomNavigationItem(&#xA;    Screen.TimeZonesScreen.title,&#xA;    Icons.Filled.Language,&#xA;    &#34;Timezones&#34;&#xA;  ),&#xA;  BottomNavigationItem(&#xA;    Screen.FindTimeScreen.title,&#xA;    Icons.Filled.Place,&#xA;    &#34;Find Time&#34;&#xA;  )&#xA;)ااز متریال آیکون و title screen ها استفاده میکنه. خب حالا یه composable بنام MainView بسازید:// 1 &#xA;@Composable&#xA;// 2 &#xA;fun MainView(actionBarFun: topBarFun = {&#xA;  emptyComposable()&#xA;}) {&#xA;  // 3 &#xA;  val showAddDialog = remember {&#xA;    mutableStateOf(false)&#xA;  }&#xA;  // 4 &#xA;  val currentTimezoneStrings = remember {&#xA;    SnapshotStateList &lt; String &gt;&#xA;      ()&#xA;  }&#xA;  // 5 &#xA;  val selectedIndex = remember {&#xA;    mutableStateOf(0)&#xA;  }&#xA;  // 6 &#xA;  AppTheme {&#xA;    // TODO: Add Scaffold &#xA;  }&#xA;}2- این فانکشن یه فانکشنی میگیره برای topbar (همون toolbar).3- اِستیت(وضعیت) نشون دادن dialog رو نگه میداره.4- استیتی که شامل یه لیستی از منطقه زمانی فعلی رو بصورت رشته نگه میداره.5- برای حفظ وضعیت index انتخاب شده از فانکشن ها remember و mutableStateOf استفاده میکنیم.6- از تمی که ساختیم استفاده میکنه.فک کنم برای این پست کافی باشه. میدونم برنامه این هفته یکم بهم خورده ولی چهارشنبه بجای null این فصل رو تموم میکنیم.و آخر اگه به باگی برخوردید، تو کامنت ها به اشتراک بزارین تا حلش کنیم.کانال Android Corner</description><category>Android Corner</category><author>Android Corner</author><pubDate>Wed, 30 Aug 2023 18:42:10 +0330</pubDate></item><item><title>اسکوپ فانکشن های کاتلین</title><link>https://virgool.ir/@cornerdroid/اسکوپ-فانکشن-های-کاتلین-knqavn01kmk4</link><description>اگه یادتون باشه، ما توی ماژول buildSrc از scope فانکشن with استفاده کردیم. امروز میخوایم scope function ها رو بررسی کنیم و ببینم چطور با استفاده از اینا به کدی تمیز میرسیم.کتابخونه استاندارد Kotlin پنج تا فانکشن داره که بهشون میگن Scope function که هدفشون ایجاد یه محدوده از یه object هستش. وقتی که اینا رو روی یه آبجکت call کنین با lambda، اینا یه محدوده موقت از اون آبجِکته رو براتون میسازن که دیگه شما میتونین بدون نام اون به شیء دسترسی پیدا کنین. این پنج تا فانکشن: let، run، with، apply و also اینا هستن.اصولاً ، این فانکشن ها یه کاری رو انجام میدن: یه بلاک کد رو رو یه شی اجرا میکنن، ولی تفاوتشون اینه که چطوری شیء رو در داخل بلوک(block) در دسترس قرار می دن و نتیجه expression چیه.ببینید scope fun ها درسته که کد شما رو میتونن مختصر تر کنن، ولی نباید بیش از حد از اینا استفاده کنیم، این میتونه خوندن کد شما رو سخت کنه و منجر به خطا شه. همچنین توصیه میشه از این توابع بصورت تو در تو استفاده نکنید و هنگام زنجیر زدن شون مراقب باشید چون به راحتی می میتونید در مورد context object فعلی شون و مقادیر it و this اشتباه بگیرید.قدم اول فرق شون رو بررسی میکنیم و بعد با مثال، نحوه استفاده شون رو میبینیم.تفاوت هااز اونجاییکه scope فانکشن ها از نظر ماهیت شبیه هم هستن، درک تفاوت بینشون مهمه. دو تفاوت اصلی بین هر scope فانکشن وجود داره:1- نحوه ای که اونا به context object اشاره میکنن.2- مقدار برگشتی شون.معنی context object: همون آبجکتی که scope فانکشن رو روش فراخوانی میکنیم.یک: context objectتوی لامبدایِ scope فانکشنمون، بجای اینکه اسم context object مون رو هی تکرار کنیم میایم از it و this استفاده میکنیم؛ حالا هر scope فانکشنی نحوه ارجاع دادنشون به scope فانکشن متفاوته:  یه lambda receiver  از this و یه lambda argument از it استفاده میکنه.اولی: thisاسکوپ فانکش های run، with و apply  کانتکس آبجکتشون رو بعنوان یه lambda receiver ارجاع میدن با واژه this.داشتن context object بعنوان یه receiver (this) برای lambda هایی پیشنهاد میشه که عموما با member های آبجکت سروکار دارن.val adam = Person(&#34;Adam&#34;).apply { &#xA;    age = 20                       // same as this.age = 20&#xA;    city = &#34;London&#34;&#xA;}&#xA;println(adam)دومی: itاسکوپ فانکش های let و also کانتکس آبجکتشون رو بعنوان یه lambda receiver ارجاع میدن با واژه it.این مثل this نیست که صریحا به مثلا property های context object مون دسترسی داشته باشیم. ولی بهتره وقتی که از شیء بیشتر به عنوان آرگومان در تماس های تابع استفاده کردیم. همچنین این بهتره وقتی که شما از چندین متغیر در بلوک کد تون استفاده می کنید.fun getRandomInt(): Int {&#xA;    return Random.nextInt(100).also {&#xA;        writeToLog(&#34;getRandomInt() generated value $it&#34;)&#xA;    }&#xA;}&#xA;&#xA;val i = getRandomInt()&#xA;println(i)دو: مقدار برگشتییک: apply و also خود &#34;context object&#34; رو برمیگردونن.دو: let، run و with &#34;نتیجه lamda&#34; رو بر میگردونن.** کانتکست آبجکت(context object)چون مقدار برگشتی Apply و also خود خود context object هستش. میتونید اونها رو توی زنجیره فراخوانی(call chain) صدا بزنید: می توانید یکی پس از دیگری تماس های عملکرد زنجیره ای را در همان شیء ادامه دهید.Codeببینید، اول ما یه لیست خالی داریم و پاسش میدیم به also و یه message پرینت میکنه و لیست رو پاس میده به apply بعنوان context؛ توی apply به سری اعدا به لیست اضافه میشن و بعد apply این لیست تغییر یافته رو return میکنه و....** نتیجه برگشتیاسکوپ فانکنش های let، run و with نتیجه lambda رو برمیگردونن. میتونید هنگام اختصاص نتیجه به یه متغیر و .. از اینا استفاده کنید.val countEndsWithE = numbers.run { &#xA;    add(&#34;four&#34;)&#xA;    add(&#34;five&#34;)&#xA;    count { it.endsWith(&#34;e&#34;) }&#xA;}&#xA;println(&#34;There are $countEndsWithE elements that end with e.&#34;)میبینین که خط آخر رو برمیگردونه.میتونیم از متد های (مثلا یه لیست) استفاده کنیم و یه temporary scope بسازیم.معنیtemporary scope: مثلا اینجا متد های first و last رو ریختیم تو دوتا متغیر، این متد ها بعد از اجرا شدن این scope function از بین میرن.val numbers = mutableListOf(&#34;one&#34;, &#34;two&#34;, &#34;three&#34;)&#xA;val countEndsWithE = numbers.run { &#xA;    add(&#34;four&#34;)&#xA;    add(&#34;five&#34;)&#xA;    count { it.endsWith(&#34;e&#34;) }&#xA;}&#xA;println(&#34;There are $countEndsWithE elements that end with e.&#34;)کاربرد هاشوناین بخش برای انتخاب مناسب هر scope فانکشن در جای مناسبشه.فانکشن letدسترسیِ context object رو با it بهمون میده.نتیجه لامبدا رو برمیگردونه.1- میتونیم از let برای استفاده کردن یک یا چند فانکشن تو یه زنجیره فراخوانی استفاده کنیم.بدون let اینجوریه:val numbers = mutableListOf(&#34;one&#34;, &#34;two&#34;, &#34;three&#34;, &#34;four&#34;, &#34;five&#34;)&#xA;val resultList = numbers.map { it.length }.filter { it &gt; 3 }&#xA;println(resultList)ببینید اینجا اول ما یه لیست string رو اومدیم map کردیم بعد filter و بعد تویه یه متغیر قرار دادیم تا بعدا استفاده کنیم ازش.با let هم اینجوری:val numbers = mutableListOf(&#34;one&#34;, &#34;two&#34;, &#34;three&#34;, &#34;four&#34;, &#34;five&#34;)&#xA;numbers.map { it.length }.filter { it &gt; 3 }.let { &#xA;    println(it)&#xA;    // and more function calls if needed&#xA;}ولی وقتی از let استفاده میکنیم دیگه لازم نیست این عملیات رو بریزیم تو یه متغیر. اینجا هم اول ما یه لیست string رو اومدیم map کردیم بعد filter کردیم و هرچی که انجام داده بودیم رو لیست رو ریختیم توی let بجای اینکه بریزیم توی یه متغیر، الان توی let هر چندتا fun ای که میخوایم رو صدا میزنیم و استفاده میکنیم.2- اگه یه تَک فانکش داشتیم میتونیم از &#34;::&#34; استفاده کنیم، اینجوری:val numbers = mutableListOf(&#34;one&#34;, &#34;two&#34;, &#34;three&#34;, &#34;four&#34;, &#34;five&#34;)&#xA;numbers.map { it.length }.filter { it &gt; 3 }.let(::println)3- با let میتونیم یه محدوده(scope) ای بسازیم که تو اون مقدار های non-null قرار بگیره.(این تو بهتر بنویسم قبلی بود):val str: String? = &#34;Hello&#34;   &#xA;//processNonNullString(str)       // compilation error: str can be null&#xA;val length = str?.let { &#xA;    println(&#34;let() called on $it&#34;)        &#xA;    processNonNullString(it)      // OK: &#39;it&#39; is not null inside &#39;?.let { }&#39;&#xA;    it.length&#xA;}4- میتونیم بجا اینکه بصورت زنجیروار مثلا اول .first() و context object رو بهمون بده و دنبالش تو let استفاده کنیم. میتونیم بجاش اینجوری کدمون رو ساده تر کنیم:val numbers = listOf(&#34;one&#34;, &#34;two&#34;, &#34;three&#34;, &#34;four&#34;)&#xA;val modifiedFirstItem = numbers.first().let { firstItem -&gt;&#xA;    println(&#34;The first item of the list is &#39;$firstItem&#39;&#34;)&#xA;    if (firstItem.length &gt;= 5) firstItem else &#34;!&#34; + firstItem + &#34;!&#34;&#xA;}.uppercase()&#xA;println(&#34;First item after modifications: &#39;$modifiedFirstItem&#39;&#34;)فانکشن withدسترسیِ context object رو با this بهمون میدهنتیجه لامبدا رو برمیگردونه.1- وقتی که نیازی به استفاده از نتیجه برگشتی ندارین، از with استفاده کنید:val numbers = mutableListOf(&#34;one&#34;, &#34;two&#34;, &#34;three&#34;)&#xA;with(numbers) {&#xA;    println(&#34;&#39;with&#39; is called with argument $this&#34;)&#xA;    println(&#34;It contains $size elements&#34;)&#xA;}2- میتونید بعنوان یه helper هم ازش استفاده کنید اینطوری که یه سری محاسبات انجام بدید و بعدا حالا ازش استفاده کنید.val numbers = mutableListOf(&#34;one&#34;, &#34;two&#34;, &#34;three&#34;)&#xA;val firstAndLast = with(numbers) {&#xA;    &#34;The first element is ${first()},&#34; +&#xA;    &#34; the last element is ${last()}&#34;&#xA;}&#xA;println(firstAndLast)فانکشن runدسترسیِ context object رو با this بهمون میده.نتیجه لامبدا رو برمیگردونه.این فانکشن مثل with با این تفاوت که run یه extension فانکشنه.1- وقتی که میخواین هم object ها رو initialize کنین و هم میخواین نتیجه برگشتی رو محاسبه کنید از run استفاده کنید.val service = MultiportService(&#34;https://example.kotlinlang.org&#34;, 80)&#xA;&#xA;val result = service.run {&#xA;    port = 8080&#xA;    query(prepareRequest() + &#34; to port $port&#34;)&#xA;}&#xA;&#xA;// the same code written with let() function:&#xA;val letResult = service.let {&#xA;    it.port = 8080&#xA;    it.query(it.prepareRequest() + &#34; to port ${it.port}&#34;)&#xA;}2- میتونیم run رو روی یه آبجکت هم call نکنیم و بعنوان non-extension function ازش استفاده کنیم و همچنان نتیجه lambda رو برمیگردونه. وقتی که میخواین چندین statement رو call کنید تا اجرا بشه تو یه بلاک از استفاده بکنید:val hexNumberRegex = run {&#xA;    val digits = &#34;0-9&#34;&#xA;    val hexDigits = &#34;A-Fa-f&#34;&#xA;    val sign = &#34;+-&#34;&#xA;&#xA;    Regex(&#34;[$sign]?[$digits$hexDigits]+&#34;)&#xA;}&#xA;&#xA;for (match in hexNumberRegex.findAll(&#34;+123 -FFFF !%*&amp; 88 XYZ&#34;)) {&#xA;    println(match.value)&#xA;}اینجا ما regex رو initialize کردیم و بعنوان نتیجه برگردوندیم.فانکشن applyدسترسیِ context object رو با this بهمون میده.خود آبجکت رو برمیگردونه- هر وقت خواستین که یه سری چیزها رو به خود آبجکت نسبت بدین از apply استفاده کنید:val adam = Person(&#34;Adam&#34;).apply {&#xA;    age = 32&#xA;    city = &#34;London&#34;        &#xA;}&#xA;println(adam)فانکشن alsoدسترسیِ context object رو با it بهمون میده.خود آبجکت رو برمیگردونه.1- هر وقت خواستین یه سری عملیات رو روی خود آبجکتتون انجام بدین از also استفاده کنید.val numbers = mutableListOf(&#34;one&#34;, &#34;two&#34;, &#34;three&#34;)&#xA;numbers&#xA;    .also { println(&#34;The list elements before adding new one: $it&#34;) }&#xA;    .add(&#34;four&#34;)خب امیدوارم خوشتون اومده باشه. اگه نظری، پیشنهادی، باگی چیزی بود لطفا تو کامنت ها به اشتراک بذارین.کانال Android Corner</description><category>Android Corner</category><author>Android Corner</author><pubDate>Wed, 30 Aug 2023 10:15:15 +0330</pubDate></item><item><title>آموزش Kotlin Multiplatform(قسمت دوم)</title><link>https://virgool.ir/@cornerdroid/کاتلین-و-چندین-پلتفرمقسمت-دوم-stuf6ta2wpb4</link><description>مقدمه و قسمت اول رو میتونین تو کانال Android Corner ببینید.ساخت BuildSrcیکی از ویژگی های خوب نسخه های جدید Gradle، مفهوم BuildSrc هستش. یه ماژوله که می تونید توش ورژن های variable و پلاگینا تون رو تعریف کنید. برای ساختن این ماژول، باید توی پوشه روت پروژه و New ▸ Directory رو بزنید و اسم پوشه رو هم بزارید buildSrc. از اونجایی که این یه ماژوله، نیاز به یه فایل build داره --&gt; پس توش یه فایل بنام build.gradle.kts (دقت کنید پسوندش.kts باشه) و کد زیر رو توش اضافه کنید.repositories {&#xA;  mavenCentral()&#xA;}&#xA;plugins {&#xA;  kotlin - dsl&#xA;}خب حالا gradle رو sync کنید. بعد دوباره کلیک-راست کنید روی buildSrc و src/main/kotlin رو انتخاب کنید. حالا توی پوشه kotlin، یه فایل بنام Dependencies بسازید. تو این فایل variable هایی رو تعریف میکنیم که plugin ها و dependency هامون رو مشخص میکنن. این فایل رو ایجاد میکنیم تا همه ورژن های dependency و plugin ها یکجا باشن و مدیریت شون راحت بشه.همه پلاگینا رو با این کد تعریف کنید:const val androidPlugin = &#34;android&#34;&#xA;const val androidApp = &#34;com.android.application&#34;&#xA;const val androidLib = &#34;com.android.library&#34;&#xA;const val multiplatform = &#34;multiplatform&#34;&#xA;const val composePlugin = &#34;org.jetbrains.compose&#34;&#xA;const val cocopods = &#34;native.cocoapods&#34;قدم بعد اضافه کردن ورژن هاست:object Versions {&#xA;  // 1&#xA;  const val min_sdk = 24&#xA;  const val target_sdk = 31&#xA;  const val compile_sdk = 31&#xA;  // 2&#xA;  // Plugins&#xA;  const val kotlin = &#34;1.6.10&#34;&#xA;  const val kotlin_gradle_plugin = &#34;1.6.10&#34;&#xA;  const val android_gradle_plugin = &#34;7.0.4&#34;&#xA;  const val desktop_compose_plugin = &#34;1.0.1&#34;&#xA;  const val compose_compiler_version = &#34;1.1.0-rc02&#34;&#xA;  const val compose_version = &#34;1.1.0-rc01&#34;&#xA;  // TODO: Add Other versions&#xA;}&#xA;// TODO: Add Deps1- اینا ورژن های sdk ی اندروید رو مشخص میکنن و برای تعداد لایبرری هم حداقل(min) SDK باید 24 باشه.2- ورژن های پلاگین تونه.یه سری ورژن های لایبرری های دیگه هم مونده اونا رو اضافه می کنیم. بجای// TODO: Add Other versionsبنویسید:const val coroutines = &#34;1.5.0-native-mt&#34;&#xA;const val junit = &#34;4.13.2&#34;&#xA;const val material = &#34;1.4.0&#34;&#xA;const val kotlinxDateTime = &#34;0.3.1&#34;&#xA;const val activity_compose = &#34;1.4.0&#34;&#xA;const val napier = &#34;2.1.0&#34;&#xA;const val junit5 = &#34;1.5.10&#34;&#xA;const val frameworkName = &#34;shared&#34;و بجای// TODO: Add Depsاینو قرار بدید:object Deps {&#xA;  const val android_gradle_plugin =&#xA;    &#34;com.android.tools.build:gradle:${Versions.android_gradle_plugin}&#34;&#xA;  const val kotlin_gradle_plugin = &#34;org.jetbrains.kotlin:kotlin-&#xA;  gradle - plugin: $ {  Versions.kotlin_gradle_plugin}  &#34;&#xA;&#xA;  const val junit = &#34;junit:junit:${Versions.junit}&#34;&#xA;  const val material =&#xA; &#34;com.google.android.material:material:${Versions.material}&#34;&#xA;  const val napier = &#34;io.github.aakira:napier:${Versions.napier}&#34;&#xA;  // TODO: Add Compose&#xA;}این variable ها برای پلاگین ها و چندتا لایبرری هستش.قدم بعد اضافه کردن کتابخونه های compose. بجای// TODO: Add Composeاینو بنویسید:object Compose {&#xA;  const val ui =&#xA;    &#34;androidx.compose.ui:ui:${Versions.compose_version}&#34;&#xA;  const val uiUtil = &#34;androidx.compose.ui:ui-&#xA;  util: $ {&#xA;    Versions.compose_version&#xA;  }&#xA;  &#34;&#xA;  const val tooling = &#34;androidx.compose.ui:ui-&#xA;  tooling: $ {&#xA;    Versions.compose_version&#xA;  }&#xA;  &#34;&#xA;  const val foundation =&#xA;    &#34;androidx.compose.foundation:foundation:${Versions.compose_version}&#34;&#xA;  const val material =&#xA;    &#34;androidx.compose.material:material:${Versions.compose_version}&#34;&#xA;  const val material_icons = &#34;androidx.compose.material:material-&#xA;  icons - extended: $ {  Versions.compose_version }&#34;&#xA;  const val runtime =&#xA;    &#34;androidx.compose.runtime:runtime:${Versions.compose_version}&#34;&#xA;  const val compiler =&#xA;    &#34;androidx.compose.compiler:compiler:${Versions.compose_version}&#34;&#xA;  const val runtime_livedata = &#34;androidx.compose.runtime:runtime-&#xA;  livedata: $ {  Versions.compose_version  }  &#34;&#xA;  const val foundationLayout =&#xA;    &#34;androidx.compose.foundation:foundation-&#xA;  layout: $ {   Versions.compose_version  }  &#34;&#xA;  const val activity = &#34;androidx.activity:activity-compose: $ { Versions.activity_compose }  &#34;&#xA;}&#xA;// TODO: Add Coroutinesحالا یه سری کتابخونه برای Coroutine رو اضافه میکنیم، بجای :// TODO: Add Coroutinesobject Coroutines {&#xA;  const val common = &#34;org.jetbrains.kotlinx:kotlinx-coroutines-&#xA;  core: $ {Versions.coroutines }&#34;&#xA;  const val android = &#34;org.jetbrains.kotlinx:kotlinx-coroutines-&#xA;  android: $ {  Versions.coroutines}  &#34;&#xA;  const val test = &#34;org.jetbrains.kotlinx:kotlinx-coroutines-&#xA;  test: $ {Versions.coroutines  }  &#34;&#xA;}&#xA;// TODO: Add JetBrainsحالا باید تعدادی کتابخونه jetbrains برای dataTime و desktop compose رو بجای// TODO: Add JetBrainsاضافه کنیم:object JetBrains {&#xA;  const val datetime = &#34;org.jetbrains.kotlinx:kotlinx-&#xA;  datetime: $ {    Versions.kotlinxDateTim  }&#34;&#xA;  const val uiDesktop = &#34;org.jetbrains.compose.ui:ui-&#xA;  desktop: $ {  Versions.desktop_compose_plugin  }  &#34;&#xA;  const val uiUtil = &#34;org.jetbrains.compose.ui:ui-&#xA;  util: $ { Versions.desktop_compose_plugin}  &#34;&#xA;}حالا میتونید پروژه sync کنید.فایل build ماژول sharedفایل build.gradle.kts رو باز کنید و اینو جایگزین پلاگین ها بکنید:kotlin(multiplatform)&#xA;id(androidLib)&#xA;kotlin(cocopods)الان gradle بصورت خودکار(اتوماتیک) این ثابت(constant) هایی که تعریف کردیم تو buildSrc رو میخونه.تو بخش اندروید هم اینو جایگزین کنید:android {&#xA;  compileSdk = Versions.compile_sdk&#xA;&#xA;  sourceSets[&#34;main&#34;].manifest.srcFile(&#34;src/androidMain/AndroidManifes&#xA;    t.xml &#34;)&#xA;    defaultConfig {&#xA;      minSdk = Versions.min_sdk&#xA;      targetSdk = Versions.target_sdk&#xA;    }&#xA;  }ورژن ها رو جایگزین کردیم با اونچه که تو buildSrc تعریف کرده بودیم.فایل build.gradle.kts رو توی root باز کنید و اینو جایگزین string های classpath کنید.classpath(Deps.android_gradle_plugin)&#xA;classpath(Deps.kotlin_gradle_plugin)حالا gradle رو sync کنید تا مطمئن شیم که کار میکنه.فایل build برای ماژول اندرویدفایل androidApp -&gt; build.gradle.kts رو باز کنید و جایگزین کنید اینو بجای بخش plugin:plugins {&#xA;  id(androidApp)&#xA;  kotlin(androidPlugin)&#xA;}بخش dependencies رو با این جایگیزین کنید:dependencies {&#xA;  // 1&#xA;  implementation(project(&#34;:shared&#34;))&#xA;  // 2&#xA;  with(Deps) {&#xA;    implementation(napier)&#xA;    implementation(material)&#xA;  }&#xA;  // 3&#xA;  //Compose&#xA;  with(Deps.Compose) {&#xA;    implementation(compiler)&#xA;    implementation(runtime)&#xA;    implementation(runtime_livedata)&#xA;    implementation(ui)&#xA;    implementation(tooling)&#xA;    implementation(foundation)&#xA;    implementation(foundationLayout)&#xA;    implementation(material)&#xA;    implementation(material_icons)&#xA;    implementation(activity)&#xA;  }&#xA;}1- این، ماژول shared رو import میکنه.2- این napier رو تعریف میکنه(یه لایبرری multiplatform واسه logging) و کتابخونه material. برای جلوگیری از تکرار کد و خوانایی از with استفاده کردیم(بزودی یه مقاله در مورد scope فانکشن ها تو کانال منتشر میکنم)3- اینا هم که کتابخونه های compose هستش.حالا بخش android رو با این عوض کنید:android {&#xA;  compileSdk = Versions.compile_sdk&#xA;  defaultConfig {&#xA;    applicationId = &#34;com.raywenderlich.findtime.android&#34;&#xA;    minSdk = Versions.min_sdk&#xA;    targetSdk = Versions.target_sdk&#xA;    versionCode = 1&#xA;    versionName = &#34;1.0&#34;&#xA;  }&#xA;  buildTypes {&#xA;    getByName(&#34;release&#34;) {&#xA;      isMinifyEnabled = false&#xA;    }&#xA;  }&#xA;  compileOptions {&#xA;    sourceCompatibility = JavaVersion.VERSION_1_8&#xA;    targetCompatibility = JavaVersion.VERSION_1_8&#xA;  }&#xA;  kotlinOptions {&#xA;    jvmTarget = &#34;1.8&#34;&#xA;  }&#xA;  buildFeatures {&#xA;    compose = true&#xA;  }&#xA;  composeOptions {&#xA;    kotlinCompilerExtensionVersion =&#xA;      Versions.compose_compiler_version&#xA;  }&#xA;}یه بار دیگه پروژه رو sync کنید و اپ رو اجرا کنید تا ببینید همه چیز درسته.شروع ساخت اپ Find Timeاین اپ چیکار میکنه: مثلا فرض کنید چندتا همکار تو ساعت های زمانی متفاوتی کار میکنن، حالا اگه بخوان یه جلسه برگزار کنن، باید بهترین تایم رو پیدا کنن تا با هم هماهنگ بشن. این اپ بهترین زمان ممکن رو پیدا میکنه.ممکنه که این پروژه بنظر ساده بیاد ولی .. کُلللیی چیز بهتون یاد میده که تو پروژه های multiplatform تون میتونین بعدا استفاده کنید.ساخت منطق بیزینس(business logic)یه مزیت که KMP داره اینه میتونیم business logic مون رو  یه بار بنویسیم تو ماژول shared و بین همه پلتفرم به اشتراک بزاریمش.برین توی ماژول shared توی androidMain ،commonMain و iosMain، کلاس Platform و Greeting رو پاک کنید و تو ماژول Android و iOS که از این کلاسا استفاده شده هم برین و پاک شون کنید.محاسبات Datetimeما از کتابخونه kotlinx-datetime استفاده میکنیم برای محاسبات bash. فایل shared/build.gradle.kts رو باز کنید، val commonMain رو پیدا کنید و این dependency ها رو اضافه کنید؛ اینجوری:val commonMain by getting {&#xA;  dependencies {&#xA;    // 1&#xA;    implementation(Deps.JetBrains.datetime)&#xA;    // 2&#xA;    implementation(Deps.napier)&#xA;  }&#xA;}و گریدل رو sync کنین.کتابخونه Datetimeلایبرری Kotlinx-DateTime یه کتابخونه مالتی پلتفرمه که کار کردن باهاش ساده است و کمک میکنه محاسبات براساس تاریخ/زمان رو انجام بدیم باهاش. از data type های مختلفی هم استفاده میکنه که:کلاس Instant به ما یه لحظه رو تو زمان نشون میده.کلاس Clock مثل یه ساعت هستش که لحظه فعلی رو به ما میگه.کلاس LocalDateTime به ما یه تاریخ و زمان نشون میده بدون زمان اون منطقه(اگه متوجه نشدید، بعدا  تو کد متوجه میشید!!)کلاس LocalDate به ما فقط یه تاریخ میده.کلاس TimeZone و ZoneOffset برای تبدیل کردن Instant و LocalDateTime به کار میره.کلاس DateTimePeriod فاصله زمانی بین دو لحظه رو بهمون میده.کلاس DatePeriod یه زیر کلاسِ DateTimePeriod هستش و تفاوت زمانی دو LocalDate رو بهمون میده.اینام کلاس(enum) Month نمایانگر ماه های ساله.اینام کلاس(enum) DayOfWeek نمایانگر روز های هفته.و  اینام کلاس DateTimeUnit یه سری واحد متداول برای محاسبات بهمون میده.کمک کننده منطقه زمانی(Time Zone Helper)قبل دست به کد شدن، برید اینجاا تمام سورس کد های کتاب داخل این ریپازیتوری هستش. ولی برای الان پروژه دوم یعنی02-getting-startedرو سعی کنید build کنید یا از پکیج name مشابه استفاده کنید چونکه این کتاب موقع توضیح دادن از پکیج name این پروژه استفاده کرده و شاید کمی گیج بشید.برید تو پکیجshared/commonMain/و یه interface بنام TimeZoneHelper بسازید:interface TimeZoneHelper {&#xA;  fun getTimeZoneStrings(): List &lt; String &gt;&#xA;    fun currentTime(): String&#xA;  fun currentTimeZone(): String&#xA;  fun hoursFromTimeZone(otherTimeZoneId: String): Double&#xA;  fun getTime(timezoneId: String): String&#xA;  fun getDate(timezoneId: String): String&#xA;  fun search(startHour: Int, endHour: Int, timezoneStrings:&#xA;    List &lt; String &gt; ): List &lt; Int &gt;&#xA;}هفت function به ترتیب:لیستی از رشته های منطقه زمانی رو برمی گردونه.زمان فرمت شده فعلی رو بهمون میده.شناسه(id) منطقه فعلی رو برمیگردونه.ساعت ها رو از منطقه زمانی معین رپرتاژ برامون  برمیگردونه.زمان فرمت شده رو برای یه منطقه زمانی مشخص برمیگردونه.تاریخ فرمت شده رو با توجه به اون منطقه زمانی مشخص برمیگردونه.لیست ساعت هایی که از Starthour شروع می شه و در Endhour به پایان میرسه رو جستجو میکنه.خب حالا باید یه کلاسی بنام TimeZoneHelperImpl رو بسازیم تا این interface رو implement کنه:class TimeZoneHelperImpl: TimeZoneHelper {&#xA;}متد هارو Implement کنید.خب بیاین با getTimeZoneStrings شروع کنیم. تو بدنه این متد، اینو قرار بدید:return TimeZone.availableZoneIds.sorted()این خط شناسه های(id) منطقه زمانی موجود رو برمی گردونه و اونها رو مرتب می کنه. اگه TimeZone قرمز شد، cursor موس تون رو روش قرار بدید و کلیک راست کنید و لایبرری رو import کنید.قدم بعد باید یه متد بنویسیم برای قالب بندی(فرمت) datetime کلاس LocalDateTime؛ این متد رو پایین کلاس(TimeZoneHelperImpl) قرار بدین:fun formatDateTime(dateTime: LocalDateTime): String {&#xA;  // 1&#xA;  val stringBuilder = StringBuilder()&#xA;  // 2&#xA;  var hour = dateTime.hour&#xA;  val minute = dateTime.minute&#xA;  var amPm = &#34; am&#34;&#xA;  // 3&#xA;  // For 12&#xA;  if (hour &gt; 12) {&#xA;    amPm = &#34; pm&#34;&#xA;    hour -= 12&#xA;  }&#xA;  // 4&#xA;  stringBuilder.append(hour.toString())&#xA;  stringBuilder.append(&#34;:&#34;)&#xA;  // 5&#xA;  if (minute &lt; 10) {&#xA;    stringBuilder.append(&#39;0&#39;)&#xA;  }&#xA;  stringBuilder.append(minute.toString())&#xA;  stringBuilder.append(amPm)&#xA;  // 6&#xA;  return stringBuilder.toString()&#xA;}1- از StringBuilder برای ساخت، تغییر و کلا مدیریت string ها استفاده میکنیم.2- فقط دوتا property رو از آرگمان این fun گرفتیم.3- اینم چک میکنه اگه ساعت(hour) از 12 بیشتر بود بیا pm نشون بده.4- رشته hour رو میسازه. خط بعدی هم اینو &#34; : &#34; اضافه میکنه به جلوی رشته hour.5- چک میکنه اگه دقیقه کمتر از 10 بود بیا یه صفر بزار اولش. و دقیقه رو میسازه و pm/am رو هم اخرش اضافه میکنه.6- رشته نهایی رو برمیگردونه.حالا بریم سراغ متد currentTime:override fun currentTime(): String {&#xA;  // 1&#xA;  val currentMoment: Instant = Clock.System.now()&#xA;  // 2&#xA;  val dateTime: LocalDateTime =&#xA;    currentMoment.toLocalDateTime(TimeZone.currentSystemDefault())&#xA;  // 3&#xA;  return formatDateTime(dateTime)&#xA;}1- زمان فعلی رو به صورت لحظه دریافت میکنه.2- لحظه فعلی رو بر اساس منطقه زمانی کاربر به یک LocalDateTime تبدیل میکنه.3- و در آخر فرمتش میکنیم با متد formatDateTime که قبلا پیاده سازی کردیم.حالا نوبت پیاده سازی متد getTime:override fun getTime(timezoneId: String): String {&#xA;  // 1&#xA;  val timezone = TimeZone.of(timezoneId)&#xA;  // 2&#xA;  val currentMoment: Instant = Clock.System.now()&#xA;  // 3&#xA;  val dateTime: LocalDateTime =&#xA;    currentMoment.toLocalDateTime(timezone)&#xA;  // 4&#xA;  return formatDateTime(dateTime)&#xA;}1- منطقه زمانی رو با استفاده از id که بهش دادیم بهمون میده.2- زمان فعلی رو به صورت لحظه بهمون میده.3- لحظه فعلی رو بر اساس منطقه زمانی کاربر به یک LocalDateTime تبدیل میکنه.4- فرمت میکنه!متد getDate هم شبیه getTime. برای getDate اینو بنویسید:override fun getDate(timezoneId: String): String {&#xA;  val timezone = TimeZone.of(timezoneId)&#xA;  val currentMoment: Instant = Clock.System.now()&#xA;  val dateTime: LocalDateTime =&#xA;    currentMoment.toLocalDateTime(timezone)&#xA;  // 1&#xA;  return &#34;${dateTime.dayOfWeek.name.lowercase().replaceFirstChar {&#xA;    it.uppercase()&#xA;  }&#xA;}, &#34; +&#xA;&#34;${dateTime.month.name.lowercase().replaceFirstChar {&#xA;it.uppercase()&#xA;}&#xA;}&#xA;$ {&#xA;  dateTime.date.dayOfMonth&#xA;}&#xA;&#34;&#xA;}1- این بخش های مختلف DateTime رو میگیره برای ساخت همچین رشته ای :Monday, October 4متد currentTimeZone هم که ساده ست. منطقه زمانی فعلی رو به عنوان یه رشته برمیگردونه:override fun currentTimeZone(): String {&#xA;  val currentTimeZone = TimeZone.currentSystemDefault()&#xA;  return currentTimeZone.toString()&#xA;}برای متد hoursFromTimeZone هم باید ساعت هارو با توجه منطقه زمانی برگردونیم:override fun hoursFromTimeZone(otherTimeZoneId: String): Double {&#xA;  // 1&#xA;  val currentTimeZone = TimeZone.currentSystemDefault()&#xA;  // 2&#xA;  val currentUTCInstant: Instant = Clock.System.now()&#xA;  // Date time in other timezone&#xA;  // 3&#xA;  val otherTimeZone = TimeZone.of(otherTimeZoneId)&#xA;  // 4&#xA;  val currentDateTime: LocalDateTime =&#xA;    currentUTCInstant.toLocalDateTime(currentTimeZone)&#xA;  // 5&#xA;  val currentOtherDateTime: LocalDateTime =&#xA;    currentUTCInstant.toLocalDateTime(otherTimeZone)&#xA;  // 6&#xA;  return abs((currentDateTime.hour - currentOtherDateTime.hour) *&#xA;    1.0)&#xA;}1- منطقه زمانی فعلی رو دریافت میکنیم2- زمان/لحظه فعلی رو دریافت میکنیم.3-یه منطقه زمانی دیگه میسازیم.4- زمان فعلی رو به یک کلاس LocalDateTime تبدیل میکنیم.5- زمان فعلی رو تو منطقه زمانی(otherTimeZone) به یک کلاس LocalDateTime تبدیل میکنیم.6- اختلاف مطلق بین ساعت ها رو برمیگردونیم (نباید منفی باشه).جستجوجستجو کمی سخت تره. با توجه به یه ساعت شروع (مثلا 8 صبح,am)و یه ساعت پایان (مثلا 5 بعد از ظهر,pm) و لیست مناطق زمانی افراد(منظور اوناییه که میخوان هماهنگ بشن)، ما می خوایم یه لیست int که نمایانگر ساعت های (0 تا 23) هست رو دربیاریم که برای همه مناسب باشه. برای مثال: توی لس آنجلس 8صبح am تا 5 بعدازظهر pm میشه این:[8, 9, 10, 11, 12, 13, 14]ولی تو همون رنج (range) ساعتی 8 تا 5 تو نیویورک میشه این:[11, 12, 13, 14, 15, 16, 17]حالا باید یه سری time ها رو بکشیم بیرون از این دوتا تا یه زمان مناسب برای هردو پیدا، مثلا 11،12و.. .خب برای اینکه بفهمیم اون ساعت ای که بین این دو منطقه پیدا کردیم درسته، باید یه متد بنام isValid اضافه کنیم(بعد از متد search قرارش بدین):private fun isValid(&#xA;  timeRange: IntRange,&#xA;  hour: Int,&#xA;  currentTimeZone: TimeZone,&#xA;  otherTimeZone: TimeZone&#xA;): Boolean {&#xA;  if (hour!in timeRange) {&#xA;    return false&#xA;  }&#xA;  // TODO: Add Current Time&#xA;}خب حالا این کد ها رو جاگیزین// TODO: Add Current Timeکنید:// 1&#xA;val currentUTCInstant: Instant = Clock.System.now()&#xA;// 2&#xA;val currentOtherDateTime: LocalDateTime =&#xA;  currentUTCInstant.toLocalDateTime(otherTimeZone)&#xA;// 3&#xA;val otherDateTimeWithHour = LocalDateTime(&#xA;  currentOtherDateTime.year,&#xA;  currentOtherDateTime.monthNumber,&#xA;  currentOtherDateTime.dayOfMonth,&#xA;  hour,&#xA;  0,&#xA;  0,&#xA;  0&#xA;)&#xA;// TODO: Add Conversions1- این برای دریافت لحظه فعلی هستش2- با داشتن یه منطقه زمانی، لحظه رو به یه منطقه زمانی دیگر تبدیل میکنه.3- دریافت کردن LocalDateTime با یک ساعت مشخص. (دقیقه، ثانیه و نانو ثانیه لازم نیست)حالا بجای// TODO: Add Conversionsاین رو قرار بدید:// 1&#xA;val localInstant = otherDateTimeWithHour.toInstant(currentTimeZone)&#xA;// 2&#xA;val convertedTime = localInstant.toLocalDateTime(otherTimeZone)&#xA;Napier.d(&#34;Hour $hour in Time Range ${otherTimeZone.id} is&#xA;    $ {&#xA;      convertedTime.hour&#xA;    }&#xA;    &#34;)&#xA;    // 3&#xA;    return convertedTime.hour in timeRangeلایبرری  Napier رو واسه گزارش کردن لازم داریم پس لایبرری اش رو import کنید اگه قرمزه.1- ساعته رو به منطقه زمانی فعلی تبدیل میکنه.2- ساعت منطقه زمانی رو به منطقه زمانی دیگه تبدیل میکنه.3- بررسی میکنه که آیا در محدوده زمانی شما است یا خیر.حالا بجای// TODO: Add Conversionsاین رو قرار بدید:// 1&#xA;val localInstant = otherDateTimeWithHour.toInstant(currentTimeZone)&#xA;// 2&#xA;val convertedTime = localInstant.toLocalDateTime(otherTimeZone)&#xA;Napier.d(&#34;Hour $hour in Time Range ${otherTimeZone.id} is&#xA;    $ {&#xA;      convertedTime.hour&#xA;    }&#xA;    &#34;)&#xA;    // 3&#xA;    return convertedTime.hour in timeRangeلایبرری  Napier رو واسه گزارش کردن لازم داریم پس لایبرری هاش رو import کنید اگه قرمزه.1- ساعته رو به منطقه زمانی فعلی تبدیل میکنه.2- ساعت منطقه زمانی رو به منطقه زمانی دیگه تبدیل میکنه.3- بررسی میکنه که آیا در محدوده زمانی شما است یا خیر.خب حالا شما متد isValid رو دارید. پس بیاین متد search رو پیاده سازی کنیم(اندازه isValid سخت نیست!!). فقط باید مناطق و ساعتهای معین رو بررسی کنیم که آیا معتبر هستن یا نه. اینجوری:// 1&#xA;val goodHours = mutableListOf &lt; Int &gt; ()&#xA;// 2&#xA;val timeRange = IntRange(max(0, startHour), min(23, endHour))&#xA;// 3&#xA;val currentTimeZone = TimeZone.currentSystemDefault()&#xA;// 4&#xA;for (hour in timeRange) {&#xA;  var isGoodHour = false&#xA;  // 5&#xA;  for (zone in timezoneStrings) {&#xA;    val timezone = TimeZone.of(zone)&#xA;    // 6&#xA;    if (timezone == currentTimeZone) {&#xA;      continue&#xA;    }&#xA;    // 7&#xA;    if (!isValid(&#xA;        timeRange = timeRange,&#xA;        hour = hour,&#xA;        currentTimeZone = currentTimeZone,&#xA;        otherTimeZone = timezone&#xA;      )) {&#xA;      Napier.d(&#34;Hour $hour is not valid for time range&#34;)&#xA;      isGoodHour = false&#xA;      break&#xA;    } else {&#xA;      Napier.d(&#34;Hour $hour is Valid for time range&#34;)&#xA;      isGoodHour = true&#xA;    }&#xA;  }&#xA;  // 8&#xA;  if (isGoodHour) {&#xA;    goodHours.add(hour)&#xA;  }&#xA;}&#xA;// 9&#xA;return goodHours1- لیست تایم هایی که valid هستن.2- یه محدوده range میسازه.3- زمان فعلی منطقه رو بهمون میده.4- سرتاسر محدوده ساعت رو میگرده5- سراسر محدوده منطقه زمانی رو میگرده تو لیست منطقه زمانی.6- اگه منطقه زمانی مشترک باشه یعنی خوبه.7- بعد از اینکه سراسر محدوده(range) رو گشت و time(ساعت) مناسب رو پیدا کرد، اونو به لیست اضافی میکنه.8-لیست ساعت های مناسب رو return میکنه.خبب! تبریک میگم، شما business logic این اپ رو نوشتین، اگه جایی براتون گنگ بود، برید دوباره اون بخش رو بخونید و یا تو کانال زیر این پست سوالتون رو به اشتراک بزارید تا حلش کنیم.امیدوارم خوشتون بیاد. اگه نظری، پیشنهادی، باگی چیزی بود لطفا تو کامنت ها به اشتراک بذارین.کانال Android Corner</description><category>Android Corner</category><author>Android Corner</author><pubDate>Mon, 21 Aug 2023 21:58:33 +0330</pubDate></item><item><title>آموزش Kotlin Multiplatform(قسمت اول)</title><link>https://virgool.ir/@cornerdroid/کاتلین-و-چندین-پلتفرمقسمت-اول-xbqdrfndhudi</link><description>فصل اول کتاب رو- چون یکم طولانی بود- توی دو قسمت منتشر میکنم. تو این پست، فصل اول(قسمت اول) این کتاب رو بررسی میکنیم و:* درباره build system های KMP حرف میزنیم.AndroidCornerنکته: اگه پست &#34;مقدمه&#34; رو ندیدین، حتما تو کانال  Android Corner ببینید.فصل اول قسمت اول:برای شروع کار، ما باید با build system ای که KMP استفاده میکنه آشنا بشیم: build system اندروید و دکستاپ همون gradle ولی iOS از CocoaPods استفاده میکنه.گریدلخود KMP از اسکریپ های kotlin استفاده میکنه برای gradle و اگه ببین پسوند فایلش .kts هست مخفف kotlin script.فایل های Buildتوی پوشه روت build.gradle.kts باز کنید:// 1&#xA;buildscript {&#xA;  // 2&#xA;  repositories {&#xA;    gradlePluginPortal()&#xA;    google()&#xA;    mavenCentral()&#xA;  }&#xA;  // 3&#xA;  dependencies {&#xA;    classpath(&#34;org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31&#34;)&#xA;    classpath(&#34;com.android.tools.build:gradle:7.0.1&#34;)&#xA;  }&#xA;}1- بخش buildscript همه اطلاعات مربوط به پلاگین ها و ورژن هاشون رو تعریف میکنه.2- بخش repositories منبعی که پلاگین ها روش هاست شدن رو تعریف میکنه.3- و توی dependencies هم ما میایم پلاگین ها رو قرار میدیم.اینم هست:// 1&#xA;allprojects {&#xA; // 2&#xA;  repositories {&#xA;  google()&#xA;  mavenCentral()&#xA; }&#xA;}1- بخش AllProjects به این معنیه: که موارد زیر رو برای همه ماژول های این پروژه اعمال کن.2- بخش repositories تمامی مخزن های مورد نیاز برای همه ماژولا رو تعریف می کنه.آخرین بخش:tasks.register(&#34;clean&#34;, Delete::class) {&#xA;  delete(rootProject.buildDir)&#xA;}این یه وظیفه Gradle به نام Clean انجام میده که با این کار، پوشه root رو حذف میکنه و در فقط صورت نیاز به build تازه(fresh build) از این استفاده می کنید.حالا androidApp/build.gradle.kts رو باز کنید، این فک کنم براتون آشناست:) توش پلاگین ها، dependency ها و بلاک اندروید(چیزای مخصوص اندروید) قرار داره.اما توی قسمت dependency :dependencies {&#xA;  // 1&#xA;  implementation(project(&#34;:shared&#34;))&#xA;  // 2&#xA;  implementation(&#34;com.google.android.material:material:1.4.0&#34;)&#xA;  implementation(&#34;androidx.appcompat:appcompat:1.3.1&#34;)&#xA;&#xA;  implementation(&#34;androidx.constraintlayout:constraintlayout:2.1.1&#34;)&#xA;}1- این میگه که اندروید وابسته اس به ماژول shared(جاییکه منطق بیزینس و network و database و .. قرار داره)2- اینا هم که dependency های مخصوص اندروید اندفایل build ماژول sharedحالا فایل shared/build.gradle.kts رو باز کنید.اولین بخش پلاگین ها هستن:plugins {&#xA;  kotlin(&#34;multiplatform&#34;)&#xA;  kotlin(&#34;native.cocoapods&#34;)&#xA;  id(&#34;com.android.library&#34;)&#xA;}* اولی پلاگین برای KMP هستش و میگه که این ماژول یه ماژول برای چند پلتفرمه (multiplatform).* دومی پلاگین CocoaPods برای IOS .* سومی هم که برای اندرویده؛ شما اینو استفاده میکنید به منظور ساخت یه لایبرری اندروید(همون کدهایی که تو shared) برای استفاده تو Android app تون.بخش kotlin:kotlin {&#xA;  // 1&#xA;  android()&#xA;  // 2&#xA;  iosX64()&#xA;  // 3&#xA;  iosArm64()&#xA;  // 4&#xA;  iosSimulatorArm64()&#xA;  // 5&#xA;  cocoapods {&#xA;    summary = &#34;Holds Time zone information&#34;&#xA;    homepage = &#34;Link to the Shared Module homepage&#34;&#xA;    ios.deploymentTarget = &#34;14.1&#34;&#xA;    framework {&#xA;      baseName = &#34;shared&#34;&#xA;    }&#xA;    podfile = project.file(&#34;../iosApp/Podfile&#34;)&#xA;  }&#xA;  ...&#xA;}این بخش از پلاگین multiplatform استفاده میکنه به منظور پیکربندی این ماژول برای KMP.1- این متد برای هدف قرار دادن اندروید بعنوان پلتفرمیه که داریم براش این پروژه رو مینویسیم.2- متد iOSX64 برای هدف قرار دادن iOS با معماری x86_64 هستش، مثل شبیه سازهای iOS ای که اجرا میشن روی دستگاه های Mac.3- متد iosArm64 هم هدفش دستگاه های IOS ای هستش که مبتنی بر معماری ARM64 هستن.4- این متد هم iosSimulatorArm64 برای هدف قرار دادن شبیه ساز iOS هستش که معماری ARM64 را در دستگاه های x86_64 تقلید می کند.5- این یه سری جزئیات برای ساخت CocoaPods Podfile(توی پوشه ios) تعریف میکنهبخش بعدی sourcsSet:sourceSets {&#xA;  // 1&#xA;  val commonMain by getting&#xA;  val commonTest by getting {&#xA;    dependencies {&#xA;      implementation(kotlin(&#34;test-common&#34;))&#xA;      implementation(kotlin(&#34;test-annotations-common&#34;))&#xA;    }&#xA;  }&#xA;  // 2&#xA;  val androidMain by getting&#xA;  val androidTest by getting {&#xA;    dependencies {&#xA;      implementation(kotlin(&#34;test-junit&#34;))&#xA;      implementation(&#34;junit:junit:4.13.2&#34;)&#xA;    }&#xA;  }&#xA;  // 3&#xA;  val iosX64Main by getting&#xA;  val iosArm64Main by getting&#xA;  val iosSimulatorArm64Main by getting&#xA;  val iosMain by creating {&#xA;    dependsOn(commonMain)&#xA;    iosX64Main.dependsOn(this)&#xA;    iosArm64Main.dependsOn(this)&#xA;    iosSimulatorArm64Main.dependsOn(this)&#xA;  }&#xA;}تو این بخش ما، source set هامون رو با variable های از پیش تعریف شده میتونیم تعریف کنیم.1- این commonMain و dependency هایی برای تست کردن تعریف میکنه- برای commonMain چیزی تعریف نکرده.2- این dependency های اندروید رو تعریف میکنه.3- اینجا هم dependency های IOS.بخش بعدی هم اندرویده که شما باهاش آشنا هستید.android {&#xA;  compileSdk = 31&#xA;&#xA;  sourceSets[&#34;main&#34;].manifest.srcFile(&#34;src/androidMain/AndroidManifes&#xA;    t.xml &#34;)&#xA;    defaultConfig {&#xA;      minSdk = 21&#xA;      targetSdk = 31&#xA;    }&#xA;  }سیستم عامل iOSخب، iOS برخلاف اندروید از چند سیستم مدیریت dependency استفاده میکنه که یکی از معمول ترینش CocoaPods هستش.اگه برین iOSApp/Podfile رو باز کنید، چیز زیادی نداره:target &#39;iosApp&#39; do&#xA;  use_frameworks!&#xA;  platform: ios, &#39;14.1&#39;&#xA;  pod &#39;shared&#39;,: path =&gt; &#39;../shared&#39;&#xA;endمهمترین قسمتش :اینه: pod &#39;shared&#39;, :path =&gt; &#39;../shared&#39;این قسمت میاد فریمورک shred ای که قراره KMP بسازه رو تعریف میکنه (واقعا یه فریمورک کوچیکه دیگه).خُبب! این قسمت دوم بود. امیدوارم خوشتون اومده باشه، اگه نظری، پیشنهادی، باگی چیزی بود لطفا تو کامنت ها به اشتراک بذارین.کانال تلگرام Android Corner</description><category>Android Corner</category><author>Android Corner</author><pubDate>Mon, 21 Aug 2023 11:53:30 +0330</pubDate></item><item><title>آموزش Kotlin Multiplatform(مقدمه)</title><link>https://virgool.ir/@cornerdroid/کاتلین-و-چندین-پلتفرم-مقدمه-xketf405bhkm</link><description>توی سری پست های &#34;کاتلین و چندین پلتفرم&#34; ما کتاب Kotlin Multiplatform by Tutorials رو (بطور خلاصه و مفید) واشکافی میکنیم.میدونم هنوز تو حالت beta قرار داره، ولی برای خود من بشخصه برام جالب و فانه که تکنولوژی های جدید رو دنبال کنم.تو این پست، مقدمه این کتاب رو بررسی میکنیم و درباره اینا حرف میزنیم:* کاتلین multiplatform* کلید واژه های expect و actual* و build اولین پروژه مقدمه:کاتلین روی پلتفرم هایی که jvm رو پشتیبانی میکنن کار میکنه (مثل پروژه های اندروید) و stable هم هست، ولی  برای پلتفرم های دیگه که jvm رو پشتیبانی نمیکنن از کاتلین native استفاده میکنه و هنوز تو حالت beta هستش. کاتلین native کد های کاتلین رو به bytecode های بومی کامپایل میکنه که اجرا میشن روی سیستم عامل های apple، ویندوز و لینوکس. و برای وب هم، کد های کاتلین کامپایل میشه به JavaScript و html که بهش میگن Kotlin-js.معمولا تو پروژه ها چند لایه های مختلف داریم مثل: لایه network، لایه database، منطق بیزینس(business logic) و لایه UI.توی پروژه های kotlin multiplatform، شما یه کد base برای business logic تون خواهید داشت برای همه  پلتفرم ها؛ میتونید لایه database رو با لایبرری SQLDelight بنویسید برای همه پلتفرم ها. اما kotlin multiplatform لایه UI رو فراهم میکنه و شما میتونید UI رو برای iOS با Swift بزنید و برای desktop میتونید از compose desktop استفاده کنید و برای اندروید هم که compose هستش. اینکه Kotlin multiplatform بهمون اجازه میده که UI رو با UI system اون پلتفرم بزنیم بنظر من واقعا یه مزیت بزرگه.خب بیاید شروع کنیم:پلاگین kotlin multiplatform رو توی Android Studio نصب کنین. و restart کنین. روی New Project کلیک و Kotlin multiplatform app رو انتخاب کنین.اسم اپلیکیشن اندروید، iOS تونو انتخاب کنین. اسم ماژول shared هم بزارید shared باشه خوبه. و finish رو بزنید تا پروژه build بشه.کلیدواژه های kotlin multiplatformحالا که پروژه رو ساختید لازمه دوتا کلیدواژه های kotlin رو که برای پشتیبانی از KMP(kotlin multiplatform project) اومده رو بدونید: expect و actual.کلیدواژه expect تقریبا مثل abstract class/fun، یعنی ما اینو تو پوشه commonMain* تعریف میکنیم (حالا میتونه expect fun/class باشه) و انتظار داریم که توی هر پلتفرم (androidMain و iosMain) پیاده سازی بشه(مثل abstract class/fun که پیاده سازی شون میکردیم) حالا این پیاده سازی رو هم با کلیدواژه actual تعریف میکنیم.خب حالا بریده توی commonMain و Platform.kt رو باز کنید.expect class Platform() {&#xA;       val platform: String&#xA;}میبینید که از expect استفاده کرده و این به ما میگه که: توی هر پلتفرم باید یه actual کلاسی بنام Platform داشته باشیم که این کلاس رو پیاده سازی کنه و باید یه variable از نوع string داشته باشه بنام platform(اینم باید actual var باید باشه قاعدتا).حالا برید actual کلاس Platform.kt رو توی Android و iOS باز کنید.actual class Platform actual constructor() {&#xA;       actual val platform: String = &#34;Android&#xA;${android.os.Build.VERSION.SDK_INT}&#34;&#xA;}نکته: توی commoMain هرچی مینویسیم باید کاتلین (خالص) باشه و نباید هیچ dependency یا هرچی که مربوط به پروژه های اندرویدی هست رو قرار بدیم، چون ما داریم برای چند پلتفرم مختلف کد رو به اشتراک میزاریم نه فقط اندروید.حالا اپ رو run کنید و .... شما اولین پروژه multiplatform تون رو با استفاده از kotlin ساختین!نکته: برای خروجی گرفتن iOS باید تو mac از xcode استفاده کنین.خُبب! امیدوارم خوشتون اومده باشه، اگه نظری، پیشنهادی، باگی چیزی بود لطفا تو کامنت ها به اشتراک بذارین.کانال تلگرام Android Corner</description><category>Android Corner</category><author>Android Corner</author><pubDate>Mon, 21 Aug 2023 11:40:48 +0330</pubDate></item><item><title>بهتر بنویسیم!</title><link>https://virgool.ir/@cornerdroid/بهتر-بنویسیم-ozixoknif8e6</link><description>توی سری پست های &#34;بهتر بنویسیم!&#34; یاد میگیریم که چطور با استفاده از ویژگی های جالب کاتلین، کدی تمیز بنویسیم.تا آخر این پست رو بخونین. شاید نکته جالبی پیدا کردین.1- وقتی که بیش از دو شرط دارید از when استفاده کنید:// Don&#39;t&#xA;fun getCategoryByEatableName(eatable: String): String {&#xA;    if (eatable == &#34;mango&#34; eatable == &#34;banana&#34; eatable == &#34;apple&#34;) {&#xA;        return &#34;FRUIT&#34;&#xA;    }else if (eatable == &#34;potato&#34; eatable == &#34;tomato&#34; eatable == &#34;onion&#34;) {&#xA;        return &#34;VEGETABLE&#34;&#xA;    }else if(eatable == &#34;milk&#34; || eatable == &#34;curd&#34;) {&#xA;        return &#34;DAIRY&#34;&#xA;    }else {&#xA;        return &#34;UNKNOWN&#34;&#xA;    }&#xA;}&#xA;&#xA;// Do&#xA;fun getCategoryByEatableName(eatable: String) =&#xA;    when(eatable) {&#xA;        &#34;mango&#34;, &#34;banana&#34;, &#34;apple&#34; -&gt; &#34;FRUIT&#34;&#xA;        &#34;potato&#34;, &#34;tomato&#34;, &#34;onion&#34; -&gt; &#34;VEGETABLE&#34;&#xA;        &#34;milk&#34;, &#34;curd&#34; -&gt; &#34;DAIRY&#34;&#xA;        else -&gt; &#34;UNKNOWN&#34;&#xA;    }2- از let به همراه ?. برای property های nullable استفاده کنید:// Don&#39;t &#xA;val user: User? = findUser()&#xA;if (user != null){&#xA;    println(user.name)&#xA;}&#xA;&#xA;// Do&#xA;findUser()?.let { println(it.name) }&#xA;//or&#xA;findUser()?.name?.let(::println)3- عملگر Elvis(?:)Without &#xA;if(user.name != null) {&#xA;    userName = user.name&#xA;} else {&#xA;    throw IllegalStateException()&#xA;}&#xA;With&#xA;val userName = user.name?: throw IllegalStateException()4- وقتی که میخواین به چندین property شی دسترسی داشته باشید از apply استفاده کنید.class Server {&#xA;    lateinit var host: String&#xA;    var port: Int = 0&#xA;    var numOfInstance: Int = 1&#xA;    var isCloudInstance: Boolean = true&#xA;}&#xA;&#xA;// Don&#39;t&#xA;var server = Server()&#xA;server.host = &#34;sg1234&#34;&#xA;server.port = 8080&#xA;server.numOfInstance = 3&#xA;&#xA;// Do&#xA;var server = Server().apply {&#xA;    host = &#34;sg1234&#34;&#xA;    port = 8080&#xA;    numOfInstance = 3&#xA;}5- برای map کردن key/value ها از عملگر to استفاده کنید:// Don&#39;t&#xA;val immutableMap = mapOf(&#xA;    Pair(&#34;A&#34;, 1),&#xA;    Pair(&#34;B&#34;, 2),&#xA;    Pair(&#34;C&#34;, 3))&#xA;&#xA;val mutableMap = mutableMapOf(&#xA;    Pair(&#34;A&#34;, 1),&#xA;    Pair(&#34;B&#34;, 2),&#xA;    Pair(&#34;C&#34;, 3))&#xA;&#xA;// Do&#xA;val immutableMap = mapOf(&#xA;    &#34;A&#34; to 1,&#xA;    &#34;B&#34; to 2,&#xA;    &#34;C&#34; to 3&#xA;)&#xA;&#xA;val mutableMap = mutableMapOf(&#xA;    &#34;A&#34; to 1,&#xA;    &#34;B&#34; to 2,&#xA;    &#34;C&#34; to 3&#xA;)یه مثال دیگه: var url = &#34;http://localhost:8080/users&#34;&#xA;var method = &#34;GET&#34;&#xA;var status = 200&#xA;&#xA;logger.debug(mapOf(&#34;url&#34; to url, &#34;method&#34; to method, &#34;status&#34; to status))6- یه راه آسون برای ایجاد کردن یه رشته توی چند خط، اول و آخر string رو این بزارین &#34;&#34;&#34; است.// Don&#39;t&#xA;println(&#34;Twinkle, Twinkle Little Bat\n&#34; +&#xA;        &#34;How I wonder what you&#39;re at!\n&#34; +&#xA;        &#34;Up above the world you fly,\n&#34; +&#xA;        &#34;Like a tea tray in the sky.\n&#34; +&#xA;        &#34;Twinkle, twinkle, little bat!\n&#34; +&#xA;        &#34;How I wonder what you&#39;re at!&#34;)&#xA;&#xA;// Do&#xA;println(&#34;&#34;&#34;&#xA;        Twinkle, Twinkle Little Bat&#xA;        How I wonder what you&#39;re at!&#xA;        Up above the world you fly,&#xA;        Like a tea tray in the sky.&#xA;        Twinkle, twinkle, little bat!&#xA;        How I wonder what you&#39;re at!&#34;&#34;&#34;)</description><category>Android Corner</category><author>Android Corner</author><pubDate>Thu, 17 Aug 2023 23:09:29 +0330</pubDate></item><item><title>اخبار هفته، بخش دوم</title><link>https://virgool.ir/@cornerdroid/اخبار-هفته-بخش-دوم-vycwjtnc8nlo</link><description>تو این بخش این خبر هارو پوشش میدیم:گوگل پلی و ویژگی/سیاست های جدیدشاندروید استودیو زرافه ?انیمیشن Lottieگوگل پلیسه تغییر مهم1- الان میتونید لیست جزئیات(store listing) اپلیکیشن تون رو کاملا برای کاربرا تون یا گروه مشخصی از کاربرا شخصی سازی (customize) بکنین. حالا فایدش چیه:مثلا میتونین برای کاربر هایی که تو اپلیکیشن تون فعال نیستند (lapsed users)، یه store listing بخصوص برای اون گروه از کاربرا ایجاد کنین تا اونا دوباره جذب اپلیکیشن بشن.بعد از اینکه برای گروه های مختلف یه store listing شخصی سازی شده قرار دادید، حالا میتونید برای این گروه های مختلف، بر اساس store listing، تبلیغات بخصوصی رو برای هر کدوم از این گروه ها با علاقه مندی های متفاوت قرار بدین.وقتی که تعداد زیادی store listing ساختین، میتونید یه لیست جزئیات پایه(base listing) بسازین و همه store listing ها رو مدیریت کنین. (جزئیات بیشتر)2- هوش مصنوعی و یادگیری ماشین برای store listingاین دو باهم به کمک شما میان، تا محتوای باکیفیت تری توی زمان کمتری برای لیست جزئیات اَپ تون(store listing) بنویسید؛ مثلا، اگه شما دارین یه listing به زبان انگلیسی مینویسین، شما میتونید مثلا موضوع و مخاطب مورد نظرتون رو مینویسین؛ بعد طبق اون، محتوای listing شما تولید میشه.3- آزمایش قیمتیکی دیگه از چیزهایی به Google play console تون اضافه شده، آزمایش کردن قیمت های مختلف از محدوده A تا B برای یه محصول درون برنامه ای در مناطق مختلف. این آزمایشات قیمت گذاری برای محصول درون برنامه ای حداقل 7 روز باید انجام بشه برای به دست آوردن داده های کافی (مثلا تو ایران قیمت ایده آل محصول درون برنامه مون 1.80$ ولی تو UK میشه 2.80$). این آزمایشات هم فقط برای کاربرانی که تو اپ جدید هستند لحاظ میشه. بنظرم ویژگی جالبی و باعث میشه رفتار کاربران رو در مناطق مختلف آنالیز کنین و بر مبنای اون برای محصول تون تصمیم بگیرین، حتما امتحانش کنین اگه اَپی توی Google play دارین.سیاست های جدیدگوگل پلی یه سری سیاست های جدیدی تعریف کرده برای اعتماد بیشتر و شفافیت business تون.این سیاست ها :1- تهیه شماره DUNSاز الان اگه برین یه account جدید ایجاد کنین تو Google play console، نیاز شماره DUNS دارین. با این شماره شما به Google play میگین بیزینس ما معتبره یه جور ارزش اعتباری داره.2- جَلب اعتماد کاربرگوگل پلی داره اسم بخش &#34;Contact details&#34; توی لیست جزئیات (store listing) به &#34;App support&#34; تغییر میده و داره یه بخش جدیدی بنام &#34;About the developer&#34; ایجاد میکنه تا کاربر اطلاعات بیشتری راجب شما بدونن. توی &#34;Contact details&#34; میتونین آدرس ایمیل، وب سایت و شماره تلفن پشتیبانی خودتون رو نشون بدین. و توی &#34;App support&#34; هم میتونین نام ، آدرس و جزئیات تماس را نشان بدین.اندروید استودیوی زرافه ?ما تو این نسخه Android studio:ظاهر و احساس بصری کاملاً جدیدپیشرفت در ویرایش زنده (Live edit)پیش نمایش(preview) انیمیشن های composeاکسپلورر دستگاه (Device Explorer)دستیار جدید برای آپدیت SDK و..امکان استفاده از Kotlin DSL توی gradle build scriptو چیزای بیشتر.پیشرفت های IDE1- رابط کاربری جدید برای Android Studio : رابط کاربری جدید برای اولین بار توی IntelliJ 2022.2 منتشر شد برای پیش نمایش تا کاربرا بتونن بازخوردشون رو بگن و اطمینان ایجاد بشه که این UI جدید میتونه جایگزین UI قبلی بشه.الان اندروید استودیو هم داره از این UI استفاده میکنه با یه سری تغییرات مثل: بهینه سازی toolbar اصلی و tool Windows configuration و عوض شدن آیکونگرافی (iconography)برای استفاده از این رابط کاربری باحال برین توی Settings &gt; Appearance &amp; Behavior &gt; New UI و New UI رو فعال کنین.2- ابزار diagnostic &amp; bug reporting جدید:ببا این ابزار شما راحتر میتونید اپ تون رو debug کنید. این ابزار رو میتونین اینجا پیدا کنین:Help &gt; Collect Logs and Diagnostic Data3- دستگاه اکسپلورر جدیدتوی device explorer، فایل ها و هرچی که مربوط به فایل میشه توی tab فایل قرار داره و همچنین یه tab دیگه بنام process وجود داره که می تونین پروسس های قابل debug رو مال دستگاه متصل شده ببینید.افزایش بهره وری کد زدن با:1- ویرایش زنده (Live edit) رو توی پست قبلی توضیح دادم.2- دستیار  Android SDK Upgradeاین دستیار به شما امکان دیدن مراحل مورد نیاز برای آپدیت کردن targetSdkVersion یا API level ای که اپ تون استفاده میکنه. همچنین مستندات سایت Android Developer رو میگیره و توی پنجره ابزاری که داره نمایش میده و دیگه نیازی نیست که بین مرورگر و IDE تون جا به جا بشین.برای باز کردن Android SDK Upgrade Assistant، برید توی :Tools &gt; Android SDK Upgrade Assistantپیشرفت توی build system1- با اومدن DSL کاتلین برای اسکریپت های Gradle build، چونکه به زبان کاتلینه، هم خوانا تر هم پشتیبانی بهتری توی زمان کامپایل رو ارائه می ده.وقتی توی Android studio giraffe یه پروژه جدید ایجاد میکند، بصورت پیش فرض از DSL کاتلین استفاده میکنه برای اسکریپت های بیلد gradle و دیگه از groovy استفاده نمیکنیم.منابع بیشتر : Gradle Blog, JetBrains Blog2- بارگیری اطلاعات در حین همگام سازی(sync) Gradle: پنجره ابزار synk جدید به شما امکان مشاهده خلاصه ای از زمان صرف شده برای دانلود dependency ها و یک نمای دقیق از بارگیری در هر مخزن است. میتونید با این ویژگی از دانلود غیر منتظره dependency ها جلوگیری کنید.3- الان دیگه میتونید با انتخاب گزینه Make Sellected Modules Build توی نوار ابزار، فقط ماژول فعلی که روی اون کار می کنید رو build کنید. این گزینه جدید خیلی کارآمده، مثلا شما روی یه ماژول کار میکنید و نمیخواید سه ساعت بشینید تا کل پروژه بیلد بشه.انیمیشن های اندرویدی Lottie 6.1 به سمت Multithread شدن میرهاین پست رو maintainer لاتی منتشر کرده و میگه : هر انیمیشنی دو فاز(مرحله) داره، upgrade و draw.فاز upgrade میره کل درخت انیمیشن رو میگرده تا ببینه value های (color, position و ...) عوض شده یا نه؛ اگه عوض شده باشه اونو محاسبه میکنه و پاسش میده به draw و اون رو میکشه. این دو فاز قبلا بصورت asynk توی main thread اجرا میشدن و اون فاز draw حتما باید توی main thread باشه، ولی upgrade که کُندتره میتونه توی background thread اجرا بشه. اگه جزئیات بیشتری میخواید، میتونین مقاله رو توی medium ببینید.خُبب! امیدوارم خوشتون اومده باشه، اگه نظری، پیشنهادی، باگی چیزی بود لطفا تو کامنت ها به اشتراک بذارین.کانال تلگرام Android Corner</description><category>Android Corner</category><author>Android Corner</author><pubDate>Tue, 15 Aug 2023 23:29:02 +0330</pubDate></item><item><title>اخبار</title><link>https://virgool.ir/@cornerdroid/اخبار-oizm62n0katp</link><description>توی این پست قراره این خبر هارو پوشش بدیم: ورژن جدید kotlin اندروید استودیو زرافهکامپوزگوگل پلی و large screen هاانیمیشن های Lottieو ...*** چون اخبار یکم زیاد بود اینا رو توی دوتا پست منتشر میکنم.ورژن جدید kotlinاخیرا jetbrains کاتلین 1.9.0 رو ارائه کرد که با خودش یه سری ویژگی های زبانی و بهبود های برای kotlin multiplatform/native به همراه داشته. عملگر ..&lt;: این عملگر برای range گرفتن استفاده میشه ولی با یه شرط که اون محدوده از عدد دوم کوچکتر باشه.مثلا اگه اینو چاپ کنیم 1..&lt;5 میاد برامون از 1 تا 4 چاپ میکنه.ویژگی (property) entries: این property برای enum class هاست و اومده که مشکل متد values رو حل کنه؛ اونم اینکه که شما وقتی میخوای یه لیست از constant های enum کلاس تون داشته باشین، این متد رو call میکنین و این متد به شما یه array جدید از اون constant ها میده- در نتیجه هر بار که call بشه یه array جدید return میکنه و این تو شرایطی میتونه باعث مشکل performance بشه.حالا این property این مشکل رو حل کرده و به شما یه لیست pre-allocated و immutable از اون constant بهتون میده و دیگه هر بار هم یه لیست جدید برنمی گردونه.متد group: این متد برای دریافت محتوای گروه regex استفاده میشه. برای مثال، یه regex ایجاد میکنیم :&#xA;val regex = &#34;&#34;&#34;\b(?&lt;city&gt;[A-Za-z\s]+),\s(?&lt;state&gt;[A-Z]{2}):\s(?&lt;areaCode&gt;[0-9]{3})\b&#34;&#34;&#34;.toRegex()بعد میتونیم به مقدار گروه ها دسترسی داشته باشیم : &#xA;    val match = regex.find(input)!!&#xA;    println(match.groups[&#34;city&#34;]?.value)&#xA;    // Austin&#xA;    println(match.groups[&#34;state&#34;]?.value)&#xA;    // TX&#xA;    println(match.groups[&#34;areaCode&#34;]?.value) کامپایلر جدید کاتلین:این کامپایلر یعنی k2، به گفته jetbrains قراره سرعت compilation رو تا 2 برابر و همچنین سرعت توسعه زبانی کاتلین رو افزایش بده؛ الان فعلا تو حالت beta است ولی میتونین بصورت آزمایشی تستش کنین. K2 توی ورژن 2.0 کاتلین stable میشه!</description><category>Android Corner</category><author>Android Corner</author><pubDate>Sun, 13 Aug 2023 00:42:02 +0330</pubDate></item><item><title>انتخاب چندگانه در compose</title><link>https://virgool.ir/@cornerdroid/انتخاب-چندگانه-در-compose-yvz6zjnzyhpw</link><description>       توی خیلی از اپ ها شاید دیده باشین که برای انتخاب کردن(selecting) چند آیتم از یه ویژگی باحال استفاده میکنن بنام انتخاب چندگانه (multiple selection)؛ اینجوریه که وقتی کاربر یه آیتم رو یکم نگه داره و بکشه به پایین یا بالا (dragging) همونقدری که بالا یا پایین کشیده، آیتم ها هم انتخاب یا از حالت انتخاب در میان(deselection).یک grid تصویر با قابلیت انتخاب چندگانهقدم هایی که باید برای پیاده سازی این برداریم :   پیاده سازی یه grid ساده اضافه کردن وضعیت انتخاب (selection state) به grid مدیریت حرکت (gesture handling)  لمس نهایی (final touches)  پیاده سازی gridبرای پیاده سازی grid از LazyVerticalGrid استفاده میکنم تا اپ مون توی screen size های مختلف خوب کار کنه؛ اینطوری که توی screen های بزرگتر، ستون های بیشتری باشه و توی screen های کوچکتر ستون کمتری نمایش داده بشه. @Composable&#xA;private fun PhotoGrid() {&#xA;  val photos by rememberSaveable { mutableStateOf(List(100) { it }) }&#xA;&#xA;  LazyVerticalGrid(&#xA;    columns = GridCells.Adaptive(minSize = 128.dp),&#xA;    verticalArrangement = Arrangement.spacedBy(3.dp),&#xA;    horizontalArrangement = Arrangement.spacedBy(3.dp)&#xA;  ) {&#xA;    items(photos, key = { it }) {&#xA;      Surface(&#xA;        tonalElevation = 3.dp,&#xA;        modifier = Modifier.aspectRatio(1f)&#xA;      ) {}&#xA;    }&#xA;  }&#xA;}توی این مثال ما از عکس بعنوان آیتم های grid مون استفاده میکنیم، اما فعلا تا به اضافه کردن عکس ها برسیم، از یه Surface استفاده میکنم. ما الان با این چند خط کُد یه grid عالی داریم که میتونیم scroll کنیم.یک grid ساده برای شروع کار اضافه کردن selection stateما باید بدونیم که الان چه آیتمی انتخاب(select) شده و اگر تو حالت انتخاب(selection) بودیم، میایم با توجه به وضعیت انتخاب (selection mode) آیتم هامون رو منعکس* میکنیم (*مثلا یه آیکون ✅ مثل این میزاریم روی آیتم ها برای منعکس کردن حالت انتخاب شده یا selected)خب، اول بیاید آیتم grid مون رو بسازیم و selection state رو اینجوری براش تعیین کنیم : 1- اگر کاربر در حالت انتخاب نبود، هیچی نشون نده2- وقتی کاربر تو حالت انتخابه ولی آیتم select نشده، بیا یه radioButton نمایش بده 3- وقتی کاربر تو حالت انتخابه و آیتم هم select شده، یه checkmark نمایش بده @Composable&#xA;private fun ImageItem(&#xA;  selected: Boolean, inSelectionMode: Boolean, modifier: Modifier&#xA;) {&#xA;  Surface(&#xA;    tonalElevation = 3.dp,&#xA;    contentColor = MaterialTheme.colorScheme.primary,&#xA;    modifier = modifier.aspectRatio(1f)&#xA;  ) {&#xA;    if (inSelectionMode) {&#xA;      if (selected) {&#xA;        Icon(Icons.Default.CheckCircle, null)&#xA;      } else {&#xA;        Icon(Icons.Default.RadioButtonUnchecked, null)&#xA;      }&#xA;    }&#xA;  }&#xA;}حالت های مختلف برای انتخاب itemاین composable چون stateless بنابراین state رو درون خودش نگه داری نمیکنه ولی بسادگی میتونیم state رو بهش پاس بدیم از طریق argument و این method با توجه به state ای که ما بهش پاس دادیم به ما پاسخ میده.برای اینکه آیتم ها به حالتهای انتخاب شده (selected) شون پاسخ بدن، grid باید این حالتها را پیگیری کنه. همچنین، کاربر باید با تعامل با آیتم های موجود در grid، مقدار انتخاب شده رو تغییر بده. الان، وقتی که کاربر روی یه آیتم کلیک کنه، به راحتی وضعیت انتخاب شده یه آیتم رو میتونیم اینجوری تغییر بدیم:@Composable&#xA;private fun PhotoGrid() {&#xA;  val photos by rememberSaveable { mutableStateOf(List(100) { it }) }&#xA;  val selectedIds = rememberSaveable { mutableStateOf(emptySet&lt;Int&gt;()) } // NEW&#xA;  val inSelectionMode by remember { derivedStateOf { selectedIds.value.isNotEmpty() } } // NEW&#xA;&#xA;&#xA;  LazyVerticalGrid(&#xA;    columns = GridCells.Adaptive(minSize = 128.dp),&#xA;    verticalArrangement = Arrangement.spacedBy(3.dp),&#xA;    horizontalArrangement = Arrangement.spacedBy(3.dp)&#xA;  ) {&#xA;    items(photos, key = { it }) { id -&gt;&#xA;      val selected = selectedIds.value.contains(id) // NEW&#xA;      ImageItem(selected, inSelectionMode, Modifier.clickable { // NEW&#xA;        selectedIds.value = if (selected) {&#xA;          selectedIds.value.minus(id)&#xA;        } else {&#xA;          selectedIds.value.plus(id)&#xA;        }&#xA;      })&#xA;    }&#xA;  }&#xA;}ما الان آیتم های انتخاب شده(selected) رو توی یه set داریم پیگیری میکنیم و کاربر هر زمانیکه خواست روی یه ImageItem کلیک کنه، id همون آیتم از set اضافه یا حذف میشه . ما زمانی تو حالت انتخاب selectionMode هستیم که آیتم های انتخاب شده وجود داشته باشن؛ هر زمانی که set آیدی های انتخاب شده (selected) تغییر کنه، متغیر selected  هم بطور اتوماتیک دوباره محاسبه میشه.خب تا الان ما میتونیم آیتم هارو اضافه یا حذف کنیم از selection با کلیک کردن روی اونها:  مدیریت حرکت (gesture handling) حالا ما وضعیت انتخاب آیتم ها(selection state) رو داریم، میتونیم حرکتی(gesture) که عناصر باید هنگام اضافه یا حذف شدن از selection داشته باشند رو پیاده سازی کنیم. قاعدتا gesture رو باید به grid اضافه کنیم.برای پیاده سازی این باید این کارا رو بکنیم : با long-pressing یک آیتم، بریم تو حالت selection.بعد از Long-press کردن، بتونیم با drag کردن همه عناصر بین آیتم اصلی و هدف رو اضافه یا حذف کنیم.وقتی توی selection mode هستیم، بتونیم آیتم ها رو با کلیک کردن اضافه یا حذف کنیم از حالت selection.با Long-press کردن روی آیتمی که select شده اتفاقی نَیوفته.مورد دوم یکم چالشی تره؛ چون برای پیاده سازی اون حرکت(gesture) برای grid مون، ما نیاز داریم :  وقتی کاربر یه آیتم رو long press میکنه و شروع به حرکت کشیدن و select کردن آیتم ها میکنه، اولا باید بدونیم کاربر از کدوم آیتم شروع به حرکت کرده (initial key) و وقتی کاربر به حرکت (gesture drag) ادامه داد ما باید اون آیتم های فعلی ای که کاربر روی اون drag کرده رو داشته باشیم  و به طور مداوم آپدیتش کنیم (current key) تا بدونیم که کدوم آیتم ها select/deselect شده. حالا این اطلاعات(initial key و current key) رو از کجا بیاریم تا توی gesture مون ازش استفاده کنیم؟ lazyGrid توی compose از طریق state اش اطلاعاتی مانند : position و size و .. رو داره و ما میتونیم به این اطلاعات دسترسی داشته باشیم با استفاده از LazyGridState و بعد پاسِش بدیم به gesture handler مون.@Composable&#xA;private fun PhotoGrid() {&#xA;  val photos by rememberSaveable { mutableStateOf(List(100) { it }) }&#xA;  val selectedIds = rememberSaveable { mutableStateOf(emptySet&lt;Int&gt;()) }&#xA;  val inSelectionMode by remember { derivedStateOf { selectedIds.value.isNotEmpty() } }&#xA;&#xA;  val state = rememberLazyGridState() // NEW&#xA;&#xA;  LazyVerticalGrid(&#xA;    state = state, // NEW&#xA;    columns = GridCells.Adaptive(minSize = 128.dp),&#xA;    verticalArrangement = Arrangement.spacedBy(3.dp),&#xA;    horizontalArrangement = Arrangement.spacedBy(3.dp),&#xA;    modifier = Modifier.photoGridDragHandler(state, selectedIds) // NEW&#xA;  ) {&#xA;    //..&#xA;  }&#xA;}ما میتونیم از pointerInput و متد detectDragGesturesAfterLongPress استفاده کنیم برای تنظیم و مدیریت drag عمل کشیدن.توضیحی در مورد pointerinput modifier : این modifier حرکت (gesture) لمس، ماوس و قلم رو تشخیص میده و مدیریت میکنه.fun Modifier.photoGridDragHandler(&#xA;  lazyGridState: LazyGridState,&#xA;  selectedIds: MutableState&lt;Set&lt;Int&gt;&gt;&#xA;) = pointerInput(Unit) {&#xA;  var initialKey: Int? = null&#xA;  var currentKey: Int? = null&#xA;  detectDragGesturesAfterLongPress(&#xA;     = { offset -&gt; .. },&#xA;    onDragCancel = { initialKey = null },&#xA;     = { initialKey = null },&#xA;     = { change, _ -&gt; .. }&#xA;  )&#xA;}خب حالا بیاین on drag start رو پیاده سازی کنیم : on DragStart = { offset -&gt;&#xA;  lazyGridState.gridItemKeyAtPosition(offset)?.let { key -&gt; // #1&#xA;    if (!selectedIds.value.contains(key)) { // #2&#xA;      initialKey = key&#xA;      currentKey = key&#xA;      selectedIds.value = selectedIds.value + key // #3&#xA;    }&#xA;  }&#xA;}بیاین ببینم چه خبره توش:  gridItemKeyAtPosition این متد میاد : 1- با استفاده از position، size و .. grid مون 2- و با استفاده از موقعیت نشانگر (pointer position) که حاوی مختصات x و y هستش که کاربر لمس کرده(همون offset) --&gt; با استفاده از اینا میاد آیتمی که کاربر long press کرده و از اونجا drag کردن رو شروع کرده رو بهمون میده.* اگه اون آیتم رو پیدا کرد و آیتِمِه select نشده باشه هنوز، میاد آیدی آیتم رو واسه initialKey و currentKey سِت میکنه و اون آیتمه(id یا key) رو به لیست انتخاب شده ها اضافه میکنه.خب ما الان باید این اکستنشن فانکشن (gridItemKeyAtPosition) رو برای LazyGridState بنویسیم :// The key of the photo underneath the pointer. Null if no photo is hit by the pointer.&#xA;fun LazyGridState.gridItemKeyAtPosition(hitPoint: Offset): Int? =&#xA;  layoutInfo.visibleItemsInfo.find { itemInfo -&gt;&#xA;    itemInfo.size.toIntRect().contains(hitPoint.round() - itemInfo.offset)&#xA;  }?.key as? Intاین متد میاد چک میکنه: اگر موقعیت نشانگر(hit point یا همون انگشت کاربر فرض کنین) مختصات x و y اش با مختصات آیتمی توی grid مطابقت داشت، میاد key یا همون id اون آیتم رو return میکنه.خب حالا فقط باید on drag lambda رو آپدیت کنیم؛ این متد : بطور مرتب وقتی که کاربر نشانگر اش(انگشت، قلم، ماوس و..) رو روی screen حرکت میده، صدا زده میشه.  on Drag = { change, _ -&gt;&#xA;  if (initialKey != null) {&#xA;    // Add or remove photos from selection based on drag position&#xA;    lazyGridState.gridItemKeyAtPosition(change.position)?.let { key -&gt;&#xA;      if (currentKey != key) {&#xA;        selectedIds.value = selectedIds.value&#xA;          .minus(initialKey!!..currentKey!!)&#xA;          .minus(currentKey!!..initialKey!!)&#xA;          .plus(initialKey!!..key)&#xA;          .plus(key..initialKey!!)&#xA;        currentKey = key&#xA;      }&#xA;    }&#xA;  }&#xA;}عمل کشیدن (dragging) قاعدتا زمانی انجام میشه که initialKey سِت شده باشه و این lambda میاد براساس initialKey و currentKey، آیتم های select شده رو آپدیت میکنه --&gt; در نتیجه مطمئن میشیم که همه آیتم های بین initialKey و currentKey انتخاب شدند. الان ما میتونیم چندین آیتم رو انتخاب کنیم با drag کردن : خود grid یه رفتاری برای کلیک کردن داره (clickable behavior)  ولی ما نمیخوایم اونجوری باشه، میخوایم رفتاری داشته باشیم که هم بتونیم آیتم هارو اضافه/حذف کنیم و هم accessibility و سرویس هایی مثل Talkback گوگل رو ساپورت کنه. ما این کار رو با استفاده از semantic property onLongClick انجام میدیم:ImageItem(inSelectionMode, selected, Modifier&#xA;  .semantics {&#xA;    if (!inSelectionMode) {&#xA;      onLongClick(&#34;Select&#34;) {&#xA;        selectedIds.value += id&#xA;        true&#xA;      }&#xA;    }&#xA;  }&#xA;  .then(if (inSelectionMode) {&#xA;    Modifier.toggleable(&#xA;      value = selected,&#xA;      interactionSource = remember { MutableInteractionSource() },&#xA;      indication = null, // do not show a ripple&#xA;      onValueChange = {&#xA;        if (it) {&#xA;          selectedIds.value += id&#xA;        } else {&#xA;          selectedIds.value -= id&#xA;        }&#xA;      }&#xA;    )&#xA;  } else Modifier)&#xA;)این Semantic modifier یه سری معنی های اضافه ای به UI تون میده تا سرویس های accessibility مثل Talkback بتونه UI تون رو بفهمه و بدون اینکه کاربر صفحه رو لمس کنه این سرویس(مثلا) میاد مثل یه واسط بین کاربر و UI ما قرار میگیره و برای کاربر توضیح میده و هر عملی که کاربر میخواد رو انجام میده با UI.و با استفاده از toggleable هم مطمئن میشیم اگه کاربری از سرویس Talkback استفاده میکنه، میتونه اطلاعاتی درباره وضعیت انتخاب شده فعلی داشته باشه.پیمایش (scroll) هنگام drag  ما میخوایم وقتی کاربر به لبه screen رسید آیتم ها scroll بشن و همچنین هرچقدر که کاربر به لبه نزدیک تر میشه، ما باید سرعت scroll رو هم افزایش بدیم.نتیجه اش یه همچین چیزی میشه : اول، ما باید کنترل کننده drag مون رو تغییر بدیم تا بتونیم سرعت پیمایش رو بر اساس فاصله از بالا یا پایین grid مون تنظیم کنیم : fun Modifier.photoGridDragHandler(&#xA;  lazyGridState: LazyGridState,&#xA;  selectedIds: MutableState&lt;Set&lt;Int&gt;&gt;,&#xA;  autoScrollSpeed: MutableState&lt;Float&gt;,&#xA;  autoScrollThreshold: Float&#xA;) = pointerInput(Unit) {&#xA;  //..&#xA;  detectDragGesturesAfterLongPress(&#xA;     = { offset -&gt; .. },&#xA;    onDragCancel = { initialKey = null; autoScrollSpeed.value = 0f },&#xA;     = { initialKey = null; autoScrollSpeed.value = 0f },&#xA;     = { change, _ -&gt;&#xA;      if (initialKey != null) {&#xA;        // NEW&#xA;        // If dragging near the vertical edges of the grid, start scrolling&#xA;        val distFromBottom =&#xA;          lazyGridState.layoutInfo.viewportSize.height - change.position.y&#xA;        val distFromTop = change.position.y&#xA;        autoScrollSpeed.value = when {&#xA;          distFromBottom &lt; autoScrollThreshold -&gt; autoScrollThreshold - distFromBottom&#xA;          distFromTop &lt; autoScrollThreshold -&gt; -(autoScrollThreshold - distFromTop)&#xA;          else -&gt; 0f&#xA;        }&#xA;&#xA;        // Add or remove photos from selection based on drag position&#xA;        lazyGridState.gridItemKeyAtPosition(change.position)&#xA;          ?.let { key -&gt; .. }&#xA;        }&#xA;      }&#xA;    }&#xA;  )&#xA;}الان این تغییراتی که برای سرعت اسکرول تو کنترل کننده gesture مون انجام دادیم کافی نیست، باید PhotoGrid رو هم آپدیت کنیم تا تغییرات سرعت توی اسکول لحاظ بشه :@Composable&#xA;private fun PhotoGrid() {&#xA;  //..&#xA;&#xA;  // How fast the grid should be scrolling at any given time. The closer the&#xA;  // user moves their pointer to the bottom of the screen, the faster the scroll.&#xA;  val autoScrollSpeed = remember { mutableStateOf(0f) }&#xA;  // Executing the scroll&#xA;  LaunchedEffect(autoScrollSpeed.floatValue) {&#xA;    if (autoScrollSpeed.floatValue != 0f) {&#xA;      while (isActive) {&#xA;        state.scrollBy(autoScrollSpeed.floatValue)&#xA;        delay(10)&#xA;      }&#xA;    }&#xA;  }&#xA;&#xA;  LazyVerticalGrid(&#xA;    //..&#xA;    modifier = Modifier.photoGridDragHandler(&#xA;      lazyGridState = state,&#xA;      selectedIds = selectedIds,&#xA;      autoScrollSpeed = autoScrollSpeed, // NEW&#xA;      autoScrollThreshold = with(LocalDensity.current) { 40.dp.toPx() } // NEW&#xA;    )&#xA;  ) {&#xA;    items(photos, key = { it }) { id -&gt;&#xA;      //..&#xA;    }&#xA;  }&#xA;}هر وقت مقدار سرعت اسکرول تغییر کرد، LaunchedEffect با خبر میشه و اونو لحاظ میکنه داخل اسکرول.لمس نهایی (final touche)خب بیاید با یه سری اضافات و اضافه کردن عکس ها، به مثال ایده آل مون برسیم :private class Photo(val id: Int, val url: String)&#xA;&#xA;@Composable&#xA;private fun PhotoGrid() {&#xA;  val photos by rememberSaveable { &#xA;    mutableStateOf(List(100) { Photo(it, randomSampleImageUrl()) })&#xA;  }&#xA;  ..&#xA;}&#xA;&#xA;/**&#xA;* A square image that can be shown in a grid, in either selected or deselected state.&#xA;*/&#xA;@Composable&#xA;private fun ImageItem(&#xA;  photo: Photo,&#xA;  inSelectionMode: Boolean,&#xA;  selected: Boolean,&#xA;  modifier: Modifier = Modifier&#xA;) {&#xA;  Surface(&#xA;    modifier = modifier.aspectRatio(1f),&#xA;    tonalElevation = 3.dp&#xA;  ) {&#xA;    Box {&#xA;      val transition = updateTransition(selected, label = &#34;selected&#34;)&#xA;      val padding by transition.animateDp(label = &#34;padding&#34;) { selected -&gt;&#xA;        if (selected) 10.dp else 0.dp&#xA;      }&#xA;      val roundedCornerShape by transition.animateDp(label = &#34;corner&#34;) { selected -&gt;&#xA;        if (selected) 16.dp else 0.dp&#xA;      }&#xA;&#xA;      Image(&#xA;        painter = rememberAsyncImagePainter(photo.url),&#xA;        contentDescription = null,&#xA;        modifier = Modifier&#xA;          .matchParentSize()&#xA;          .padding(padding.value)&#xA;          .clip(RoundedCornerShape(roundedCornerShape.value))&#xA;      )&#xA;      if (inSelectionMode) {&#xA;        if (selected) {&#xA;          val bgColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)&#xA;          Icon(&#xA;            Icons.Filled.CheckCircle,&#xA;            tint = MaterialTheme.colorScheme.primary,&#xA;            contentDescription = null,&#xA;            modifier = Modifier&#xA;              .padding(4.dp)&#xA;              .border(2.dp, bgColor, CircleShape)&#xA;              .clip(CircleShape)&#xA;              .background(bgColor)&#xA;          )&#xA;        } else {&#xA;          Icon(&#xA;            Icons.Filled.RadioButtonUnchecked,&#xA;            tint = Color.White.copy(alpha = 0.7f),&#xA;            contentDescription = null,&#xA;            modifier = Modifier.padding(6.dp)&#xA;          )&#xA;        }&#xA;      }&#xA;    }&#xA;  }&#xA;}&#xA;&#xA;fun randomSampleImageUrl() = &#34;https://picsum.photos/seed/${(0..100000).random()}/256/256&#34;خُبب! امیدوارم خوشتون اومده باشه، اگه نظری، پیشنهادی، باگی چیزی بود لطفا تو کامنت ها به اشتراک بذارین. منبعکد هارو یکجا میتونین تو این gist ببینینکانال تلگرام Android Corner</description><category>Android Corner</category><author>Android Corner</author><pubDate>Wed, 09 Aug 2023 18:16:15 +0330</pubDate></item></channel></rss>