পোস্টটি পড়া হয়েছে 2,369 বার
android mvp weather app kotlin and retrofit bengali tutorial

MVP Architectural Pattern in Android – (Weather App: Kotlin + Retrofit)

Post updated on 22nd January, 2020 at 10:10 pm

Android App Development শেখার একদম শুরুতে আমরা কোডিং আর্কিটেকচার ও অন্যান্য স্ট্যান্ডার্ড প্র্যাক্টিসের সাথে পরিচিত থাকি না। শেখার সুবিধার্থে শুরুর দিকে অ্যাপের যাবতীয় কাজগুলো সব Activity বা Fragment এর ভিতর আমরা করে থাকি। কিন্তু যখন বড় প্রোজেক্ট করি, সেটাকে বছরের পর বছর মেইনটেইন করতে হয়, নতুন ফিচার আসে বা বিজনেস লজিক চেঞ্জ হয় তখন আগের মত কোড করলে আর চলে না। যত রকম চেঞ্জ আসে বা লজিক চেঞ্জ হয় আমরা যদি Activity তে সব অ্যাড করতে থাকি কোডটা তখন আস্তে আস্তে buggy হওয়া শুরু করে। Activity এর সাইজ হয়ে যায় বিরাট বড়। সফটওয়্যার ইঞ্জিনিয়ারিংয়ে আমার ছোট্ট ক্যারিয়ারে একটা Activity তে প্রায় ৪০০০ লাইনের কোড আছে এমন প্রোজেক্টের মেইনটেনেন্স বা বাগ ফিক্স করার (এবং আরো কিছু বাগ add করার :p ) সৌভাগ্য (!) আমার হয়েছে! এই গড অ্যাক্টিভিটির লজিক না যায় বুঝা, না পারা যায় কোনো নতুন ফিচার নিয়ে কাজ করা। এই টাইপের প্রোজেক্টে কাজ করে অ্যাপটাকে workable রাখা অনেক বড় চ্যালেঞ্জ। তাই আমাদেরকে Architectural Pattern শিখতে হয়। কোনো একটা প্যাটার্ন অনুযায়ী আমাদের প্রোজেক্টটাকে সাজালে, পরে মেইনটেইন করতে সুবিধা হয়। প্রোজেক্টের যাবতীয় কাজগুলোকে কয়েকটা লেয়ারে ভাগ করে দিলে কোড split হয়ে যায় অনেকগুলো ক্লাসে। বাগ খুঁজে বের করা বা লজিক চেঞ্জ করা তখন সহজ হয়।

Android App Development এর এক সময়কার জনপ্রিয় আর্কিটেকচার হচ্ছে MVP – Model View Presenter. যদিও সবাই এখন MVVM Architecture এ কাজ করতে বলেন, তবুও MVP এখনো বাতিলর খাতায় চলে যায় নাই। আমার ব্যক্তিগত অভিমত হচ্ছে আগে MVP architecture টা ভাল ভাবে বুঝে এরপর MVVM architecture শুরু করা। তাই এই পোস্টের লেখাটা পুরোপুরি বুঝে আসলে এরপর Android Architectural Pattern সিরিজের আমার পরবর্তী পোস্ট MVVM Architectural Pattern এর লেখাটা পড়তে পারেন। এই পোস্টটি বুঝার পর MVVM এর পোস্টটা পড়লে সেটা বুঝতে হয়ত আধা ঘন্টার বেশি লাগবে না।

MVP আর্কিটেকচার অনুযায়ী কোড করলে অ্যাপের কাজগুলো মোটামুটি তিনটা layer এ ভাগ হয়ে যায়। সেগুলো হচ্ছে Model বা data layer. ডেটা কোথা থেকে আসবে আর কোথায় যাবে এই লেয়ারের ক্লাসগুলো এটা হ্যান্ডেল করে। Presenter বা Presentation layer. ইউজাররা কী কী ডেটা দেখতে পাবে সেগুলোকে Model থেকে নিয়ে সাজিয়ে গুছিয়ে UI-তে শো করা। একই সাথে ইউজারের যে কোনো ইনপুট দেয়ার পর যাচাই বাছাই সাপেক্ষে সেগুলোকে db তে স্টোর বা সার্ভারে পাঠানোর জন্য Model এর কাছে পাঠিয়ে দেয়া। আর View layer এর কাজ হচ্ছে প্রেজেন্টার থেকে শো করার জন্য ডেটা নেয়া আর ইউজারের থেকে ইনপুট নিয়ে সেই ডেটা প্রেজেন্টারকে পাঠানো। এতে করে প্রোজেক্টের রেসপন্সিবিলিটিগুলো অনেকগুলো ক্লাসে ভাগ হয়ে যায়। হাজার হাজার লাইনের ভার থেকে Activity class পরিত্রাণ পায়।

আর এভাবে ভিউ লেয়ার থেকে বিজনেস লজিক আলাদা করতে পারার অন্যতম প্রধান সুবিধা হচ্ছে Unit Testing করার সুযোগ। যদিও বাংলাদেশের প্রেক্ষাপটে আমরা একেকজন সুপার প্রোগ্রামার (!) তাই আমাদের ইউনিট টেস্টিং লাগে না। ইউনিট টেস্টিং ছাড়াই আমরা আমাদের বেশির ভাগ ওয়েব অ্যাপ্লিকেশন ও মোবাইল অ্যাপ্লিকেশন ডেভেলপ করি এবং এগুলো কাস্টমারের পারপাস সার্ভ করছে। ইউনিট টেস্টিংয়ের জন্য আমাদের দেশের বা অন্যান্য দেশের অনেক ক্লায়েন্টই বাজেট বা সময় দেন না। প্রোজেক্টের টাইমলাইন আর বাজেট কমানোর জন্য বেস্ট প্র্যাক্টিসগুলো বাদ দেয়া আমাদের রেগুলার প্র্যাক্টিসে পরিণত হয়েছে। এরপরেও যদি কেউ নিজ দায়িত্বে টেস্টিং করতে চান বা শিখতে চান সেজন্য অবশ্যই এরকম লেয়ার বাই লেয়ার অ্যাবস্ট্রাকশনের মাধ্যমে প্রোজেক্ট সাজাতে হবে। আপনি দেশের বাইরে জব নিয়ে যেতে চাইলে বা দেশে বসে বাইরের কোনো টিমের সাথে রিমোটলি কাজ করতে চান তাহলে জবের ইন্টারভিউতে ইউনিট টেস্টিং নিয়ে জিজ্ঞেস করবেই।

আজকের পোস্টে এই MVP Architecture নিয়েই কথা বলব এবং একটা রিয়েল অ্যাপ ডেভেলপ করব।

Prerequisite

আপনি যদি basic Android App Development জেনে থাকেন তাহলে, MVP Architecture অনুযায়ী কোড করার জন্য আপনাকে নতুন করে কোনো কিছু শিখতে হবে না। ব্যাসিক OOP জানা থাকলেই হবে। আর বিশেষ করে interface এর ইউজ জানা থাকতে হবে। interface কিভাবে, কেন ইউজ হয় সেটা জানা থাকতে হবে। না জানা থাকলেও আশা করি কোড দেখে বুঝে ফেলবেন। এই পোস্টে স্যাম্পল কোড দেখানোর জন্য Kotlin ব্যবহার করা হয়েছে। তাই আগে থেকে কটলিনের সাথে পরিচয় থাকলে সুবিধা হবে। ওয়েব সার্ভারের সাথে communication বা network call এর জন্য আমরা Retrofit Network Library ইউজ করেছি। আপনি Retrofit ইউজ না করে থাকলে এই ব্লগ পোস্ট থেকে Retrofit সম্পর্কে জেনে নিতে পারেন। Retrofit এর উপর আমার দ্বিতীয় ব্লগ পোস্টে দেখিয়েছিলাম কিভাবে View layer থেকে network layer কে আলাদা করা যায়। দ্বিতীয় পোস্টটি পড়ে বুঝে থাকলে আপনার জন্য MVP বুঝা ডাল ভাতের মত হয়ে যাবে।

mvp architecture android tutorial bengali
MVC vs MVP. Photo credit: Toptal

MVP – Model View Presenter কী?

MVP আর্কিটেকচারটা আমাদের সুপরিচিত MVC – Model View Controller আর্কিটেকচার থেকে এসেছে। উপরের ছবিতে ব্লক ডায়াগ্রাম দিয়ে দুইটার পার্থক্য বুঝানো হয়েছে। আপাতত এটা দেখে কিছুটা আইডিয়া পেলে ভাল, না পেলেও ক্ষতি নাই। অনেক বিজ্ঞ জনের মতে MVP আসলে কোনো আর্কিটেকচারাল প্যাটার্ন না। এটাকে তারা একটা কোডিং বা ডিজাইন প্যাটার্ন বলেন। আবার অনেকের মতে MVP আর্কিটেকচার প্যাটার্ন। (দেখুন, শুধু ইসলামের বিভিন্ন মাসআলা-ফতোয়ার বিষয়েই স্কলারদের মধ্যে মতবিরোধ হয় না। সফটওয়্যার ইন্ডাস্ট্রিতেও এরকম অসংখ্য বিষয়ে ‘ইখতেলাফ’ বা মত পার্থক্য রয়েছে! এটা সমস্যা নয়, সম্ভাবনা। সংকীর্ণতা নয়, প্রশস্থতা!) আমরা এখন এই আর্কিটেকচার হওয়া না হওয়ার বিতর্কে না গেলাম। আপাতত এটাকে Architectural patter বলেই সাব্যস্ত করি।

MVP সম্পর্কে Wikipedia বলছে এভাবেঃ

Model–view–presenter (MVP) is a derivation of the model–view–controller (MVC) architectural pattern, and is used mostly for building user interfaces.

In MVP, the presenter assumes the functionality of the “middle-man”. In MVP, all presentation logic is pushed to the presenter.

Vogella ব্লগে পাওয়া যায় এই কথাঃ

The Model View Presenter (MVP) architecture pattern improve the application architecture to increase testability. The MVP pattern separates the data model, from a view through a presenter.

এর থেকে আমরা এই সারমর্মে পৌঁছতে পারি যে, MVP এমন একটা আর্কিটেকচার অনুযায়ী কোড করার নাম, যেই আর্কিটেকচারে model, view ও presenter বলে তিনটা আলাদা লেয়ার define করতে পারি। যেখানে model layer এ থাকে ডেটার উৎস আর view layer এ থাকে user interface এর কাজকর্ম। Model ও view এর মধ্যে মিডল ম্যান বা ব্রিজ লাইন হিসাবে কাজ করে presenter. Presenter এর কাজ হচ্ছে মডেল ও ভিউয়ের মধ্যে ডেটার আদান প্রদান নিশ্চিত করা।

View

View হচ্ছে এমন কতগুলো ক্লাসের গুচ্ছ যাদের কাজ হচ্ছে UI-তে ডেটা দেখানো। আর ইউজারের থেকে ইনপুট নেয়া। এর বাইরে সে কোনো কাজ করবে না। কোনো হিসাব নিকাশ বা ডেটা fetch করা বা নেটওয়ার্ক কল করা কিচ্ছু করবে না। বলা হয়ে থাকে View should be the dumb one! অর্থাৎ সে বোকাসোকা একটা লেয়ার। কোনো চিন্তা ভাবনার মধ্যে সে থাকবে না। ভিউতে দেখানোর জন্য কোনো ডেটা দরকার হলে সে প্রেজেন্টারের মেথড কল দিয়ে বসে থাকবে। ডেটা পাওয়া গেলে প্রেজেন্টার যখন ভিউয়ের মেথড ট্রিগার করবে তখন ভিউ সেই ডেটা শুধু শো করবে। এই ডেটা আনার মাঝে সময় দরকার হলে আমরা progress bar বা loader দেখাই। সেটাও প্রেজেন্টার ভিউকে বলবে শো করতে বা হাইড করতে। তাহলেই শুধু এই কাজ ভিউ করবে। কোনো লজিকের উপর ভিত্তি করে ভিউ নিজের থেকে ডিসাইড করবে না এই লোডার শো হাইড করার কাজটা। একই ভাবে কোনো বাটন ক্লিক হলে বা টেক্সট ইনপুট হলে সে সেটাকে প্রেজেন্টারকে জাস্ট জানিয়ে দিবে। সেই ডেটা নিয়ে ভিউ কোনো কাজ করবে না।

Presenter

Presenter হচ্ছে view ও model এর মধ্যে middle-man বা ব্রিজ লাইন বলতে পারি। যে কিনা মডেল ও ভিউয়ের মধ্যে সেতু বন্ধন করে। প্রেজেন্টার ভিউয়ের থেকে ডেটার রিকুজিশন নেয়। অর্থাৎ ভিউয়ের কী ডেটা দরকার সেটা ভিউ প্রেজেন্টারকে জানায়। এই ডেটা কিন্তু প্রেজেন্টার নিজে নেওয়ার্ক থেকে নিয়ে আসে না বা ডিবি থেকে আনে না। সে এই ডেটার রিকুজিশন পাঠায় মডেলের কাছে। মডেল এই ডেটা প্রেজেন্টারকে ব্যাক করলে প্রেজেন্টার সেই ডেটার উপর প্রয়োজনীয় কাজকর্ম করে। প্রেজেন্টারের কাছে ভিউ হয়ত জানতে চেয়েছে ইউজারের বয়স কত (একটা integer value)। বয়সটা ভিউতে দেখাতে হবে। প্রেজেন্টার মডেলকে ডেটা দিতে বলল। মডেল প্রেজেন্টারকে পাঠাল ইউজারের জন্ম তারিখ, একটা String। কিন্তু ভিউতে শো করতে হবে বয়স, integer। প্রেজেন্টার তখন এই তারিখের সাথে আজকের তারিখ ক্যালকুলেট করে বয়সের একটা integer বের করবে। এরপর সেটা ভিউতে পাঠিয়ে দিবে। প্রেজেন্টারের মধ্যে আমরা আমাদের বিজনেস লজিকগুলো রাখি। আবার অনেকে বিজনেস লজিক মডেলের মধ্যে রাখে। একেক জনের ইম্প্লিমেন্টেশন একেক রকম হতে পারে। এটার সেরকম কোনো standard নাই।

আদর্শ Presentation layer এর বৈশিষ্ট্য হচ্ছে এটা Android এর SDK মুক্ত হবে। Android এর Context বা এমন কোনো ক্লাসের ব্যবহার এখানে হবে না যেটা কিনা অ্যান্ড্রয়েডের কোনো ক্লাস। এখানে পিওর জাভা বা কটলিনের কাজকর্ম হবে। এর উদ্দেশ্য হচ্ছে Presenter-কে যেন JVM এর মাধ্যমে Unit Test করা যায়। এটাকে টেস্ট করার জন্য যদি Android এর কোনো ক্লাসের দরকার হয় তখন শুধু JVM এ টেস্ট রান করা সম্ভব না। তাই আমাদের টার্গেট থাকবে Presentation layer কে Android SDK এর কোড থেকে মুক্ত রাখা।

Model

Model এর কাজ হচ্ছে ডেটা সোর্স থেকে ডেটা নিয়ে প্রেজেন্টারকে সাপ্লাই দেয়া। এবং প্রেজেন্টার থেকে ডেটা নিয়ে ডেটা সোর্সকে আপডেট করা। যখন ভিউতে কোনো কিছু দেখানোর দরকার হবে তখন মডেল প্রেজেন্টারকে ডেটা সাপ্লাই দিবে। মডেল এই ডেটাটা লোকাল ডেটাবেজ থেকে নিতে পারে, shared preference থেকে নিতে পারে, assets ফোল্ডার থেকে নিতে পারে, আবার কোনো ওয়েব সার্ভার থেকেও নেটওয়ার্ক কল করে নিতে পারে। কোন ডেটা কোথা থেকে নিতে হবে সেটা আমরা মডেলের মধ্যে লিখে দিব। একই ভাবে যখন ইউজার কোনো ইনপুট দেয়, প্রেজেন্টার তখন ভিউ থেকে সেই ইনপুট রিসিভ করে মডেলকে দেয়। মডেলের দায়িত্ব থাকে লজিক অনুযায়ী সেই ডেটা ডিবি, প্রিফারেন্স, ফাইলে write করা বা নেটওয়ার্ক কল দিয়ে সার্ভারে পাঠিয়ে দেয়া। অর্থাৎ ডেটা স্টোর করা। এই স্টোর সফল হলে বা ব্যর্থ হলে সেই স্ট্যাটাসটা প্রেজেন্টারকে জানানোও মডেলের দায়িত্ব।

বইয়ের ভাষা ছেড়ে একটু সহজ ভাষায় বলা যাক। আমরা কোনো একটা ভিউতে (Activity) সার্ভার থেকে ডেটা এনে দেখানোর জন্য কী করি? Activity এর onCreate() মেথডের ভিতরে নেটওয়ার্ক কল করি। সার্ভার থেকে ডেটা আসলে সেটাকে এখানেই চেক করি ভ্যালিড কিনা, বা নেটওয়ার্ক কলটি success হয়েছে কিনা। এরপর সেই ডেটার থেকে কিছু ডেটা নিয়ে কিছু ক্যালকুলেশন দরকার হলে সেটা করে ভিউতে দেখিয়ে দিই।

MVP এই কাজগুলোকে তিনটা ভাগে ভাগ করে। সে রিকমেন্ড করে যে, নেটওয়ার্ক কল করার কাজটা ভিউতে হবে না। এটা আলাদা একটা (model) লেয়ারে হবে। ডেটা ভ্যালিড কিনা, কোনো ক্যালকুলেশন লাগলে সেটা ভিউ করবে না। সেটা করবে প্রেজেন্টার। ভিউ শুধু প্রেজেন্টারকে বলবে “প্রেজেন্টার ভাই! আমার এই টাইপের একটা ডেটা লিস্ট লাগবে। তুমি ম্যানেজ করে আমাকে জানিয়ে দিও“। প্রেজেন্টার জানে না অ্যাপের ডেটা কি ডিবি থেকে আসবে নাকি সার্ভার থেকে আসবে। সে তাই মডেলকে বলে “ভাই মডেল! তুমি যেখান থেকে পার এই ডেটা জোগার কর। জোগার হলে আমাকে জানিয়ে দিও“।

মডেল তখন ডিসিশন নিবে ডেটা কোথা থেকে সংগ্রহ করা যায়। লজিক লেখা থাকবে মডেলে, কখন কোথা থেকে ডেটা জোগার করতে হবে। সেই অনুযায়ী সে ডেটা জোগার করে প্রেজেন্টারকে জানিয়ে  দিবে। সে কিন্তু জানে না ভিউতে কী দেখানো হবে, কী ক্যালকুলেশন করা হবে। সে ডেটা পাঠায় দিয়েই আবার কম্বল টেনে ঘুম দিবে। প্রেজেন্টার যখন দেখবে তাকে ডেটা পাঠানো হয়েছে তখন সে সেই ডেটা যাচাই বাছাই করবে। দরকার লাগলে মোডিফাই করবে। এরপর ভিউকে জানিয়ে দিবে  ফরমেট করা ডেটা ভিউতে দেখানোর জন্য। ভিউ তখন জাস্ট ডেটাগুলো শো করবে।

একই ভাবে ভিউ যদি ইউজারের থেকে কোনো ইনপুট পায় সেটা সে প্রেজেন্টারকে পাঠাবে আর প্রেজেন্টারের ফিডব্যাকের জন্য অপেক্ষা করবে। প্রেজেন্টার সেটা যাচাই বাছাই বা মোডিফাই করে মডেলকে পাঠাবে, আর মডেলের ফিডব্যাকের জন্য অপেক্ষা করবে। মডেল সেটা ডিবিতে রাখার দরকার হলে ডিবিতে রাখবে, সার্ভারে পাঠানোর দরকার হলে সার্ভারে পাঠাবে। পাঠানোর কাজ হলে সে প্রেজেন্টারকে বলবে “ভাই তোমার কাজ করে দিছি”। প্রেজেন্টার তখন অপেক্ষারত ভিউকে বলবে “তোমার ডেটা জায়গা মত পৌঁছে গেছে। তুমি ইউজারকে বল Success”. ভিউ তখন ইউজারকে হয়ত একটা Toast message এ দেখাবে “Success”. এই হল পুরো গল্প! কোনো অস্পষ্টতা আছে? কোথাও বুঝতে সমস্যা হলে আগের ২-১ টি প্যারা সহ আবার পড়ে দেখতে পারেন। অথবা পুরোটা শেষ করে অস্পষ্ট অংশের ব্যাপারে কমেন্ট করতে পারেন।

MVP সম্পর্কিত থিওরিট্যাল কথাবার্তা এ পর্যন্তই। এরপর আমরা কোডিংয়ে চলে যাব। একটা কথা বলে রাখা ভাল, এই যে আমরা view, presenter ও model এর মধ্যে ডেটার আদান প্রদান করছি; সব কিছুই হবে interface ব্যবহার করে। তিনটা লেয়ারের মাঝে abstraction নিয়ে আসার জন্যেই এই ইন্টারফেসের ব্যবহার। একটা লেয়ারে কিভাবে কাজ হচ্ছে সেটাকে অন্য লেয়ার থেকে আড়াল করার জন্যেই এই ব্যবস্থা। এই আড়াল করা কেন দরকার? এই জন্য দরকার যেন একটা লেয়ারের সাথে আরেকটা লেয়ার শক্ত ভাবে যুক্ত না থাকে বা tightly coupled না হয়। এই coupling আরো loose করা যায়, আরো বেশি abstraction নিয়ে আসা যায় MVP এর সাথে Dependency Injection ব্যবহার করে। আপাতত MVP বুঝার উপর আমরা জোর দিব, তাই Dependency Injection এখানে ইউজ করব না। পরের কোনো লেখায় MVP এর সাথে Dependency Injection ইউজ করে কিভাবে আরো সুন্দর কোড করা যায় তা দেখাবো ইনশাআল্লাহ।

Problem Description

android MVP architecture tutorial in Bengaliএকটা Weather Forecast App ডেভেলপ করতে হবে। যেখানে কয়েকটি শহরের আবহাওয়া সংক্রান্ত তথ্য দেখা যাবে। শহরের নামের লিস্ট অ্যাপের মধ্যেই লোকাল্যি একটা ফাইলে সেভ থাকবে। আর শহর অনুযায়ী আবহাওয়ার তথ্য নেয়া হবে Open Weather API ব্যবহার করে।

আর এই অ্যাপটি ডেভেলপ করতে হবে MVP Architecture অনুসরণ করে।

Open Weather এর ওয়েবসাইটে ফ্রি অ্যাকাউন্ট খুলে আপনি আপনার API KEY বা App ID সংগ্রহ করতে পারেন। ফ্রি ভার্সনে আপনি প্রতি মিনিটে ৬০ টি API Call করতে পারবেন। এর বেশি প্রয়োজন হলে ওদেরকে pay করে ইউজ করতে হবে। ওদের API documentation দেখে আপনার প্রয়োজন মত ডেটা অ্যাপে শো করাতে পারেন।

Android MVP Architecture Source Code

প্রোজেক্টের রিকোয়ার্মেন্ট থেকে বুঝতে পারলাম আমাদের প্রোজেক্টে একটাই মাত্র অ্যাক্টিভিটি থাকবে। সেখানে উপরে একটা Spinner থাকবে। সেখানে ক্লিক করলে City List শো করবে। এই সিটি লিস্টটা অ্যাপের লোকাল কোনো ফাইলে সেভ করা থাকবে। সিটি সিলেক্ট করে VIEW WEATHER বাটনে ক্লিক করলে একটা network request যাবে Open Weather এর সার্ভারে। সেই রিকোয়েস্টে শহরের নাম বা আইডি জাতীয় কিছু একটা পাঠাতে হবে। তার উপর ভিত্তি করে সার্ভার আমাদেরকে response করবে ঐ শহরের আবহাওয়ার তথ্য দিয়ে। আর এই নেটওয়ার্ক কল হবার সময় একটা Progress Bar দেখানো লাগবে। ডেটা লোড হয়ে গেলে progress bar টা হাইড হয়ে যাবে।

Open Weather এর API Documentation থেকে আমরা নেটওয়ার্ক কলের request-response সম্পর্কে ধারণা নিতে পারি। আবহাওয়ার তথ্যের ডান পাশে Haze এর জন্য যেই আইকন দেখা যাচ্ছে সেটা সার্ভার থেকে রেসপন্সে পাওয়া যাবে। আইকনের লিংক পাঠানো হবে, তাই এই ইমেজ লিংক ImageView তে শো করানোর জন্য Glide Image Loading লাইব্রেরি ইউজ করব। Network request এর জন্য ব্যবহার করব Retrofit Library. JSON ডেটাকে Serialize-Deserialize করার জন্য ব্যবহার করব GSON Library.

এবার Android Studio দিয়ে একটা নতুন প্রোজেক্ট খুলি। এরপর ঝটপট কিছু প্যাকেজ আর কিছু ক্লাস তৈরি করে ফেলি। আমার project structure-টা দেখতে নিচের মত হয়েছে।

Android MVP Architecture Tutorial Project Bangla
Project Structure of MVP Architecture Android App

feature package এর ভিতরকার weather_info_show এই ফিচার বা এই প্যাকেজের কোডগুলোই আসলে MVP অনুসরণ করে সাজানো হয়েছে। তাই আজকের পোস্টে এগুলোই আলোচনা করব। network বা অন্যান্য প্যাকেজের কোডগুলোর ব্যাখ্যা এই পোস্টে আর করব না। কারণ ওগুলো রেট্রোফিটের আলাদা ব্লগ পোস্টে আলোচনা করা হয়েছে। এই ব্লগের শুরুর Prerequisite সেকশনে পোস্টগুলোর লিংক পাওয়া যাবে।

View layer Source Code – MVP Android

আমাদের একটাই Activity. তার নাম দিলাম যথারীতি MainActivity. এটা আমাদের ভিউ লেয়ারের implementation class বলতে পারি। আমরা ভিউয়ের জন্য একটা interface লিখব। সেই ইন্টারফেসকে MainActivity implement করবে। তাই একে implementation class বললাম। View interface টা নিম্নরূপঃ

interface MainActivityView {
    fun handleProgressBarVisibility(visibility: Int)
    fun onCityListFetchSuccess(cityList: MutableList<City>)
    fun onCityListFetchFailure(errorMessage: String)
    fun onWeatherInfoFetchSuccess(weatherDataModel: WeatherDataModel)
    fun onWeatherInfoFetchFailure(errorMessage: String)
}

প্রথম মেথডটা কাজ করবে প্রোগ্রেসবার দেখানো আর না দেখানোর জন্য। ২য় ও ৩য় মেথড দুটি কাজ করবে Activity কে শহরের লিস্ট provide করার বিষয়ে। ৪র্থ ও ৫ম মেথড দুটি কাজ করবে আবহাওয়ার তথ্য জানানোর জন্য। এবার এই ইন্টারফেসকে আমাদের MainActivity ক্লাসে implement করা যাক।
class MainActivity : AppCompatActivity(), MainActivityView {

    private lateinit var model: WeatherInfoShowModel
    private lateinit var presenter: WeatherInfoShowPresenter

    private var cityList: MutableList<City> = mutableListOf()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // initialize model and presenter
        model = WeatherInfoShowModelImpl(applicationContext)
        presenter = WeatherInfoShowPresenterImpl(this, model)

        // call for fetching city list
        presenter.fetchCityList()

        btn_view_weather.setOnClickListener {
            output_group.visibility = View.GONE

            val spinnerSelectedItemPos = spinner.selectedItemPosition

            // fetch weather info of specific city
            presenter.fetchWeatherInfo(cityList[spinnerSelectedItemPos].id)
        }
    }

    override fun onDestroy() {
        presenter.detachView()
        super.onDestroy()
    }

    override fun handleProgressBarVisibility(visibility: Int) {
        progressBar?.visibility = visibility
    }

    override fun onCityListFetchSuccess(cityList: MutableList<City>) {
        this.cityList = cityList

        val arrayAdapter = ArrayAdapter(
                this,
                R.layout.support_simple_spinner_dropdown_item,
                cityList.convertToListOfCityName()
        )

        spinner.adapter = arrayAdapter
    }

    override fun onCityListFetchFailure(errorMessage: String) {
        Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
    }

    override fun onWeatherInfoFetchSuccess(weatherDataModel: WeatherDataModel) {
        output_group.visibility = View.VISIBLE
        tv_error_message.visibility = View.GONE

        tv_date_time?.text = weatherDataModel.dateTime
        tv_temperature?.text = weatherDataModel.temperature
        tv_city_country?.text = weatherDataModel.cityAndCountry
        Glide.with(this).load(weatherDataModel.weatherConditionIconUrl).into(iv_weather_condition)
        tv_weather_condition?.text = weatherDataModel.weatherConditionIconDescription

        tv_humidity_value?.text = weatherDataModel.humidity
        tv_pressure_value?.text = weatherDataModel.pressure
        tv_visibility_value?.text = weatherDataModel.visibility

        tv_sunrise_time?.text = weatherDataModel.sunrise
        tv_sunset_time?.text = weatherDataModel.sunset
    }

    override fun onWeatherInfoFetchFailure(errorMessage: String) {
        output_group.visibility = View.GONE
        tv_error_message.visibility = View.VISIBLE
        tv_error_message.text = errorMessage
    }
}

দেখুন, ইন্টারফেসের ৫টা মেথডকেই MainActivity ক্লাস override করেছে। আর onCreate() মেথডে প্রথমে আমরা presenter ও model এর একটা instance নিয়েছি। এরপর প্রেজেন্টারের মেথডে কল দিয়ে শহরের লিস্ট চেয়েছি। আর বাটন কল করা হলে তার ক্লিক ইভেন্টে প্রেজেন্টারের কাছে শহরের আইডি পাঠিয়ে আবহাওয়ার তথ্য চেয়েছি। আমরা ভিউতে কী চাচ্ছি সেটা onCreate() এ বলে দিয়েছি। এখন যা চাচ্ছি তা নিচের override হওয়া মেথডগুলোর মধ্যে চলে আসবে। যখন আসবে তখন সেগুলো আমরা view তে শো করার কোড ঐসব মেথডের ভিতর লিখে দিচ্ছি। এই ওভাররাইড করা মেথডগুলো কিন্তু Activity নিজ ইচ্ছায় কল করতে পারবে না। প্রেজেন্টার এই মেথডগুলোতে যথাযথ ডেটা দিয়ে কল করবে। তাহলে ভিউ, জাস্ট ডেটা চেয়েই বসে থাকবে। ডেটা আসলে সে শুধু সেটা শো করবে। আর ইউজার বাটন ক্লিক করে কোনো ইনপুট দিলে সেটাও সে প্রেজেন্টারকে জানিয়ে কাজ শেষ করে হাত গুটিয়ে বসে থাকবে। প্রেজেন্টার যখন মডেলের থেকে আবহাওয়ার ডেটা ঠিকঠাক পাবে তখন onWeatherInfoFetchSuccess() মেথড কল করে দিবে। তখন অ্যাক্টিভিটি এই মেথডের ভিতরে বসে UI-তে ডেটা শো করবে।

Activity এর onDestroy() মেথডে presenter এর detachView() মেথড কল করা হয়েছে। এটা দিয়ে প্রেজেন্টারকে বুঝানো হয়েছে যে অ্যাক্টিভিটি destroy হয়ে গেছে। প্রেজেন্টার থেকে আর কোনো ডেটা আসতে বাকি থাকলে সেগুলো যেন না পাঠানো হয়।

Presentation layer Source Code – MVP Android

View এর মত Presenter লেয়ারেও একটা ইন্টারফেস আর তার একটা ইমপ্লিমেন্টেশন ক্লাস বানাব। ইন্টারফেসটা এরকমঃ

interface WeatherInfoShowPresenter {
    fun fetchCityList()
    fun fetchWeatherInfo(cityId: Int)
    fun detachView()
}

Activity থেকে উপরের মেথডগুলোকে জাস্ট কল করা হয়েছিল। এই মেথডগুলোর ভিতরে কী লেখা আছে সেটা অ্যাক্টিভিটি জানেও না সেটা সে কেয়ারও করে না। প্রেজেন্টারকে ইমপ্লিমেন্ট করে নিচের ক্লাসটিঃ
class WeatherInfoShowPresenterImpl(
        private var view: MainActivityView?,
        private val model: WeatherInfoShowModel) : WeatherInfoShowPresenter {

    override fun fetchCityList() {
        
        model.getCityList(object : RequestCompleteListener<MutableList<City>> {

            override fun onRequestSuccess(data: MutableList<City>) {
                view?.onCityListFetchSuccess(data) //let view know the formatted city list data
            }

            override fun onRequestFailed(errorMessage: String) {
                view?.onCityListFetchFailure(errorMessage) //let view know about failure
            }
        })
    }

    override fun fetchWeatherInfo(cityId: Int) {

        view?.handleProgressBarVisibility(View.VISIBLE) // let view know about progress bar visibility

        model.getWeatherInformation(cityId, object : RequestCompleteListener<WeatherInfoResponse> {

            override fun onRequestSuccess(data: WeatherInfoResponse) {

                view?.handleProgressBarVisibility(View.GONE) // let view know about progress bar visibility

                val weatherDataModel = WeatherDataModel(
                    dateTime = data.dt.unixTimestampToDateTimeString(),
                    temperature = data.main.temp.kelvinToCelsius().toString(),
                    cityAndCountry = "${data.name}, ${data.sys.country}",
                    weatherConditionIconUrl = "http://openweathermap.org/img/w/${data.weather[0].icon}.png",
                    weatherConditionIconDescription = data.weather[0].description,
                    humidity = "${data.main.humidity}%",
                    pressure = "${data.main.pressure} mBar",
                    visibility = "${data.visibility/1000.0} KM",
                    sunrise = data.sys.sunrise.unixTimestampToTimeString(),
                    sunset = data.sys.sunset.unixTimestampToTimeString()
                )

                view?.onWeatherInfoFetchSuccess(weatherDataModel) //let view know the formatted weather data
            }

            override fun onRequestFailed(errorMessage: String) {
                view?.handleProgressBarVisibility(View.GONE) // let view know about progress bar visibility

                view?.onWeatherInfoFetchFailure(errorMessage) //let view know about failure
            }
        })
    }

    override fun detachView() {
        view = null
    }
}

ইন্টারফেসের method signature গুলোর full body এখানে রয়েছে। অ্যাক্টিভিটি থেকে ইন্টারফেসের মেথড কল হলে পর্দার আড়ালে আসলে এখানকার মেথডই execute হয়। এই ক্লাসের constructor এ ভিউ আর মডেলের একটা করে instance পাঠানো হয়েছে। মেথডগুলোর ভিতর দেখুন, মডেলের কোনো একটা মেথডে কল দেয়া হয়েছে। মেথডে প্যারামিটার হিসাবে একটা callback interface এর instance পাঠানো হয়েছে। মডেল ডেটা পাওয়ার পর এখানকার success বা fail মেথড কল করবে। তখন এই মেথডগুলোর ভিতর থেকে view এর instance গুলোর মেথড কল করে ডেটা view তে পাঠানো হবে।

দ্বিতীয় মেথডে মডেলকে কল করা হলে মডেল Open Weather এর API তে কল দেয়। এটার জন্য কিছু সময় দরকার হয়। তাই মডেলের মেথড কল করার আগেই প্রেজেন্টার আমাদের ভিউকে বলছে প্রোগ্রেসবার দেখাতে। মডেলের থেকে response আসার পর এই প্রোগ্রেসবার আবার হাইড করে দেয়া হয়েছে। এই মেথডের ভিতরই মডেল থেকে প্রাপ্ত ডেটা ফরমেট করে ভিউকে পাঠানো হচ্ছে। পরবর্তীতে যদি আমরা চাই এই ডেটা অন্য ভাবে মোডিফাই করে দেখাতে সেটা এখানেই চেঞ্জ করতে হবে। যেমন আমরা চাইতে পারি pressure বা humidity এর ডেটা দেখানোর পাশে বলে দিব High, Medium, Low. তাহলে এখান থেকেই শুধু data manipulation হবে। View তে বা মডেলে হাত দেয়াই লাগবে না।

detachView() মেথডে কল করা হলে এই ক্লাসের constructor এ view এর যেই instance পাঠানো হয়েছিল সেটাকে null করে দিচ্ছে। লক্ষ্য করে দেখুন, অন্য দুটি মেথডের ভিতর view এর কোনো মেথড কল করার সময় “? চিহ্ন” এর সাহায্যে null safe চেক রাখা হয়েছে। যেমনঃ view?.handleProgressBarVisibility(View.GONE). কটলিনে কাজ করে থাকলে আপনি নিশ্চয়ই জানেন এর মানে হচ্ছে “যদি view null না হয় তাহলে handleProgressBarVisibility() কল কর”। আর null হলে ignore কর। অ্যাক্টিভিটির onDestroy() থেকে যখন detachView() কল করা হয় তখন আদতে আমাদের উপরের এই ক্লাসের detachView() কল হয়ে তার ভিউকে নাল করে দেয়। ফলে অ্যাক্টিভিটি বন্ধ হয়ে যাওয়ার পরেও যদি প্রেজেন্টার ভিউয়ের কোনো মেথড কল করতে যায় তখন সে দেখে ভিউ null. তাই সে আর ভিউকে আপডেট করার জন্য মেথড কল করে না। তাই অ্যাপ ক্র্যাশও করে না। কিন্তু অ্যাক্টিভিটি যদি প্রেজেন্টারকে তার বন্ধ হয়ে যাবার খবর না জানাত, তাহলে কিন্তু প্রেজেন্টার থেকে ভিউ আপডেট করতে গেলে ক্র্যাশ করত।

Model layer Source Codes – MVP Android

Model layer এর ইন্টারফেসের কোড নিম্নরূপঃ

interface WeatherInfoShowModel {
    fun getCityList(callback: RequestCompleteListener<MutableList<City>>)
    fun getWeatherInformation(cityId: Int, callback: RequestCompleteListener<WeatherInfoResponse>)
}

Presenter থেকে data পাওয়ার জন্য এই দুইটা মেথডে কল করা হয়। মেথড দুইটার implementation করা হয়েছে এই ক্লাসেঃ
class WeatherInfoShowModelImpl(private val context: Context) : WeatherInfoShowModel {

    override fun getCityList(callback: RequestCompleteListener<MutableList<City>>) {

        try {
            val stream = context.assets.open("city_list.json")

            val size = stream.available()
            val buffer = ByteArray(size)
            stream.read(buffer)
            stream.close()
            val tContents  = String(buffer)

            val groupListType = object : TypeToken<ArrayList<City>>() {}.type
            val gson = GsonBuilder().create()
            val cityList: MutableList<City> = gson.fromJson(tContents, groupListType)

            callback.onRequestSuccess(cityList) //let presenter know the city list

        } catch (e: IOException) {
           e.printStackTrace()
            callback.onRequestFailed(e.localizedMessage!!) //let presenter know about failure
        }
    }

    override fun getWeatherInformation(cityId: Int, callback: RequestCompleteListener<WeatherInfoResponse>) {

        val apiInterface: ApiInterface = RetrofitClient.client.create(ApiInterface::class.java)
        val call: Call<WeatherInfoResponse> = apiInterface.callApiForWeatherInfo(cityId)

        call.enqueue(object : Callback<WeatherInfoResponse> {

            override fun onResponse(call: Call<WeatherInfoResponse>, response: Response<WeatherInfoResponse>) {
                if (response.body() != null)
                    callback.onRequestSuccess(response.body()!!) //let presenter know the weather information data
                else
                    callback.onRequestFailed(response.message()) //let presenter know about failure
            }

            override fun onFailure(call: Call<WeatherInfoResponse>, t: Throwable) {
                callback.onRequestFailed(t.localizedMessage!!) //let presenter know about failure
            }
        })
    }
}

প্রথম মেথডে city list নেয়া হয়েছে। ডেটা সোর্স এখানে assets ফোল্ডারে রাখা একটা JSON ফাইল। JSON ডেটাগুলো সংগ্রহ করা হয়েছে Open Weather API থেকে। যেহেতু আমরা ফিক্সড কিছু শহরের আবহাওয়া দেখাতে চাই, তাই এই ডেটাগুলো প্রতিবার সার্ভার থেকে না এনে অ্যাপের মধ্যেই লোকাল্যি রেখে দিয়েছি।

JSON ডেটা ফরমেটটা নিচে দেয়া হল। এখানে পুরো লিস্টটা নাই। গিটহাব থেকে প্রোজেক্ট ক্লোন করলে সেখানে পুরো লিস্টটা পাওয়া যাবে।

[
  {
    "id": 1185241,
    "name": "Dhaka",
    "country": "BD"
  },
  {
    "id": 1336135,
    "name": "Khulna",
    "country": "BD"
  },
  {
    "id": 1337200,
    "name": "Chittagong",
    "country": "BD"
  }
]

দ্বিতীয় override মেথডে Retrofit ইউজ করে weather info নিয়ে আসা হচ্ছে open weather API থেকে। নেটওয়ার্ক রিকোয়েস্ট সফল হলে সেই ডেটা প্রেজেন্টারের callback এর মাধ্যমে প্রেজেন্টারকে পাঠিয়ে দেয়া হচ্ছে। Fail করলেও error message টা পাঠানো হচ্ছে।

Model layer টা আলাদা করার একটা সুবিধার কথা বলা যাক। ধরুন আমাদের অ্যাপটা প্লে স্টোরে পাবলিশড হয়েছে। আমরা দেখতে পেলাম আমাদের ফিক্সড শহরগুলো ছাড়াও অন্যান্য শহরের আবহাওয়ার আপডেট ইউজাররা জানতে চান। তো আমরা ডিসিশন নিলাম আমাদের শহরের লিস্টটা লোকাল থেকে আর নেয়া হবে না। সার্ভারে API call করে শহরের লিস্ট নিয়ে এসে Spinner এ শো করাতে হবে। তখন কিন্তু আমাদের view বা presentation layer এর কোডে হাত দিতে হবে না। জাস্ট উপরের এই ক্লাসের প্রথম মেথডের ভিতর Retrofit দিয়ে একটা API কল করে দিব। তাহলেই আমাদের কাজটা হয়ে যাবে।

আবার আমরা যদি চাই ইউজারের ফোনে নেট না থাকলে তাকে পুরাতন তথ্য শো করব, তাহলে কিভাবে করা যায়? আমরা তখন অ্যাপে ডেটাবেজ ইউজ করতে পারি। প্রতিটা API কল success হলে আমরা ডেটাবেজে সেই ডেটাগুলো সেভ করে রাখতে পারি। যখন মডেল দেখবে ফোন নেটওয়ার্কের সাথে কানেক্টেড না, তখন ডেটাবেজে ঐ শহরের পুরাতন কোনো ডেটা থেকে থাকলে ডেটাবেজ থেকে ডেটা নিয়ে প্রেজেন্টারকে পাঠিয়ে দিবে। আমরা যদি চাই ইউজারকে ১ ঘন্টার চেয়ে পুরাতন কোনো ডেটা দেখাব না, তখন প্রেজেন্টারে একটা চেক বসিয়ে দেখব যে ডেটাটা ১ ঘন্টার চেয়ে বেশি পুরাতন কিনা। ১ ঘন্টার চেয়ে বেশি পুরাতন হলে প্রেজেন্টার ভিউকে বলে দিতে পারে যেন ভিউ নেট অন করার কোনো মেসেজ দেখায়।

কোডের স্ট্রাকচার দেখে একথা স্পষ্ট হয়ে গেছেন নিশ্চয়ই যে, প্রতিটা ফিচার বা UI এর জন্য এরকম আলাদা আলদা প্যাকেজ বানিয়ে তার ভিতর আলাদা মডেল, ভিউ, প্রেজেন্টার বানাতে পারেন। আবার চাইলে একটা মডেল দিয়ে কয়েকটা রিপোজিটরিকে ডেটা সার্ভ করতে পারেন। যখন যেটা দরকার হবে সেটা তখন সেভাবে করতে হবে। যখন অনেকগুলো UI বা অনেকগুলো ফিচার থাকে তখন সাধারণত আমরা BaseView, BasePresenter, BaseModel বানিয়ে রাখি। কমন কাজগুলো এইসব ইন্টারফেসে রেখে ফিচারগুলোর ইন্টারফেসে এইসব base interface গুলো extend করি। যেমন প্রোগ্রেস বার দেখানোর কাজ, ভিউ destroy হলে সেটা প্রেজেন্টারকে জানানো এগুলো কমন। প্রায় সব ভিউতেই এগুলো থাকবে। তাই BaseView এর ভিতর প্রোগ্রেসবার আর ভিউ ডিটাচের মেথড সিগনেচারটা রেখে দিতে পারি। প্র্যাক্টিস করতে থাকলে আস্তে আস্তে নিজেই বুঝে যাবেন কোন মেথডগুলোকে Base Interface এ উঠিয়ে নিয়ে আসা যায়।

Some other classes and codes

MVP Architecture সংক্রান্ত কথাবার্তা এই প্রোজেক্টের জন্য এখানেই শেষ। এবার প্রোজেক্টের অন্যান্য দুই-একটা ক্লাস নিয়ে কথা বলা যাক।

object RetrofitClient {

    private var retrofit: Retrofit? = null
    private val gson = GsonBuilder().setLenient().create()

    val client: Retrofit
        get() {
            if (retrofit == null) {
                synchronized(Retrofit::class.java) {
                    if (retrofit == null) {

                        val httpClient = OkHttpClient.Builder()
                                .addInterceptor(QueryParameterAddInterceptor())

                        // for pretty log of HTTP request-response
                        httpClient.addInterceptor(
                                LoggingInterceptor.Builder()
                                        .loggable(BuildConfig.DEBUG)
                                        .setLevel(Level.BASIC)
                                        .log(Platform.INFO)
                                        .request("LOG")
                                        .response("LOG")
                                        .executor(Executors.newSingleThreadExecutor())
                                        .build())

                        val client = httpClient.build()

                        retrofit = Retrofit.Builder()
                                .baseUrl(BuildConfig.BASE_URL)
                                .addConverterFactory(GsonConverterFactory.create(gson))
                                .client(client)
                                .build()
                    }
                }

            }
            return retrofit!!
        }
}

এখানে Retrofit এর একটা Singleton client বানানো হয়েছে। প্রতিটা request এর সাথে query parameter হিসাবে App ID পাঠানোর জন্য একটা interceptor ব্যবহার করা হয়েছে। আরেকটি interceptor ইউজ করা হয়েছে প্রতিটা request এর যাবতীয় তথ্য log cat এ দেখানোর জন্য। Retrofit client এর BASE URL সেট করা হয়েছে BuildConfig.BASE_URL দিয়ে।

প্রতিটা রিকোয়েস্টের সাথে APP ID কে query parameter হিসাবে পাঠানোর জন্য নিচের interceptor class টা ব্যবহার করা হয়েছেঃ

class QueryParameterAddInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {

        val url = chain.request().url().newBuilder()
                .addQueryParameter("appid", BuildConfig.APP_ID)
                .build()

        val request = chain.request().newBuilder()
                // .addHeader("Authorization", "Bearer token")
                .url(url)
                .build()

        return chain.proceed(request)
    }
}

এখানেও দেখা যাচ্ছে APP ID হিসাবে সেট করা হচ্ছেঃ BuildConfig.APP_ID. অর্থাৎ BuildConfig থেকে ডেটা নেয়া হচ্ছে। এটা আসলে কী? কোথায় থাকে এটা? আমরা কোথা থেকে এই ভ্যালুটা সেট করলাম?

উত্তর হচ্ছে build.gradle থেকে আমরা এটা সেট করেছি। আমার প্রোজেক্টের গ্র্যাডল ফাইলের defaultConfig টা দেখবেন এরকমঃ

defaultConfig {
        applicationId "com.hellohasan.weatherforecast"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

        buildConfigField "String", "BASE_URL", "\"" + getBaseUrl() + "\""
        buildConfigField "String", "APP_ID", "\"" + getAppId() + "\""
}

শেষ দুইটা লাইনের মাধ্যমে BuildConfig ফাইলে BASE_URL ও APP_ID যোগ করা হয়েছে। বুঝতেই পারছেন দুইটার জন্য আলাদা দুইটা মেথড কল করা হয়েছে getBaseUrl() ও getAppId() নামের। গ্র্যাডল ফাইলের একদম শেষে এই দুইটা মেথড লিখা আছে।
def getBaseUrl() {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())

    String baseUrl = properties.getProperty("base_url")
    if(baseUrl==null)
        throw new GradleException("Add 'base_url' field at local.properties file. For more details: https://github.com/hasancse91/weather-app-android-mvp-architecture/blob/master/README.md")

    return baseUrl
}

def getAppId() {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())

    String appId = properties.getProperty("app_id")
    if(appId==null)
        throw new GradleException("Add 'app_id' field at local.properties file. For more details: https://github.com/hasancse91/weather-app-android-mvp-architecture/blob/master/README.md")

    return appId
}

অর্থাৎ আমাদের গ্র্যাডল ফাইল local.properties থেকে base_url ও app_id নিচ্ছে। আমি আমার প্রোজেক্টের local.properties ফাইলে এই দুইটা ভ্যালু সেট করে দিয়েছি। কিন্তু আপনি গিটহাব থেকে যেই প্রোজেক্ট clone করেছেন সেটা ওপেন করে দেখবেন সেখানে এই ভ্যালুগুলো সেট করা নাই। কারণ local.properties ফাইলকে আমি .gitignore ফাইলে add করে রেখেছি। তাই আমার কম্পিউটারের local.properties ফাইলটি গিটহাবে আপলোড হয় নাই। আপনি প্রোজেক্টটা অ্যান্ড্রয়েড স্টুডিও দিয়ে ওপেন করে রান করার বা gradle sync করার চেষ্টা করলে দেখবেন একটা error দেখাচ্ছে। Error message এ দেখবেন উপরের দুইটা মেথডের ভিতর থেকে যেই exception throw করা হয়েছে সেই মেসেজটাই শো করছে। আপনি local.properties ফাইলে ভ্যালু দুইটা না দেয়ার আগ পর্যন্ত প্রোজেক্ট রানই করতে পারবেন না। আপাতত প্রোজেক্টটা রান করার জন্য নিচের কোডটুকু local.properties এর একদম শেষে add করুন। এই project setup instruction-গুলো গিটহাবের README পেজে আপনি পাবেন।
#this is sample Base URL
base_url=https://samples.openweathermap.org/data/2.5/

#this is sample App ID of Open Weather API
app_id=b6907d289e10d714a6e88b30761fae22

এখন গ্র্যাডল বিল্ড হবার সময় local.properties ফাইলে base_url ও app_id ভ্যালু দুটি পেয়ে যাবে। তাই এবার gradle sync হতে ও project run হতে কোনো সমস্যা হবার কথা না। অ্যাপ ইন্সটল হবার পরে আপনি যে কোনো শহর সিলেক্ট করে বাটন ক্লিক করুন না কেন সব সময় একই শহরের আবহাওয়ার তথ্য দেখানো হবে। কারণ উপরের এই base url ও app id সত্যিকারের ডেটার জন্য নয়। এগুলো open weather API এর স্যাম্পল বা টেস্ট করার জন্য ইউজ হয়। অর্থাৎ ওদের API কাজ করে কিনা বা API গুলো রানিং আছে কিনা বা request-response contract ঠিক আছে কিনা এগুলো টেস্ট করার জন্য উপরের URL আর App ID ইউজ করা যায়। কিন্তু আপনি যদি real data চান তাহলে আপনাকে কষ্ট করে ওদের সাইটে সাইন আপ করে real App ID সংগ্রহ করতে হবে। ফ্রি ভার্সনে আপনি প্রতি মিনিটে একটা App ID এর against এ সর্বোচ্চ ৬০ টি API call করতে পারবেন। এরচেয়ে বেশি দরকার হলে আপনাকে পেইড সার্ভিসে যেতে হবে। আপনার রিয়েল App ID সংগ্রহের পর local.properties এর শেষে যোগ করুন নিচের code block. Sample এর জন্য যেই ডেটা এড করেছিলেন সেটা এখন আর রাখা যাবে না।
#this is real Base URL
base_url=http://api.openweathermap.org/data/2.5/

#this is real App ID of Open Weather API
app_id=YOUR_OWN_APP_ID

রিয়েল Base Url ও আপনার নিজের app id দিয়ে যখন প্রোজেক্টটা বিল্ড দিয়ে ইন্সটল করবেন, ইনশাআল্লাহ আপনি যেই শহর সিলেক্ট করবেন সেই শহরের ডেটাই UI তে দেখতে পাবেন। মাঝে মধ্যে ওদের App ID টা activate হতে কয়েক ঘন্টা সময় লাগতে পারে।

এখন প্রশ্ন হচ্ছে, এত্ত গ্যাঞ্জাম কেন করলাম? এই URL, App ID তো নির্দিষ্ট ক্লাসে স্ট্রিংয়ের ভিতর দিয়ে দিতে পারতাম। তাতে সমস্যা কী ছিল? সমস্যা আছে!!! ধরেন আমি আমার App ID টা আপনাকে দিলাম। বা আমার App ID ক্লাসের মধ্যে স্ট্রিং আকারে রেখে দিলাম। তাহলে কিন্তু যে কেউ ইচ্ছা করলে আমার অ্যাপটাকে decompile করে ঐ App ID নিয়ে, নিজের অ্যাপে ইউজ করতে পারবে। ধরেন আমি এই API এর পেইড ভার্সন ইউজ করি। তখন দেখা যাবে, আমি টাকা দিচ্ছি আমার ইউজাররা যেন এই সার্ভিস ইউজ করতে পারে, কিন্তু হ্যাকার সাহেব আমার APP ID ব্যবহার করে নতুন অ্যাপ ছাড়লেন প্লে স্টোরে। তখন তার অ্যাপের ইউজাররা এই সার্ভিস ইউজ করা বাবদ যত টাকা বিল হবে সেগুলাও কিন্তু আমাকেই দিতে হবে।

সিমপ্লি আমি যদি গিটহাবে আমার App ID বা API Secret টা দিয়ে দিতাম তাহলে যতজন এটা ডাউনলোড করবে আর রান করতে এটা কিন্তু কাউন্ট হতে থাকবে। একই মিনিটে যদি ১০০ জন এই অ্যাপটা ফোনে ইন্সটল করে ওপেন করত তাহলে কিন্তু সর্বোচ্চ ৬০ জন ডেটা পেত, বাকিরা ডেটা পেত না। আমি যদি তখন ডেভেলপমেন্টের জন্য অ্যাপ টেস্ট করার ট্রাই করতাম, দেখা যেত আমি নিজেও ডেটা পাচ্ছি না। তাই আমার অ্যাপের আইডিটা আমি হাইড করেছি। প্লেইন টেক্সট হিসাবে অ্যাপে কখনোই API KEY রাখা উচিত নয়, সিকিউরিটির জন্য। অ্যাপের রিভার্স ইঞ্জিনিয়ারিং রোধ করতে আপনি Proguard ইউজ করতে পারেন। কিন্তু এটা ইউজ করলেও প্লেইন স্ট্রিং থেকে API Secret বের করা খুব বেশি কঠিন কাজ না। তাই BuildConfig এর মধ্যে যখন APP ID রাখি, এটা retrieve করা হ্যাকারের জন্য আরো বেশি কঠিন হয়ে যায়। বলে রাখা ভাল এটাও bullet proof কোনো সিসটেম না। BuildConfig থেকেও data retrieve করা সম্ভব। কিন্তু প্লেইন টেক্সটের চেয়ে একটু বেশি কঠিন।

এই প্রোজেক্টটি একসাথে পাওয়া যাবে আমার গিটহাব রিপোজিটরি থেকে। সেখান থেকে নামিয়ে কোডগুলো ওপেন করে দেখলে ইনশাআল্লাহ বুঝতে সহজ হবে। কারণ যেখানে যেই মেথডে যা দরকার, সে অনুযায়ী কমেন্ট করা রয়েছে। আমি MVP যতটুকু যেভাবে বুঝি সেটা বুঝানোর চেষ্টা করেছি। কোথাও কোনো ভুল বা অস্পষ্টতা থাকলে কমেন্ট করতে দ্বিধা করবেন না। আমি সাধ্যমত চেষ্টা করব improve করার জন্য। আপনি যদি পোস্টটার মাধ্যমে MVP এর ক্লিয়ার ধারণা পেয়ে থাকেন, তাহলে কষ্ট করে কমেন্ট করে জানাবেন। এতে অন্তত বুঝতে পারব আজকের ছুটির দিনের ৮-৯ ঘন্টা ব্যয় করাটা স্বার্থক হয়েছে।

MVP Architecture পুরোপুরি আত্মস্থ করার পর MVVM architecture শেখা শুরু করতে পারেন। এজন্য পড়তে পারেন MVVM Architectural Pattern এর উপর আমার ব্লগপোস্ট এখান থেকে। 

আপনার নামাজের দুয়ায় আমাকে রাখবেন। আল্লাহ যেন আমার দুনিয়া ও আখিরাতে কল্যাণ দান করেন। আল্লাহ যেন আমার দুনিয়া ও আখিরাতের জীবনে সুখ, শান্তি ও নিরাপত্তা দান করেন। আমীন।

 

9 thoughts on “MVP Architectural Pattern in Android – (Weather App: Kotlin + Retrofit)

  1. Jazakallah khairan brother,
    I have benefited from your post. May Allah give you rewards here and hereafter 🙂

  2. আলহামদুলিল্লাহ আল্লাহ আপনাকে নেক হায়াত দান করুক, পার্থিব ও পরকালের জীবন সহজ ও সুন্দর করে দিক।

    আপনার এই ৮ ৯ ঘণ্টা পরিশ্রম সার্থক হয়েছে। আমি আশা করবো আপনি এন্ড্রয়েড এর বাকী যেই বিষয় গুলা আছে একটু এডভ্যান্স এবং সচারচর বাংলা মান সম্মত রিসোর্স পাওয়া যায় না সেই বিষয় গুলো নিয়ে এভাবেই বুজানোর পাশাপাশি প্রজেক্ট আকারে উপস্থাপন করবেন।

    আল্লাহ আপনাকে সেই ধৈর্য ও ইচ্ছাশক্তি দান করুক।
    সর্বপরি আপনার সুস্থতা কামনা করছি । আমিন

  3. আপনার এক্সপ্লেনেইশন গুলো বেশ ভালো ছিলো তবে কোড গুলা kotlin এ হওয়ার কারণে পুরোপুরি বুঝি নাই । কোড গুলো Java তে হলে পারফেক্টলি বুঝা যেতো ।

    আমি অনুরোধ করবো এই পোস্ট গুলোর কোড যাতে java তেও করা হয় । ধন্যবাদ

    1. ভাই, পুরো বিশ্ব ও জব ইন্ডাস্ট্রি কটলিনে যাচ্ছে। সব টিউটোরিয়াল আমি আস্তে আস্তে কটলিনে নিয়ে যাব। আলাদা করে জাভায় প্রোজেক্ট করার সময় সুযোগ নাই। জব ইন্ডাস্ট্রিতে ভাল অবস্থানে যাওয়ার জন্য কটলিন শিখে ফেলুন। ৬ মাস ১ বছর পর world wide কোনো নতুন টিউটোরিয়াল জাভাতে পাওয়া যাবে কিনা সন্দেহ আছে।

Leave a Reply

Your email address will not be published. Required fields are marked *