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. در Android Studio، کد را کاوش کنید تا نحوه عملکرد برنامه را درک کنید.
  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 فقط باید دارای منطقی باشد که تعاملات رابط کاربری و سیستم عامل مانند نمایش نماها و گرفتن ورودی کاربر را مدیریت کند. منطق تصمیم گیری، مانند منطقی که متن برای نمایش را تعیین می کند، در کنترلر 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 از بین برود، callback 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 در برنامه شروع به شرح زیر است:

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

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

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

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

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

    متغیر binding، GameFragmentBinding را جابه جا نکنید، زیرا حاوی ارجاعاتی به view ها است. این متغیر برای بزرگ کردن طرح، تنظیم شنوندگان کلیک و نمایش داده ها بر روی صفحه - مسئولیت های قطعه استفاده می شود.
  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( 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 اضافه کنید. 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 ، اندروید استودیو خطای یک عضو انتزاعی اجرا نشده را نشان می‌دهد. برای رفع خطا، متد 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 تغییر دادید. شما مشکل چرخه عمر برنامه را حل کردید و اکنون داده های بازی از تغییرات پیکربندی جان سالم به در می برند. همچنین یاد گرفتید که چگونه یک سازنده پارامتری برای ایجاد ViewModel با استفاده از رابط ViewModelFactory ایجاد کنید.

پروژه اندروید استودیو: 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 مراجعه کنید.