EventBus হল Android-এর জন্য একটি লাইব্রেরি যা ইভেন্ট বাসের মাধ্যমে Publisher-Subscriber প্যাটার্ন বাস্তবায়ন করে, যা সরাসরি নির্ভরতা ছাড়াই কম্পোনেন্টগুলির মধ্যে ডেটা আদান-প্রদানের অনুমতি দেয়। GreenRobot দ্বারা বিকশিত, লাইব্রেরিটি Activity, Fragment, Service এবং Background Thread-এর মধ্যে যোগাযোগ সহজ করে। GitHub ডেটা (2025) অনুসারে, EventBus-এর 25 হাজারেরও বেশি স্টার রয়েছে এবং হাজার হাজার Android অ্যাপ্লিকেশনে ব্যবহৃত হয়। প্রধান ক্রিয়াকলাপগুলি হল subscribe (একটি ইভেন্টে সাবস্ক্রাইব করা), post (একটি ইভেন্ট পাঠানো) এবং sticky event (নতুন সাবস্ক্রাইবারদের জন্য বিলম্বিত ইভেন্ট)।
মূল বিষয়
EventBus Android-এর জন্য একটি ইভেন্ট বাস লাইব্রেরি যা Publisher-Subscriber প্যাটার্ন বাস্তবায়ন করে। এটি অ্যাপ্লিকেশন কম্পোনেন্টগুলির (Activity, Fragment, Service, ViewModel) মধ্যে স্পষ্ট নির্ভরতা তৈরি না করেই ইভেন্ট পাঠানোর অনুমতি দেয়। স্ট্যান্ডার্ড Android মেকানিজম (Intent, BroadcastReceiver) থেকে ভিন্ন, EventBus প্রক্রিয়ার ভিতরে কাজ করে এবং IPC ব্যবহার করে না। লাইব্রেরিটি কর্মক্ষমতার জন্য অপ্টিমাইজ করা হয়েছে এবং Subscriber Index সঠিকভাবে কনফিগার করা থাকলে রিফ্লেকশন ব্যবহার করে না।
EventBus আর্কিটেকচার তিনটি মূল উপাদান নিয়ে গঠিত: Event (ডেটা সহ POJO ক্লাস), Subscriber (@Subscribe দিয়ে চিহ্নিত মেথড সহ অবজেক্ট) এবং EventBus (কেন্দ্রীয় ডিসপ্যাচার)। সাবস্ক্রাইবার EventBus.getDefault().register(this)-এর মাধ্যমে নিবন্ধন করে এবং unregister(this)-এর মাধ্যমে নিবন্ধন বাতিল করে। ইভেন্টগুলি টাইপ করা হয়: হ্যান্ডলাররা একটি নির্দিষ্ট ইভেন্ট ক্লাসে সাবস্ক্রাইব করে এবং শুধুমাত্র তখনই কল করা হয় যখন সেই ক্লাস বা তার সাবক্লাসের একটি ইভেন্ট পোস্ট করা হয়।
// POJO ইভেন্ট
data class MessageEvent(
val message: String,
val timestamp: Long = System.currentTimeMillis()
)
// Activity-তে সাবস্ক্রাইবার
class MainActivity : AppCompatActivity() {
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
}
override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onMessageEvent(event: MessageEvent) {
textView.text = event.message
}
}
// অন্য কম্পোনেন্ট থেকে ইভেন্ট পাঠানো
EventBus.getDefault().post(MessageEvent("Hello from Service"))
ডিফল্টরূপে, EventBus register()-এর সময় @Subscribe মেথড খুঁজতে রিফ্লেকশন ব্যবহার করে। Subscriber Index একটি অ্যানোটেশন প্রসেসরের মাধ্যমে কম্পাইল টাইমে হ্যান্ডলার ইনডেক্স তৈরি করে। এটি রিফ্লেকশন ওভারহেড দূর করে এবং নিবন্ধন দ্রুত করে। এটি সক্ষম করতে, build.gradle-এ eventbus-annotation-processor যোগ করুন। EventBus স্বয়ংক্রিয়ভাবে ইনডেক্স ব্যবহার করে যদি এটি classpath-এ উপলব্ধ থাকে। ইনডেক্স ছাড়া, লাইব্রেরি এখনও কাজ করে তবে সামান্য কর্মক্ষমতা হ্রাসের সাথে।
যখন EventBus.getDefault().post(event) কল করা হয়, লাইব্রেরি ইভেন্ট টাইপ নির্ধারণ করে, সেই টাইপ গ্রহণ করে এমন @Subscribe মেথড সহ সমস্ত নিবন্ধিত সাবস্ক্রাইবার খুঁজে পায় এবং নির্দিষ্ট ThreadMode অনুসারে তাদের কল করে। সাবস্ক্রাইবার অনুসন্ধান নিবন্ধনের সময় নির্মিত Class → CopyOnWriteArrayList
একটি সাবস্ক্রাইবারের onStart()-এ নিবন্ধন করা উচিত এবং onStop()-এ নিবন্ধন বাতিল করা উচিত। যদি আপনি onCreate()-এ নিবন্ধন করেন এবং onDestroy()-এ নিবন্ধন বাতিল করেন, তাহলে finish()-এর কারণে onDestroy কল না করেই ধ্বংস হওয়া একটি Activity সাবস্ক্রাইবার তালিকায় থাকতে পারে। সাবস্ক্রাইবার লিক EventBus-এর প্রধান সমস্যাগুলির মধ্যে একটি: সাবস্ক্রাইবার তালিকায় থাকা একটি Activity যতক্ষণ না নিবন্ধন বাতিল করে ততক্ষণ GC মুক্ত করতে পারে না। সর্বদা সঠিক জীবনচক্র মেথডে register/unregister জোড়া দিন।
@Subscribe অ্যানোটেশন priority প্যারামিটার (পূর্ণসংখ্যা, ডিফল্ট 0) সমর্থন করে। উচ্চতর অগ্রাধিকারযুক্ত হ্যান্ডলাররা প্রথমে কল করা হয়। cancelEventDelivery() অবশিষ্ট সাবস্ক্রাইবারদের কাছে ইভেন্ট ডেলিভারি বাধা দেওয়ার অনুমতি দেয়। এটি অগ্রাধিকার হ্যান্ডলারদের (লগিং, প্রমাণীকরণ) জন্য দরকারী যা নিচের সাবস্ক্রাইবারদের দ্বারা ইভেন্ট প্রক্রিয়াকরণ বাতিল করতে পারে। এই ফাংশনটি শুধুমাত্র ইভেন্ট পোস্টিং থ্রেডে উপলব্ধ।
// অগ্রাধিকার সহ জটিল উদাহরণ
data class NavigationEvent(val screen: String, val data: Bundle)
class NavigationInterceptor {
@Subscribe(priority = 10, threadMode = ThreadMode.POSTING)
fun onNavigationEvent(event: NavigationEvent) {
if (event.screen == "restricted" && !isAuthorized) {
EventBus.getDefault().cancelEventDelivery(event)
}
}
}
class AnalyticsLogger {
@Subscribe(priority = 5)
fun logNavigation(event: NavigationEvent) {
analytics.logScreen(event.screen)
}
}
// ইভেন্ট পাঠানো
EventBus.getDefault().post(NavigationEvent("profile", bundle))
Android ইন্ট্রা-প্রসেস যোগাযোগের জন্য বেশ কয়েকটি মেকানিজম অফার করে: EventBus, LocalBroadcastManager (অপ্রচলিত) এবং LiveData/Flow। প্রতিটির নিজস্ব সুবিধা এবং অসুবিধা রয়েছে। পছন্দটি আর্কিটেকচারাল পদ্ধতি এবং কর্মক্ষমতা প্রয়োজনীয়তার উপর নির্ভর করে। Google-এর আধুনিক সুপারিশগুলি Lifecycle ইন্টিগ্রেশন এবং লিকের অনুপস্থিতির কারণে LiveData এবং Flow-এর দিকে ঝুঁকছে।
| বৈশিষ্ট্য | EventBus | LocalBroadcastManager | LiveData / Flow |
|---|---|---|---|
| টাইপিং | ইভেন্ট ক্লাসের মাধ্যমে | Intent ফিল্টার (String) মাধ্যমে | জেনেরিক টাইপের মাধ্যমে |
| Lifecycle-aware | না (ম্যানুয়াল আনরজিস্টার) | না (ম্যানুয়াল আনরজিস্টার) | হ্যাঁ (স্বয়ংক্রিয়) |
| Sticky | হ্যাঁ (postSticky) | না | হ্যাঁ (LiveData সর্বদা sticky) |
| ThreadMode | MAIN, POSTING, BACKGROUND, ASYNC | শুধুমাত্র main | observe/observeOn মাধ্যমে |
| কর্মক্ষমতা | উচ্চ (Subscriber Index) | মাঝারি (IPC র্যাপার) | উচ্চ (পর্যবেক্ষণ) |
EventBus উল্লেখযোগ্য লিগ্যাসি কোড সহ প্রকল্পে এবং যেখানে LiveData/Flow উপলব্ধ নয় (শুধুমাত্র Java প্রকল্প) সেখানে দরকারী। EventBus-এর sticky events LocalBroadcastManager-এ অনুপস্থিত নমনীয়তা প্রদান করে। EventBus ViewModel ছাড়া Service থেকে Activity-তে ইভেন্ট পাঠানোর জন্যও সহজ — বিশেষ করে যখন আপনাকে ব্যাকগ্রাউন্ড টাস্কের অগ্রগতি সম্পর্কে জানাতে হবে। লাইব্রেরিটির আকার ন্যূনতম (প্রায় 50 KB) এবং কোনো নির্ভরতা যোগ করে না।
LiveData এবং Flow Android Jetpack-এর অংশ এবং Lifecycle-এর সাথে সংহত। তারা কম্পোনেন্ট ধ্বংস হলে স্বয়ংক্রিয়ভাবে আনসাবস্ক্রাইব করে, মেমরি লিক দূর করে। Flow coroutine এবং জটিল রূপান্তর অপারেটর সমর্থন করে। Google UI স্তরের জন্য LiveData এবং রিপজিটরির জন্য Flow সুপারিশ করে। EventBus ক্রস-মডিউল ইভেন্টের জন্য দরকারী থাকে যেখানে নেভিগেশন এবং বিজনেস লজিক MVVM-এ ফিট হয় না।
Subscribe হল @Subscribe অ্যানোটেশনের মাধ্যমে একটি ইভেন্ট হ্যান্ডলার নিবন্ধন করা। মেথডটি public, void হতে হবে এবং ঠিক একটি প্যারামিটার — ইভেন্ট টাইপ গ্রহণ করতে হবে। Post হল EventBus.getDefault().post(event)-এর মাধ্যমে সমস্ত সাবস্ক্রাইবড হ্যান্ডলারদের কাছে একটি ইভেন্ট পাঠানো। post মেথড কোনো ফলাফল ফেরত দেয় না এবং কতগুলি হ্যান্ডলার কল করা হয়েছে তা নির্দেশ করে না। প্রতিক্রিয়া সহ ইভেন্টের জন্য, একটি ফলাফল ফিল্ড সহ একটি পৃথক Event ক্লাস ব্যবহার করুন।
একটি ইভেন্ট হল যেকোনো Java/Kotlin ক্লাস। অপরিবর্তনীয় ইভেন্টের জন্য data class এবং পরিবর্তনযোগ্য ফিল্ড সহ ইভেন্টের জন্য সাধারণ ক্লাস ব্যবহার করার পরামর্শ দেওয়া হয়। ইভেন্টের নামকরণ কর্ম প্রতিফলিত করা উচিত: UserLoggedInEvent, DataLoadedEvent, NetworkErrorEvent। একটি String টাইপ ফিল্ড সহ একটি সাধারণ Event ক্লাস এড়িয়ে চলুন — এটি টাইপিংয়ের সুবিধাগুলি দূর করে। একটি ইভেন্ট শ্রেণিবিন্যাস (প্যারেন্ট Event) সম্পর্কিত ইভেন্টের একটি গ্রুপে সাবস্ক্রাইব করার অনুমতি দেয়।
// ইভেন্ট শ্রেণিবিন্যাস
open class UserEvent
data class UserLoggedIn(val userId: String) : UserEvent()
data class UserLoggedOut(val reason: String) : UserEvent()
// বেস ক্লাসে সাবস্ক্রাইব করা
class SessionManager {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUserEvent(event: UserEvent) {
when (event) {
is UserLoggedIn -> startSession(event.userId)
is UserLoggedOut -> endSession(event.reason)
}
}
}
// পাঠানো
EventBus.getDefault().post(UserLoggedIn("user_123"))
EventBus.getDefault().register(this) কল করা রিফ্লেকশন বা Subscriber Index-এর মাধ্যমে সাবস্ক্রাইবার ক্লাস স্ক্যান করে এবং পাওয়া @Subscribe মেথডগুলি ইভেন্ট ম্যাপে সংরক্ষণ করে। Unregister সাবস্ক্রাইবারকে ম্যাপ থেকে সরিয়ে দেয়। নিবন্ধন বাতিল না করে পুনরায় নিবন্ধন একটি ত্রুটি (MultipleSubscriberException নিক্ষেপ করবে)। Fragment-এর জন্য, onStart()-এ নিবন্ধন করুন এবং onStop()-এ বাতিল করুন। Service-এর জন্য, onCreate() এবং onDestroy()-এ। ViewModel-এর জন্য, সুপারিশ করা হয় না — LiveData ব্যবহার করুন।
একটি sticky event হল একটি ইভেন্ট যা পাঠানোর পরে EventBus-এ স্থায়ী হয়। postSticky()-এর পরে নিবন্ধিত নতুন সাবস্ক্রাইবাররা অবিলম্বে সংশ্লিষ্ট টাইপের শেষ sticky event গ্রহণ করে। এটি প্রাথমিক অবস্থা পাস করার জন্য সুবিধাজনক: একটি স্ক্রিন খোলার সময়, এটি তার নিবন্ধনের আগে পাঠানো সর্বশেষ ডেটা গ্রহণ করে। আপনি EventBus.getDefault().removeStickyEvent(Class)-এর মাধ্যমে একটি sticky event সরাতে পারেন।
ThreadMode নির্ধারণ করে কোন থ্রেডে হ্যান্ডলার এক্সিকিউট হয়। POSTING (ডিফল্ট) — হ্যান্ডলার একই থ্রেডে চলে যেখানে post কল করা হয়েছিল। MAIN — হ্যান্ডলার Handler-এর মাধ্যমে main থ্রেডে চলে। BACKGROUND — হ্যান্ডলার ব্যাকগ্রাউন্ড থ্রেডে চলে; যদি post main থ্রেডে কল করা হয়, EventBus হ্যান্ডলারকে ব্যাকগ্রাউন্ড থ্রেড সারিতে রাখে। ASYNC — প্রতিটি হ্যান্ডলার থ্রেড পুল থেকে একটি পৃথক ব্যাকগ্রাউন্ড থ্রেডে চলে। UI আপডেটের জন্য, MAIN ব্যবহার করুন।
// Sticky Event
data class LocationEvent(val lat: Double, val lng: Double)
// LocationService থেকে sticky ইভেন্ট পাঠানো
EventBus.getDefault().postSticky(LocationEvent(55.7558, 37.6173))
// সাবস্ক্রাইবার নিবন্ধনের সাথে সাথে শেষ অবস্থান পায়
class MapFragment : Fragment() {
override fun onStart() {
super.onStart()
EventBus.getDefault().register(this)
// postSticky কল করা হলে অবিলম্বে LocationEvent পাবে
}
override fun onStop() {
EventBus.getDefault().unregister(this)
super.onStop()
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
fun onLocationEvent(event: LocationEvent) {
moveMapTo(event.lat, event.lng)
}
}
// sticky ইভেন্ট সরানো
EventBus.getDefault().removeStickyEvent(LocationEvent::class.java)
BACKGROUND সমস্ত হ্যান্ডলারের জন্য একটি ব্যাকগ্রাউন্ড থ্রেড ব্যবহার করে — তারা ক্রমান্বয়ে এক্সিকিউট হয়। ASYNC প্রতিটি হ্যান্ডলারের জন্য পুল থেকে একটি নতুন থ্রেড তৈরি করে — তারা সমান্তরালভাবে এক্সিকিউট হয়। BACKGROUND শেয়ার্ড ডাটাবেসের সাথে I/O অপারেশনের জন্য উপযুক্ত। ASYNC স্বাধীন দীর্ঘ অপারেশনের (নেটওয়ার্ক অনুরোধ) জন্য। উভয় মোডে শেয়ার্ড রিসোর্সে থ্রেড-নিরাপদ অ্যাক্সেস প্রয়োজন। থ্রেড সংখ্যা মনে রাখবেন: ASYNC পুল সীমাহীন।
EventBus ব্যবহার করার সময়, ডেভেলপাররা প্রায়শই এমন ভুল করে যা মেমরি লিক, অপ্রত্যাশিত কল এবং কর্মক্ষমতা হ্রাসের দিকে নিয়ে যায়। সবচেয়ে গুরুত্বপূর্ণ: Activity-তে নিবন্ধন বাতিল করতে ভুলে যাওয়া, onCreate-এ নিবন্ধন (onStart/onStop-এর পরিবর্তে), Object-এ (সব ইভেন্ট) সাবস্ক্রাইব করা, অসীম লুপে ইভেন্ট পাঠানো। Android Profiler-এর সাথে প্রোফাইলিং সমস্যা চিহ্নিত করতে সাহায্য করে।
সবচেয়ে সাধারণ ভুল হল onDestroy()-এ নিবন্ধন বাতিল না করেই onCreate()-এ Activity নিবন্ধন করা। ফলাফল: EventBus Activity-র একটি রেফারেন্স ধরে রাখে, GC এটি মুক্ত করতে পারে না। স্ক্রিন ঘোরানোর সময়, একটি নতুন Activity তৈরি হয় যখন আগেরটি মেমরিতে থাকে। সমাধান: সর্বদা onStart/onStop-এ register/unregister জোড়া দিন। Fragment-এর জন্য, একই প্যাটার্ন ব্যবহার করুন। finish-এর পরে যদি Activity EventBus দ্বারা ধরে রাখা হয়, Memory Profiler দিয়ে পরীক্ষা করুন।
Subscriber Index ছাড়া, EventBus প্রতিটি register()-এ @Subscribe মেথড খুঁজতে রিফ্লেকশন ব্যবহার করে। Android 6-7 ডিভাইসে, রিফ্লেকশন ধীর, যার ফলে 50 ms পর্যন্ত বিলম্ব হয়। Subscriber Index রিফ্লেকশন সম্পূর্ণরূপে দূর করে: মেথডগুলি একটি অ্যানোটেশন প্রসেসরের মাধ্যমে কম্পাইল টাইমে ইনডেক্স করা হয়। 20+ সাবস্ক্রাইবার সহ প্রকল্পের জন্য, ইনডেক্স বাধ্যতামূলক। নিশ্চিত করুন যে build.gradle-এ kapt বা annotationProcessor কনফিগার করা আছে।
// build.gradle (app) — Subscriber Index যোগ করা
dependencies {
implementation 'org.greenrobot:eventbus:3.3.1'
annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.3.1'
}
// Kotlin-এর জন্য kapt ব্যবহার করুন
plugins {
id 'kotlin-kapt'
}
dependencies {
implementation 'org.greenrobot:eventbus:3.3.1'
kapt 'org.greenrobot:eventbus-annotation-processor:3.3.1'
}
// ইনডেক্স কনফিগারেশন (defaultConfig-এ)
kapt {
arguments {
arg('eventBusIndex', 'com.app.EventBusIndex')
}
}
Kotlin এবং Jetpack Compose ব্যবহার করে আধুনিক প্রকল্পগুলি kotlinx.coroutines লাইব্রেরি থেকে SharedFlow এবং Channel পছন্দ করে। SharedFlow রিপ্লে (sticky), বাফারিং এবং ব্যাকপ্রেশার সমর্থন করে। Channel একক-ব্যবহারের ইভেন্ট (toast, নেভিগেশন) হ্যান্ডল করে। উভয় সমাধান repeatOnLifecycle-এর মাধ্যমে Lifecycle-এর সাথে সংহত এবং ম্যানুয়াল আনসাবস্ক্রিপশনের প্রয়োজন হয় না। নতুন প্রকল্পের জন্য, EventBus-এর পরিবর্তে SharedFlow সুপারিশ করা হয়। বিদ্যমান প্রকল্পের জন্য, রিফ্যাক্টরিংয়ের সময় মাইগ্রেশন ন্যায্য।
সচরাচর জিজ্ঞাসিত প্রশ্ন
EventBus যেকোনো কম্পোনেন্টের (Activity, Fragment, Service) মধ্যে ডেটা বিনিময়ের জন্য একটি ইভেন্ট বাস। LiveData হল UI কম্পোনেন্ট দ্বারা পর্যবেক্ষিত ডেটার জন্য lifecycle-aware র্যাপার। LiveData Lifecycle-এর মাধ্যমে স্বয়ংক্রিয়ভাবে সাবস্ক্রিপশন পরিচালনা করে। EventBus-এর ম্যানুয়াল register/unregister প্রয়োজন। LiveData UI স্তরের জন্য সুপারিশ করা হয়, EventBus ক্রস-মডিউল যোগাযোগের জন্য যেখানে LiveData অসুবিধাজনক।
একটি sticky event হল একটি ইভেন্ট যা পাঠানোর পরে EventBus-এ স্থায়ী হয়। postSticky()-এর পরে নিবন্ধিত নতুন সাবস্ক্রাইবাররা অবিলম্বে শেষ sticky event গ্রহণ করে। এটি প্রাথমিক অবস্থার জন্য ব্যবহৃত হয়: একটি স্ক্রিন খোলার সময়, এটি একটি নতুন অনুরোধ ছাড়াই সর্বশেষ ডেটা গ্রহণ করে। এটি removeStickyEvent()-এর মাধ্যমে বা একই টাইপের একটি নতুন sticky event পাঠানোর সময় সরানো হয়।
হ্যাঁ, EventBus থ্রেড-নিরাপদ। post() কল যেকোনো থ্রেড থেকে সম্ভব। সাবস্ক্রাইবারদের কাছে ইভেন্ট ডেলিভারি অভ্যন্তরীণভাবে সিঙ্ক্রোনাইজ করা হয়। ThreadMode হ্যান্ডলার এক্সিকিউশন থ্রেড নির্ধারণ করে: MAIN (Handler-এর মাধ্যমে main থ্রেড), POSTING (কলার থ্রেড), BACKGROUND (ব্যাকগ্রাউন্ড টাস্ক সারি), ASYNC (পৃথক থ্রেড)। UI আপডেটের জন্য, MAIN ব্যবহার করুন। ভারী অপারেশনের জন্য, ASYNC ব্যবহার করুন।
EventBus.builder().logNoSubscriberMessages(true).sendNoSubscriberEvent(true).install()-এর মাধ্যমে লগিং সক্ষম করুন। হ্যান্ডলার ছাড়া ইভেন্ট ট্র্যাক করতে NoSubscriberEvent-এ সাবস্ক্রাইব করুন। গ্লোবাল এক্সেপশন হ্যান্ডলিংয়ের জন্য SubscriberExceptionEvent ব্যবহার করুন। Android Profiler লিক খুঁজতে সাহায্য করে। জটিল পরিস্থিতির জন্য, একটি পরীক্ষা লিখুন: EventBus.getDefault().register(mock) + post(event) + verify(mock).
না, EventBus (GreenRobot) Android SDK এবং JVM-এর সাথে আবদ্ধ। Kotlin Multiplatform-এর জন্য, Kotlin Multiplatform SharedFlow বা KMMBus ব্যবহার করুন — শেয়ার্ড কোড সমর্থনকারী লাইব্রেরি। EventBus KMM প্রকল্পের Android পাশে কাজ করে কিন্তু commonMain-এ উপলব্ধ নয়। ক্রস-প্ল্যাটফর্ম ইভেন্টের জন্য, প্ল্যাটফর্মের নেটিভ মেকানিজম বা expect/actual-এর মাধ্যমে অ্যাবস্ট্রাকশন পছন্দ করুন।
সারাংশ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন