Android Kotlin Fundamentals 05.1: ViewModel و ViewModelFactory

این کد لبه بخشی از دوره آموزشی Android Kotlin Fundamentals است. اگر به ترتیب روی کدها کار کنید، بیشترین ارزش را از این دوره خواهید گرفت. همه کد لبه های دوره در صفحه فرود کد لبه های Android Kotlin Fundamentals فهرست شده اند.

صفحه عنوان

صفحه نمایش بازی

صفحه نمایش امتیاز

مقدمه

در این کد لبه، با یکی از اجزای معماری اندروید، ViewModel آشنا می شوید:

  • شما از کلاس ViewModel برای ذخیره و مدیریت داده های مرتبط با رابط کاربری به روشی آگاهانه از چرخه حیات استفاده می کنید. کلاس ViewModel به داده ها اجازه می دهد تا از تغییرات پیکربندی دستگاه مانند چرخش صفحه و تغییرات در دسترس بودن صفحه کلید زنده بمانند.
  • شما از کلاس ViewModelFactory برای نمونه سازی و برگرداندن شی ViewModel که از تغییرات پیکربندی جان سالم به در می برد استفاده می کنید.

آنچه از قبل باید بدانید

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

چیزی که یاد خواهید گرفت

  • نحوه استفاده از معماری برنامه اندروید توصیه شده
  • چگونه از کلاس های Lifecycle ، ViewModel و ViewModelFactory در برنامه خود استفاده کنید.
  • نحوه حفظ داده های رابط کاربری از طریق تغییرات پیکربندی دستگاه
  • الگوی طراحی روش کارخانه چیست و چگونه از آن استفاده کنیم.
  • نحوه ایجاد یک شی ViewModel با استفاده از رابط ViewModelProvider.Factory .

کاری که خواهی کرد

  • یک ViewModel به برنامه اضافه کنید تا داده های برنامه ذخیره شود تا داده ها از تغییرات پیکربندی جان سالم به در ببرند.
  • از ViewModelFactory و الگوی طراحی با روش کارخانه برای نمونه سازی یک شی ViewModel با پارامترهای سازنده استفاده کنید.

در بخش کدهای درس 5، برنامه GuessTheWord را توسعه می دهید که با کد شروع شروع می شود. GuessTheWord یک بازی دو نفره به سبک charades است که در آن بازیکنان برای دستیابی به بالاترین امتیاز ممکن با یکدیگر همکاری می کنند.

بازیکن اول به کلمات موجود در برنامه نگاه می کند و هر کدام را به نوبه خود عمل می کند و مطمئن می شود که کلمه را به بازیکن دوم نشان نمی دهد. بازیکن دوم سعی می کند کلمه را حدس بزند.

برای انجام بازی، اولین بازیکن برنامه را روی دستگاه باز می کند و کلمه ای را می بیند، به عنوان مثال "گیتار"، همانطور که در تصویر زیر نشان داده شده است.

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

  • وقتی بازیکن دوم کلمه را به درستی حدس زد، بازیکن اول دکمه Got It را فشار می دهد که تعداد را یک عدد افزایش می دهد و کلمه بعدی را نشان می دهد.
  • اگر بازیکن دوم نتواند کلمه را حدس بزند، بازیکن اول دکمه Skip را فشار می دهد که تعداد را یک کاهش می دهد و به کلمه بعدی می رود.
  • برای پایان بازی، دکمه پایان بازی را فشار دهید. (این قابلیت در کد شروع برای اولین آزمایشگاه کد در این سری نیست.)

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

مرحله 1: شروع کنید

  1. کد شروع GuessTheWord را دانلود کرده و پروژه را در Android Studio باز کنید.
  2. برنامه را روی یک دستگاه مجهز به اندروید یا شبیه ساز اجرا کنید.
  3. روی دکمه ها ضربه بزنید. توجه داشته باشید که دکمه Skip کلمه بعدی را نمایش می دهد و امتیاز را یک بار کاهش می دهد و دکمه Got It کلمه بعدی را نشان می دهد و امتیاز را یک افزایش می دهد. دکمه پایان بازی اجرا نمی شود، بنابراین با ضربه زدن روی آن هیچ اتفاقی نمی افتد.

مرحله 2: یک مرور کد انجام دهید

  1. در اندروید استودیو، کد را کاوش کنید تا از نحوه عملکرد برنامه مطلع شوید.
  2. مطمئن شوید که به فایل‌هایی که در زیر توضیح داده شده‌اند، که اهمیت ویژه‌ای دارند، نگاه کنید.

MainActivity.kt

این فایل فقط حاوی کدهای پیش‌فرض و ایجاد شده توسط الگو است.

res/layout/main_activity.xml

این فایل حاوی طرح اصلی برنامه است. NavHostFragment سایر قطعات را در حین حرکت کاربر در برنامه میزبانی می کند.

قطعات UI

کد شروع دارای سه قطعه در سه بسته مختلف در زیر بسته com.example.android.guesstheword.screens است:

  • title/TitleFragment برای صفحه عنوان
  • game/GameFragment برای صفحه بازی
  • score/ScoreFragment برای صفحه نمایش امتیاز

screens/title/TitleFragment.kt

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

screens/game/GameFragment.kt

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

  • متغیرهایی برای کلمه فعلی و نمره فعلی تعریف شده است.
  • wordList تعریف شده در متد resetList() یک لیست نمونه از کلماتی است که در بازی استفاده می شود.
  • متد onSkip() کنترل کننده کلیک برای دکمه Skip است. امتیاز را 1 کاهش می دهد، سپس کلمه بعدی را با استفاده از متد nextWord() نمایش می دهد.
  • متد onCorrect() کنترل کننده کلیک برای دکمه Got It است. این متد مشابه متد onSkip() پیاده سازی می شود. تنها تفاوت این است که این روش به جای کم کردن 1 به امتیاز اضافه می کند.

screens/score/ScoreFragment.kt

ScoreFragment صفحه آخر بازی است و امتیاز نهایی بازیکن را نمایش می دهد. در این کد لبه، پیاده سازی را اضافه می کنید تا این صفحه نمایش داده شود و امتیاز نهایی نمایش داده شود.

res/navigation/main_navigation.xml

نمودار ناوبری نحوه اتصال قطعات از طریق ناوبری را نشان می دهد:

  • از قسمت عنوان، کاربر می تواند به قطعه بازی حرکت کند.
  • از قطعه بازی، کاربر می تواند به قطعه امتیاز حرکت کند.
  • از قسمت امتیاز، کاربر می تواند به قطعه بازی برگردد.

در این کار، مشکلات مربوط به برنامه شروع GuessTheWord را پیدا می کنید.

  1. کد شروع را اجرا کنید و بازی را با چند کلمه اجرا کنید، روی Skip یا Got It بعد از هر کلمه ضربه بزنید.
  2. صفحه بازی اکنون یک کلمه و امتیاز فعلی را نشان می دهد. جهت صفحه نمایش را با چرخاندن دستگاه یا شبیه ساز تغییر دهید. توجه داشته باشید که امتیاز فعلی از دست رفته است.
  3. بازی را با چند کلمه دیگر اجرا کنید. وقتی صفحه بازی با مقداری امتیاز نمایش داده شد، برنامه را ببندید و دوباره باز کنید. توجه داشته باشید که بازی از ابتدا ریستارت می شود، زیرا وضعیت برنامه ذخیره نشده است.
  4. بازی را با چند کلمه اجرا کنید، سپس روی دکمه پایان بازی ضربه بزنید. توجه کنید که هیچ اتفاقی نمی افتد.

مشکلات برنامه:

  • برنامه شروع کننده در طول تغییرات پیکربندی، مانند زمانی که جهت دستگاه تغییر می کند، یا زمانی که برنامه خاموش می شود و دوباره راه اندازی می شود، وضعیت برنامه را ذخیره و بازیابی نمی کند.
    می توانید این مشکل را با استفاده از callback onSaveInstanceState() حل کنید. با این حال، استفاده از متد onSaveInstanceState() مستلزم نوشتن کد اضافی برای ذخیره وضعیت در یک بسته و پیاده سازی منطق برای بازیابی آن حالت است. همچنین، مقدار داده ای که می توان ذخیره کرد حداقل است.
  • وقتی کاربر روی دکمه پایان بازی ضربه می‌زند، صفحه بازی به صفحه امتیاز حرکت نمی‌کند.

می‌توانید این مشکلات را با استفاده از اجزای معماری برنامه‌ای که در این نرم‌افزار یاد می‌گیرید حل کنید.

معماری اپلیکیشن

معماری برنامه راهی برای طراحی کلاس های برنامه های شما و روابط بین آنها است، به گونه ای که کد سازماندهی شده است، در سناریوهای خاص به خوبی عمل می کند و کار با آن آسان است. در این مجموعه چهار کد، بهبودهایی که در برنامه GuessTheWord ایجاد می‌کنید از دستورالعمل‌های معماری برنامه اندروید پیروی می‌کنند و از اجزای معماری Android استفاده می‌کنید. معماری اپلیکیشن اندروید مشابه الگوی معماری MVVM (model-view-viewmodel) است.

برنامه GuessTheWord از اصل طراحی جداسازی نگرانی‌ها پیروی می‌کند و به کلاس‌هایی تقسیم می‌شود که هر کلاس به نگرانی جداگانه‌ای می‌پردازد. در این اولین کد لبه درس، کلاس هایی که با آنها کار می کنید یک کنترلر UI، یک ViewModel و یک ViewModelFactory هستند.

کنترلر رابط کاربری

یک کنترلر UI یک کلاس مبتنی بر UI مانند Activity یا Fragment است. یک کنترلر UI فقط باید دارای منطقی باشد که تعاملات رابط کاربری و سیستم عامل مانند نمایش نماها و گرفتن ورودی کاربر را کنترل کند. منطق تصمیم گیری، مانند منطقی که متن مورد نظر را تعیین می کند، در کنترلر رابط کاربری قرار ندهید.

در کد شروع GuessTheWord، کنترل‌کننده‌های رابط کاربری سه بخش هستند: GameFragment ، ScoreFragment, و TitleFragment . با پیروی از اصل طراحی «جداسازی نگرانی‌ها»، GameFragment تنها وظیفه ترسیم عناصر بازی روی صفحه و دانستن زمانی که کاربر روی دکمه‌ها ضربه می‌زند را بر عهده دارد و نه بیشتر. وقتی کاربر روی دکمه ای ضربه می زند، این اطلاعات به GameViewModel منتقل می شود.

ViewModel

یک ViewModel داده هایی را برای نمایش در یک قطعه یا فعالیت مرتبط با ViewModel نگه می دارد. یک ViewModel می تواند محاسبات و تبدیل های ساده ای را روی داده ها انجام دهد تا داده ها را برای نمایش توسط کنترلر UI آماده کند. در این معماری، ViewModel تصمیم گیری را انجام می دهد.

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

ViewModelFactory

یک ViewModelFactory اشیاء ViewModel با یا بدون پارامترهای سازنده نمونه‌سازی می‌کند.

در کدهای بعدی، با سایر مؤلفه‌های معماری Android مرتبط با کنترل‌کننده‌های رابط کاربری و ViewModel آشنا می‌شوید.

کلاس ViewModel برای ذخیره و مدیریت داده های مربوط به UI طراحی شده است. در این برنامه، هر ViewModel با یک قطعه مرتبط است.

در این کار، اولین ViewModel خود را به برنامه خود اضافه می کنید، GameViewModel برای GameFragment . همچنین می آموزید که به چه معناست که ViewModel از چرخه حیات آگاه است.

مرحله 1: کلاس GameViewModel را اضافه کنید

  1. فایل build.gradle(module:app) را باز کنید. در داخل بلوک dependencies ، وابستگی Gradle را برای ViewModel اضافه کنید.

    اگر از آخرین نسخه کتابخانه استفاده می کنید، برنامه راه حل باید همانطور که انتظار می رود کامپایل شود. اگر نشد، سعی کنید مشکل را حل کنید یا به نسخه نشان داده شده در زیر برگردید.
//ViewModel
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
  1. در پوشه بسته screens/game/ یک کلاس Kotlin جدید به نام GameViewModel ایجاد کنید.
  2. کلاس GameViewModel را گسترش دهید تا کلاس انتزاعی ViewModel گسترش دهد.
  3. برای کمک به درک بهتر اینکه ViewModel چگونه از چرخه حیات آگاه است، یک بلوک init با یک دستور log اضافه کنید.
class GameViewModel : ViewModel() {
   init {
       Log.i("GameViewModel", "GameViewModel created!")
   }
}

مرحله 2: onCleared() را لغو کنید و لاگ را اضافه کنید

ViewModel زمانی که قطعه مرتبط جدا می شود یا زمانی که فعالیت به پایان می رسد از بین می رود. درست قبل از اینکه ViewModel از بین برود، فراخوانی onCleared() برای پاکسازی منابع فراخوانی می شود.

  1. در کلاس GameViewModel ، متد onCleared() لغو کنید.
  2. برای ردیابی چرخه عمر GameViewModel یک بیانیه log در داخل onCleared() اضافه کنید.
override fun onCleared() {
   super.onCleared()
   Log.i("GameViewModel", "GameViewModel destroyed!")
}

مرحله 3: GameViewModel را با قطعه بازی مرتبط کنید

یک ViewModel باید با یک کنترلر UI مرتبط شود. برای مرتبط کردن این دو، یک مرجع به ViewModel در داخل کنترلر UI ایجاد می کنید.

در این مرحله، شما یک مرجع از GameViewModel را در داخل کنترلر UI مربوطه ایجاد می کنید که GameFragment است.

  1. در کلاس GameFragment ، یک فیلد از نوع GameViewModel در سطح بالا به عنوان متغیر کلاس اضافه کنید.
private lateinit var viewModel: GameViewModel

مرحله 4: ViewModel را راه اندازی کنید

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

نحوه عملکرد ViewModelProvider :

  • ViewModelProvider یک ViewModel موجود را در صورت وجود برمی‌گرداند، یا اگر قبلاً وجود نداشته باشد، یک مدل جدید ایجاد می‌کند.
  • ViewModelProvider یک نمونه ViewModel را در ارتباط با محدوده داده شده (یک فعالیت یا یک قطعه) ایجاد می کند.
  • ViewModel ایجاد شده تا زمانی که scope زنده است حفظ می شود. به عنوان مثال، اگر محدوده یک قطعه باشد، ViewModel تا زمانی که قطعه جدا نشود حفظ می شود.

ViewModel با استفاده از روش ViewModelProviders.of() برای ایجاد ViewModelProvider راه اندازی کنید:

  1. در کلاس GameFragment ، متغیر viewModel مقداردهی اولیه کنید. این کد را بعد از تعریف متغیر binding داخل onCreateView() قرار دهید. از متد ViewModelProviders.of() استفاده کنید و در زمینه GameFragment مرتبط و کلاس GameViewModel عبور دهید.
  2. بالای مقداردهی اولیه شی ViewModel ، یک دستور log اضافه کنید تا فراخوانی متد ViewModelProviders.of() را ثبت کنید.
Log.i("GameFragment", "Called ViewModelProviders.of")
viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)
  1. برنامه را اجرا کنید. در Android Studio، پنجره Logcat را باز کنید و روی Game فیلتر کنید. روی دکمه Play در دستگاه یا شبیه ساز خود ضربه بزنید. صفحه بازی باز می شود.

    همانطور که در Logcat نشان داده شده است، متد onCreateView() GameFragment متد ViewModelProviders.of() را برای ایجاد GameViewModel فراخوانی می کند. عبارات گزارشی که به GameFragment و GameViewModel اضافه کردید در Logcat نشان داده می شوند.

  1. تنظیم چرخش خودکار را در دستگاه یا شبیه ساز خود فعال کنید و جهت صفحه را چند بار تغییر دهید. GameFragment هر بار از بین می رود و دوباره ایجاد می شود، بنابراین ViewModelProviders.of() هر بار فراخوانی می شود. اما GameViewModel فقط یک بار ایجاد می شود و برای هر تماس دوباره ایجاد یا از بین نمی رود.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
  1. از بازی خارج شوید یا از قطعه بازی خارج شوید. GameFragment نابود می شود. GameViewModel مرتبط نیز از بین می رود و callback onCleared() فراخوانی می شود.
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel created!
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameFragment: Called ViewModelProviders.of
I/GameViewModel: GameViewModel destroyed!

ViewModel از تغییرات پیکربندی جان سالم به در می‌برد، بنابراین مکان مناسبی برای داده‌هایی است که نیاز به تغییرات پیکربندی دارند:

  • داده هایی را برای نمایش روی صفحه و کدی را برای پردازش آن داده ها در ViewModel قرار دهید.
  • ViewModel هرگز نباید حاوی ارجاع به قطعات، فعالیت‌ها یا نماها باشد، زیرا فعالیت‌ها، قطعات و نماها از تغییرات پیکربندی دوام نمی‌آورند.

برای مقایسه، قبل از اینکه ViewModel اضافه کنید و بعد از اضافه کردن ViewModel ، داده های GameFragment UI را چگونه در برنامه شروع کار می کنند:

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

در این کار، داده های رابط کاربری برنامه را به همراه روش های پردازش داده ها به کلاس GameViewModel منتقل می کنید. شما این کار را انجام می دهید تا داده ها در طول تغییرات پیکربندی حفظ شوند.

مرحله 1: فیلدهای داده و پردازش داده ها را به ViewModel منتقل کنید

فیلدهای داده و متدهای زیر را از GameFragment به GameViewModel منتقل کنید:

  1. فیلدهای داده word ، score و wordList را جابه جا کنید. مطمئن شوید که word و score private نیستند.

    متغیر binding، GameFragmentBinding جابه جا نکنید، زیرا حاوی ارجاعاتی به نماها است. این متغیر برای افزایش طرح، تنظیم شنوندگان کلیک و نمایش داده ها بر روی صفحه استفاده می شود - مسئولیت های قطعه.
  2. متدهای resetList() و nextWord() را جابه جا کنید. این روش ها تصمیم می گیرند که چه کلمه ای روی صفحه نمایش داده شود.
  3. از داخل متد onCreateView() ، فراخوانی های متد را به resetList() و nextWord() به بلوک init GameViewModel منتقل کنید.

    این متدها باید در بلوک init باشند، زیرا باید لیست کلمات را هنگام ایجاد ViewModel بازنشانی کنید، نه هر بار که قطعه ایجاد می شود. می توانید عبارت log را در بلوک init GameFragment حذف کنید.

کنترل کننده های کلیک onSkip() و onCorrect() در GameFragment حاوی کدهایی برای پردازش داده ها و به روز رسانی رابط کاربری هستند. کد به‌روزرسانی رابط کاربری باید در قطعه باقی بماند، اما کد پردازش داده‌ها باید به ViewModel منتقل شود.

در حال حاضر، روش های یکسان را در هر دو مکان قرار دهید:

  1. متدهای onSkip() و onCorrect() را از GameFragment در GameViewModel کپی کنید.
  2. در GameViewModel ، مطمئن شوید که متدهای onSkip() و onCorrect() private نیستند، زیرا به این متدها از قطعه ارجاع خواهید داد.

در اینجا کد کلاس GameViewModel پس از refactoring آمده است:

class GameViewModel : ViewModel() {
   // The current word
   var word = ""
   // The current score
   var score = 0
   // The list of words - the front of the list is the next word to guess
   private lateinit var wordList: MutableList<String>

   /**
    * Resets the list of words and randomizes the order
    */
   private fun resetList() {
       wordList = mutableListOf(
               "queen",
               "hospital",
               "basketball",
               "cat",
               "change",
               "snail",
               "soup",
               "calendar",
               "sad",
               "desk",
               "guitar",
               "home",
               "railway",
               "zebra",
               "jelly",
               "car",
               "crow",
               "trade",
               "bag",
               "roll",
               "bubble"
       )
       wordList.shuffle()
   }

   init {
       resetList()
       nextWord()
       Log.i("GameViewModel", "GameViewModel created!")
   }
   /**
    * Moves to the next word in the list
    */
   private fun nextWord() {
       if (!wordList.isEmpty()) {
           //Select and remove a word from the list
           word = wordList.removeAt(0)
       }
       updateWordText()
       updateScoreText()
   }
 /** Methods for buttons presses **/
   fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }

   override fun onCleared() {
       super.onCleared()
       Log.i("GameViewModel", "GameViewModel destroyed!")
   }
}

در اینجا کد کلاس GameFragment ، پس از refactoring آمده است:

/**
* Fragment where the game is played
*/
class GameFragment : Fragment() {


   private lateinit var binding: GameFragmentBinding


   private lateinit var viewModel: GameViewModel


   override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                             savedInstanceState: Bundle?): View? {

       // Inflate view and obtain an instance of the binding class
       binding = DataBindingUtil.inflate(
               inflater,
               R.layout.game_fragment,
               container,
               false
       )

       Log.i("GameFragment", "Called ViewModelProviders.of")
       viewModel = ViewModelProviders.of(this).get(GameViewModel::class.java)

       binding.correctButton.setOnClickListener { onCorrect() }
       binding.skipButton.setOnClickListener { onSkip() }
       updateScoreText()
       updateWordText()
       return binding.root

   }


   /** Methods for button click handlers **/

   private fun onSkip() {
       if (!wordList.isEmpty()) {
           score--
       }
       nextWord()
   }

   private fun onCorrect() {
       if (!wordList.isEmpty()) {
           score++
       }
       nextWord()
   }


   /** Methods for updating the UI **/

   private fun updateWordText() {
       binding.wordText.text = word
   }

   private fun updateScoreText() {
       binding.scoreText.text = score.toString()
   }
}

مرحله 2: مراجع را برای کنترل کننده های کلیک و فیلدهای داده در GameFragment به روز کنید

  1. در GameFragment ، متدهای onSkip() و onCorrect() به روز کنید. کد را برای به روز رسانی امتیاز حذف کنید و در عوض متدهای onSkip() و onCorrect() مربوطه را در viewModel فراخوانی کنید.
  2. از آنجایی که متد nextWord() را به ViewModel منتقل کردید، قطعه بازی دیگر نمی تواند به آن دسترسی داشته باشد.

    در GameFragment ، در متدهای onSkip() و onCorrect() ، فراخوانی به nextWord() با updateScoreText() و updateWordText() جایگزین کنید. این روش ها داده ها را روی صفحه نمایش می دهند.
private fun onSkip() {
   viewModel.onSkip()
   updateWordText()
   updateScoreText()
}
private fun onCorrect() {
   viewModel.onCorrect()
   updateScoreText()
   updateWordText()
}
  1. در GameFragment ، متغیرهای score و word را برای استفاده از متغیرهای GameViewModel به روز کنید، زیرا این متغیرها اکنون در GameViewModel هستند.
private fun updateWordText() {
   binding.wordText.text = viewModel.word
}

private fun updateScoreText() {
   binding.scoreText.text = viewModel.score.toString()
}
  1. در GameViewModel ، در متد nextWord() فراخوانی متدهای updateWordText() و updateScoreText() حذف کنید. اکنون این متدها از GameFragment فراخوانی می شوند.
  2. برنامه را بسازید و مطمئن شوید که هیچ خطایی وجود ندارد. اگر خطا دارید، پروژه را تمیز و بازسازی کنید.
  3. برنامه را اجرا کنید و بازی را از طریق چند کلمه انجام دهید. در حالی که در صفحه بازی هستید، دستگاه را بچرخانید. توجه داشته باشید که نمره فعلی و کلمه فعلی پس از تغییر جهت حفظ می شوند.

کار عالی! اکنون تمام داده های برنامه شما در ViewModel ذخیره می شود، بنابراین در طول تغییرات پیکربندی حفظ می شود.

در این کار، شنونده کلیک را برای دکمه پایان بازی پیاده سازی می کنید.

  1. در GameFragment ، متدی به نام onEndGame() اضافه کنید. متد onEndGame() زمانی فراخوانی می شود که کاربر روی دکمه پایان بازی ضربه بزند.
private fun onEndGame() {
   }
  1. در GameFragment ، در داخل متد onCreateView() ، کدی را پیدا کنید که شنوندگان کلیک را برای دکمه‌های Got It و Skip تنظیم می‌کند. درست در زیر این دو خط، یک کلیک شنونده برای دکمه پایان بازی تنظیم کنید. از متغیر binding، binding استفاده کنید. در داخل شنونده کلیک، متد onEndGame() فراخوانی کنید.
binding.endGameButton.setOnClickListener { onEndGame() }
  1. در GameFragment ، روشی به نام gameFinished() اضافه کنید تا برنامه را به صفحه امتیاز هدایت کنید. با استفاده از Safe Args ، امتیاز را به عنوان استدلال ارسال کنید.
/**
* Called when the game is finished
*/
private fun gameFinished() {
   Toast.makeText(activity, "Game has just finished", Toast.LENGTH_SHORT).show()
   val action = GameFragmentDirections.actionGameToScore()
   action.score = viewModel.score
   NavHostFragment.findNavController(this).navigate(action)
}
  1. در متد onEndGame() ، متد gameFinished() فراخوانی کنید.
private fun onEndGame() {
   gameFinished()
}
  1. برنامه را اجرا کنید، بازی را انجام دهید و برخی از کلمات را مرور کنید. روی دکمه پایان بازی ضربه بزنید. توجه داشته باشید که برنامه به صفحه امتیاز حرکت می کند، اما امتیاز نهایی نمایش داده نمی شود. شما این را در کار بعدی حل می کنید.

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

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

در این کار، یک ViewModel با یک سازنده پارامتری برای قطعه امتیاز و یک روش کارخانه برای نمونه سازی ViewModel ایجاد می کنید.

  1. تحت بسته score ، یک کلاس Kotlin جدید به نام ScoreViewModel ایجاد کنید. این کلاس ViewModel برای قطعه امتیاز خواهد بود.
  2. کلاس ScoreViewModel را از ViewModel. یک پارامتر سازنده برای نمره نهایی اضافه کنید. یک بلوک init با یک دستور log اضافه کنید.
  3. در کلاس ScoreViewModel متغیری به نام score اضافه کنید تا امتیاز نهایی ذخیره شود.
class ScoreViewModel(finalScore: Int) : ViewModel() {
   // The final score
   var score = finalScore
   init {
       Log.i("ScoreViewModel", "Final score is $finalScore")
   }
}
  1. در زیر بسته score ، یک کلاس Kotlin دیگر به نام ScoreViewModelFactory ایجاد کنید. این کلاس مسئول نمونه سازی شی ScoreViewModel خواهد بود.
  2. کلاس ScoreViewModelFactory از ViewModelProvider.Factory گسترش دهید. یک پارامتر سازنده برای نمره نهایی اضافه کنید.
class ScoreViewModelFactory(private val finalScore: Int) : ViewModelProvider.Factory {
}
  1. در ScoreViewModelFactory ، Android Studio خطایی را در مورد یک عضو انتزاعی اجرا نشده نشان می دهد. برای رفع خطا، متد create() لغو کنید. در متد create() ، شی ScoreViewModel جدید ساخته شده را برگردانید.
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
   if (modelClass.isAssignableFrom(ScoreViewModel::class.java)) {
       return ScoreViewModel(finalScore) as T
   }
   throw IllegalArgumentException("Unknown ViewModel class")
}
  1. در ScoreFragment ، متغیرهای کلاس را برای ScoreViewModel و ScoreViewModelFactory ایجاد کنید.
private lateinit var viewModel: ScoreViewModel
private lateinit var viewModelFactory: ScoreViewModelFactory
  1. در ScoreFragment ، داخل onCreateView() ، پس از مقداردهی اولیه متغیر binding ، viewModelFactory مقداردهی اولیه کنید. از ScoreViewModelFactory استفاده کنید. امتیاز نهایی را از بسته آرگومان به عنوان پارامتر سازنده به ScoreViewModelFactory() منتقل کنید.
viewModelFactory = ScoreViewModelFactory(ScoreFragmentArgs.fromBundle(arguments!!).score)
  1. در onCreateView( )، پس از مقداردهی اولیه viewModelFactory ، شی viewModel مقداردهی اولیه کنید. متد ViewModelProviders.of() را فراخوانی کنید، در زمینه قطعه امتیاز مرتبط و viewModelFactory عبور دهید. این شیء ScoreViewModel با استفاده از متد کارخانه ای تعریف شده در کلاس viewModelFactory ایجاد می کند .
viewModel = ViewModelProviders.of(this, viewModelFactory)
       .get(ScoreViewModel::class.java)
  1. در متد onCreateView() پس از مقداردهی اولیه viewModel ، متن نمای scoreText را روی امتیاز نهایی تعریف شده در ScoreViewModel قرار دهید.
binding.scoreText.text = viewModel.score.toString()
  1. برنامه خود را اجرا کنید و بازی را انجام دهید. برخی یا همه کلمات را دور بزنید و روی پایان بازی ضربه بزنید. توجه داشته باشید که بخش امتیاز اکنون امتیاز نهایی را نشان می دهد.

  1. اختیاری: گزارش های ScoreViewModel را در Logcat با فیلتر کردن در ScoreViewModel بررسی کنید. مقدار امتیاز باید نمایش داده شود.
2019-02-07 10:50:18.328 com.example.android.guesstheword I/ScoreViewModel: Final score is 15

در این کار، ScoreFragment برای استفاده ViewModel پیاده سازی کردید. همچنین یاد گرفتید که چگونه با استفاده از رابط ViewModelFactory یک سازنده پارامتری برای ViewModel ایجاد کنید.

تبریک می گویم! شما معماری برنامه خود را برای استفاده از یکی از اجزای معماری Android، ViewModel تغییر دادید. شما مشکل چرخه عمر برنامه را حل کردید و اکنون داده های بازی از تغییرات پیکربندی جان سالم به در می برند. همچنین یاد گرفتید که چگونه با استفاده از رابط ViewModelFactory یک سازنده پارامتری برای ایجاد ViewModel ایجاد کنید.

پروژه اندروید استودیو: GuessTheWord

  • دستورالعمل‌های معماری برنامه اندروید جداسازی کلاس‌هایی را که مسئولیت‌های متفاوتی دارند توصیه می‌کند.
  • یک کنترلر UI کلاس مبتنی بر UI مانند Activity یا Fragment است. کنترل‌کننده‌های UI فقط باید دارای منطقی باشند که تعاملات UI و سیستم عامل را مدیریت می‌کند. آنها نباید حاوی داده هایی برای نمایش در UI باشند. آن داده ها را در ViewModel قرار دهید.
  • کلاس ViewModel داده های مربوط به UI را ذخیره و مدیریت می کند. کلاس ViewModel به داده ها اجازه می دهد تا از تغییرات پیکربندی مانند چرخش صفحه زنده بمانند.
  • ViewModel یکی از مؤلفه‌های معماری اندروید توصیه شده است.
  • ViewModelProvider.Factory یک رابط است که می توانید از آن برای ایجاد یک شی ViewModel استفاده کنید.

جدول زیر کنترل‌کننده‌های UI را با نمونه‌های ViewModel که داده‌ها را برای آنها نگه می‌دارد مقایسه می‌کند:

کنترلر رابط کاربری

ViewModel

نمونه ای از یک کنترلر UI ScoreFragment است که در این کد لبه ایجاد کرده اید.

نمونه ای از ViewModel ScoreViewModel است که در این کد لبه ایجاد کرده اید.

هیچ داده ای برای نمایش در رابط کاربری ندارد.

حاوی داده هایی است که کنترلر رابط کاربری در رابط کاربری نمایش می دهد.

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

حاوی کد برای پردازش داده ها

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

تنها زمانی از بین می‌رود که کنترل‌کننده رابط کاربری مرتبط برای همیشه از بین برود - برای یک فعالیت، زمانی که فعالیت تمام می‌شود، یا برای یک قطعه، وقتی قطعه جدا می‌شود.

شامل نماها

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

حاوی ارجاع به ViewModel مرتبط است.

هیچ ارجاعی به کنترلر رابط کاربری مربوطه ندارد.

دوره بی ادبی:

مستندات توسعه دهنده اندروید:

دیگر:

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

  • در صورت نیاز تکالیف را تعیین کنید.
  • نحوه ارسال تکالیف را با دانش آموزان در میان بگذارید.
  • تکالیف را نمره دهید.

مربیان می‌توانند از این پیشنهادات به اندازه‌ای که می‌خواهند استفاده کنند، و باید با خیال راحت هر تکلیف دیگری را که فکر می‌کنند مناسب است، محول کنند.

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

به این سوالات پاسخ دهید

سوال 1

برای جلوگیری از از دست رفتن داده ها در طول تغییر پیکربندی دستگاه، باید داده های برنامه را در کدام کلاس ذخیره کنید؟

  • ViewModel
  • LiveData
  • Fragment
  • Activity

سوال 2

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

  • درست است
  • نادرست

سوال 3

چه زمانی یک ViewModel از بین می رود؟

  • هنگامی که کنترلر رابط کاربری مرتبط با تغییر جهت گیری دستگاه از بین می رود و دوباره ایجاد می شود.
  • در یک تغییر جهت.
  • وقتی کنترلر UI مرتبط تمام شد (اگر یک فعالیت باشد) یا جدا شد (اگر قطعه باشد).
  • هنگامی که کاربر دکمه بازگشت را فشار می دهد.

سوال 4

رابط ViewModelFactory برای چیست؟

  • نمونه سازی یک شی ViewModel .
  • حفظ داده ها در طول تغییر جهت.
  • تازه کردن داده هایی که روی صفحه نمایش داده می شود.
  • دریافت اعلان ها هنگام تغییر داده های برنامه.

درس بعدی را شروع کنید: 5.2: ناظران LiveData و LiveData

برای پیوند به سایر کدهای این دوره، به صفحه فرود کد لبه‌های کد پایه Android Kotlin Fundamentals مراجعه کنید.