هدف از Architecture Components ارائه راهنمایی در مورد معماری برنامه، با کتابخانه هایی برای کارهای رایج مانند مدیریت چرخه حیات و تداوم داده است. اجزای معماری به شما کمک میکنند تا برنامهتان را به گونهای طراحی کنید که قوی، قابل آزمایش و قابل نگهداری با کد دیگ بخار کمتر باشد. کتابخانه های اجزای معماری بخشی از Android Jetpack هستند.
این نسخه Kotlin از Codelab است. نسخه به زبان برنامه نویسی جاوا را می توانید در اینجا بیابید .
اگر در حین کار با این کد با مشکلاتی (اشکالات کد، خطاهای دستوری، عبارت نامشخص و غیره) مواجه شدید، لطفاً مشکل را از طریق پیوند گزارش یک اشتباه در گوشه سمت چپ پایین صفحه کد گزارش کنید.
پیش نیازها
شما باید با Kotlin، مفاهیم طراحی شی گرا و اصول توسعه اندروید آشنا باشید، به ویژه:
-
RecyclerView
و آداپتورها - پایگاه داده SQLite و زبان پرس و جو SQLite
- کوروتینهای اولیه (اگر با کوروتینها آشنا نیستید، میتوانید از طریق استفاده از Kotlin Coroutines در برنامه Android خود بروید .)
همچنین به آشنایی با الگوهای معماری نرم افزاری که داده ها را از رابط کاربری جدا می کند، مانند MVP یا MVC کمک می کند. این Codelab معماری تعریف شده در راهنمای معماری برنامه را پیاده سازی می کند.
این Codelab بر روی اجزای معماری اندروید متمرکز شده است. مفاهیم و کدهای خارج از موضوع برای شما ارائه شده است تا به سادگی کپی و پیست کنید.
اگر با Kotlin آشنایی ندارید، نسخه ای از این کد لبه به زبان برنامه نویسی جاوا در اینجا ارائه شده است.
کاری که خواهی کرد
در این کد لبه، یاد خواهید گرفت که چگونه با استفاده از Architecture Components Room، ViewModel و LiveData یک اپلیکیشن طراحی و بسازید و اپلیکیشنی بسازید که کارهای زیر را انجام دهد:
- معماری توصیه شده ما را با استفاده از اجزای معماری Android پیاده سازی می کند.
- برای دریافت و ذخیره داده ها با پایگاه داده کار می کند و پایگاه داده را با چند کلمه از قبل پر می کند.
- تمام کلمات موجود در
RecyclerView
را درMainActivity
نمایش می دهد. - وقتی کاربر روی دکمه + ضربه میزند، دومین فعالیت را باز میکند. هنگامی که کاربر کلمه ای را وارد می کند، کلمه را به پایگاه داده و لیست اضافه می کند.
این برنامه بدون حاشیه است، اما به اندازه کافی پیچیده است که می توانید از آن به عنوان یک الگو برای ساختن استفاده کنید. در اینجا یک پیش نمایش است:
آنچه شما نیاز دارید
- Android Studio نسخه 3.0 یا بالاتر و آگاهی از نحوه استفاده از آن. اطمینان حاصل کنید که Android Studio و همچنین SDK و Gradle شما به روز شده است.
- یک دستگاه اندروید یا شبیه ساز.
این لبه کد تمام کدهایی را که برای ساختن برنامه کامل نیاز دارید را ارائه می کند.
مراحل زیادی برای استفاده از اجزای معماری و اجرای معماری پیشنهادی وجود دارد. مهمترین چیز این است که یک مدل ذهنی از آنچه در حال وقوع است ایجاد کنید، درک اینکه چگونه قطعات با هم قرار می گیرند و چگونه جریان داده می شود. در حین کار با این کد، فقط کد را کپی و جایگذاری نکنید، بلکه سعی کنید شروع به ایجاد این درک درونی کنید.
اجزای معماری توصیه شده چیست؟
برای معرفی اصطلاحات، در اینجا معرفی کوتاهی از اجزای معماری و نحوه کار آنها با یکدیگر ارائه شده است. توجه داشته باشید که این کد لبه روی زیرمجموعه ای از مؤلفه ها، یعنی LiveData، ViewModel و Room تمرکز می کند. هر جزء با استفاده از آن بیشتر توضیح داده می شود.
این نمودار یک شکل اساسی از معماری را نشان می دهد:
Entity : کلاس Annotated که یک جدول پایگاه داده را هنگام کار با Room توصیف می کند.
پایگاه داده SQLite: در حافظه دستگاه. کتابخانه تداوم اتاق این پایگاه داده را برای شما ایجاد و نگهداری می کند.
DAO : شی دسترسی به داده. نگاشت پرس و جوهای SQL به توابع. وقتی از DAO استفاده میکنید، متدها را فراخوانی میکنید و Room به بقیه رسیدگی میکند.
پایگاه داده اتاق : کار پایگاه داده را ساده می کند و به عنوان یک نقطه دسترسی به پایگاه داده زیرین SQLite عمل می کند ( SQLiteOpenHelper)
. پایگاه داده اتاق از DAO برای ارسال پرس و جو به پایگاه داده SQLite استفاده می کند.
Repository: کلاسی که ایجاد می کنید و در درجه اول برای مدیریت چندین منبع داده استفاده می شود.
ViewModel : به عنوان یک مرکز ارتباطی بین مخزن (داده) و UI عمل می کند. رابط کاربری دیگر نیازی به نگرانی در مورد منشا داده ها ندارد. نمونه های ViewModel از Activity/Fragment recreation جان سالم به در می برند.
LiveData : یک کلاس دارنده داده که قابل مشاهده است. همیشه آخرین نسخه داده ها را در حافظه پنهان نگه می دارد و در صورت تغییر داده ها به ناظران خود اطلاع می دهد. LiveData
از چرخه حیات آگاه است. اجزای UI فقط داده های مربوطه را مشاهده می کنند و مشاهده را متوقف نمی کنند یا از سر نمی گیرند. LiveData به طور خودکار همه اینها را مدیریت می کند زیرا از تغییرات وضعیت چرخه حیات مربوطه هنگام مشاهده آگاه است.
نمای کلی معماری RoomWordSample
نمودار زیر تمام قطعات برنامه را نشان می دهد. هر یک از کادرهای محصور (به جز پایگاه داده SQLite) نشان دهنده کلاسی است که شما ایجاد خواهید کرد.
- Android Studio را باز کنید و روی Start a new Android Studio کلیک کنید.
- در پنجره Create New Project، Empty Activity را انتخاب کرده و Next را بزنید.
- در صفحه بعدی، نام برنامه را RoomWordSample بگذارید و روی Finish کلیک کنید.
در مرحله بعد، باید کتابخانه های مؤلفه را به فایل های Gradle خود اضافه کنید.
- در اندروید استودیو، روی تب Projects کلیک کنید و پوشه Gradle Scripts را باز کنید.
build.gradle
باز کنید ( ماژول: برنامه ).
- با اضافه کردن افزونههای دیگر تعریفشده در بالای
build.gradle
( Module: app )، پلاگینkapt
Annotation Processor Kotlin را اعمال کنید.
apply plugin: 'kotlin-kapt'
- بلوک
packagingOptions
را داخل بلوکandroid
اضافه کنید تا ماژول توابع اتمی را از بسته حذف کنید و از هشدارها جلوگیری کنید.
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}
- کد زیر را در انتهای بلوک
dependencies
ها اضافه کنید.
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
- در
build.gradle
( Project: RoomWordsSample ) خود، همانطور که در کد زیر آمده است، شماره نسخه را به انتهای فایل اضافه کنید.
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}
داده های این برنامه کلمات هستند و برای نگهداری آن مقادیر به یک جدول ساده نیاز دارید:
Room به شما امکان می دهد جداول را از طریق یک Entity ایجاد کنید. بیایید این کار را اکنون انجام دهیم.
- یک فایل کلاس Kotlin جدید به نام
Word
ایجاد کنید که حاوی کلاس دادهWord
است.
این کلاس Entity (که نشان دهنده جدول SQLite است) را برای کلمات شما توصیف می کند. هر ویژگی در کلاس نشان دهنده یک ستون در جدول است. Room در نهایت از این ویژگی ها برای ایجاد جدول و نمونه سازی اشیاء از ردیف های پایگاه داده استفاده می کند.
این هم کد:
data class Word(val word: String)
برای معنی دار کردن کلاس Word
برای پایگاه داده اتاق، باید آن را حاشیه نویسی کنید. حاشیه نویسی نشان می دهد که هر بخش از این کلاس چگونه با یک ورودی در پایگاه داده ارتباط دارد. Room از این اطلاعات برای تولید کد استفاده می کند.
اگر خودتان یادداشتها را تایپ کنید (بهجای چسباندن)، Android Studio کلاسهای حاشیهنویسی را بهطور خودکار وارد میکند.
- همانطور که در این کد نشان داده شده است، کلاس
Word
خود را با حاشیه نویسی به روز کنید:
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
بیایید ببینیم این حاشیه نویسی چه کار می کند:
-
@Entity(tableName =
"word_table"
)
هر کلاس@Entity
نشان دهنده یک جدول SQLite است. بیانیه کلاس خود را حاشیه نویسی کنید تا نشان دهید که یک موجودیت است. اگر می خواهید نام جدول با نام کلاس متفاوت باشد، می توانید نام آن را مشخص کنید. این نام جدول را "word_table" می گذارد. -
@PrimaryKey
هر موجودیتی به یک کلید اولیه نیاز دارد. برای ساده نگه داشتن همه چیز، هر کلمه به عنوان کلید اصلی خود عمل می کند. -
@ColumnInfo(name =
"word"
)
اگر میخواهید با نام متغیر عضو متفاوت باشد، نام ستون را در جدول مشخص میکند. این نام ستون را "کلمه" می گذارد. - هر ویژگی که در پایگاه داده ذخیره میشود باید دید عمومی داشته باشد، که پیشفرض Kotlin است.
میتوانید فهرست کاملی از حاشیهنویسیها را در مرجع خلاصه بسته اتاق پیدا کنید.
DAO چیست؟
در DAO (شیء دسترسی به داده)، کوئری های SQL را مشخص می کنید و آنها را با فراخوانی متد مرتبط می کنید. کامپایلر SQL را بررسی می کند و پرس و جوهایی را از حاشیه نویسی های راحت برای جستارهای رایج، مانند @Insert
می کند. Room از DAO برای ایجاد یک API تمیز برای کد شما استفاده می کند.
DAO باید یک کلاس رابط یا انتزاعی باشد.
به طور پیش فرض، تمام پرس و جوها باید در یک رشته جداگانه اجرا شوند.
اتاق دارای پشتوانه روالها است، که به درخواستهای شما اجازه میدهد با اصلاحکننده suspend
حاشیهنویسی شوند و سپس از یک کوروتین یا از یک تابع تعلیق دیگر فراخوانی شوند.
DAO را پیاده سازی کنید
بیایید یک DAO بنویسیم که پرس و جوهایی برای:
- ترتیب همه کلمات بر اساس حروف الفبا
- درج کلمه
- حذف تمام کلمات
- یک فایل کلاس Kotlin جدید به نام
WordDao
کنید. - کد زیر را کپی کرده و در
WordDao
و در صورت لزوم، واردات را برای کامپایل کردن آن اصلاح کنید.
@Dao
interface WordDao {
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): List<Word>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
بیایید از آن عبور کنیم:
-
WordDao
یک رابط است. DAO ها باید یا رابط باشند یا کلاس های انتزاعی. - حاشیه نویسی
@Dao
آن را به عنوان یک کلاس DAO برای Room شناسایی می کند. -
suspend fun insert(word: Word)
: یک تابع suspend را برای درج یک کلمه اعلام می کند. - حاشیه نویسی
@Insert
یک حاشیه نویسی ویژه روش DAO است که در آن نیازی به ارائه SQL نیست! (همچنین@Delete
و@Update
برای حذف و بهروزرسانی ردیفها وجود دارد، اما شما از آنها در این برنامه استفاده نمیکنید.) -
onConflict = OnConflictStrategy.IGNORE
: استراتژی onConflict انتخاب شده در صورتی که کلمه جدیدی دقیقاً مشابه کلمه قبلی در لیست باشد، نادیده می گیرد. برای دانستن بیشتر در مورد راهبردهای درگیری موجود، مستندات را بررسی کنید. -
suspend fun deleteAll()
: یک تابع suspend را برای حذف همه کلمات اعلام می کند. - هیچ حاشیه نویسی راحتی برای حذف چندین نهاد وجود ندارد، بنابراین با
@Query
عمومی حاشیه نویسی می شود. -
@Query
("DELETE FROM word_table")
:@Query
مستلزم آن است که یک پرس و جوی SQL را به عنوان پارامتر رشته ای برای حاشیه نویسی ارائه دهید، که امکان خواندن عبارت های پیچیده و سایر عملیات را فراهم می کند. -
fun getAlphabetizedWords(): List<Word>
: روشی برای دریافت همه کلمات و بازگرداندنList
Words
. -
@Query(
"SELECT * from word_table ORDER BY word ASC"
)
: پرسشی که فهرستی از کلمات مرتب شده به ترتیب صعودی را برمی گرداند.
وقتی داده ها تغییر می کنند، معمولاً می خواهید اقداماتی مانند نمایش داده های به روز شده در رابط کاربری انجام دهید. این بدان معنی است که شما باید داده ها را مشاهده کنید تا زمانی که تغییر می کنند، بتوانید واکنش نشان دهید.
بسته به نحوه ذخیره داده ها، این می تواند مشکل باشد. مشاهده تغییرات داده ها در چندین مؤلفه برنامه شما می تواند مسیرهای وابستگی صریح و سفت و سخت بین مؤلفه ها ایجاد کند. این امر آزمایش و اشکال زدایی را از جمله موارد دیگر دشوار می کند.
LiveData
، یک کلاس کتابخانه چرخه حیات برای مشاهده داده ها، این مشکل را حل می کند. از یک مقدار بازگشتی از نوع LiveData
در توضیحات روش خود استفاده کنید، و Room تمام کدهای لازم را برای به روز رسانی LiveData
هنگام به روز رسانی پایگاه داده تولید می کند.
در WordDao
، امضای متد getAlphabetizedWords()
را تغییر دهید تا List<Word>
با LiveData
پیچیده شود.
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): LiveData<List<Word>>
بعداً در این کد، تغییرات دادهها را از طریق Observer
در MainActivity
ردیابی میکنید.
پایگاه داده اتاق چیست؟
- Room یک لایه پایگاه داده در بالای پایگاه داده SQLite است.
- اتاق از وظایف دنیوی که قبلاً با
SQLiteOpenHelper
انجام میدادید، مراقبت میکند. - Room از DAO برای ارسال پرس و جو به پایگاه داده خود استفاده می کند.
- بهطور پیشفرض، برای جلوگیری از عملکرد ضعیف رابط کاربری، Room به شما اجازه نمیدهد در رشته اصلی درخواستهایی صادر کنید. وقتی کوئریهای اتاق
LiveData
را برمیگردانند، کوئریها بهطور خودکار بهصورت ناهمزمان بر روی یک رشته پسزمینه اجرا میشوند. - Room بررسی های زمان کامپایل عبارات SQLite را فراهم می کند.
پایگاه داده اتاق را پیاده سازی کنید
کلاس پایگاه داده اتاق شما باید انتزاعی باشد و RoomDatabase
را گسترش دهد. معمولاً برای کل برنامه فقط به یک نمونه از پایگاه داده اتاق نیاز دارید.
حالا یکی بسازیم
- یک فایل کلاس Kotlin به نام
WordRoomDatabase
کنید و این کد را به آن اضافه کنید:
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
return instance
}
}
}
}
بیایید از طریق کد عبور کنیم:
- کلاس پایگاه داده اتاق باید
abstract
باشد وRoomDatabase
را گسترش دهد - کلاس را به عنوان پایگاه داده اتاق با
@Database
حاشیه نویسی می کنید و از پارامترهای annotation برای اعلام موجودیت های متعلق به پایگاه داده و تنظیم شماره نسخه استفاده می کنید. هر موجودیت مربوط به جدولی است که در پایگاه داده ایجاد می شود. انتقال پایگاه داده فراتر از محدوده این Codelab است، بنابراین برای جلوگیری از هشدار ساخت،exportSchema
را در اینجا روی false قرار دادیم. در یک برنامه واقعی، باید فهرستی را برای اتاق تنظیم کنید تا از آن برای صادر کردن طرحواره استفاده کنید تا بتوانید طرح فعلی را در سیستم کنترل نسخه خود بررسی کنید. - پایگاه داده DAO ها را از طریق یک روش انتزاعی "گیرنده" برای هر @Dao نشان می دهد.
- ما برای جلوگیری از باز شدن همزمان چندین نمونه از پایگاه داده، یک تک تن،
WordRoomDatabase,
تعریف کرده ایم. -
getDatabase
تک تن را برمی گرداند. این پایگاه داده را اولین باری که به آن دسترسی پیدا کرد، با استفاده از سازنده پایگاه داده اتاق ایجاد می کند تا یک شیRoomDatabase
در زمینه برنامه از کلاسWordRoomDatabase
و نام آن را"word_database"
.
مخزن چیست؟
یک کلاس مخزن، دسترسی به چندین منبع داده را خلاصه می کند. مخزن بخشی از کتابخانه های اجزای معماری نیست، اما بهترین روش پیشنهادی برای جداسازی کد و معماری است. یک کلاس Repository یک API تمیز برای دسترسی به داده ها به بقیه برنامه ارائه می دهد.
چرا از یک مخزن استفاده کنیم؟
یک مخزن پرس و جوها را مدیریت می کند و به شما امکان می دهد از چندین پشتیبان استفاده کنید. در رایجترین مثال، Repository منطقی را برای تصمیمگیری برای واکشی دادهها از شبکه یا استفاده از نتایج ذخیرهشده در یک پایگاه داده محلی پیادهسازی میکند.
پیاده سازی مخزن
یک فایل کلاس Kotlin به نام WordRepository
کنید و کد زیر را در آن قرار دهید:
// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {
// Room executes all queries on a separate thread.
// Observed LiveData will notify the observer when the data has changed.
val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
مواد اولیه اصلی:
- DAO بر خلاف کل پایگاه داده به سازنده مخزن منتقل می شود. این به این دلیل است که فقط نیاز به دسترسی به DAO دارد، زیرا DAO شامل تمام روشهای خواندن/نوشتن برای پایگاه داده است. نیازی نیست کل پایگاه داده را در معرض مخزن قرار دهید.
- فهرست کلمات یک ملک عمومی است. با دریافت لیست کلمات
LiveData
از Room مقداردهی اولیه می شود. ما می توانیم این کار را به دلیل نحوه تعریف متدgetAlphabetizedWords
برای برگرداندنLiveData
در مرحله "کلاس LiveData" انجام دهیم. Room همه پرس و جوها را در یک رشته جداگانه اجرا می کند. سپسLiveData
مشاهده شده، هنگامی که داده ها تغییر کردند، ناظر را در رشته اصلی مطلع می کند. - اصلاحکننده
suspend
به کامپایلر میگوید که باید از یک کوروتین یا تابع تعلیق دیگری فراخوانی شود.
ViewModel چیست؟
نقش ViewModel
ارائه داده به UI و حفظ تغییرات پیکربندی است. ViewModel
به عنوان یک مرکز ارتباطی بین Repository و UI عمل می کند. همچنین می توانید از ViewModel
برای اشتراک گذاری داده ها بین قطعات استفاده کنید. ViewModel بخشی از کتابخانه چرخه حیات است .
برای راهنمای مقدماتی این موضوع، به ViewModel Overview
یا ViewModels: A Simple Example پست وبلاگ مراجعه کنید.
چرا از ViewModel استفاده کنیم؟
ViewModel
دادههای رابط کاربری برنامه شما را به شیوهای مبتنی بر چرخه حیات نگهداری میکند که از تغییرات پیکربندی جان سالم به در میبرد. جدا کردن دادههای رابط کاربری برنامه از کلاسهای Activity
و Fragment
به شما امکان میدهد بهتر از اصل مسئولیت واحد پیروی کنید: فعالیتها و قطعات شما مسئول کشیدن دادهها به صفحه هستند، در حالی که ViewModel
شما میتواند از نگهداری و پردازش تمام دادههای مورد نیاز برای رابط کاربری مراقبت کند. .
در ViewModel
، از LiveData
برای داده های قابل تغییری که UI استفاده می کند یا نمایش می دهد، استفاده کنید. استفاده از LiveData
چندین مزیت دارد:
- می توانید یک ناظر روی داده ها قرار دهید (به جای نظرسنجی برای تغییرات) و فقط آن را به روز کنید
UI زمانی که داده ها واقعا تغییر می کنند. - Repository و UI کاملاً توسط
ViewModel
از هم جدا شده اند. - هیچ تماس پایگاه داده از
ViewModel
وجود ندارد (همه اینها در مخزن انجام می شود) که باعث می شود کد قابل آزمایش تر باشد.
viewModelScope
در Kotlin، تمام کوروتین ها در یک CoroutineScope
اجرا می شوند. یک scope طول عمر کوروتین ها را از طریق کار خود کنترل می کند. هنگامی که کار یک محدوده را لغو می کنید، تمام کارهای انجام شده در آن محدوده را لغو می کند.
کتابخانه AndroidX lifecycle-viewmodel-ktx
viewModelScope
را به عنوان تابعی از کلاس ViewModel
اضافه می کند که به شما امکان می دهد با scope ها کار کنید.
برای کسب اطلاعات بیشتر در مورد کار با کوروتین ها در ViewModel، مرحله 5 استفاده از Kotlin Coroutine ها را در نرم افزار کد برنامه Android یا Easy Coroutines در Android بررسی کنید: viewModelScope blogpost .
ViewModel را پیاده سازی کنید
یک فایل کلاس Kotlin برای WordViewModel
کنید و این کد را به آن اضافه کنید:
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}
در اینجا ما داریم:
- کلاسی به نام
WordViewModel
ایجاد کرد کهApplication
را به عنوان پارامتر دریافت می کند وAndroidViewModel
گسترش می دهد. - یک متغیر عضو خصوصی برای نگهداری ارجاع به مخزن اضافه کرد.
- یک متغیر عمومی عضو
LiveData
برای ذخیره لیست کلمات اضافه شده است. - یک بلوک
init
ایجاد کرد که یک مرجع بهWordDao
از پایگاهWordRoomDatabase
می کند. - در بلوک
init
،WordRepository
بر اساسWordRoomDatabase
شد. - در بلوک
init
،allWords
LiveData را با استفاده از مخزن مقداردهی اولیه کرد. - یک متد
insert()
insert()
را فراخوانی می کند. به این ترتیب، پیاده سازیinsert()
از UI کپسوله می شود. ما نمیخواهیم insert رشته اصلی را مسدود کند، بنابراین یک کوروتین جدید راهاندازی میکنیم و insert مخزن را فراخوانی میکنیم، که یک تابع تعلیق است. همانطور که گفته شد، ViewModel ها بر اساس چرخه زندگی خود دارای یک محدوده کاری به نامviewModelScope
هستند که در اینجا از آن استفاده می کنیم.
در مرحله بعد، باید طرح XML را برای لیست و موارد اضافه کنید.
این کد لبه فرض می کند که شما با ایجاد طرح بندی در XML آشنا هستید، بنابراین ما فقط کد را در اختیار شما قرار می دهیم.
با قرار دادن والد AppTheme
روی Theme.MaterialComponents.Light.DarkActionBar
، قالب برنامه خود را متریال کنید. یک سبک برای موارد لیست در values/styles.xml
:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:background">@android:color/holo_orange_light</item>
<item name="android:textAppearance">@android:style/TextAppearance.Large</item>
</style>
</resources>
یک layout/recyclerview_item.xml
اضافه کنید:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
در layout/activity_main.xml
، TextView
را با RecyclerView
جایگزین کنید و یک دکمه عمل شناور (FAB) اضافه کنید. اکنون طرح شما باید به شکل زیر باشد:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
tools:listitem="@layout/recyclerview_item"
android:padding="@dimen/big_padding"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"/>
</androidx.constraintlayout.widget.ConstraintLayout>
ظاهر FAB شما باید با عملکرد موجود مطابقت داشته باشد، بنابراین ما می خواهیم نماد را با نماد '+' جایگزین کنیم.
ابتدا باید یک دارایی برداری جدید اضافه کنیم:
- File > New > Vector Asset را انتخاب کنید.
- روی نماد ربات اندروید در قسمت Clip Art: کلیک کنید.
- «افزودن» را جستجو کنید و دارایی «+» را انتخاب کنید. روی OK کلیک کنید.
- پس از آن، روی Next کلیک کنید.
- مسیر نماد را به عنوان
main > drawable
تأیید کنید و برای افزودن دارایی روی Finish کلیک کنید. - هنوز در
layout/activity_main.xml
، FAB را بهروزرسانی کنید تا کشش جدید را نیز در بر گیرد:
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"
android:src="@drawable/ic_add_black_24dp"/>
شما قرار است داده ها را در یک RecyclerView
نمایش دهید، که کمی بهتر از پرتاب کردن داده ها در یک TextView
است. این کد لبه فرض می کند که می دانید RecyclerView
، RecyclerView.LayoutManager
، RecyclerView.ViewHolder
و RecyclerView.Adapter
چگونه کار می کنند.
توجه داشته باشید که words
متغیر در آداپتور داده ها را در حافظه پنهان ذخیره می کند. در کار بعدی، کدی را اضافه می کنید که داده ها را به طور خودکار به روز می کند.
یک فایل کلاس Kotlin برای WordListAdapter
کنید که RecyclerView.Adapter
را گسترش دهد. این هم کد:
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
RecyclerView
را در onCreate()
MainActivity
اضافه کنید.
در onCreate()
بعد از setContentView
:
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
برنامه خود را اجرا کنید تا مطمئن شوید همه چیز کار می کند. هیچ موردی وجود ندارد، زیرا هنوز دادهها را متصل نکردهاید.
هیچ داده ای در پایگاه داده وجود ندارد. دادهها را به دو صورت اضافه میکنید: وقتی پایگاه داده باز میشود، مقداری داده اضافه کنید و یک Activity
برای افزودن کلمات اضافه کنید.
برای حذف همه محتوا و پر کردن مجدد پایگاه داده هر زمان که برنامه راه اندازی می شود، یک RoomDatabase.Callback
ایجاد می کنید و onOpen()
را لغو می کنید. از آنجایی که نمیتوانید عملیات پایگاه داده اتاق را روی رشته UI انجام دهید، onOpen()
یک coroutine در IO Dispatcher راهاندازی میکند.
برای راه اندازی یک کوروتین به یک CoroutineScope
نیاز داریم. متد getDatabase
از کلاس WordRoomDatabase
را به روز کنید تا یک محدوده Coroutine را نیز به عنوان پارامتر دریافت کنید:
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
مقداردهی اولیه بازیابی پایگاه داده را در بلوک init
WordViewModel
به روز کنید تا محدوده زیر را نیز بگذرانید:
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()
در WordRoomDatabase
، ما یک پیاده سازی سفارشی از RoomDatabase.Callback()
ایجاد می کنیم که یک CoroutineScope
را نیز به عنوان پارامتر سازنده دریافت می کند. سپس، متد onOpen
را لغو می کنیم تا پایگاه داده پر شود.
در اینجا کد ایجاد callback در کلاس WordRoomDatabase
است:
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
}
}
در نهایت، درست قبل از فراخوانی .build()
در Room.databaseBuilder()
callback را به دنباله ساخت پایگاه داده اضافه کنید:
.addCallback(WordDatabaseCallback(scope))
در اینجا کد نهایی باید شبیه به آن باشد:
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
word = Word("TODO!")
wordDao.insert(word)
}
}
}
}
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
)
.addCallback(WordDatabaseCallback(scope))
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
این منابع رشته را در values/strings.xml
اضافه کنید:
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>
این منبع رنگ را در value/colors.xml
اضافه کنید:
<color name="buttonLabel">#FFFFFF</color>
یک فایل منبع بعد جدید ایجاد کنید:
- روی ماژول برنامه در پنجره Project کلیک کنید.
- File > New > Android Resource File را انتخاب کنید
- از واجد شرایط، گزینه Dimension را انتخاب کنید
- نام فایل را تنظیم کنید: dimens
این منابع ابعاد را در values/dimens.xml
:
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>
با الگوی Empty Activity یک Activity
اندروید خالی جدید ایجاد کنید:
- File > New > Activity > Empty Activity را انتخاب کنید
- برای نام Activity
NewWordActivity
را وارد کنید. - بررسی کنید که فعالیت جدید به مانیفست Android اضافه شده باشد.
<activity android:name=".NewWordActivity"></activity>
فایل activity_new_word.xml
را در پوشه layout با کد زیر به روز کنید:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit_word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/min_height"
android:fontFamily="sans-serif-light"
android:hint="@string/hint_word"
android:inputType="textAutoComplete"
android:layout_margin="@dimen/big_padding"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:layout_margin="@dimen/big_padding"
android:textColor="@color/buttonLabel" />
</LinearLayout>
کد فعالیت را به روز کنید:
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView: EditText
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(editWordView.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
مرحله آخر اتصال UI به پایگاه داده با ذخیره کلمات جدیدی که کاربر وارد می کند و نمایش محتوای فعلی پایگاه داده کلمه در RecyclerView
است.
برای نمایش محتویات فعلی پایگاه داده، ناظری اضافه کنید که LiveData
را در ViewModel
مشاهده کند.
هر زمان که داده ها تغییر می کنند، onChanged()
فراخوانی می شود که setWords()
آداپتور را برای به روز رسانی داده های کش آداپتور و بازخوانی لیست نمایش داده شده فراخوانی می کند.
در MainActivity
، یک متغیر عضو برای ViewModel
ایجاد کنید:
private lateinit var wordViewModel: WordViewModel
از ViewModelProvider
برای مرتبط کردن ViewModel
خود با Activity
خود استفاده کنید.
هنگامی که Activity
شما برای اولین بار شروع می شود، ViewModel
ViewModelProviders
را ایجاد می کند. وقتی اکتیویتی از بین میرود، مثلاً از طریق تغییر پیکربندی، ViewModel
باقی میماند. هنگامی که فعالیت دوباره ایجاد می شود، ViewModelProviders
ViewModel
را برمی گرداند. برای اطلاعات بیشتر، ViewModel
را ببینید.
در onCreate()
زیر بلوک کد RecyclerView
، یک ViewModel
از ViewModelProvider
دریافت کنید:
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
همچنین در onCreate()
یک مشاهده گر برای ویژگی allWords LiveData
از WordViewModel
کنید.
onChanged()
(روش پیشفرض برای Lambda ما) زمانی فعال میشود که دادههای مشاهدهشده تغییر کنند و فعالیت در پیشزمینه باشد:
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
میخواهیم هنگام ضربه زدن روی FAB، NewWordActivity
را باز کنیم و پس از بازگشت به MainActivity
، کلمه جدید را در پایگاه داده وارد کنیم یا یک Toast
نشان دهیم. برای رسیدن به این هدف، اجازه دهید با تعریف یک کد درخواست شروع کنیم:
private val newWordActivityRequestCode = 1
در MainActivity
، کد onActivityResult()
را برای NewWordActivity
کنید.
اگر اکتیویتی با RESULT_OK برمی گردد، کلمه برگشتی را با فراخوانی متد insert()
RESULT_OK
در پایگاه داده وارد WordViewModel
:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
در MainActivity,
زمانی که کاربر روی FAB ضربه می زند، NewWordActivity
را شروع کنید. در MainActivity
onCreate
، FAB را پیدا کنید و یک onClickListener
با این کد اضافه کنید:
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
کد تمام شده شما باید به شکل زیر باشد:
class MainActivity : AppCompatActivity() {
private const val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
}
اکنون برنامه خود را اجرا کنید! وقتی کلمه ای را به پایگاه داده در NewWordActivity
اضافه می کنید، رابط کاربری به طور خودکار به روز می شود.
اکنون که یک برنامه کاربردی دارید، بیایید آنچه را که ساختهاید خلاصه کنیم. در اینجا دوباره ساختار برنامه آمده است:
اجزای برنامه عبارتند از:
-
MainActivity
: کلمات را در یک لیست با استفاده ازRecyclerView
وWordListAdapter
می دهد. درMainActivity
، یکObserver
وجود دارد که کلمات LiveData را از پایگاه داده مشاهده می کند و هنگامی که آنها تغییر می کنند به آنها اطلاع می دهد. -
NewWordActivity:
یک کلمه جدید به لیست اضافه می کند. -
WordViewModel
: روش هایی را برای دسترسی به لایه داده ارائه می دهد و LiveData را برمی گرداند تا MainActivity بتواند رابطه مشاهدهگر را تنظیم کند.* -
LiveData<List<Word>>
: به روز رسانی خودکار در اجزای UI را امکان پذیر می کند. درMainActivity
، یکObserver
وجود دارد که کلمات LiveData را از پایگاه داده مشاهده می کند و زمانی که آنها تغییر می کنند به او اطلاع می دهند. -
Repository:
یک یا چند منبع داده را مدیریت می کند.Repository
روش هایی را برای ViewModel برای تعامل با ارائه دهنده داده های اساسی نشان می دهد. در این برنامه، آن باطن یک پایگاه داده اتاق است. -
Room
: یک بسته بندی در اطراف است و یک پایگاه داده SQLite را پیاده سازی می کند. اتاق کارهای زیادی را برای شما انجام می دهد که قبلاً باید خودتان انجام می دادید. - DAO: فراخوانیهای متد را به کوئریهای پایگاه داده نشان میدهد، به طوری که وقتی Repository متدی مانند
getAlphabetizedWords()
را فراخوانی میکند، Room میتواندSELECT * from word_table ORDER BY word ASC
را اجرا کند. -
Word
: کلاس موجودیتی است که حاوی یک کلمه است.
* Views
و Activities
(و Fragments
) فقط از طریق ViewModel
با داده ها تعامل دارند. به این ترتیب، مهم نیست که داده ها از کجا آمده اند.
جریان داده برای بهروزرسانیهای خودکار UI (واسطه کاربر واکنشگرا)
به روز رسانی خودکار امکان پذیر است زیرا ما از LiveData استفاده می کنیم. در MainActivity
، یک Observer
وجود دارد که کلمات LiveData را از پایگاه داده مشاهده می کند و زمانی که آنها تغییر می کنند به او اطلاع می دهند. هنگامی که تغییری ایجاد می شود، onChange()
ناظر اجرا می شود و mWords
WordListAdapter
روز می کند.
داده ها را می توان مشاهده کرد زیرا LiveData
است. و آنچه مشاهده می شود LiveData<List<Word>>
است که توسط ویژگی WordViewModel
a llWords
می شود.
WordViewModel
همه چیز را در مورد backend از لایه UI پنهان می کند. روش هایی برای دسترسی به لایه داده ارائه می دهد و LiveData
را برمی گرداند تا MainActivity
بتواند رابطه مشاهدهگر را تنظیم کند. Views
و Activities
(و Fragments
) فقط از طریق ViewModel
با داده ها تعامل دارند. به این ترتیب، مهم نیست که داده ها از کجا آمده اند.
در این مورد، داده ها از یک Repository
می آیند. ViewModel
نیازی به دانستن اینکه مخزن با چه چیزی تعامل دارد، ندارد. فقط باید بداند که چگونه با Repository
تعامل داشته باشد، که از طریق روش هایی است که توسط Repository
در معرض دید قرار می گیرد.
Repository یک یا چند منبع داده را مدیریت می کند. در برنامه WordListSample
، آن باطن یک پایگاه داده اتاق است. Room یک بسته بندی در اطراف است و یک پایگاه داده SQLite را پیاده سازی می کند. اتاق کارهای زیادی را برای شما انجام می دهد که قبلاً باید خودتان انجام می دادید. برای مثال، Room هر کاری را که قبلاً با کلاس SQLiteOpenHelper
انجام می دادید، انجام می دهد.
روش DAO maps کوئری های پایگاه داده را فراخوانی می کند، به طوری که وقتی Repository متدی مانند getAllWords()
را فراخوانی می کند، Room می تواند SELECT * from word_table ORDER BY word ASC
اجرا کند.
از آنجایی که نتیجه برگردانده شده از پرس و جو LiveData
مشاهده می شود، هر بار که داده های اتاق تغییر می کند، onChanged()
واسط Observer
اجرا می شود و UI به روز می شود.
[اختیاری] کد راه حل را دانلود کنید
اگر قبلاً این کار را نکردهاید، میتوانید به کد راهحل مربوط به Codelab نگاهی بیندازید. می توانید به مخزن github نگاه کنید یا کد را از اینجا دانلود کنید:
فایل فشرده دانلود شده را باز کنید. با این کار یک پوشه ریشه، android-room-with-a-view-kotlin
که شامل برنامه کامل است، باز می شود.