मैन्युअल डिपेंडेंसी इंजेक्शन

Android के लिए सुझाए गए ऐप्लिकेशन आर्किटेक्चर में, कोड को क्लास में बांटने का सुझाव दिया जाता है. इससे, सेपरेशन ऑफ़ कंसर्न के सिद्धांत का फ़ायदा मिलता है. इस सिद्धांत के तहत, क्रम में मौजूद हर क्लास की एक तय ज़िम्मेदारी होती है. इससे ज़्यादा और छोटी क्लास बनती हैं. इन्हें एक-दूसरे की डिपेंडेंसी पूरी करने के लिए कनेक्ट करना पड़ता है.

Android ऐप्लिकेशन में आम तौर पर कई क्लास होती हैं. इनमें से कुछ क्लास एक-दूसरे पर निर्भर करती हैं.
पहला डायग्राम. Android ऐप्लिकेशन के ऐप्लिकेशन ग्राफ़
का मॉडल

क्लास के बीच की डिपेंडेंसी को ग्राफ़ के तौर पर दिखाया जा सकता है. इसमें हर क्लास, उन क्लास से कनेक्ट होती है जिन पर वह निर्भर करती है. आपकी सभी क्लास और उनकी डिपेंडेंसी को मिलाकर, ऐप्लिकेशन ग्राफ़ बनता है. पहले डायग्राम में, ऐप्लिकेशन ग्राफ़ का ऐब्स्ट्रैक्शन देखा जा सकता है. जब क्लास A (ViewModel), क्लास B (Repository) पर निर्भर करती है, तो A से B की ओर एक लाइन होती है. यह लाइन, उस डिपेंडेंसी को दिखाती है.

डिपेंडेंसी इंजेक्शन की मदद से, इन कनेक्शन को बनाया जा सकता है. साथ ही, टेस्ट करने के लिए, लागू करने के तरीके को बदला जा सकता है. उदाहरण के लिए, किसी ऐसे ViewModel की जांच करते समय जो किसी रिपॉज़िटरी पर निर्भर करता है, Repository के अलग-अलग तरीके लागू किए जा सकते हैं. इसके लिए, फ़ेक या मॉक का इस्तेमाल किया जा सकता है, ताकि अलग-अलग मामलों की जांच की जा सके.

डिपेंडेंसी इंजेक्शन के लिए मैन्युअल तरीके की बुनियादी जानकारी

इस सेक्शन में, Android ऐप्लिकेशन के किसी असली उदाहरण में, डिपेंडेंसी इंजेक्शन के लिए मैन्युअल तरीके का इस्तेमाल करने का तरीका बताया गया है. इसमें, आपके ऐप्लिकेशन में डिपेंडेंसी इंजेक्शन का इस्तेमाल शुरू करने के लिए, एक-एक करके चरण बताए गए हैं. यह तरीका तब तक बेहतर होता जाता है, जब तक यह उस स्थिति में नहीं पहुंच जाता जो Dagger आपके लिए अपने-आप जनरेट करता है. Dagger के बारे में ज़्यादा जानने के लिए, Dagger की बुनियादी जानकारी पढ़ें.

फ़्लो को अपने ऐप्लिकेशन में स्क्रीन के एक ग्रुप के तौर पर समझें, जो किसी सुविधा से जुड़ा होता है. लॉग इन करना, रजिस्टर करना, और चेकआउट करना, ये सभी फ़्लो के उदाहरण हैं.

किसी सामान्य Android ऐप्लिकेशन के लिए, लॉग इन फ़्लो को कवर करते समय, LoginActivity, LoginViewModel पर निर्भर करता है. वहीं, LoginViewModel, UserRepository पर निर्भर करता है. इसके बाद UserRepository पर निर्भर करता है. वहीं, UserLocalDataSource और UserRemoteDataSource, Retrofit सेवा पर निर्भर करते हैं.

LoginActivity , लॉग इन फ़्लो का एंट्री पॉइंट है. उपयोगकर्ता, इस गतिविधि के साथ इंटरैक्ट करता है. इसलिए, LoginActivity को अपनी सभी डिपेंडेंसी के साथ LoginViewModel बनाना पड़ता है.

फ़्लो की Repository और DataSource क्लास इस तरह दिखती हैं:

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource { ... }
class UserRemoteDataSource(
    private val loginService: LoginRetrofitService
) { ... }

Compose में, ComponentActivity एंट्री पॉइंट होता है. डिपेंडेंसी वायरिंग, onCreate में एक बार होती है. साथ ही, यूज़र इंटरफ़ेस (यूआई) को setContent से कॉल किए गए कंपोज़ेबल से दिखाया जाता है:

class ApiService {
    /* Your API implementation here */
}

class UserRepository(private val apiService: ApiService) {
    /* Your implementation here */
}

class LoginActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Satisfy the dependencies of LoginViewModel recursively,
        // then pass what the UI needs into setContent.
        val apiService = ApiService()
        val userRepository = UserRepository(apiService)

        setContent {
            LoginScreen(userRepository)
        }
    }
}

@Composable
fun LoginScreen(userRepository: UserRepository) {
    val viewModel: LoginViewModel = viewModel(
        factory = LoginViewModelFactory(userRepository)
    )
    // ...
}

इस तरीके में ये समस्याएं आती हैं:

  1. डिपेंडेंसी को क्रम से तय करना पड़ता है. LoginViewModel बनाने के लिए, आपको UserRepository को इंस्टैंशिएट करना होगा.
  2. ऑब्जेक्ट को फिर से इस्तेमाल करना मुश्किल होता है. अगर आपको कई सुविधाओं के लिए UserRepository को फिर से इस्तेमाल करना है, तो आपको इसे सिंगलटन पैटर्नके हिसाब से बनाना होगा. सिंगलटन पैटर्न की वजह से, टेस्ट करना ज़्यादा मुश्किल हो जाता है, क्योंकि सभी टेस्ट, सिंगलटन के एक ही इंस्टेंस को शेयर करते हैं.

कंटेनर की मदद से डिपेंडेंसी मैनेज करना

ऑब्जेक्ट को फिर से इस्तेमाल करने की समस्या को हल करने के लिए, डिपेंडेंसी कंटेनर क्लास बनाई जा सकती है. इसका इस्तेमाल, डिपेंडेंसी पाने के लिए किया जाता है. इस कंटेनर से दिए गए सभी इंस्टेंस, सार्वजनिक हो सकते हैं. उदाहरण के लिए, आपको सिर्फ़ UserRepository के इंस्टेंस की ज़रूरत होती है. इसलिए, इसकी डिपेंडेंसी को निजी बनाया जा सकता है. हालांकि, अगर आने वाले समय में इनकी ज़रूरत पड़ती है, तो इन्हें सार्वजनिक बनाया जा सकता है:

// Container of objects shared across the whole app
class AppContainer {

    // apiService and userRepository aren't private and will be exposed
    val apiService = ApiService()
    val userRepository = UserRepository(apiService)
}

इन डिपेंडेंसी का इस्तेमाल पूरे ऐप्लिकेशन में किया जाता है. इसलिए, इन्हें किसी ऐसी जगह पर रखना होगा जिसका इस्तेमाल सभी गतिविधियां कर सकें: Application क्लास. कोई कस्टम Application क्लास बनाएं, जिसमें AppContainer का इंस्टेंस शामिल हो.

// Custom Application class that needs to be specified
// in the AndroidManifest.xml file
class MyApplication : Application() {

    // Instance of AppContainer that will be used by all the Activities of the app
    val appContainer = AppContainer()
}

Compose में, AppContainer को अब भी Application की सबक्लास में बनाया जाता है. setContent को कॉल करने से पहले, इसे गतिविधि में या LocalContext का इस्तेमाल करके, कंपोज़ेबल में ऐक्सेस किया जा सकता है:

class LoginActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val appContainer = (application as MyApplication).appContainer

        setContent {
            LoginScreen(appContainer.userRepository)
        }
    }
}

// Alternatively, read AppContainer from inside a composable:
@Composable
fun LoginScreen() {
    val context = LocalContext.current
    val appContainer = (context.applicationContext as MyApplication).appContainer
    val viewModel: LoginViewModel = viewModel(
        factory = LoginViewModelFactory(appContainer.userRepository)
    )
    // ...
}

हमारा सुझाव है कि ट्री में गहराई से LocalContext में पहुंचने के बजाय, डिपेंडेंसी को कंपोज़ेबल पैरामीटर के तौर पर पास करें. इससे कंपोज़ेबल को टेस्ट किया जा सकता है और उनके इनपुट साफ़ तौर पर दिखते हैं. स्क्रीन रूट पर कंटेनर को एक बार हल करें और ज़रूरत के हिसाब से इसे नीचे की ओर पास करें.

इस तरह, आपके पास सिंगलटन UserRepository नहीं होता. इसके बजाय, आपके पास एक AppContainer होता है, जिसे सभी गतिविधियों में शेयर किया जाता है. इसमें ग्राफ़ के ऑब्जेक्ट शामिल होते हैं. साथ ही, यह उन ऑब्जेक्ट के इंस्टेंस बनाता है जिनका इस्तेमाल अन्य क्लास कर सकती हैं.

अगर LoginViewModel की ज़रूरत ऐप्लिकेशन में ज़्यादा जगहों पर है, तो LoginViewModel के इंस्टेंस बनाने के लिए, एक केंद्रीकृत जगह होना सही है. LoginViewModel को बनाने की प्रोसेस को कंटेनर में ले जाया जा सकता है. साथ ही, फ़ैक्ट्री की मदद से, उस टाइप के नए ऑब्जेक्ट उपलब्ध कराए जा सकते हैं. LoginViewModelFactory का कोड इस तरह दिखता है:

// Definition of a Factory interface with a function to create objects of a type
interface Factory<T> {
    fun create(): T
}

// Factory for LoginViewModel.
// Since LoginViewModel depends on UserRepository, in order to create instances of
// LoginViewModel, you need an instance of UserRepository that you pass as a parameter.
class LoginViewModelFactory(private val userRepository: UserRepository) : Factory<LoginViewModel> {
    override fun create(): LoginViewModel {
        return LoginViewModel(userRepository)
    }
}

Compose में, AppContainer को अपडेट करने पर भी फ़ैक्ट्री दिखती है. इसके बाद, फ़ैक्ट्री का इस्तेमाल viewModel कंपोज़ेबल करता है, ताकि ViewModel को सबसे नज़दीकी ViewModelStoreOwner (आम तौर पर, होस्ट गतिविधि या Navigation Compose के साथ, कोई नेविगेशन एंट्री) के दायरे में रखा जा सके:

// AppContainer exposing the factory (unchanged from the snippet above)
class AppContainer {
    // ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)
    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

// Compose entry point + screen composable
class LoginActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val appContainer = (application as MyApplication).appContainer
        setContent {
            LoginScreen(appContainer.loginViewModelFactory)
        }
    }
}

@Composable
fun LoginScreen(factory: LoginViewModelFactory) {
    val viewModel: LoginViewModel = viewModel(factory = factory)
    // ...
}

यह तरीका, पिछले तरीके से बेहतर है. हालांकि, इसमें अब भी कुछ समस्याएं हैं:

  1. AppContainer को खुद मैनेज करना पड़ता है. साथ ही, सभी डिपेंडेंसी के लिए इंस्टेंस, मैन्युअल तरीके से बनाने पड़ते हैं.

  2. इसमें अब भी बहुत ज़्यादा बॉयलरप्लेट कोड होता है. अगर किसी ऑब्जेक्ट को फिर से इस्तेमाल करना है, तो आपको फ़ैक्ट्री या पैरामीटर, मैन्युअल तरीके से बनाने होंगे.

ऐप्लिकेशन फ़्लो में डिपेंडेंसी मैनेज करना

प्रोजेक्ट में ज़्यादा सुविधाएं शामिल करने पर, AppContainer जटिल हो जाता है. जब आपका ऐप्लिकेशन बड़ा हो जाता है और उसमें अलग-अलग फ़्लो शामिल किए जाते हैं, तो और भी समस्याएं आती हैं:

  1. अलग-अलग फ़्लो होने पर, हो सकता है कि आपको ऑब्जेक्ट को सिर्फ़ उस फ़्लो के दायरे में रखना हो. उदाहरण के लिए, LoginUserData (जिसमें सिर्फ़ लॉग इन फ़्लो में इस्तेमाल किया गया उपयोगकर्ता नाम और पासवर्ड शामिल हो सकता है) बनाते समय, आपको किसी दूसरे उपयोगकर्ता के पुराने लॉग इन फ़्लो का डेटा सेव नहीं करना है. आपको हर नए फ़्लो के लिए, एक नया इंस्टेंस चाहिए. इसके लिए, AppContainer में FlowContainer ऑब्जेक्ट बनाए जा सकते हैं. इसके बारे में, कोड के अगले उदाहरण में बताया गया है.

  2. ऐप्लिकेशन ग्राफ़ और फ़्लो कंटेनर को ऑप्टिमाइज़ करना भी मुश्किल हो सकता है. आपको उन इंस्टेंस को मिटाना होगा जिनकी ज़रूरत नहीं है. यह इस बात पर निर्भर करता है कि आप किस फ़्लो में हैं.

उदाहरण के कोड में, LoginContainer जोड़ते हैं. आपको ऐप्लिकेशन में LoginContainer के कई इंस्टेंस बनाने हैं. इसलिए, इसे सिंगलटन बनाने के बजाय, एक ऐसी क्लास बनाएं जिसमें AppContainer से, लॉग इन फ़्लो के लिए ज़रूरी डिपेंडेंसी शामिल हों.

class LoginContainer(val userRepository: UserRepository) {

    val loginData = LoginUserData()

    val loginViewModelFactory = LoginViewModelFactory(userRepository)
}

// AppContainer contains LoginContainer now
class AppContainer {
    ...
    val userRepository = UserRepository(localDataSource, remoteDataSource)

    // LoginContainer will be null when the user is NOT in the login flow
    var loginContainer: LoginContainer? = null
}

Compose में, फ़्लो कंटेनर का लाइफ़टाइम, होस्ट Activity के बजाय कंपोज़िशन से जुड़ा होता है. आपको शेयर किए गए AppContainer.loginContainer में बदलाव करने की ज़रूरत नहीं है, क्योंकि कंपोज़ेबल को उनकी डिपेंडेंसी, पैरामीटर के तौर पर मिलती हैं या वे उन्हें होस्ट किए गए ViewModel से पढ़ते हैं. आपके पास दो विकल्प हैं:

  1. Navigation Compose का नेस्ट किया गया ग्राफ़ (एक से ज़्यादा स्क्रीन वाले फ़्लो के लिए बेहतर). लॉग इन फ़्लो की सभी स्क्रीन को नेस्ट किए गए नेविगेशन ग्राफ़ में रखें. साथ ही, कंटेनर को उस ग्राफ़ के NavBackStackEntry के दायरे में रखें. जब उपयोगकर्ता फ़्लो में शामिल होता है, तब कंटेनर बनता है. वहीं, जब बैक स्टैक एंट्री को पॉप किया जाता है, तब कंटेनर मिट जाता है. इसके लिए, लाइफ़साइकल के लिए मैन्युअल तरीके से कॉल करने की ज़रूरत नहीं होती. ज़्यादा जानकारी के लिए, अपना नेविगेशन ग्राफ़ डिज़ाइन करना लेख पढ़ें.
  2. स्क्रीन रूट पर remember (एक स्क्रीन वाले फ़्लो के लिए या जब Navigation Compose का इस्तेमाल नहीं किया जा रहा हो). remember में कंटेनर बनाएं, ताकि कंपोज़िशन में हर एंट्री के लिए यह एक बार बने. साथ ही, कंपोज़ेबल के बंद होने पर, इसे गार्बेज-कलेक्ट किया जाए:
@Composable
fun LoginFlow(appContainer: AppContainer) {
    val loginContainer = remember(appContainer) {
        LoginContainer(appContainer.userRepository)
    }
    val viewModel: LoginViewModel = viewModel(
        factory = loginContainer.loginViewModelFactory
    )
    // Render the login flow using loginContainer.loginData and viewModel.
}

नतीजा

डिपेंडेंसी इंजेक्शन, Android ऐप्लिकेशन को स्केल करने और टेस्ट करने के लिए एक अच्छा तरीका है. कंटेनर का इस्तेमाल, अपने ऐप्लिकेशन के अलग-अलग हिस्सों में क्लास के इंस्टेंस शेयर करने के लिए करें. साथ ही, फ़ैक्ट्री का इस्तेमाल करके, क्लास के इंस्टेंस बनाने के लिए, इसे एक केंद्रीकृत जगह के तौर पर इस्तेमाल करें.

जब आपका ऐप्लिकेशन बड़ा हो जाता है, तो आपको बॉयलरप्लेट कोड (जैसे, फ़ैक्ट्री) लिखना पड़ता है. इसमें गड़बड़ियां होने की संभावना होती है. आपको कंटेनर के दायरे और लाइफ़साइकल को खुद मैनेज करना पड़ता है. साथ ही, मेमोरी खाली करने के लिए, उन कंटेनर को ऑप्टिमाइज़ और मिटाना पड़ता है जिनकी अब ज़रूरत नहीं है. ऐसा गलत तरीके से करने पर, आपके ऐप्लिकेशन में मामूली गड़बड़ियां और मेमोरी लीक हो सकती हैं.

Dagger सेक्शन में, आपको Dagger का इस्तेमाल करके इस प्रोसेस को अपने-आप करने और वही कोड जनरेट करने का तरीका बताया जाएगा जो आपको मैन्युअल तरीके से लिखना पड़ता.

अन्य संसाधन

Views का कॉन्टेंट