این راهنما شامل بهترین روش ها و معماری توصیه شده برای ساختن برنامه های قوی و با کیفیت ساخت بالاست.
تجربیات کاربر برنامه تلفن همراه
در اکثر موارد ، برنامه های دسک تاپ دارای یک نقطه ورودی از یک دسک تاپ یا راه انداز برنامه هستند ، سپس به صورت یک فرآیند یکپارچه اجرا می شوند. از طرف دیگر ، برنامه های اندروید ساختار بسیار پیچیده تری دارند. یک برنامه معمولی اندروید شامل چندین کامپوننت است ، از جمله (activity)، (fragment)، (service)، (content provider) ،(broadcast receiver)
این مولفه ها را در مانیفست برنامه خود اعلام می کنید . سپس سیستم عامل Android با استفاده از این پرونده تصمیم می گیرد که چگونه برنامه شما را در تجربه کاربری کلی دستگاه ادغام کند. با توجه به اینکه یک برنامه آندروید که به درستی نوشته شده است ، دارای چندین مولفه است و کاربران اغلب در مدت زمان کوتاهی با چندین برنامه ارتباط برقرار می کنند ، برنامه ها باید با انواع مختلف کارها و وظایف کاربر محور سازگار شوند.
به عنوان مثال ، در نظر بگیرید چه اتفاقی می افتد وقتی عکسی را در برنامه شبکه اجتماعی مورد علاقه خود به اشتراک می گذارید:
1. این برنامه intent دوربین را صدا میزند. سپس سیستم عامل Android یک برنامه دوربین را برای رسیدگی به درخواست راه اندازی می کند. در این مرحله ، کاربر برنامه شبکه های اجتماعی را ترک کرده است ، اما تجربه آنها هنوز یکپارچه است.
2. برنامه دوربین ممکن است اهداف دیگری مانند راه اندازی انتخابگر پرونده را که ممکن است برنامه دیگری را راه اندازی کند ، ایجاد کند.
3. در نهایت ، کاربر به برنامه شبکه های اجتماعی بازمی گردد و عکس را به اشتراک می گذارد.
در هر مرحله ، ارتباط کاربر با برنامه می تواند با یک تماس تلفنی یا اعلان قطع شود. پس از بازگشت از تماس، کاربر انتظار دارد كه بتواند به فرآیند اشتراک گذاری عکس برگردد و از سر بگیرد. این رفتار جابجایی و پریدن بین برنامه، در دستگاه های تلفن همراه معمول است ، بنابراین برنامه شما باید این جریانات را به درستی کنترل کند.
به خاطر داشته باشید که دستگاه های تلفن همراه نیز محدودیت منابع ( حافظه و رم ) دارند ، بنابراین در هر زمان ، سیستم عامل ممکن است برخی از فرایندهای برنامه را از بین ببرد تا جای فرایندهای جدید را بگیرد.
با توجه به شرایط این محیط ، این امکان وجود دارد که اجزای برنامه شما به صورت جداگانه و خارج از سفارش بسته شوند و سیستم عامل یا کاربر می تواند آنها را در هر زمان از بین ببرد. از آنجا که این رویدادها تحت کنترل شما نیستند ، شما نباید هیچ داده یا وضعیت برنامه را در اجزای برنامه خود ذخیره کنید و اجزای برنامه شما نباید به یکدیگر وابسته باشند.
اصول معماری مشترک
اگر شما نباید از اجزای برنامه برای ذخیره داده ها و حالت برنامه استفاده کنید ، چگونه باید برنامه خود را طراحی کنید؟
تفکیک رابطه ها
مهمترین اصلی که باید رعایت شود تفکیک رابطه است . نوشتن تمام کدهای خود در یک Activity یا Fragment یک اشتباه معمول است . این کلاسهای مبتنی بر رابط کاربر فقط باید دارای منطقی باشند که تعاملات رابط کاربر و سیستم عامل را مدیریت می کند. با پایین نگه داشتن حجم کدهای این کلاس ها تا حد ممکن ، می توانید از بسیاری از مشکلات مربوط به چرخه زندگی جلوگیری کنید.
به خاطر داشته باشید که شما پیاده سازی Activity وFragment را ندارید، بلکه این فقط کلاسهای مرتبط با هم است که قرارداد بین سیستم عامل Android و برنامه شما را نشان می دهد. سیستم عامل می تواند در هر زمان بر اساس تعاملات کاربر یا به دلیل شرایط سیستم مانند کمبود حافظه ، آنها را از بین ببرد. برای ارائه یک تجربه کاربری رضایت بخش و تجربه نگهداری برنامه با مدیریت بیشتر ، بهتر است وابستگی خود را به آنها به حداقل برسانید.
رابط کاربری را با یک مدل اجرا کنید
یک اصل مهم دیگر این است که شما باید UI خود را از یک مدل ، ترجیحاً یک مدل پایدار(دیتابیس) راه اندازی کنید. مدل ها اجزایی هستند که مسئولیت رسیدگی به داده های یک برنامه را دارند. آنها از View ، اشیا و اجزای برنامه موجود در برنامه شما مستقل هستند ، بنابراین تحت تأثیر چرخه حیات برنامه و نگرانی های مرتبط با آن نیستند.
پایداری به دلایل زیر ایده آل است:
● اگر سیستم عامل Android برنامه شما را برای آزاد کردن منابع از بین ببرد ، کاربران شما داده خود را از دست نمی دهند.
● برنامه شما در مواردی که اتصال شبکه منقطع باشد یا در دسترس نباشد ، به کار خود ادامه می دهد.
با قرار دادن برنامه خود تحت کلاسهای مدل با مسئولیت کاملاً مشخص مدیریت داده ها ، برنامه شما تست پذیری و سازگاری بیشتری دارد.
معماری برنامه پیشنهادی
در این بخش ، ما نشان می دهیم که چگونه با استفاده از یک مورد استفاده از انتها به انتها ، برنامه را با استفاده از اجزای معماری ساختار می دهیم .
توجه: وجود یک روش برای نوشتن برنامه ها که برای هر سناریو بهترین نتیجه را داشته باشد غیرممکن است. همانطور که گفته شد ، این معماری توصیه شده نقطه شروع خوبی برای بیشتر شرایط و گردش کار است. اگر در حال حاضر روش خوبی برای نوشتن برنامه های اندروید دارید که از اصول معماری رایج پیروی می کند ، نیازی به تغییر آن ندارید.
تصور کنید در حال ساخت رابط کاربر هستیم که نمایه کاربری را نشان می دهد. ما برای واکشی داده ها برای یک نمایه مشخص از یک backend خصوصی و REST API استفاده می کنیم.
بررسی اجمالی
برای شروع ، نمودار زیر را در نظر بگیرید ، که نشان می دهد چگونه همه ماژول ها پس از طراحی برنامه باید با یکدیگر تعامل داشته باشند:
توجه داشته باشید که هر مولفه فقط به مولفه یک سطح زیر آن بستگی دارد. به عنوان مثال ، activity/fragment ها فقط به یک viewModel بستگی دارد Repository تنها کلاسی است که به چندین کلاس دیگر بستگی دارد. در این مثال ، Repository به یک مدل داده دیتابیس داخلی و یک منبع داده تغذیه شونده از راه دور بستگی دارد.
این طراحی یک تجربه کاربری سازگار و دلپذیر را ایجاد می کند. صرف نظر از اینکه کاربر چند دقیقه یا چند روز بعد از آخرین باری که آن برنامه را بسته به برنامه بازگردد ، بلافاصله اطلاعات کاربر را که به صورت محلی در جریان است ، می بیند. اگر این داده ها قدیمی باشند ، ماژول Repository شروع به بروزرسانی داده ها در پس زمینه می کند.
رابط کاربری ایجاد کنید.
برای راه اندازی رابط کاربری ، دیتا مدل ما باید عناصر داده زیر را در خود داشته باشد:
شناسه ای برای هر کاربر(UserId). بهتر است این اطلاعات را با استفاده از آرگومان های فرگمنت انتقال دهید. اگر سیستم عامل Android روند ما را از بین ببرد ، این اطلاعات حفظ می شود ، بنابراین دفعه بعدی که برنامه ما دوباره راه اندازی می شود ، ID در دسترس است.
کلاس داده ای که جزئیات مربوط به کاربر را در خود جای داده است.(userObject)
ما برای حفظ این اطلاعات از یک UserProfileViewModel، که بر اساس مولفه معماری ViewModel است استفاده می کنیم.
یک شی ViewModel داده ها را برای یک UI خاص ، مانند یک فرگمنت یا اکتیویتی فراهم می کند و شامل منطق مدیریت داده برای ارتباط با مدل است. به عنوان مثال ، ViewModel می تواند برای بارگذاری داده ها با اجزای دیگر ارتباط بگیرد و می تواند درخواست های کاربر را برای اصلاح داده ها ارسال کند. این ViewModel از اجزای رابط کاربری چیزی نمی داند ، بنابراین تحت تأثیر تغییرات پیکربندی ، مانند ایجاد مجدد فعالیت هنگام چرخاندن دستگاه ، قرار نمی گیرد.
اکنون پرونده های زیر را تعریف کرده ایم:
● user_profile.xml : تعریف طرح UIبرای صفحه.
● UserProfileFragment : کنترل کننده UI که داده ها را نمایش می دهد.
● UserProfileViewModel : کلاسی که داده ها را برای مشاهده آماده می کند و به تعاملات کاربر واکنش نشان می دهد.
قطعه کد زیر محتوای اولیه این پرونده ها را نشان می دهد. (فایل لایه برای سادگی حذف شده است)
UserProfileViewModel
class UserProfileViewModel : ViewModel() { val userId : String = TODO() val user : User = TODO() }
UserProfileFragment
class UserProfileFragment : Fragment() { private val viewModel: UserProfileViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { return inflater.inflate(R.layout.main_fragment, container, false) } }
اکنون که این ماژول های کد را داریم ، چگونه آنها را به هم متصل کنیم؟ از این گذشته ، وقتی فیلد user در UserProfileViewModel مقداردهی می شود ، ما به روشی برای اطلاع رسانی UI نیاز داریم.
برای دستیابی به user، ViewModel های ما نیاز دارند که به آرگومان های Fragment دسترسی پیدا کنند. ما می توانیم آنها را از Fragment منتقل کنیم یا راه بهتر استفاده از ماژول SavedState است، می توانیم ViewModel خود را مستقیماً بخوانیم:
توجه: SavedStateHandle به ViewModel اجازه می دهد تا به state و آرگومان های Fragment یا Activity مرتبط دسترسی پیدا کند.
// UserProfileViewModel
valuserId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : User = TODO() } // UserProfileFragment private valviewModel: UserProfileViewModel by viewModels( factoryProducer = { SavedStateVMFactory(this) } ... )
اکنون ما باید هنگام به دست آوردن شی کاربر ، Fragment خود را مطلع کنیم. اینجاست که مولفه LiveData در معماری ظاهر می شود.
LiveData یک نگهدارنده داده با قابلیت مشاهده تغییرات داده است. سایر مولفه های برنامه شما می توانند تغییرات اشیا را با استفاده از این نگهدارنده بدون ایجاد مسیرهای وابستگی صریح و سخت بین آنها کنترل کنند. مولفه LiveDataهمچنین به چرخه حیات اجزای برنامه شما - مانند اکتیویتی ها ، فرگمنت ها و سرویس ها – متصل و همگام می شود و داده ها را شامل منطق پاکسازی برای جلوگیری از نشت اشیا و مصرف زیاد حافظه می کند.
توجه: اگر همچنان از کتابخانه ای مانند RxJava استفاده می کنید ، می توانید به جای LiveData از آنها استفاده کنید. وقتی از کتابخانه ها و رویکردهایی مانند اینها استفاده می کنید ، مطمئن شوید که چرخه حیات برنامه خود را به درستی اداره می کنید.
برای ترکیب کردن مولفه LiveDataدر برنامه خود ، ما نوع داده user را به LiveData<User> تغییر می دهیم، اطلاعات هنگام بروزرسانی به آنها اطلاع داده می شود. بعلاوه ، از آنجا که این LiveData از چرخه حیات آگاه است ، پس از اینکه دیگر نیازی به ایجاد کننده نیست ، به طور خودکار پاک می کند.
UserProfileViewModel
class UserProfileViewModel( savedStateHandle: SavedStateHandle ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : LiveData<User> = TODO() }
اکنون ما UserProfileFragment برای رصد داده ها و به روزرسانی رابط کاربر اصلاح می کنیم:
UserProfileFragment
override funonViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) viewModel.user.observe(viewLifecycleOwner) { // update UI } }
هر بار که اطلاعات پروفایل کاربر به روز می شود ، کال بک d() فراخوانی می شود و UI آپدیت می شود.
اگر با کتابخانه های دیگری که در آن از بازگشت پاسخ قابل مشاهده استفاده می شود ، آشنا باشید ، ممکن است متوجه شده باشید که ما متد () این فرگمنت را برای جلوگیری از مشاهده داده ها ، فراخوانی نکردیم. این مرحله برای LiveData لازم نیست زیرا از چرخه حیات آگاه است ، این بدان معنی است که کال بک d() را فراخوانی نمی کند مگر اینکه فرگمنت در حالت فعال باشد. یعنی () دریافت کرده اما هنوز () دریافت نکرده است. LiveData همچنین هنگام فراخوانی متد onDestroy () فرگمنت ، مشاهده کننده را به طور خودکار حذف می کند.
واکشی داده ها
اکنون که از LiveDataبرای اتصال UserProfileViewModel به UserProfileFragment استفاده کردیم ، چگونه می توانیم اطلاعات پروفایل کاربر را واکشی کنیم؟
برای این مثال ، ما فرض می کنیم که backend ما REST API را فراهم می کند. ما از کتابخانه Retrofit برای دسترسی به بکند خود استفاده می کنیم ، اگرچه شما آزاد هستید که از کتابخانه دیگری استفاده کنید که با همان هدف انجام شود.
در اینجا تعریف ما از Webservice آن است که با بکند ما ارتباط برقرار می کند:
WebService
interface Webservice { @GET("/users/{user}") fun getUser(@Path("user") userId: String): Call<User> }
اولین ایده برای اجرای ViewModel ممکن است شامل تماس مستقیم سرویس Webs برای واکشی داده ها و اختصاص این داده ها به شی LiveData ما باشد. این طراحی موثر است ، اما با استفاده از آن ، برنامه ما با رشد بیشتر و دشوارتر می شود. این مسئولیت بیش از حد به کلاس UserProfileViewModel می دهد ، که اصل تفکیک نگرانی ها را نقض می کند. علاوه بر این ، دامنه ViewModel به چرخه حیات Activity یا Fragment گره خورده است ، به این معنی که با پایان یافتن چرخه حیات شی UI مرتبط ، داده های سرویس Webs از دست می رود. این رفتار یک تجربه کاربری نامطلوب ایجاد می کند.در عوض ، ViewModel ما فرآیند واکشی داده را به یک ماژول جدید ، مخزن ، تفویض می کند.
ماژول های مخزن عملیات داده را مدیریت می کنند. آنها یک API تمیز ارائه می دهند تا بقیه برنامه بتواند به راحتی این داده ها را بازیابی کند. آنها می دانند که داده ها را از کجا بگیرند و APIبرای بروزرسانی داده ها چه چیزی را فراخوانی می کند. می توانید مخازن را به عنوان واسطه بین منابع مختلف داده مانند مدل ها ، وب سرویس و حافظه کش در نظر بگیرید.
کلاس ما که در قطعه کد زیر نشان داده شده است ، از نمونه وب سرویس برای واکشی داده های کاربر استفاده می کند:
UserRepository
class UserRepository { private val webservice: Webservice = TODO() // ... fun getUser(userId: String): LiveData<User> { val data = MutableLiveData<User>() webservice.getUser(userId).enqueue(object :Callback<User> { override fun onResponse(call: Call<User>, response: Response<User>) { data.value = response.body() } // Error case is left out for brevity. override funonFailure(call: Call<User>, t: Throwable) { TODO() } }) return data } }
حتی اگر ماژول مخزن غیرضروری به نظر برسد ، یک هدف مهم را دنبال می کند: منابع داده را از بقیه برنامه جدا می کند. اکنون ، ما در UserProfileViewModelنمی دانیم که چگونه داده ها واکشی می شوند ، بنابراین می توانیم ویو مدل را با داده های به دست آمده از چندین پیاده سازی مختلف جمع آوری داده ارائه دهیم.
توجه: ما برای خطای ساده پرونده خطای شبکه را کنار گذاشته ایم. برای یک اجرای جایگزین که خطاها و وضعیت بارگیری را آشکار می کند ، به ضمیمه مراجعه کنید : نمایش وضعیت شبکه .
وابستگی ها را بین اجزا مدیریت کنید
● تزریق وابستگی : تزریق وابستگی به کلاسها اجازه می دهد تا وابستگی های خود را بدون ساختن آنها تعریف کنند. در زمان اجرا ، کلاس دیگری مسئول تأمین این وابستگی ها است. ما کتابخانه Dagger 2 را برای پیاده سازی تزریق وابستگی در برنامه های Android توصیه می کنیم . دگر 2 با حرکت در درخت وابستگی به طور خودکار اشیا را می سازد و تضمین های مربوط به زمان کامپایل را در مورد وابستگی ها ارائه می دهد.
● سرویس یاب : الگوی یاب سرویس ، رجیستری را فراهم می کند که در آن کلاس ها می توانند وابستگی های خود را به جای ساختن ، بدست آورند.این الگوها به شما امکان می دهند کد خود را مقیاس بندی کنید زیرا الگوهای روشنی برای مدیریت وابستگی ها بدون تکثیر کد یا افزودن پیچیدگی ارائه می دهد. علاوه بر این ، این الگوها به شما امکان می دهد تا به سرعت بین پیاده سازی های آزمایش و تولید داده جابجا شوید.
class UserProfileViewModel @Inject constructor( savedStateHandle: SavedStateHandle, userRepository: UserRepository ) : ViewModel() { val userId : String = savedStateHandle["uid"] ?: throw IllegalArgumentException("missing user id") val user : LiveData<User> = userRepository.getUser(userId) }
داده های حافظه پنهان
مشکل اصلی درپیاده سازی Repository این است که پس از آنکه داده ها را از قسمت بکند ما دریافت کرد، این داده ها را در هیچ کجا ذخیره نمی کند. بنابراین ، اگر کاربر از آن فرگمنت خارج شود ، سپس به آن بازگردد ، برنامه ما باید داده ها را دوباره واکشی کند ، حتی اگر تغییری نکرده باشد.
به دلایل زیر این طرح بهینه نیست:
● پهنای باند با ارزش شبکه را هدر می دهد.
● کاربر را وادار می کند منتظر تکمیل درخواست جدید شود.
برای رفع این کمبودها ، ما یک منبع داده جدید به مخزن خود اضافه می کنیم ، این اشیا موجود در حافظه را ذخیره می کند:
UserRepository
@Singleton class UserRepository @Inject constructor( private val webservice: Webservice, private val userCache: UserCache) {
fun getUser(userId: String): LiveData<User> { val cached : LiveData<User> = userCache.get(userId) if (cached != null) { returncached } val data = MutableLiveData<User>() userCache.put(userId, data) webservice.getUser(userId).enqueue(object : Callback<User> { override funonResponse(call: Call<User>, response: Response<User>) { data.value = response.body() } override funonFailure(call: Call<User>, t: Throwable) { TODO() } }) return data } }
چرخه حیات داده
با استفاده از اجرای فعلی ما ، اگر کاربر دستگاه را بچرخاند یا خارج شود و بلافاصله به برنامه برگردد ، UI موجود فوراً قابل مشاهده می شود زیرا مخزن داده ها را از حافظه کش ما بازیابی می کند.
با این حال ، چه اتفاقی می افتد اگر کاربر برنامه را ترک کند و ساعاتی بعد ، پس از اینکه سیستم عامل Android فرآیند را از بین برد ، دوباره برگردد؟ با تکیه بر پیاده سازی فعلی خود در این شرایط ، باید داده ها را دوباره از شبکه دریافت کنیم. این فرایند بازیابی فقط یک تجربه کاربری بد نیست. همچنین بی مصرف است زیرا داده های ارزشمند تلفن همراه را مصرف می کند.
با ذخیره سازی درخواست های وب می توانید این مشکل را برطرف کنید ، اما این یک مشکل اساسی جدید ایجاد می کند: اگر داده های کاربر مشابه از نوع دیگری از درخواست ، مانند واکشی لیست دوستان ، نشان داده شود ، چه اتفاقی می افتد؟ این برنامه داده های متناقضی را نشان می دهد که در بهترین حالت گیج کننده است. به عنوان مثال ، اگر کاربر در زمان های مختلف درخواست لیست دوستان و درخواست تک کاربر را ارائه دهد ، ممکن است برنامه ما دو نسخه مختلف از داده های یک کاربر را نشان دهد. برنامه ما باید بفهمد چگونه این داده های ناسازگار را ادغام می کند.
روش مناسب برای کنترل این وضعیت استفاده از یک پایگاه داده است. این جایی است که کتابخانه روم به کمک می آید.
تعریف: room یک کتابخانه نقشه برداری شی است که ماندگاری داده های محلی را با حداقل کدنویسی در زمان کامپایل فراهم می کند . برخی از جزئیات پیاده سازی اساسی کار با جداول و پرس و جوهای SQL خام را خلاصه می کند. همچنین به شما امکان می دهد تغییراتی را در داده های پایگاه داده ، از جمله مجموعه ها و پیوستن به نمایش داده ها مشاهده کنید ، و با استفاده از اشیا LiveData چنین تغییراتی را نشان می دهید. حتی به صراحت محدودیت های اجرایی را تعریف می کند که به موضوعات متصل به نخ متصل می شوند ، مانند دسترسی به ذخیره سازی در موضوع اصلی.
توجه: اگر برنامه شما از قبل از دیتابیس دیگری مانند SQLite نقشه برداری رابطه ای شی (ORM) استفاده کرده است ، نیازی نیست که محلول موجود خود را با Room جایگزین کنید . با این حال ، اگر در حال نوشتن یک برنامه جدید هستید یا در حال ساخت مجدد یک برنامه موجود هستید ، توصیه می کنیم از Room برای ادامه داده های برنامه خود استفاده کنید. به این ترتیب می توانید از قابلیت انتزاع کتابخانه و اعتبار سنجی استفاده کنید.
برای استفاده از Room، باید طرحواره محلی خود را تعریف کنیم. ابتدا @Entityحاشیه نویسی را به Userکلاس مدل داده خود و یک @PrimaryKeyحاشیه را به قسمت ایدی کلاس اضافه میکنیم. این حاشیه نویسی به عنوان یک جدول User در پایگاه داده ما و idبه عنوان کلید اصلی جدول علامت گذاری می شود :
User
@Entity data class User( @PrimaryKey private val id: String, private val name: String, private val lastName: String )
سپس ، با پیاده سازی RoomDatabaseبرای برنامه خود ، کلاس پایگاه داده ایجاد می کنیم:
UserDatabase
@Database(entities = [User::class], version = 1) abstract class UserDatabase : RoomDatabase()
توجه داشته باشید که UserDatabaseانتزاعی است. روم به طور خودکار اجرای آن را فراهم می کند. .
اکنون به روشی برای درج داده های کاربر در پایگاه داده نیاز داریم. برای این کار ، ما یک شی دسترسی به داده (DAO) ایجاد می کنیم .
UserDao
@Dao interface UserDao {
@Insert(onConflict = REPLACE) fun save(user: User) @Query("SELECT * FROM user WHERE id = :userId") fun load(userId: String): LiveData<User> }
روم می داند که چه زمانی پایگاه داده اصلاح می شود و هنگام تغییر داده ها ، به طور خودکار همه مشاهده کنندگان فعال را مطلع می کند. از آنجا که Room از LiveData استفاده می کند ، این عملیات کارآمد است. این داده ها را فقط در صورت وجود حداقل یک ناظر فعال به روز می کند.
توجه: روم نامعتبرها را براساس تغییرات جدول بررسی می کند ، به این معنی که ممکن است اعلان های مثبت نادرست ارسال شود.
با تعریف کلاسUserDao خود ، ما DAO را از کلاس پایگاه داده خود ارجاع می دهیم:
UserDatabase
@Database(entities = [User::class], version = 1) abstract class UserDatabase : RoomDatabase() { abstract fun userDao(): UserDao }
اکنون می توانیم Repositoryخود را تغییر دهیم تا روم را در آن بگنجانیم:
// Informs Dagger that this class should be constructed only once. @Singleton class UserRepository @Inject constructor( private val webservice: Webservice, // Simple in-memory cache. Details omitted for brevity. private val executor: Executor, private val userDao: UserDao ) { fun getUser(userId: String): LiveData<User> { refreshUser(userId) // Returns a LiveData object directly from the database. return userDao.load(userId) } private fun refreshUser(userId: String) { // Runs in a background thread. executor.execute { // Check if user data was fetched recently. valuserExists = userDao.hasUser(FRESH_TIMEOUT) if(!userExists) { // Refreshes the data. valresponse = webservice.getUser(userId).execute() // Check for errors here. // Updates the database. The LiveData object automatically // refreshes, so we don't need to do anything else here. userDao.save(response.body()!!) } } } companion object { val FRESH_TIMEOUT = TimeUnit.DAYS.toMillis(1) } }
توجه داشته باشید که حتی اگر مکان ورود اطلاعات UserRepository را تغییر دهیم ، نیازی به تغییر UserProfileViewModelیا تغییر داده خود نداریم . این بروزرسانی محدود ، انعطاف پذیری معماری برنامه ما را نشان می دهد. همچنین برای تست عالی است ، زیرا ما می توانیم UserRepository جعلی تهیه کنیم و همزمان UserProfileViewModel خود را آزمایش کنیم .
اگر کاربران قبل از بازگشت به برنامه ای که از این معماری استفاده می کند ، چند روز صبر کنند ، به احتمال زیاد اطلاعات خارج از تاریخ را مشاهده می کنند تا زمانی که مخزن بتواند اطلاعات به روز شده را واکشی کند. بسته به مورد استفاده ، ممکن است نخواهید این اطلاعات قدیمی را نشان دهید. در عوض ، می توانید داده های جایگزین یا place holder را نمایش دهید ، که مقادیر مثالی را نشان می دهد و نشان می دهد برنامه شما در حال واکشی اطلاعات بارگیری شده و در حال بارگیری است.
منبع واحد حقیقت
معمولاً برای نقاط مختلف REST API بازگشت داده های مشابه است. به عنوان مثال ، اگر backend ما دارای نقطه پایانی دیگری باشد که لیستی از دوستان را برمی گرداند ، همان شی user کاربر می تواند از دو نقطه انتهایی API متفاوت باشد ، حتی ممکن است از سطوح مختلف دانه بندی استفاده کند. اگر بخواهیم همانطور که هست UserRepositoryپاسخ را از روی Webserviceدرخواست برگردانیم ، بدون بررسی سازگاری ، رابط کاربر ما می تواند اطلاعات گیج کننده ای را نشان دهد زیرا نسخه و قالب داده ها از مخزن به نقطه پایانی که اخیراً فراخوانی شده بستگی دارد.
به همین دلیل ، UserRepositoryپیاده سازی ما پاسخ های وب سرویس را در پایگاه داده ذخیره می کند. تغییرات موجود در پایگاه داده باعث بازگشت تماس بر روی اشیا فعال LiveData می شود. با استفاده از این مدل ، پایگاه داده به عنوان منبع واحد حقیقت عمل می کند و سایر قسمتهای برنامه با استفاده از ما به آن دسترسی پیدا می کنند UserRepository. صرف نظر از اینکه از کش دیسک استفاده می کنید ، توصیه می کنیم مخزن خود یک منبع داده را به عنوان منبع واقعی حقیقت برای بقیه برنامه خود تعیین کنید.
نمایش عملیات در حال انجام
در برخی یوزکیس ها ، مانند کشیدن برای تازه کردن ، برای UI مهم است که به کاربر نشان دهد در حال حاضر یک عملیات شبکه در حال انجام است. این عملکرد خوبی است که عملکرد UI را از داده های واقعی جدا کنید زیرا ممکن است داده ها به دلایل مختلف به روز شوند. به عنوان مثال ، اگر لیستی از دوستان را واکشی کنیم ، ممکن است همان کاربر مجدداً به صورت خودکار واکشی شود و باعث بروزرسانی LiveData<User> شود. از دید UI ، این واقعیت که یک درخواست در پرواز وجود دارد ، یک نقطه داده دیگر است ، شبیه به هر داده دیگر در خود شی User.
● یک تابع عمومی دیگر در UserRepository ارائه دهید که بتواند وضعیت تازه سازی Userرا بازگرداند . این گزینه بهتر است اگر بخواهید وضعیت شبکه را در رابط کاربری خود نشان دهید فقط زمانی که فرآیند واکشی داده از یک اقدام آشکار کاربر مانند کشش برای تازه کردن نشات گرفته باشد.
.
بهترین روش ها
برنامه نویسی یک زمینه خلاقانه است و ساخت برنامه های اندروید نیز از این قاعده مستثنی نیست. راه های زیادی برای حل یک مشکل وجود دارد ، اعم از برقراری ارتباط داده ها بین اکتیویتی های متعدد یا فرگمنت ها ، بازیابی داده های از راه دور و ادامه آن به صورت محلی برای حالت آفلاین ، یا هر تعداد سناریوی رایج دیگری که برنامه های غیرمتعارف با آن روبرو می شوند.
اگرچه توصیه های زیر اجباری نیستند ، اما تجربه ما این است که پایه کد شما در طولانی مدت قوی تر ، قابل آزمایش و نگهداری می شود:
از تعیین نقاط ورود برنامه خود – مانند اکتیویتی ، سرویس ، ریسیور - به عنوان منابع داده خودداری کنید.
در عوض ، آنها فقط باید با سایر مولفه ها برای بازیابی زیرمجموعه داده های مربوط به آن نقطه ورود هماهنگ شوند. هر یک از مولفه های برنامه بسته به تعامل کاربر با دستگاه خود و سلامت کلی سیستم نسبتاً کوتاه مدت است.
مرزهای مسئولیت ثابت و مشخصی بین ماژول های مختلف برنامه خود ایجاد کنید.
به عنوان مثال ، کدی که داده ها را از شبکه لود می کند در چندین کلاس یا بسته در کد خود پخش نکنید. به همین ترتیب ، چندین مسئولیت غیر مرتبط - مانند ذخیره داده و اتصال داده - در یک کلاس تعریف نکنید.
از هر ماژول کمترین حد ممکن را نشان دهید.
وسوسه نشوید که میانبر "فقط آن یک" ایجاد کنید که جزئیات اجرای داخلی را از یک ماژول نشان دهد. ممکن است در کوتاه مدت کمی وقت خود را بدست آورید ، اما بعداً با تکوین پایگاه کد خود چندین برابر بدهی فنی می گیرید.
در نظر بگیرید که چگونه هر ماژول را به صورت جداگانه آزمایش کنیم.
به عنوان مثال ، داشتن یک APIکاملاً مشخص برای واکشی داده ها از شبکه ، آزمایش ماژولی را که ادامه داده را در پایگاه داده محلی آسان می کند ، آسان می کند. در عوض ، اگر منطق این دو ماژول را در یک مکان مخلوط کنید یا کد شبکه خود را در کل کد خود پخش کنید ، آزمایش آن بسیار دشوارتر می شود - اگر غیرممکن نباشد.
بر هسته منحصر به فرد برنامه خود تمرکز کنید تا از سایر برنامه ها متمایز شود.
چرخ را با نوشتن مجدد همان کد دیگ بخار دوباره اختراع نکنید. درعوض ، وقت و انرژی خود را بر روی آنچه منحصر به فرد برنامه شماست متمرکز کنید و به اجزای معماری Androidو سایر کتابخانه های توصیه شده اجازه دهید دیگ بخار تکراری را کنترل کنند.
تا آنجا که ممکن است داده های مرتبط و تازه را ادامه دهید.
به این ترتیب ، حتی اگر دستگاه آنها در حالت آفلاین باشد ، کاربران می توانند از عملکرد برنامه شما لذت ببرند. به یاد داشته باشید که همه کاربران شما از اتصال ثابت و پرسرعت برخوردار نیستند.
یک منبع داده را به عنوان منبع واحد حقیقت اختصاص دهید.
هر زمان که برنامه شما نیاز به دسترسی به این داده داشته باشد ، همیشه باید از همین منبع حقیقت سرچشمه بگیرد .
آخرین به روزرسانی: 08/02/2020
https://developer.android.com/reference/android/arch/lifecycle/LiveData