व्यक्तिगत प्रोग्रामर अपने मोबाइल ऐप्स को अपनी दृष्टि के अनुसार विकसित करते हैं, जिसमें विभिन्न कार्यों को कैसे करना है, इस पर उनके विचार और दृष्टिकोण शामिल होते हैं। कभी-कभी वे ऑब्जेक्ट-ओरिएंटेड या फंक्शनल प्रोग्रामिंग के मुख्य सिद्धांतों की अवहेलना कर सकते हैं, जिससे डेवलपर्स में भटकाव पैदा हो सकता है।
यह बुरा है - वे अपने कोड से निपटने में सक्षम नहीं होंगे। और अगला डेवलपर जिसे प्रोजेक्ट को बनाए रखने या संशोधित करने की आवश्यकता है, पागल हो सकता है। ऐसे प्रोजेक्ट्स को स्क्रैच से पुनर्निर्माण करना बेहतर है, क्योंकि रखरखाव एक जटिल प्रक्रिया बन जाती है।
जब तक Google ने अपनी पहली समर्थित आर्किटेक्चर जारी नहीं की थी, लगभग हर सॉफ्टवेयर डेवलपमेंट कंपनी अपनी आर्किटेक्चर का उपयोग करती थी। इसने उन्हें अपना कोड स्पष्ट बनाने और प्रोजेक्ट्स के बीच स्विच करना संभव बनाया। लेकिन अगर कोई डेवलपर कंपनी बदलता है, तो उसे उस नई आर्किटेक्चर के साथ-साथ नए प्रोजेक्ट को सीखने में कुछ समय लगेगा।
फिलहाल, Google की बदौलत Android डेवलपर्स के लिए 16 अलग-अलग आर्किटेक्चर हैं:
- 6 स्थिर सैंपल (Java);
- 2 स्थिर सैंपल (Kotlin):
- 4 बाहरी सैंपल;
- 3 डिप्रिकेटेड सैंपल;
- 1 प्रगति में सैंपल।
आप जिस आर्किटेक्चर का उपयोग करते हैं वह आपके विशिष्ट उद्देश्य, दृष्टिकोण, और विभिन्न कार्यात्मकताओं के कार्यान्वयन के लिए विभिन्न टूलकिट्स के अनुप्रयोग पर निर्भर करता है। और यह प्रोग्रामिंग भाषा पर निर्भर करता है।
हालांकि, इन सभी आर्किटेक्चर में एक सामान्य आर्किटेक्चरल फाउंडेशन है जो नेटवर्क, डेटाबेस, निर्भरता, और कॉलबैक प्रोसेसिंग के साथ काम करने के लिए तर्क को लगभग समान रूप से विभाजित करता है।
प्रक्रिया के दौरान उपयोग किए जाने वाले टूल्स
इन सभी आर्किटेक्चर का अध्ययन करने के बाद, मैंने एक सरलीकृत दृष्टिकोण बनाया और कम लेयर्स वाली एक आर्किटेक्चर के साथ आया। मैं आपको दिखाऊंगा कि एक साधारण Android ऐप कैसे लागू करें जो एक समाचार सूची लोड करता है, आपको कहानियों को पसंदीदा में सहेजने की अनुमति देता है, और फिर यदि आवश्यक हो तो हटाने के लिए मेरे दृष्टिकोण का उपयोग करता है।
यहां मेरे द्वारा उपयोग की गई तकनीक का सारांश है:
- Kotlin AndroidX लाइब्रेरी के साथ ऐप विकसित करने के लिए
- Room SQLite डेटाबेस के रूप में
- Stetho डेटा को बेस में ब्राउज़ करने के लिए
- Retrofit2 RxJava2 के साथ सर्वर अनुरोधों को लॉग करने और सर्वर प्रतिक्रियाएं प्राप्त करने में मदद करने के लिए।
- Glide छवियों को प्रोसेस करने के लिए
- Android Architecture Components (LiveData, ViewModel, Room) और ReactiveX (RxJava2, RxKotlin और RxAndroid) निर्भरता बनाने, डायनामिक डेटा परिवर्तन, और एसिंक्रोनसिटी प्रोसेसिंग के लिए।
तो यह वह मोबाइल ऐप टेक्नोलॉजी स्टैक है जिसका मैंने अपने प्रोजेक्ट के लिए उपयोग किया।
चलिए शुरू करते हैं
पहले कदम
AndroidX कनेक्ट करें। ऐप लेवल पर gradle.properties में, निम्नलिखित लिखें:
android.enableJetifier=true android.useAndroidX=true
अब ऐप मॉड्यूल लेवल पर build.gradle में Android से AndroidX में निर्भरता को बदलना आवश्यक है। आपको सभी निर्भरताओं को ext में निकालना चाहिए, जैसा कि आप build.gradle में Kotlin आउट-ऑफ-द-बॉक्स वर्जनिंग के उदाहरण में देख सकते हैं ऐप लेवल पर। और फिर मैं वहां ग्रेडल वर्जनिंग जोड़ता हूं:
buildscript { ext.kotlin_version = '1.3.0' ext.gradle_version = '3.2.1' repositories { google() jcenter() maven { url 'https://jitpack.io' } mavenCentral() } dependencies { classpath "com.android.tools.build:gradle:$gradle_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } }
अन्य सभी निर्भरताओं के लिए, मैं इसकी ext फाइल बनाऊंगा, जहां मैं SDK संस्करणों सहित सभी निर्भरताओं को जोड़ूंगा, वर्जनिंग को विभाजित करूंगा और निर्भरता मैसिव्स बनाऊंगा जिन्हें ऐप लेवल पर build.gradle में आगे लागू किया जाएगा। यह निम्नलिखित जैसा दिखेगा:
ext { compileSdkVersion = 28 minSdkVersion = 22 buildToolsVersion = '28.0.3' targetSdkVersion = 28 appcompatVersion = '1.0.2' supportVersion = '1.0.0' supportLifecycleExtensionsVersion = '2.0.0' constraintlayoutVersion = '1.1.3' multiDexVersion = "2.0.0" testJunitVersion = '4.12' testRunnerVersion = '1.1.1' testEspressoCoreVersion = '3.1.1' testDependencies = [ junit : "junit:junit:$testJunitVersion", runner : "androidx.test:runner:$testRunnerVersion", espressoCore: "androidx.test.espresso:espresso-core:$testEspressoCoreVersion" ] supportDependencies = [ kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version", appCompat : "androidx.appcompat:appcompat:$appcompatVersion", recyclerView : "androidx.recyclerview:recyclerview:$supportVersion", design : "com.google.android.material:material:$supportVersion", lifecycleExtension: "androidx.lifecycle:lifecycle-extensions:$supportLifecycleExtensionsVersion", constraintlayout : "androidx.constraintlayout:constraintlayout:$constraintlayoutVersion", multiDex : "androidx.multidex:multidex:$multiDexVersion" ] }
संस्करण और मैसिव नाम बेतरतीब ढंग से लागू किए गए हैं। उसके बाद, हम ऐप लेवल पर build.gradle में निर्भरताओं को इस प्रकार लागू करेंगे:
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' android { compileSdkVersion rootProject.ext.compileSdkVersion as Integer buildToolsVersion rootProject.ext.buildToolsVersion as String
dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) //Test testImplementation testDependencies.junit androidTestImplementation testDependencies.runner androidTestImplementation testDependencies.espressoCore //Support implementation supportDependencies.kotlin implementation supportDependencies.appCompat implementation supportDependencies.recyclerView implementation supportDependencies.design implementation supportDependencies.lifecycleExtension implementation supportDependencies.constraintlayout implementation supportDependencies.multiDex
multiDexEnabled true को डिफ़ॉल्ट कॉन्फिग में निर्दिष्ट करना न भूलें। ज्यादातर मामलों में, आप उपयोग की जाने वाली विधियों की संख्या की सीमा तक जल्दी पहुंच जाएंगे।
उसी तरह, आपको ऐप की सभी निर्भरताओं की घोषणा करने की आवश्यकता है। आइए अपने ऐप को इंटरनेट से जोड़ने के लिए अनुमतियां जोड़ें:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.INTERNET" />
यदि मैनिफेस्ट में कोई नाम नहीं जोड़ा गया है, तो आपको ऐसा करना चाहिए क्योंकि Stetho नामहीन ऐप को नहीं देख पाएगा और आप डेटाबेस में देखने में सक्षम नहीं होंगे।
मूल घटक निर्माण
यह ध्यान देने योग्य है कि इस आर्किटेक्चर के निर्माण के आधार के रूप में MVVM (Model-View-ViewModel) पैटर्न का उपयोग किया गया था।
चलिए विकास शुरू करते हैं। पहली चीज जो आपको करने की आवश्यकता है वह एक क्लास बनाना है जो Application() को इनहेरिट करेगा। इस क्लास में, हम ऐप कॉन्टेक्स्ट तक पहुंच देंगे ताकि इसका आगे उपयोग किया जा सके।
@SuppressWarnings("all") class App : Application() { companion object { lateinit var instance: App private set } override fun onCreate() { super.onCreate() instance = this Stetho.initializeWithDefaults(this) DatabaseCreator.createDatabase(this) } }
दूसरा चरण बेसिक ऐप कंपोनेंट्स बनाना है, जिसकी शुरुआत ViewModel से होगी, जिसका उपयोग मैं प्रत्येक Activity या Fragment के लिए करूंगा।
abstract class BaseViewModel constructor(app: Application) : AndroidViewModel(app) { override fun onCleared() { super.onCleared() } }
इस ऐप में जटिल कार्यक्षमता नहीं है। लेकिन बेसिक ViewModel में हम 3 मुख्य LiveData रखेंगे:
- एरर प्रोसेसिंग
- प्रोग्रेस बार प्रदर्शित के साथ लोडिंग प्रोसेसिंग
- और, चूंकि मेरे पास सूचियों वाला ऐप है, एडाप्टर में रसीद और डेटा उपलब्धता को प्रोसेस करना एक प्लेसहोल्डर के रूप में जो उनकी अनुपस्थिति में प्रदर्शित होता है।
val errorLiveData = MediatorLiveData<String>() val isLoadingLiveData = MediatorLiveData<Boolean>() val isEmptyDataPlaceholderLiveData = MediatorLiveData<Boolean>()
फंक्शन के कार्यान्वयन के परिणाम को LiveData में स्थानांतरित करने के लिए मैं Consumer का उपयोग करूंगा।
ऐप में किसी भी स्थान पर त्रुटियों को प्रोसेस करने के लिए, आपको एक Consumer बनाने की आवश्यकता है जो Throwable.message मान को errorLiveData में स्थानांतरित करेगा।
इसके अलावा, बेसिक ViewModel में, आपको एक मेथड बनाने की आवश्यकता होगी जो प्रोग्रेस बार को उनके कार्यान्वयन के दौरान प्रदर्शित करने के लिए एक LiveData सूची प्राप्त करेगा।
हमारा बेसिक ViewModel इस तरह दिखेगा:
abstract class BaseViewModel constructor(app: Application) : AndroidViewModel(app) { val errorLiveData = MediatorLiveData<String>() val isLoadingLiveData = MediatorLiveData<Boolean>() val isEmptyDataPlaceholderLiveData = MediatorLiveData<Boolean>() private var compositeDisposable: CompositeDisposable? = null protected open val onErrorConsumer = Consumer<Throwable> { errorLiveData.value = it.message } fun setLoadingLiveData(vararg mutableLiveData: MutableLiveData<*>) { mutableLiveData.forEach { liveData -> isLoadingLiveData.apply { this.removeSource(liveData) this.addSource(liveData) { this.value = false } } } } override fun onCleared() { isLoadingLiveData.value = false isEmptyDataPlaceholderLiveData.value = false clearSubscription() super.onCleared() } private fun clearSubscription() { compositeDisposable?.apply { if (!isDisposed) dispose() compositeDisposable = null } } }
हमारे ऐप में दो स्क्रीन (समाचार सूची स्क्रीन और पसंदीदा सूची स्क्रीन) के लिए कुछ Activities बनाने का कोई मतलब नहीं है। लेकिन चूंकि यह सैंपल इष्टतम और आसानी से विस्तार योग्य आर्किटेक्चर के कार्यान्वयन को दिखाता है, मैं एक बेसिक ऐप बनाऊंगा।
हमारा ऐप 1 Activity और 2 Fragments पर बनाया जाएगा जिन्हें हम Container एक्टिविटी में इन्फ्लेट करेंगे। हमारे Activity की XML फाइल निम्नलिखित होगी:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <FrameLayout android:id="@+id/flContainer" android:layout_width="match_parent" android:layout_height="match_parent" /> <include layout="@layout/include_placeholder"/> <include layout="@layout/include_progress_bar" /> </FrameLayout>
जहां include_placeholder और include_progressbar इस तरह दिखेंगे:
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="https://schemas.android.com/apk/res/android" android:id="@+id/flProgress" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/bg_black_40"> <ProgressBar android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:background="@color/transparent" /> </FrameLayout>
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="https://schemas.android.com/apk/res/android" android:id="@+id/flPlaceholder" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/bg_transparent"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:background="@color/transparent" android:src="@drawable/ic_business_light_blue_800_24dp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginTop="40dp" android:text="@string/empty_data" android:textColor="@color/colorPrimary" android:textStyle="bold" /> </FrameLayout>
हमारा BaseActivity इस तरह दिखेगा:
abstract class BaseActivity<T : BaseViewModel> : AppCompatActivity(), BackPressedCallback, ProgressViewCallback, EmptyDataPlaceholderCallback { protected abstract val viewModelClass: Class<T> protected abstract val layoutId: Int protected abstract val containerId: Int protected open val viewModel: T by lazy(LazyThreadSafetyMode.NONE) { ViewModelProviders.of(this).get(viewModelClass) } protected abstract fun observeLiveData(viewModel: T) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(layoutId) startObserveLiveData() } private fun startObserveLiveData() { observeLiveData(viewModel) } }
आइए सभी भविष्य की Activities की प्रक्रियाओं में संभावित त्रुटियों को प्रदर्शित करने का तरीका लागू करें। मैं इसे सामान्य Toast के रूप में सरलता के लिए करूंगा।
protected open fun processError(error: String) = Toast.makeText(this, error, Toast.LENGTH_SHORT).show()
और इस एरर टेक्स्ट को डिस्प्ले मेथड में भेजें:
protected open val errorObserver = Observer<String> { it?.let { processError(it) } }
बेसिक Activity में मैं बेसिक View Model में स्थित errorLiveData मान के परिवर्तनों पर नज़र रखना शुरू करूंगा। startObserveLiveData() मेथड इस प्रकार बदलेगा:
private fun startObserveLiveData() { observeLiveData(viewModel) with(viewModel) { errorLiveData.observe(this@BaseActivity, errorObserver) } }
अब बेसिक ViewModel के onErrorConsumer का उपयोग onError प्रोसेसर के रूप में करके, आप कार्यान्वित मेथड त्रुटि के बारे में संदेश देखेंगे।
एक मेथड बनाएं जो बैक स्टैक में जोड़ने की क्षमता के साथ Activity में Fragments को बदलने की अनुमति देता है।
protected open fun replaceFragment(fragment: Fragment, needToAddToBackStack: Boolean = true) { val name = fragment.javaClass.simpleName with(supportFragmentManager.beginTransaction()) { replace(containerId, fragment, name) if (needToAddToBackStack) { addToBackStack(name) } commit() } }
आइए आवश्यक ऐप स्थानों में प्रोग्रेस और प्लेसहोल्डर प्रदर्शित करने के लिए इंटरफेस बनाएं।
interface EmptyDataPlaceholderCallback { fun onShowPlaceholder() fun onHidePlaceholder() }
interface ProgressViewCallback { fun onShowProgress() fun onHideProgress() }
उन्हें बेसिक Activity में लागू करें। मैंने प्रोग्रेस बार और प्लेसहोल्डर के लिए ID सेटिंग के फंक्शन बनाए, और इन Views को भी इनिशियलाइज़ किया।
protected open fun hasProgressBar(): Boolean = false protected abstract fun progressBarId(): Int protected abstract fun placeholderId(): Int private var vProgress: View? = null private var vPlaceholder: View? = null
override fun onShowProgress() { vProgress?.visibility = View.VISIBLE } override fun onHideProgress() { vProgress?.visibility = View.GONE } override fun onShowPlaceholder() { vPlaceholder?.visibility = View.VISIBLE } override fun onHidePlaceholder() { vPlaceholder?.visibility = View.INVISIBLE } public override fun onStop() { super.onStop() onHideProgress() }
और अंत में onCreate मेथड में मैं View के लिए एक ID सेट करता हूं:
if (hasProgressBar()) { vProgress = findViewById(progressBarId()) vProgress?.setOnClickListener(null) } vPlaceholder = findViewById(placeholderId()) startObserveLiveData()
मैंने बेसिक ViewModel और Basic Activity के निर्माण को वर्तनीबद्ध किया है। Basic Fragment को उसी सिद्धांत का पालन करके बनाया जाएगा।
जब आप प्रत्येक अलग स्क्रीन बनाते हैं, यदि आप आगे विस्तार और संभावित परिवर्तनों पर विचार कर रहे हैं, तो आपको अपने ViewModel के साथ एक अलग Fragment बनाने की आवश्यकता है।
नोट: उस मामले में जब Fragments को एक क्लस्टर में जोड़ा जा सकता है, और व्यावसायिक तर्क बड़े पैमाने पर जटिलता नहीं दर्शाता है, तो कई Fragments एक ViewModel का उपयोग कर सकते हैं।
Fragments के बीच स्विचिंग उन इंटरफेस के कारण होता है जो Activity में लागू होते हैं। ऐसा करने के लिए, प्रत्येक Fragment में companion object{ } होना चाहिए जिसमें Bundle में आर्गुमेंट ट्रांसफर करने की क्षमता के साथ Fragment ऑब्जेक्ट बनाने का मेथड हो:
companion object { fun newInstance() = FavoriteFragment().apply { arguments = Bundle() } }
आर्किटेक्चर समाधान
जब बेसिक कंपोनेंट्स बन जाते हैं, तो आर्किटेक्चर पर ध्यान केंद्रित करने का समय आ जाता है। योजनाबद्ध रूप से यह प्रसिद्ध रॉबर्ट सी. मार्टिन या अंकल बॉब द्वारा बनाई गई क्लीन आर्किटेक्चर जैसा दिखेगा। लेकिन चूंकि मैं RxJava2 का उपयोग करता हूं, मैं Boundaries इंटरफेस ( Dependency Rule निष्पादन सुनिश्चित करने के तरीके के रूप में) से छुटकारा पा लेता हूं मानक Observable और Subscriber के पक्ष में।
इसके अलावा, RxJava2 टूल्स का उपयोग करके मैंने अधिक लचीले काम के लिए डेटा रूपांतरण को एकीकृत किया है। यह सर्वर प्रतिक्रियाओं और डेटाबेस दोनों के साथ काम करने से संबंधित है।
प्राथमिक मॉडल के अलावा, मैं एक सर्वर प्रतिक्रिया मॉडल और Room के लिए एक अलग टेबल मॉडल बनाऊंगा। इन दो मॉडलों के बीच डेटा परिवर्तित करके, आप रूपांतरण प्रक्रिया के दौरान कोई भी परिवर्तन कर सकते हैं, सर्वर प्रतिक्रियाओं को परिवर्तित कर सकते हैं, और UI पर प्रदर्शित होने से पहले आवश्यक डेटा को बेस में सहेज सकते हैं।
Fragments UI के लिए जिम्मेदार हैं, और ViewModel Fragments व्यावसायिक तर्क निष्पादन के लिए जिम्मेदार हैं। यदि व्यावसायिक तर्क पूरी गतिविधि से संबंधित है, तो ViewModel Activity।
ViewModels val … by lazy{}, के माध्यम से इसके इनिशियलाइज़ेशन द्वारा एक प्रदाता से डेटा प्राप्त करते हैं, यदि आपको एक अपरिवर्तनीय ऑब्जेक्ट की आवश्यकता है, या lateinit var, यदि इसके विपरीत। व्यावसायिक तर्क के निष्पादन के बाद, यदि आपको UI, बदलने के लिए डेटा स्थानांतरित करने की आवश्यकता है, तो आप ViewModel में नया MutableLiveData बनाते हैं जिसका उपयोग आप हमारे Fragment के observeLiveData() मेथड में करेंगे।
यह काफी आसान लगता है। कार्यान्वयन भी सीधा है।
हमारी आर्किटेक्चर का एक अनिवार्य घटक एक डेटा कनवर्टर है जो एक डेटा प्रकार से दूसरे में सरल रूपांतरण पर आधारित है। RxJava डेटा स्ट्रीम के रूपांतरण के लिए, प्रकार के आधार पर SingleTransformer या FlowableTransformer का उपयोग किया जाता है। हमारे ऐप के मामले में, कनवर्टर का इंटरफेस और एब्सट्रैक्ट क्लास इस तरह दिखते हैं:
interface BaseDataConverter<IN, OUT> { fun convertInToOut(inObject: IN): OUT fun convertOutToIn(outObject: OUT): IN fun convertListInToOut(inObjects: List<IN>?): List<OUT>? fun convertListOutToIn(outObjects: List<OUT>?): List<IN>? fun convertOUTtoINSingleTransformer(): SingleTransformer<IN?, OUT> fun convertListINtoOUTSingleTransformer(): SingleTransformer<List<OUT>, List<IN>> } abstract class BaseDataConverterImpl<IN, OUT> : BaseDataConverter<IN, OUT> { override fun convertInToOut(inObject: IN): OUT = processConvertInToOut(inObject) override fun convertOutToIn(outObject: OUT): IN = processConvertOutToIn(outObject) override fun convertListInToOut(inObjects: List<IN>?): List<OUT> = inObjects?.map { convertInToOut(it) } ?: listOf() override fun convertListOutToIn(outObjects: List<OUT>?): List<IN> = outObjects?.map { convertOutToIn(it) } ?: listOf() override fun convertOUTtoINSingleTransformer() = SingleTransformer<IN?, OUT> { it.map { convertInToOut(it) } } override fun convertListINtoOUTSingleTransformer() = SingleTransformer<List<OUT>, List<IN>> { it.map { convertListOutToIn(it) } } protected abstract fun processConvertInToOut(inObject: IN): OUT protected abstract fun processConvertOutToIn(outObject: OUT): IN }
इस उदाहरण में, मैं बेसिक रूपांतरण जैसे मॉडल-मॉडल, मॉडल की सूची - मॉडल की सूची, और वही संयोजन लेकिन केवल सर्वर प्रतिक्रियाओं और डेटाबेस में अनुरोधों के प्रोसेसिंग के लिए SingleTransformer का उपयोग कर रहा हूं।
आइए नेटवर्क से शुरू करें - RestClient. retrofitBuilder मेथड निम्नलिखित होगा:
fun retrofitBuilder(): Retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(NullOrEmptyConverterFactory().converterFactory()) .addConverterFactory(GsonConverterFactory.create(createGsonBuilder())) .client(createHttpClient()) .build()
//base url const val BASE_URL = "https://newsapi.org"
थर्ड-पार्टी APIs का उपयोग करते समय, सर्वर से पूर्ण शून्य प्रतिक्रिया प्राप्त करने का हमेशा एक मौका होता है, और इसके बहुत सारे कारण हो सकते हैं। इसलिए एक अतिरिक्त NullOrEmptyConverterFactory स्थिति को संभालने में मदद करेगा। यह इस तरह दिखता है:
class NullOrEmptyConverterFactory : Converter.Factory() { fun converterFactory() = this override fun responseBodyConverter(type: Type?, annotations: Array<Annotation>, retrofit: Retrofit): Converter<ResponseBody, Any>? { return Converter { responseBody -> if (responseBody.contentLength() == 0L) { null } else { type?.let { retrofit.nextResponseBodyConverter<Any>(this, it, annotations)?.convert(responseBody) } } } } }
मॉडल बनाने के लिए, एक API पर निर्माण करना आवश्यक है। एक उदाहरण के रूप में, मैं newsapi.org से गैर-व्यावसायिक उपयोग के लिए मुफ्त APU का उपयोग करूंगा। इसमें अनुरोधित कार्यक्षमता की काफी व्यापक सूची है, लेकिन मैं इस उदाहरण के लिए एक छोटे हिस्से का उपयोग करूंगा। एक त्वरित पंजीकरण के बाद, आपको API तक पहुंच प्राप्त होती है और आपका api key जो प्रत्येक अनुरोध के लिए आवश्यक है।
एंडपॉइंट के रूप में, मैं https://newsapi.org/v2/everything. का उपयोग करूंगा। सुझाए गए query से मैं निम्नलिखित चुनता हूं: q - खोज क्वेरी, from - तारीख से सॉर्टिंग, to - तारीख तक सॉर्टिंग, sortBy - चयनित मानदंड द्वारा सॉर्टिंग, और अनिवार्य apiKey।
RestClient बनाने के बाद, मैं चयनित क्वेरी के साथ हमारे ऐप के लिए एक API इंटरफेस बनाता हूं:
interface NewsApi { @GET(ENDPOINT_EVERYTHING) fun getNews(@Query("q") searchFor: String?, @Query("from") fromDate: String?, @Query("to") toDate: String?, @Query("sortBy") sortBy: String?, @Query("apiKey") apiKey: String?): Single<NewsNetworkModel> }
//endpoints const val ENDPOINT_EVERYTHING = "/v2/everything"
हम इस प्रतिक्रिया को NewsNetworkModel में प्राप्त करेंगे:
data class NewsNetworkModel(@SerializedName("articles") var articles: List<ArticlesNetworkModel>? = listOf())
data class ArticlesNetworkModel(@SerializedName("title") var title: String? = null, @SerializedName("description") var description: String? = null, @SerializedName("urlToImage") var urlToImage: String? = null)
पूरी प्रतिक्रिया से ये डेटा एक तस्वीर, शीर्षक और समाचार विवरण के साथ एक सूची प्रदर्शित करने के लिए पर्याप्त होंगे।
हमारे आर्किटेक्चरल दृष्टिकोण के कार्यान्वयन के लिए, आइए सामान्य मॉडल बनाएं:
interface News { var articles: List<Article>? } class NewsModel(override var articles: List<Article>? = null) : News
interface Article { var id: Long? var title: String? var description: String? var urlToImage: String? var isAddedToFavorite: Boolean? var fragmentName: FragmentsNames? } class ArticleModel(override var id: Long? = null, override var title: String? = null, override var description: String? = null, override var urlToImage: String? = null, override var isAddedToFavorite: Boolean? = null, override var fragmentName: FragmentsNames? = null) : Article
चूंकि Article मॉडल का उपयोग डेटाबेस के साथ कनेक्शन और एडाप्टर में डेटा प्रदर्शित करने के लिए किया जाएगा, हमें 2 मार्जिन जोड़ने की आवश्यकता है जिनका उपयोग मैं सूची में UI तत्वों को बदलने के लिए करूंगा।
जब सब कुछ नेटवर्क मॉडल के लिए कनवर्टर बनाने के लिए तैयार है, तो हम उनका उपयोग NetworkModule के माध्यम से समाचार प्राप्त करने के क्वेरी में करेंगे।
कनवर्टर्स नेस्टिंग के रिवर्स ऑर्डर में बनाए जाते हैं, और वे डायरेक्ट ऑर्डर में तदनुसार परिवर्तित करते हैं। इसलिए पहला मैं Article पर बनाता हूं, दूसरा News पर:
interface ArticlesBeanConverter class ArticlesBeanDataConverterImpl : BaseDataConverterImpl<ArticlesNetworkModel, Article>(), ArticlesBeanConverter { override fun processConvertInToOut(inObject: ArticlesNetworkModel): Article = inObject.run { ArticleModel(null, title, description, urlToImage, false, FragmentsNames.NEWS) } override fun processConvertOutToIn(outObject: Article): ArticlesNetworkModel = outObject.run { ArticlesNetworkModel(title, description, urlToImage) } }
interface NewsBeanConverter class NewsBeanDataConverterImpl : BaseDataConverterImpl<NewsNetworkModel, News>(), NewsBeanConverter { private val articlesConverter by lazy { ArticlesBeanDataConverterImpl() } override fun processConvertInToOut(inObject: NewsNetworkModel): News = inObject.run { NewsModel(articles?.let { articlesConverter.convertListInToOut(it) }) } override fun processConvertOutToIn(outObject: News): NewsNetworkModel = outObject.run { NewsNetworkModel(articles?.let { articlesConverter.convertListOutToIn(it) }) } }
जैसा कि आप ऊपर देख सकते हैं, News ऑब्जेक्ट रूपांतरण के दौरान, Article ऑब्जेक्ट्स की सूची का रूपांतरण भी निष्पादित होता है।
एक बार नेटवर्क मॉडल के लिए कनवर्टर बन जाने के बाद, आइए मॉड्यूल (रिपॉजिटरी नेटवर्क) के निर्माण पर आगे बढ़ें। चूंकि आमतौर पर 1 या 2 से अधिक API इंटरफेस होते हैं, आपको BaseModule, टाइप्ड API, Network Module, और ConversionModel बनाने की आवश्यकता है।
यह इस तरह दिखता है:
abstract class BaseNetworkModule<A, NM, M>(val api: A, val dataConverter: BaseDataConverter<NM, M>)
तदनुसार, यह NewsModule पर निम्नलिखित होगा:
interface NewsModule { fun getNews(fromDate: String? = null, toDate: String? = null, sortBy: String? = null): Single<News> } class NewsModuleImpl(api: NewsApi) : BaseNetworkModule<NewsApi, NewsNetworkModel, News>(api, NewsBeanDataConverterImpl()), NewsModule { override fun getNews(fromDate: String?, toDate: String?, sortBy: String?): Single<News> = api.getNews(searchFor = SEARCH_FOR, fromDate = fromDate, toDate = toDate, sortBy = sortBy, apiKey = API_KEY) .compose(dataConverter.convertOUTtoINSingleTransformer()) .onErrorResumeNext(NetworkErrorUtils.rxParseError()) }
इस API के लिए, API key किसी भी सुझाए गए एंडपॉइंट्स द्वारा अनुरोध के लिए एक महत्वपूर्ण पैरामीटर है। इसलिए आपको यह सुनिश्चित करने की आवश्यकता है कि वैकल्पिक पैरामीटर पहले से निर्दिष्ट नहीं किए जाएंगे, और आपको उन्हें डिफ़ॉल्ट रूप से नल करना होगा।
जैसा कि आप ऊपर देख सकते हैं, मैंने प्रतिक्रिया प्रोसेसिंग के दौरान डेटा रूपांतरण लागू किया।
आइए डेटाबेस के साथ काम करें। मैं ऐप डेटाबेस बनाता हूं, इसे AppDatabase कहता हूं और RoomDatabase() से इनहेरिट करता हूं।
डेटाबेस इनिशियलाइज़ेशन के लिए, DatabaseCreator बनाना आवश्यक है, जिसे App क्लास में इनिशियलाइज़ किया जाना चाहिए।
object DatabaseCreator { lateinit var database: AppDatabase private val isDatabaseCreated = MutableLiveData<Boolean>() private val mInitializing = AtomicBoolean(true) @SuppressWarnings("CheckResult") fun createDatabase(context: Context) { if (mInitializing.compareAndSet(true, false).not()) return isDatabaseCreated.value = false Completable.fromAction { database = Room.databaseBuilder(context, AppDatabase::class.java, DB_NAME).build() } .compose { completableToMain(it) } .subscribe({ isDatabaseCreated.value = true }, { it.printStackTrace() }) } }
अब App क्लास के onCreate() मेथड में मैं Stetho और डेटाबेस इनिशियलाइज़ करता हूं:
override fun onCreate() { super.onCreate() instance = this Stetho.initializeWithDefaults(this) DatabaseCreator.createDatabase(this) }
जब डेटाबेस बन जाता है, तो मैं एक बेसिक Dao बनाता हूं जिसमें अंदर एक सिंगल insert() मेथड होता है:
@Dao interface BaseDao<in I> { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(obj: I) }
हमारे ऐप के विचार के आधार पर, मैं पसंद की गई खबरों को सहेजूंगा, सहेजे गए लेखों की सूची प्राप्त करूंगा, सहेजी गई खबर को उसके ID द्वारा हटाऊंगा, या टेबल से सभी खबरें हटाऊंगा। हमारा NewsDao निम्नलिखित होगा:
@Dao interface NewsDao : BaseDao<NewsDatabase> { @Query("SELECT * FROM $NEWS_TABLE") fun getNews(): Single<List<NewsDatabase>> @Query("DELETE FROM $NEWS_TABLE WHERE id = :id") fun deleteNewsById(id: Long) @Query("DELETE FROM $NEWS_TABLE") fun deleteFavoriteNews() }
और समाचार टेबल निम्नलिखित होगी:
@Entity(tableName = NEWS_TABLE) data class NewsDatabase(@PrimaryKey var id: Long?, var title: String?, var description: String?, var urlToImage: String?)
जब टेबल बन जाती है, तो आइए इसे डेटाबेस से लिंक करें:
@Database(entities = [NewsDatabase::class], version = DB_VERSION) abstract class AppDatabase : RoomDatabase() { abstract fun newsDao(): NewsDao }
अब हम डेटाबेस के साथ काम कर सकते हैं, इसमें डेटा सहेज और निकाल सकते हैं।
नेटवर्क के लिए मॉड्यूल (रिपॉजिटरी) के रूप में, मैं एक मॉडल कनवर्टर - डेटाबेस टेबल मॉडल बनाऊंगा:
interface NewsDatabaseConverter class NewsDatabaseDataConverterImpl : BaseDataConverterImpl<Article, NewsDatabase>(), NewsDatabaseConverter { override fun processConvertInToOut(inObject: Article): NewsDatabase = inObject.run { NewsDatabase(id, title, description, urlToImage) } override fun processConvertOutToIn(outObject: NewsDatabase): Article = outObject.run { ArticleModel(id, title, description, urlToImage, true, FragmentsNames.FAVORITES) } }
विभिन्न टेबल्स के साथ काम करने के लिए BaseRepository उपलब्ध है। आइए इसे लिखें। यह अपने सरलतम संस्करण में इस तरह दिखेगा जो ऐप के लिए पर्याप्त है:
abstract class BaseRepository<M, DBModel> { protected abstract val dataConverter: BaseDataConverter<M, DBModel> protected abstract val dao: BaseDao<DBModel> }
BaseRepository बनाने के बाद, मैं NewsRepository बनाऊंगा:
interface NewsRepository { fun saveNew(article: Article): Single<Article> fun getSavedNews(): Single<List<Article>> fun deleteNewsById(id: Long): Single<Unit> fun deleteAll(): Single<Unit> } object NewsRepositoryImpl : BaseRepository<Article, NewsDatabase>(), NewsRepository { override val dataConverter by lazy { NewsDatabaseDataConverterImpl() } override val dao by lazy { DatabaseCreator.database.newsDao() } override fun saveNew(article: Article): Single<Article> = Single.just(article) .map { dao.insert(dataConverter.convertInToOut(it)) } .map { article } override fun getSavedNews(): Single<List<Article>> = dao.getNews().compose(dataConverter.convertListINtoOUTSingleTransformer()) override fun deleteNewsById(id: Long): Single<Unit> = Single.just(dao.deleteNewsById(id)) override fun deleteAll(): Single<Unit> = Single.just(dao.deleteFavoriteNews()) }
जब स्थायी रिपॉजिटरी और मॉड्यूल बन जाते हैं, तो डेटा एक ऐप प्रदाता से प्रवाहित होना चाहिए जो आवश्यकताओं के आधार पर नेटवर्क या डेटाबेस से डेटा का अनुरोध करेगा। एक प्रदाता को दोनों रिपॉजिटरी को जोड़ना चाहिए। विभिन्न मॉडल और रिपॉजिटरी की क्षमताओं को ध्यान में रखते हुए, मैं BaseProvider बनाऊंगा:
abstract class BaseProvider<NM, DBR> { val repository: DBR = this.initRepository() val networkModule: NM = this.initNetworkModule() protected abstract fun initRepository(): DBR protected abstract fun initNetworkModule(): NM }
फिर NewsProvider इस तरह दिखेगा:
interface NewsProvider { fun loadNewsFromServer(fromDate: String? = null, toDate: String? = null, sortBy: String? = null): Single<News> fun saveNewToDB(article: Article): Single<Article> fun getSavedNewsFromDB(): Single<List<Article>> fun deleteNewsByIdFromDB(id: Long): Single<Unit> fun deleteNewsFromDB(): Single<Unit> } object NewsProviderImpl : BaseProvider<NewsModule, NewsRepositoryImpl>(), NewsProvider { override fun initRepository() = NewsRepositoryImpl override fun initNetworkModule() = NewsModuleImpl(RestClient.retrofitBuilder().create(NewsApi::class.java)) override fun loadNewsFromServer(fromDate: String?, toDate: String?, sortBy: String?) = networkModule.getNews(fromDate, toDate, sortBy) override fun saveNewToDB(article: Article) = repository.saveNew(article) override fun getSavedNewsFromDB() = repository.getSavedNews() override fun deleteNewsByIdFromDB(id: Long) = repository.deleteNewsById(id) override fun deleteNewsFromDB() = repository.deleteAll() }
अब हम आसानी से समाचारों की सूची प्राप्त करेंगे। NewsViewModel में हम अपने प्रदाता के सभी मेथड्स की घोषणा करेंगे ताकि उनका आगे उपयोग किया जा सके:
val loadNewsSuccessLiveData = MutableLiveData<News>() val loadLikedNewsSuccessLiveData = MutableLiveData<List<Article>>() val deleteLikedNewsSuccessLiveData = MutableLiveData<Boolean>() private val loadNewsSuccessConsumer = Consumer<News> { loadNewsSuccessLiveData.value = it } private val loadLikedNewsSuccessConsumer = Consumer<List<Article>> { loadLikedNewsSuccessLiveData.value = it } private val deleteLikedNewsSuccessConsumer = Consumer<Unit> { deleteLikedNewsSuccessLiveData.value = true } private val dataProvider by lazy { NewsProviderImpl } init { isLoadingLiveData.apply { addSource(loadNewsSuccessLiveData) { value = false } }
@SuppressLint("CheckResult") fun loadNews(fromDate: String? = null, toDate: String? = null, sortBy: String? = null) { isLoadingLiveData.value = true isEmptyDataPlaceholderLiveData.value = false dataProvider.loadNewsFromServer(fromDate, toDate, sortBy) .compose(RxUtils.ioToMainTransformer()) .subscribe(loadNewsSuccessConsumer, onErrorConsumer) } @SuppressLint("CheckResult") fun saveLikedNew(article: Article) { Single.fromCallable { Unit } .flatMap { dataProvider.saveNewToDB(article) } .compose(RxUtils.ioToMainTransformerSingle()) .subscribe({}, { onErrorConsumer }) } @SuppressLint("CheckResult") fun removeLikedNew(id: Long) { Single.fromCallable { Unit } .flatMap { dataProvider.deleteNewsByIdFromDB(id) } .compose(RxUtils.ioToMainTransformerSingle()) .subscribe({}, { onErrorConsumer }) } @SuppressLint("CheckResult") fun loadLikedNews() { Single.fromCallable { Unit } .flatMap { dataProvider.getSavedNewsFromDB() } .compose(RxUtils.ioToMainTransformerSingle()) .subscribe(loadLikedNewsSuccessConsumer, onErrorConsumer) } @SuppressLint("CheckResult") fun removeLikedNews() { Single.fromCallable { Unit } .flatMap { dataProvider.deleteNewsFromDB() } .compose(RxUtils.ioToMainTransformerSingle()) .subscribe(deleteLikedNewsSuccessConsumer, onErrorConsumer) }
ViewModel में व्यावसायिक तर्क को निष्पादित करने वाले सभी मेथड्स घोषित करने के बाद, हम उन्हें Fragment से वापस कॉल करेंगे जहां observeLiveData() में प्रत्येक घोषित LiveData के परिणाम प्रोसेस किए जाएंगे।
इसे आसानी से लागू करने के लिए, SEARCH_FOR पैरामीटर्स में मैंने बेतरतीब ढंग से Apple चुना, और आगे की सॉर्टिंग popularity टैग द्वारा की जाएगी। यदि आवश्यक हो, तो आप इन पैरामीटर्स को बदलने के लिए न्यूनतम कार्यक्षमता जोड़ सकते हैं।
चूंकि newsapi.org आपको समाचार ID प्रदान नहीं करता है, मैं एलिमेंट इंडेक्स को ID के रूप में स्वीकार करता हूं। लोकप्रियता टैग द्वारा सॉर्टिंग भी API के माध्यम से लागू की जाती है। लेकिन लोकप्रियता द्वारा सॉर्टिंग के दौरान बेस में समान IDs के साथ डेटा रीराइटिंग से बचने के लिए, मैं समाचार सूची लोड करने से पहले बेस में डेटा उपलब्धता सत्यापित करूंगा। यदि बेस खाली है - नई सूची लोड हो रही है, यदि नहीं - अधिसूचना दिखाई जाती है।
आइए NewsFragment के onViewCreated() मेथड में निम्नलिखित मेथड कॉल करें:
private fun loadLikedNews() { viewModel.loadLikedNews() }
चूंकि हमारा बेस खाली है, loadNews() मेथड लॉन्च होगा। observeLiveData मेथड में मैं हमारे लोडिंग LiveData - viewModel.loadNewsSuccessLiveData.observe(..){news →}, का उपयोग करूंगा, जहां हम सफल अनुरोध होने पर समाचार लेखों की सूची प्राप्त करेंगे, और फिर इसे एडाप्टर में स्थानांतरित करेंगे:
isEmptyDataPlaceholderLiveData.value = news.articles?.isEmpty() with(newsAdapter) { news.articles?.toMutableList()?.let { clear() addAll(it) } notifyDataSetChanged() } loadNewsSuccessLiveData.value = null
ऐप लॉन्च करने पर, आपको निम्नलिखित परिणाम दिखाई देगा:
टूलबार मेनू में दाईं ओर, आप 2 विकल्प देख सकते हैं - सॉर्टिंग और पसंदीदा। आइए सूची को लोकप्रियता द्वारा सॉर्ट करें और निम्नलिखित परिणाम प्राप्त करें:
यदि आप पसंदीदा पर जाते हैं, तो आप केवल एक प्लेसहोल्डर देखेंगे, क्योंकि बेस में कोई डेटा नहीं है। पसंदीदा स्क्रीन निम्नलिखित जैसी दिखेगी:
पसंदीदा का UI फ्रैगमेंट पसंद की गई समाचारों की सूची प्रदर्शित करने के लिए एक स्क्रीन और डेटाबेस सफाई के लिए टूलबार में केवल एक विकल्प है। जब आप "लाइक" पर क्लिक करके डेटा सहेजते हैं, तो स्क्रीन निम्नलिखित जैसी दिखेगी:
जैसा कि मैंने ऊपर लिखा, मानक मॉडल में सामान्य मॉडल में 2 अतिरिक्त मार्जिन जोड़े गए थे, और इन मार्जिन का उपयोग एडाप्टर में डेटा प्रदर्शित करने के लिए किया जाता है। अब आप देख सकते हैं कि सहेजी गई समाचार सूची के तत्वों में पसंदीदा में जोड़ने का विकल्प नहीं है।
var isAddedToFavorite: Boolean? var fragmentName: FragmentsNames?
यदि आप फिर से "लाइक" पर क्लिक करते हैं, तो सहेजा गया तत्व बेस से हटा दिया जाएगा।
समेटना
इस प्रकार, मैंने आपको Android ऐप डेवलपमेंट के लिए एक सरल और स्पष्ट दृष्टिकोण दिखाया। हमने क्लीन आर्किटेक्चर के मुख्य सिद्धांतों का पालन किया लेकिन इसे जितना संभव हो उतना सरल बनाया।
मेरे द्वारा प्रदान की गई आर्किटेक्चर और मिस्टर मार्टिन की क्लीन आर्किटेक्चर में क्या अंतर है? शुरुआत में ही, मैंने नोट किया है कि मेरी आर्किटेक्चर CA के समान है क्योंकि इसे आधार के रूप में उपयोग किया जाता है। यहां नीचे CA योजना है:
इवेंट Presenter के पास जाता है, और फिर Use Case के पास। Use Case Repository का अनुरोध करता है। Repository डेटा प्राप्त करता है, Entity बनाता है, और इसे UseCase में स्थानांतरित करता है। इस प्रकार, Use Case सभी आवश्यक Entities प्राप्त करता है। व्यावसायिक तर्क के कार्यान्वयन के बाद, आपको परिणाम मिलता है जो Presenter के पास वापस आता है, और यह, बदले में, परिणाम को UI में स्थानांतरित करता है।
नीचे की योजना में, Controller InputPort से मेथड्स को कॉल करता है जो UseCase को लागू करता है, और OutputPort इंटरफेस इस प्रतिक्रिया को प्राप्त करता है और Presenter इसे लागू करता है। UseCase के सीधे Presenter पर निर्भर होने के बजाय, यह अपनी लेयर्स में इंटरफेस पर निर्भर करता है, और यह Dependency Rule का विरोध नहीं करता, और Presenter को इस इंटरफेस को लागू करना चाहिए।
इस प्रकार, बाहरी लेयर में कार्यान्वित प्रक्रियाएं आंतरिक लेयर में प्रक्रियाओं को प्रभावित नहीं करती हैं। क्लीन आर्किटेक्चर में Entity क्या है? वास्तव में, यह वह सब कुछ है जो किसी विशिष्ट ऐप पर निर्भर नहीं करता है, और यह कई ऐप्स के लिए एक सामान्य अवधारणा होगी। लेकिन मोबाइल डेवलपमेंट प्रक्रिया में Entity ऐप के व्यावसायिक ऑब्जेक्ट्स हैं, जिनमें सामान्य और उच्च-स्तरीय नियम (ऐप बिजनेस लॉजिक) होते हैं।
Gateways के बारे में क्या? जैसा कि मैं इसे देखता हूं, Gateways डेटाबेस के साथ काम करने के लिए एक रिपॉजिटरी और नेटवर्क के साथ काम करने के लिए एक मॉड्यूल है। हम कंट्रोलर से छुटकारा पा लेते हैं क्योंकि प्रारंभ में क्लीन आर्किटेक्चर उच्च जटिलता के व्यावसायिक ऐप्स की संरचना के लिए बनाया गया था, और डेटा कनवर्टर्स मेरे ऐप में इसके कार्यों को करते हैं। ViewModels UI प्रोसेसिंग के लिए Fragments को डेटा स्थानांतरित करते हुए Presenters को प्रतिस्थापित करते हैं।
मेरे दृष्टिकोण में, मैं Dependency Rule का सख्ती से पालन भी करता हूं, और रिपॉजिटरी, मॉड्यूल, मॉडल, और प्रदाताओं का तर्क एनकैप्सुलेटेड है, और उन तक पहुंच इंटरफेस के माध्यम से संभव है। इस प्रकार, बाहरी लेयर्स में परिवर्तन आंतरिक लेयर्स को प्रभावित नहीं करते हैं। और RxJava2, KotlinRx, और Kotlin LiveData का उपयोग करके कार्यान्वयन प्रक्रिया डेवलपर के कार्यों को आसान, स्पष्ट बनाती है, और कोड अच्छी तरह से पठनीय और आसानी से विस्तार योग्य हो जाता है।