অ্যান্ড্রয়েডের প্রস্তাবিত অ্যাপ আর্কিটেকচার ‘ সেপারেশন অফ কনসার্নস’-এর সুবিধা পেতে আপনার কোডকে ক্লাসে ভাগ করতে উৎসাহিত করে। এটি এমন একটি নীতি যেখানে হায়ারার্কির প্রতিটি ক্লাসের একটিমাত্র সুনির্দিষ্ট দায়িত্ব থাকে। এর ফলে আরও বেশি সংখ্যক ছোট ছোট ক্লাস তৈরি হয়, যেগুলোকে একে অপরের নির্ভরতা পূরণের জন্য সংযুক্ত করার প্রয়োজন হয়।

ক্লাসগুলোর মধ্যকার নির্ভরশীলতা একটি গ্রাফ হিসেবে উপস্থাপন করা যেতে পারে, যেখানে প্রতিটি ক্লাস তার উপর নির্ভরশীল ক্লাসগুলোর সাথে সংযুক্ত থাকে। আপনার সমস্ত ক্লাস এবং তাদের নির্ভরশীলতার উপস্থাপনাটিই অ্যাপ্লিকেশন গ্রাফ তৈরি করে। চিত্র ১-এ, আপনি অ্যাপ্লিকেশন গ্রাফের একটি বিমূর্ত রূপ দেখতে পারেন। যখন ক্লাস A ( ViewModel ) ক্লাস B ( Repository )-এর উপর নির্ভর করে, তখন A থেকে B-এর দিকে নির্দেশকারী একটি রেখা থাকে যা সেই নির্ভরশীলতাকে উপস্থাপন করে।
ডিপেন্ডেন্সি ইনজেকশন এই সংযোগগুলো তৈরি করতে সাহায্য করে এবং পরীক্ষার জন্য ইমপ্লিমেন্টেশন অদলবদল করার সুযোগ দেয়। উদাহরণস্বরূপ, যখন কোনো রিপোজিটরির উপর নির্ভরশীল একটি ViewModel পরীক্ষা করা হয়, তখন বিভিন্ন কেস পরীক্ষা করার জন্য আপনি ফেক বা মক ব্যবহার করে Repository এর ভিন্ন ভিন্ন ইমপ্লিমেন্টেশন পাস করতে পারেন।
ম্যানুয়াল ডিপেন্ডেন্সি ইনজেকশনের মূল বিষয়গুলি
এই বিভাগে একটি বাস্তব অ্যান্ড্রয়েড অ্যাপের ক্ষেত্রে কীভাবে ম্যানুয়াল ডিপেন্ডেন্সি ইনজেকশন প্রয়োগ করতে হয় তা আলোচনা করা হয়েছে। আপনার অ্যাপে কীভাবে ডিপেন্ডেন্সি ইনজেকশন ব্যবহার শুরু করতে পারেন, তার একটি পুনরাবৃত্তিমূলক পদ্ধতি এখানে দেখানো হয়েছে। এই পদ্ধতিটি ধীরে ধীরে উন্নত হতে হতে এমন একটি পর্যায়ে পৌঁছায় যা ড্যাগার (Dagger) আপনার জন্য স্বয়ংক্রিয়ভাবে যা তৈরি করে তার সাথে খুবই সাদৃশ্যপূর্ণ। ড্যাগার সম্পর্কে আরও তথ্যের জন্য, 'ড্যাগার বেসিকস' (Dagger basics) পড়ুন।
আপনার অ্যাপের কোনো একটি ফিচারের সাথে সম্পর্কিত স্ক্রিনগুলোর একটি সমষ্টিকে ফ্লো হিসেবে বিবেচনা করুন। লগইন, রেজিস্ট্রেশন এবং চেকআউট সবই ফ্লো-এর উদাহরণ।
একটি সাধারণ অ্যান্ড্রয়েড অ্যাপের লগইন ফ্লো বর্ণনা করার সময়, LoginActivity নির্ভর করে 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 থেকে কল করা কম্পোজেবলগুলোর মাধ্যমে UI বর্ণনা করা হয়:
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)
)
// ...
}
এই পদ্ধতিতে কিছু সমস্যা রয়েছে:
- ডিপেন্ডেন্সিগুলো ক্রমানুসারে ঘোষণা করতে হবে।
UserRepositoryতৈরি করার জন্য,LoginViewModelএর আগে এটিকে ইনস্ট্যানশিয়েট করতে হবে। - অবজেক্ট পুনঃব্যবহার করা কঠিন। আপনি যদি একাধিক ফিচারের মধ্যে
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 ব্যবহার করলেও Application সাবক্লাসের মধ্যেই একই AppContainer তৈরি হয়। আপনি এটিকে অ্যাক্টিভিটির মধ্যে 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 থাকে যা সমস্ত অ্যাক্টিভিটি জুড়ে শেয়ার করা হয়। এই 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 composable দ্বারা ব্যবহৃত হয়, ফলে 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)
// ...
}
এই পদ্ধতিটি আগেরটির চেয়ে ভালো, কিন্তু এখনও কিছু বিবেচ্য বিষয় রয়েছে:
আপনাকে
AppContainerনিজেই পরিচালনা করতে হবে এবং সমস্ত ডিপেন্ডেন্সির ইনস্ট্যান্স হাতে তৈরি করতে হবে।এখনও অনেক গতানুগতিক কোড রয়েছে। আপনি কোনো অবজেক্ট পুনরায় ব্যবহার করতে চান কি না, তার উপর নির্ভর করে আপনাকে হাতে করে ফ্যাক্টরি বা প্যারামিটার তৈরি করতে হবে।
অ্যাপ্লিকেশন ফ্লোতে নির্ভরতা পরিচালনা করা
প্রজেক্টে আরও কার্যকারিতা অন্তর্ভুক্ত করতে চাইলে AppContainer জটিল হয়ে ওঠে। যখন আপনার অ্যাপ আরও বড় হয়ে যায় এবং আপনি বিভিন্ন ফিচার ফ্লো যুক্ত করতে শুরু করেন, তখন আরও অনেক সমস্যা দেখা দেয়:
যখন আপনার বিভিন্ন ফ্লো থাকে, তখন আপনি চাইতে পারেন যে অবজেক্টগুলো শুধু সেই ফ্লো-এর আওতার মধ্যেই থাকুক। উদাহরণস্বরূপ,
LoginUserDataতৈরি করার সময় (যেটিতে শুধুমাত্র লগইন ফ্লো-তে ব্যবহৃত ইউজারনেম এবং পাসওয়ার্ড থাকতে পারে), আপনি অন্য কোনো ব্যবহারকারীর পুরোনো লগইন ফ্লো-এর ডেটা সংরক্ষণ করতে চাইবেন না। আপনি প্রতিটি নতুন ফ্লো-এর জন্য একটি নতুন ইনস্ট্যান্স চাইবেন। পরবর্তী কোড উদাহরণে দেখানো পদ্ধতি অনুযায়ীAppContainerভিতরেFlowContainerঅবজেক্ট তৈরি করার মাধ্যমে আপনি এটি করতে পারেন।অ্যাপ্লিকেশন গ্রাফ এবং ফ্লো কন্টেইনার অপ্টিমাইজ করাও কঠিন হতে পারে। আপনি যে ফ্লো-তে আছেন, তার উপর নির্ভর করে অপ্রয়োজনীয় ইনস্ট্যান্সগুলো ডিলিট করার কথা আপনাকে মনে রাখতে হবে।
চলুন উদাহরণ কোডে একটি 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 থেকে সেগুলো পড়ে নেয়। আপনার কাছে দুটি বিকল্প আছে:
- নেভিগেশন কম্পোজ নেস্টেড গ্রাফ (একাধিক স্ক্রিন ফ্লো-এর জন্য পছন্দনীয়)। লগইন ফ্লো-এর সমস্ত স্ক্রিনকে একটি নেস্টেড ন্যাভ গ্রাফের অধীনে রাখুন এবং কন্টেইনারটিকে সেই গ্রাফের
NavBackStackEntryতে স্কোপ করুন। ব্যবহারকারী যখন ফ্লো-তে প্রবেশ করেন তখন কন্টেইনারটি তৈরি হয় এবং ব্যাক স্ট্যাক এন্ট্রি পপ করা হলে তা খালি হয়ে যায়, এর জন্য কোনো ম্যানুয়াল লাইফসাইকেল কলের প্রয়োজন হয় না। আরও তথ্যের জন্য, আপনার ন্যাভিগেশন গ্রাফ ডিজাইন করুন দেখুন। - স্ক্রিন রুটে
remember(একটি একক-স্ক্রিন ফ্লো-এর জন্য অথবা যখন আপনি নেভিগেশন কম্পোজ ব্যবহার করছেন না)। রিমেম্বার-এর ভিতরে কন্টেইনারটি তৈরি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.
}
উপসংহার
স্কেলেবল এবং টেস্টেবল অ্যান্ড্রয়েড অ্যাপ তৈরির জন্য ডিপেন্ডেন্সি ইনজেকশন একটি ভালো কৌশল। আপনার অ্যাপের বিভিন্ন অংশে ক্লাসের ইনস্ট্যান্স শেয়ার করার উপায় হিসেবে এবং ফ্যাক্টরি ব্যবহার করে ক্লাসের ইনস্ট্যান্স তৈরি করার জন্য একটি কেন্দ্রীভূত স্থান হিসেবে কন্টেইনার ব্যবহার করুন।
আপনার অ্যাপ্লিকেশনটি যখন বড় হতে থাকে, তখন আপনি দেখবেন যে আপনাকে প্রচুর বয়লারপ্লেট কোড (যেমন ফ্যাক্টরি) লিখতে হচ্ছে, যেগুলোতে ভুল হওয়ার সম্ভাবনা থাকে। এছাড়াও, মেমরি খালি করার জন্য আপনাকে কন্টেইনারগুলোর স্কোপ এবং লাইফসাইকেল নিজে থেকেই পরিচালনা করতে হয় এবং অপ্রয়োজনীয় কন্টেইনারগুলোকে অপটিমাইজ ও বাতিল করতে হয়। এই কাজটি ভুলভাবে করলে আপনার অ্যাপে সূক্ষ্ম বাগ এবং মেমরি লিক দেখা দিতে পারে।
ড্যাগার (Dagger) অংশে আপনি শিখবেন, কীভাবে ড্যাগার ব্যবহার করে এই প্রক্রিয়াটিকে স্বয়ংক্রিয় করা যায় এবং সেই একই কোড তৈরি করা যায় যা অন্যথায় আপনাকে হাতে লিখতে হতো।