পোস্টটি পড়া হয়েছে 7,065 বার
Android MVVM architecture Bengali tutorial

MVVM Architectural Pattern in Android – (Weather App: Kotlin + ViewModel + LiveData + Retrofit)

Post updated on 24th September, 2023 at 07:21 am

Android App development এর জন্য ব্যবহৃত Architectural pattern-গুলোর মধ্যে বর্তমানে MVVM সবচেয়ে জনপ্রিয়। গুগল থেকে এই প্যাটার্নে প্রোজেক্ট ডেভেলপ করার জন্য রিকমেন্ড করা হচ্ছে। আপনি ইন্টারনেটে সার্চ করলে MVVM এর যেসব ভাল ভাল টিউটোরিয়াল খুঁজে পাবেন তাদের বেশির ভাগেই ইউজ করা হয়েছে Dagger – Dependency Injection এবং Reactive Programming এর জন্য Rx. তাই আপনার মনেই হতে পারে যে, আপনার প্রোজেক্টটি MVVM এর প্রিন্সিপাল অনুসরণ করতে চাইলে Dagger ও Rx লাগবেই! কিন্তু ব্যাপারটা আসলে সেরকম না। আপনি dagger ও Rx ছাড়াও MVVM ফলো করতে পারবেন। MVVM শেখার বেশির ভাগ টিউটোরিয়ালে Dagger ও Rx এর ইমপ্লিমেন্টেশন থাকায়, এটা শেখার শুরুর দিকে আমার জন্য সময়টা ছিল রীতিমত বিভীষিকাময়! কারণ আমার Dagger আর Rx জানা ছিল না। আমি MVVM শিখতে গিয়ে খেই হারিয়ে ফেলছি Dagger আর Rx এর জন্য। একসাথে তিনটা নতুন জিনিস শেখাটা আমার জন্য কঠিনই ছিল। MVVM architecture এ কিছুদিন কাজের অভিজ্ঞতার পর আস্তে আস্তে সব পরিষ্কার হতে শুরু করল। আর তাই আমি সিদ্ধান্ত নিলাম শুধু MVVM শেখার জন্য একটা ব্লগ লিখার। পাঠককে যেন MVVM শিখতে এসে Dagger, Rx এর সাগরে হাবুডুবু খেতে না হয়। এ ব্লগ পোস্টে MVVM architecture অনুসরণ করে কটলিন ল্যাঙ্গুয়েজে ViewModel, LiveData ব্যবহার করে একটা Weather Forecast App ডেভেলপ করব।

Prerequisites

আপনি যদি অ্যান্ড্রয়েড ডেভেলপমেন্টে একদম নতুন হয়ে থাকেন সেক্ষেত্রে বলব এই পোস্টটি হয়ত এই মুহূর্তে আপনার উপযোগি নয়। অ্যান্ড্রয়েড শেখার ব্যাসিক গাইডলাইন দেখে নিতে পারেন এখান থেকে। এই পোস্টটি পড়ে উপকৃত হবার জন্য আপনাকে অ্যান্ড্রয়েডের ব্যাসিক ডেভেলপমেন্ট জানা থাকতে হবে। প্রোজেক্টের জন্য আমরা Kotlin language ব্যবহার করব। তাই কটলিন জানা থাকলে কোড বুঝতে সুবিধা হবে। প্রোজেক্টে আবহাওয়ার তথ্য দেখাতে হবে। সেজন্য আমরা web API তে কল দিয়ে ডেটা নিয়ে আসব। নেটওয়ার্ক কলের জন্য আমরা ব্যবহার করেছি Retrofit library. এখানে রেট্রোফিট কিভাবে কাজ করে তা ব্যাখ্যা করা হবে না। আপনি আগে এটা ব্যবহার করে নেটওয়ার্ক কল না করে থাকলে Retrofit এর টিউটোরিয়াল পাবেন এখানে। MVVM এর জন্য আলাদা দুই একটা ক্লাস ব্যবহার করতে হবে। সেগুলো পোস্টেই বিস্তারিত বলা হবে। কিন্তু আর্কিটেকচারগত কিছু নলেজ আগের থেকে থাকলে সুবিধা হবে। আপনার যদি MVP Architecture সম্পর্কে ধারণা থেকে থাকে তাহলে আপনার জন্য MVVM বুঝা খুব সহজ হবে। আমি highly recommend করি MVP বুঝার পর MVVM বুঝার জন্য। কারণ MVP ইমপ্লিমেন্ট করার জন্য আমাদের নতুন কোনো কিছু শিখতে হয় না। জাস্ট কিছু interface ব্যবহার করেই আমরা MVP ইমপ্লিমেন্ট করতে পারি। তাই আপনি পোস্টটি শুরু করার আগে MVP Architecture এর পোস্টটি পড়ে আসতে পারেন। তাহলে MVVM বুঝতে আপনার ১৫-২০ মিনিটের বেশি লাগবে না। আর এই পোস্টের অনেক জায়গায়ই MVP এর সাথে MVVM এর তুলনা করা হয়েছে। তাই দুটির মধ্যে সাদৃশ্য ও বৈসাদৃশ্য বুঝার জন্য MVP জানা থাকা দরকার।

MVVM tutorial in Bengali
MVVM Architecture. [Image Source: fossasia.org]

MVVM – Model View ViewModel Architecture কী?

MVVM architecture-কে বলা যায় MVC ও MVP architecture এর একটা variation. এই আর্কিটেকচারটি প্রোপোজ করেছেন জন গসম্যান। MVP এর মত MVVM আর্কিটেকচারেও আমরা আমাদের ক্লাসগুলোকে তিনটা ভাগে ভাগ করি। Model, View ও ViewModel. MVP এর মত এই আর্কিটেকচারের উদ্দেশ্যেও view layer থেকে business logic ও data layer কে আলাদা করা। উদ্দেশ্য এক হলেও এই আলাদা করার কাজের মধ্যে কিছুটা ভিন্নতা রয়েছে। View layer এ আমরা Activity বা Fragment কে রাখি। Model layer এ আমরা ডেটা নিয়ে কাজ করি। নেটওয়ার্ক থেকে বা লোকাল কোনো সোর্স থেকে ডেটা আনা-নেয়া করার কাজ মডেলের। Model ও View এর মধ্যে সংযোগ ঘটায় ViewModel. MVP এর সাথে তুলনা করলে বলা যায় Model ও View এর ধারণা ও কাজ MVVM ও MVP তে প্রায় একই। শুধু MVP এর Presenter কে রিপ্লেস করা হয়েছে ViewModel দ্বারা।

Megaminds Learning প্ল্যাটফর্মে পাবলিশড হয়েছে MVVM আর্কিটেকচারের উপর আমার একটি ভিডিও কোর্স! আজই এনরোল করুন!

আপাতত MVVM-কে আমরা এভাবে সংজ্ঞায়িত করতে পারিঃ

MVVM এমন একটি আর্কিটেকচার, বা ক্লাসগুলোকে সাজানোর এমন একটা নীতি যেখানে পুরো অ্যাপের কাজগুলোকে তিনটা ভাগে ভাগ করা হয়। যথাঃ Model, View ও ViewModel. যেখানে Model এর কাজ ডেটা সোর্সের সাথে যোগাযোগ রক্ষা করে ডেটা নিয়ে আসা। ViewModel এর কাজ model এর সাথে ডেটা আদান-প্রদান করে ডেটার উপর প্রয়োজনীয় manipulation চালানো এবং ডেটা UI তে রেন্ডার করার জন্য view এর কাছে পৌঁছে দেয়া। View এর কাজ হচ্ছে ViewModel থেকে ডেটা ready হয়ে আসার পর UI তে রেন্ডার করা ও ইউজারের থেকে ইনপুট নিয়ে ডেটা সোর্সে ডেটা পাঠানোর প্রয়োজন হলে ViewModel-কে সেই ইনপুটের ডেটা পাঠানো। MVP এর presenter এর সাথে ViewModel এর অন্যতম মূল পার্থক্য হচ্ছে প্রেজেন্টারের মত ভিউমডেলের মধ্যে view এর কোনো reference থাকে না। অপর গুরুত্বপূর্ণ পার্থক্য হচ্ছে ViewModel ক্লাসটি তার owner এর (যেমনঃ Activity, Fragment, Dialog, Bottom Sheet ইত্যাদির) Lifecycle aware.

MVVM এর তিনটা গ্রুপকে নিচে ব্যাখ্যা করা হল।

ViewModel

MVP এর Presenter কে ViewModel ক্লাস দিয়ে রিপ্লেস করা হয়েছে। প্রেজেন্টার যে কাজ করত ভিউমডেলও একই কাজ করে। কিন্তু প্রেজেন্টারের সাথে এর দুটি মূল পার্থক্য বিদ্যমান। যা উপরে উল্লেখ করা সংজ্ঞায় বলা হয়েছে। এখানে আরেকটু বিস্তারিত বলি।

ViewModel এ View এর রেফারেন্স থাকে না, তাহলে এটা কিভাবে View তে ডেটা পাঠায়?

MVP এর প্রেজেন্টারে দেখেছিলাম যে, presenter এর মধ্যে view এর (অর্থাৎ activity এর) একটা রেফারেন্স পাঠানো হয়। প্রেজেন্টারের কাছে Model থেকে ডেটা আসার পরে, ভিউয়ের সেই রেফারেন্সের (interface এর) মেথড কল দিয়ে ভিউকে ডেটা পাঠায়। Activity ঐ একই interface implement করে তার মেথডগুলো অভাররাইড করে রাখে। প্রেজেন্টার থেকে ভিউয়ের রেফারেন্সের মেথড কল দিয়ে ডেটা পাঠালে, Activity এর সেই অভাররাইড করা মেথড ট্রিগার হয়। ঐ মেথডের মধ্যে UI তে ডেটা সেট করার কোড লেখা থাকে। ভিউমডেল এই কাজটা অন্যভাবে করে। সংজ্ঞা থেকে আমরা জেনেছি ViewModel ক্লাসে View বা Activity এর কোনো রেফারেন্স থাকে না। তাই Activity কে আলাদা কোনো ইন্টারফেসও ইমপ্লিমেন্ট করতে হয় না।

ViewModel ডেটা নিয়ে আসে model এর কাছ থেকে। মডেল বলতে বুঝানো হচ্ছে data provider. মডেলের থেকে ডেটা আনার প্রকৃয়াটা MVP এর মতই। Interface এর মাধ্যেমে model থেকে viewModel এর কাছে ডেটা আসে। Model এর পার্টটুকু যেহেতু MVP এর মত হুবহু এক, তাই আর এখানে সেটা নিয়ে বিস্তারিত বললাম না। 

ভিউমডেল থেকে দুই ভাবে View তে ডেটা পাঠানো যায়। একটা উপায় হচ্ছে Data Binding ব্যবহার করে। অন্য উপায় হচ্ছে LiveData ব্যবহার করে। আজকের এই পোস্টে আমরা LiveData নিয়ে কাজ করব। কারণ ডেটা বাইন্ডিংয়ের চেয়ে এটার ইমপ্লিমেন্টেশন সহজ। অন্য কোনো লেখায় Data Binding নিয়ে লিখব ইনশাআল্লাহ।

LiveData কী ও কিভাবে কাজ করে?

LiveData হচ্ছে একটা observable type এর data holder. Observable টা তাহলে আবার কী জিনিস? Observable হচ্ছে এমন কিছু, যাকে observe করা যায় বা যেই জিনিসটা observe করার বা পর্যবেক্ষণ করার যোগ্য। এটা দ্বারা আসলে কী বুঝায়? একটা জিনিস কল্পনা করুন। ধরেন আপনার Activity তে আপনি দেখাচ্ছেন আপনার অ্যাপে কয়টা নোটিফিকেশন এখনো unread আছে তার কাউন্ট। মেসেঞ্জার যেরকম দেখায় আর কি। অ্যাপ ওপেন থাকা অবস্থায় নতুন একটা মেসেজ আসলো। আপনি কিভাবে আপনার কাউন্টারটা ঐ মুহূর্তে আপডেট করবেন? আমরা একটা কাজ করতে পারি যে, নোটিফিকেশন আসলে EventBus দিয়ে নোটিফিকেশন আসার খবরটা Activity তে পাঠিয়ে দিতে পারি। Activity তখন ইভেন্ট রিসিভ করে কাউন্টার ভেরিয়েবলের মান ১ বাড়িয়ে TextView তে কাউন্টারের আপডেটেড ভ্যালুটা সেট করবে। EventBus নিয়ে এখানে আর বেশি কথা বাড়ালাম না। আগ্রহী পাঠক এখান থেকে EventBus এর বিস্তারিত জেনে নিতে পারেন।

এবার একটু অন্য ভাবে চিন্তা করি। আমাদের নোটিফিকেশনের কাউন্টার ভেরিয়েবলের মধ্যে যদি button click listener এর মত একটা listener type কিছু বসিয়ে রাখতে পারতাম! যেন যখনই এই কাউন্টার ভেরিয়েবলের মান চেঞ্জ হবে সাথে সাথে সেই লিসেনার ট্রিগার হয়। তাহলে ঐ লিসেনারের ভিতরে TextView তে নতুন ভ্যালু সেট করার কাজটা করে দিতাম! আগের চেয়ে ভাল হত না ব্যাপারটা?

LiveData ঠিক এই কাজটাই করে! View তে যেই ক্লাসের অবজেক্টের ডেটা শো করা দরকার ViewModel ক্লাসের মধ্যে সেই ক্লাসের একটা লাইভ ডেটা declare করে রাখতে হয়। যেমনঃ

val notificationCountLiveData = MutableLiveData<Int>()

এটা integer type এর একটা লাইভ ডেটা। এই notificationCountLiveData অবজেক্টকে Activity থেকে অবজার্ভ করা হবে। মানে এই অবজেক্টের মধ্যে বলতে পারেন লিসেনার লাগায় রাখবে Activity. যখন কাউন্টার আপডেট করার দরকার হবে, ViewModel তখন notificationCountLiveData.postValue(1) এভাবে নতুন ডেটা পোস্ট করবে। ViewModel এর কাজ শেষ। এরপর যত জায়গায় notificationCountLiveData কে অবজার্ভ করে রাখা হয়েছে তত জায়গায় লিসেনারটা ট্রিগার করে জানান দিবে যে “notificationCountLiveData এর ভ্যালু কিন্তু আপডেট হইছে!”  Activity তখন সেই আপডেটেড ভ্যালুটা UI তে সেট করবে। শুধু যে primitive type এর লাইভডেটা declare করা যাবে ব্যাপারটা এমন নয়। আপনার বানানো যে কোনো ক্লাস টাইপের লাইভ ডেটা আপনি বানিয়ে নিতে পারবেন। প্রোজেক্ট ডেভেলপ করার অংশে লাইভডেটা আবার আলোচনা করা হবে।

ViewModel ক্লাস Lifecycle aware

Lifecycle awareness বৈশিষ্ট্যটি ViewModel এর ক্ষেত্রে বেশ গুরুত্বপূর্ণ একটা বৈশিষ্ট্য। যে কোনো ভিউমডেল ক্লাস তার owner এর (অর্থাৎ Activity বা Fragment এর) lifecycle মেইনটেইন করে চলে। Activity এর লাইফসাইকেলের কথা মনে আছে? Activity Lifecycle এর বিস্তারিত জানা যাবে এখান থেকে। MVP এর ক্ষেত্রে Activity যদি rotate করে তখন কী ঘটত? Activity টা destroy হয়ে আবার onCreate() call হত এবং presenter এর instance নতুন করে create হত এবং নতুন করে প্রেজেন্টারে কল দিয়ে ডেটা নিয়ে এসে UI তে ডেটা সেট করতে হত। এতে unnecessary অনেক API কলও হত। অথবা onSaveInstanceState() মেথড কল দিয়ে serializable কিছু ডেটা আগে থেকে স্টোর করে রাখলে restore করা যেত। কিন্তু ViewModel এসব থেকে আমাদেরকে বাঁচিয়ে দিয়েছে। কোনো একটা Activity যখন ViewModel এর একটা instance create করবে, ঐ ভিউমডেলটা ততক্ষণ মেমরিতে alive থাকবে যতক্ষণ পর্যন্ত না ঐ Activity সম্পূর্ণ ভাবে finish হবে। একই ভাবে কোনো একটা Fragment যদি একটা ViewModel এর instance create করে, তবে fragment টি activity থেকে detach হবার আগ পর্যন্ত fragment এর ViewModel টি মেমরিতে থাকবে।

তাই Activity rotation এর কারণে যদি Activity recreate হয়, তাহলে নতুন করে ViewModel এর instance create হবে না। ক্রিয়েট করার মেথড কল দিলেও মেমরিতে থাকা আগের ভিউমডেলটিই রিটার্ন হবে। আর Activity ঐ একই ভিউমডেলের instance এর LiveData-কে observe করবে। LiveData তখন তার কাছে থাকা ডেটাগুলো দিয়ে Activity কে notify করবে। ফলে নতুন API কল করার দরকার হবে না। onSaveInstanceState() মেথড কল করেও ডেটা রিস্টোর করতে হবে না। আগের ডেটাই, নতুন করে তৈরি হওয়া activity বা fragment এ শো করবে।

android ViewModel Lifecycle in Bengali
Lifecycle of ViewModel

উপরের ছবিটি দেখলে ViewModel এর Scope ও Lifetime আরেকটু ক্লিয়ার হবে। Activity এর লাইফসাইকেলের পুরো সময় জুড়েই ViewModel alive থাকে। onDestroy() স্টেট থেকে যখন Activity টি shutdown হয় অর্থাৎ সম্পূর্ণ ভাবে finished হয় তখন ViewModel এর স্কোপ শেষ হয়। সেই মুহূর্তে ভিউমডেলের onCleared() মেথডটি trigger হয়। যদি ভিউমডেল destroy হওয়ার সময় আমাদের কোনো কাজ করার দরকার হয় সেটা আমরা এই মেথডের ভিতর করব। আজকের এই প্রোজেক্টের জন্য আপাতত এই মেথডটা নিয়ে আমাদের কোনো কাজ করতে হবে না। তাই এটা এড়িয়ে গেলাম।

ViewModel সংক্রান্ত থিওরিটিক্যাল কথাবার্তা এখানেই শেষ করছি। বাকি জিনিসগুলো ক্লিয়ার হবে কোড দেখার সময়।

View

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

MVP architecture এ View একটা interface implement করে তার মেথডগুলো override করত। আর নিজের instance টা পাঠিয়ে দিত presenter এর কাছে। একই ভাবে presenter এর instance-ও ভিউয়ের কাছে থাকত। MVVM এ Activity কোনো data callback পাওয়ার জন্য interface implement করবে না। এই coupling টা MVVM এ কমানো হয়েছে। ViewModel এর বর্ণনায় তার অনেকটাই উঠে এসেছে। MVVM এ আমরা ViewModel এর instance রাখি View তে বা activity তে। কিন্তু ViewModel এ ভিউয়ের কোনো রেফারেন্স থাকে না। ভিউয়ের ভিতর বসে ViewModel এ থাকা LiveData কে অবজার্ভ করে রাখা হয়। যখন লাইভডেটা ট্রিগার হয়ে ডেটা আপডেট হবার সংকেত দেয়, তখনই ভিউ মানে Activity তার UI তে ডেটা শো করে। বাকি বর্ণনা কোড দেখানোর সময় করা হবে।

Model

Model এর কাজ হচ্ছে ডেটা সোর্স থেকে ডেটা নিয়ে এসে ViewModel কে সাপ্লাই দেয়া। এবং ভিউমডেল থেকে ডেটা নিয়ে ডেটা সোর্সকে আপডেট করা। যখন ভিউতে কোনো কিছু দেখানোর দরকার হবে তখন মডেল, ভিউমডেলকে ডেটা সাপ্লাই দিবে। মডেল এই ডেটা লোকাল ডেটাবেজ থেকে নিতে পারে, shared preference থেকে নিতে পারে, assets ফোল্ডার থেকে নিতে পারে, আবার কোনো ওয়েব সার্ভার থেকেও নেটওয়ার্ক কল করে নিতে পারে। কোন ডেটা কোথা থেকে নিতে হবে সেটা আমরা মডেলের মধ্যে লিখে দিব। একই ভাবে যখন ইউজার কোনো ইনপুট দেয়, প্রেজেন্টার তখন ভিউ থেকে সেই ইনপুট রিসিভ করে মডেলকে দেয়। মডেলের দায়িত্ব থাকে লজিক অনুযায়ী সেই ডেটা ডিবি, প্রিফারেন্স, ফাইলে write করা বা নেটওয়ার্ক কল দিয়ে সার্ভারে পাঠিয়ে দেয়া। অর্থাৎ ডেটা স্টোর করা। এই স্টোর সফল হলে বা ব্যর্থ হলে সেই স্ট্যাটাসটা ViewModel-কে জানানোও মডেলের দায়িত্ব। আর এই কলব্যাকের কাজটা আমরা করে থাকি MVP এর মতই Interface ব্যবহার করে। যদি আপনি Kotlin Coroutines বা Rx ইউজ করেন সেক্ষেত্রে Model থেকে ViewModel এ ডেটা pass করতে interface দরকার হবে না। আমরা যেহেতু MVVM শিখতে চাই, তাই আপাতত Coroutines বা Rx টা আপাতত পাশ কাটিয়ে যাচ্ছি। এগুলো নিয়ে পরে আলাদা পোস্ট করব ইনশাআল্লাহ।

আমরা যখন MVVM ফলো করে production level কোনো অ্যাপে কাজ করি তখন মডেলকে সরাসরি মডেল হিসাবে  কোডে লিখি না। কয়েকটা প্যাকেজের কোডকে একত্রে মডেল বলি। যার মধ্যে একটা প্যাকেজের নাম দেই repository. আরেকটা প্যাকেজ বানাতে পারি local আর আরেকটা প্যাকেজ বানাতে পারি remote নামের। ViewModel থেকে repository এর interface এর মেথড কল করে ডেটা চাওয়া হয়। Repository ইন্টারফেসকে যেই ক্লাস implement করে সেখানে লেখা থাকে এই ডেটা কি লোকাল থেকে আসবে নাকি রিমোট কোনো ডেটা সোর্স থেকে আসবে। তখন সে অনুযায়ী local বা remote এর interface এর মেথডে call দিয়ে callback এর জন্য অপেক্ষা করা হয়। ফলে আরো extra layer এর abstraction নিয়ে আসা গেল। কাজগুলো তখন ছোট ছোট ক্লাসে ভাগ হয়ে গেল। কোনো বাগ ধরা পড়লে বা চেঞ্জ আসলে তখন সহজেই তা করে যায়। এই প্রোজেক্টে আমরা repository, local ও remote এর কাজগুলো আলাদা আলাদা না করে এক জায়গায়ই করব বুঝার সুবিধার জন্য। পরের কোনো পোস্টে এই repository নিয়ে দেখানোর চেষ্টা করব ইনশাআল্লাহ।

MVVM Architecture Bangla Android Tutorial
MVVM Architecture Flow Diagram [Image Source: jayrambhia.com]

Summary

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

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

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

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

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

Problem Description

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

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

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

Android MVVM 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 MVVM tutorial Bangla
Project Structure of MVVM Architecture Android App

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

ViewModel ও LiveData ব্যবহার করার জন্য Google এর একটা ডিপেন্ডেন্সি gradle file এ যোগ করতে হবে। সকল ডিপেন্ডেন্সি নিচে দেয়া হলোঃ

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    // ViewModel and LiveData
    implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"

    // material design
    implementation 'com.google.android.material:material:1.2.0-alpha04'

    // network call related libraries
    implementation 'com.squareup.retrofit2:retrofit:2.6.2' // REST API calling library
    implementation 'com.squareup.retrofit2:converter-gson:2.6.2' // JSON parsing library
    implementation('com.github.ihsanbal:LoggingInterceptor:3.0.0') { // HTTP pretty log printing library
        exclude group: 'org.json', module: 'json'
    }

    // glide image loading library
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
}

Model layer Source Codes – MVVM Android

MVP এর মত একই রকম MVVM এর মডেল লেয়ার। আরো সোজাসুজি বললে বলতে হবে আমি MVP project থেকে মডেলের কোডগুলো সরাসরি কপি-পেস্ট করে নিয়ে এসেছি।

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

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

ViewModel থেকে 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 বা ViewModel layer এর কোডে হাত দিতে হবে না। জাস্ট উপরের এই ক্লাসের প্রথম মেথডের ভিতর Retrofit দিয়ে একটা API কল করে দিব। তাহলেই আমাদের কাজটা হয়ে যাবে।

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

পোস্টের শুরুর দিকে model এর ব্যাখ্যা করার সময় বলেছিলাম আমরা চাইলে মডেলের মধ্যে repository, local ও remote নামের তিনটা আলাদা লেয়ার রাখতে পারি। এখানে যেমন মডেলের interface এর মেথড কল করছি। আর তার implementation ক্লাসে ডেটা fetch করা হচ্ছে। চাইলে এই কাজটায় আরো কিছু abstraction নিয়ে আসা যায়। আমরা এরকম কিছু ক্লাস ও ইন্টারফেস বানাতে পারিঃ WeatherInfoRepository, WeatherInfoRepositoryImpl, WeatherInfoLocal, WeatherInfoLocalImpl, WeatherInfoRemote, WeatherInfoRemoteImpl. ViewModel থেকে city list এর জন্য WeatherInfoRepository ইন্টারফেসের মেথডে কল দেয়া হবে ডেটার জন্য। WeatherInfoRepositoryImpl ক্লাসের implementation এ বলা থাকবে সিটি লিস্টের জন্য WeatherInfoLocal ইন্টারফেসের মেথডে কল দিতে। WeatherInfoLocalImpl এর ইমপ্লিমেন্টেশনে বলা থাকবে ডেটা আসবে assets folder থেকে। আবার ViewModel থেকে Weather Information জানার জন্য WeatherInfoRepository ইন্টারফেসের মেথডে কল দিলে WeatherInfoRepositoryImpl ক্লাসে বলা থাকবে ওয়েদার ইনফোর জন্য কল দিতে হবে WeatherInfoRemote ইন্টারফেসের মেথডে। কারণ এই ডেটা রিমোট ডেটা সোর্স থেকে আসবে। WeatherInfoRemoteImpl ক্লাসে বলা থাকবে Retrofit এর মাধ্যমে ডেটা নিয়ে আসার কোড। তাহলে ওয়েদার ইনফো সংক্রান্ত যত ডেটা লোকাল থেকে আসবে তার ইমপ্লিমেন্টেশন একটা জায়গায় থাকবে। যত ডেটা রিমোট থেকে আসবে তার ইমপ্লিমেন্টেশন একটা জায়গায় থাকবে। এভাবে আমাদের কাজগুলোকে ছোট ছোট মডিউলে ভাগ করে ফেলতে পারি। এখানে জাস্ট একটা আইডিয়া দিয়ে রাখলাম। এভাবে এই প্রোজেক্টে কোড করব না। পরের কোনো প্রোজেক্টে সুযোগ হবে এভাবে কোড করে দেখাব ইনশাআল্লাহ।

ViewModel Class – MVVM Android

Weather information দেখানোর জন্য ভিউমডেল ক্লাসটি নিচে তুলে ধরা হলোঃ

class WeatherInfoViewModel : ViewModel() {

    val cityListLiveData = MutableLiveData<MutableList<City>>()
    val cityListFailureLiveData = MutableLiveData<String>()
    val weatherInfoLiveData = MutableLiveData<WeatherData>()
    val weatherInfoFailureLiveData = MutableLiveData<String>()
    val progressBarLiveData = MutableLiveData<Boolean>()

    fun getCityList(model: WeatherInfoShowModel) {

        model.getCityList(object :
            RequestCompleteListener<MutableList<City>> {
            override fun onRequestSuccess(data: MutableList<City>) {
                cityListLiveData.postValue(data) // PUSH data to LiveData object
            }

            override fun onRequestFailed(errorMessage: String) {
                cityListFailureLiveData.postValue(errorMessage) // PUSH error message to LiveData object
            }
        })
    }

    fun getWeatherInfo(cityId: Int, model: WeatherInfoShowModel) {

        progressBarLiveData.postValue(true) // PUSH data to LiveData object to show progress bar

        model.getWeatherInfo(cityId, object :
            RequestCompleteListener<WeatherInfoResponse> {
            override fun onRequestSuccess(data: WeatherInfoResponse) {

                // business logic and data manipulation tasks should be done here
                val weatherData = WeatherData(
                    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()
                )

                progressBarLiveData.postValue(false) // PUSH data to LiveData object to hide progress bar

                // After applying business logic and data manipulation, we push data to show on UI
                weatherInfoLiveData.postValue(weatherData) // PUSH data to LiveData object
            }

            override fun onRequestFailed(errorMessage: String) {
                progressBarLiveData.postValue(false) // hide progress bar
                weatherInfoFailureLiveData.postValue(errorMessage) // PUSH error message to LiveData object
            }
        })
    }
}

Activity তে দুই ধরনের ডেটা আমাদের শো করতে হবে। সিটি লিস্ট আর আবহাওয়ার তথ্য। সিটি লিস্ট fetch করার সময় বা Weather information fetch করার সময় কোনো error হতে পারে। তাই ২টা ২টা মোট চারটা MutableLiveData এর instance create করা হয়েছে। অপর একটি MutableLiveData এর instance নেয়া হয়েছে progress bar শো করা বা হাইড করার জন্য। Activity থেকে এই পাঁচটা লাইভডেটা অবজার্ভ করে রাখা হবে। ভিউমডেল থেকে কোনোটা আপডেট হলে automagically ডেটা আপডেট সেখানে পৌঁছে যাবে।

উপরের ক্লাসের দুইটি মেথড কল করা হবে Activity থেকে। মেথডের প্যারামিটার হিসাবে পাঠানো হয়েছে model এর একটা reference. যদিও এভাবে ViewModel এর মেথডের মধ্যে model এর রেফারেন্স পাঠানো ভাল প্র্যাক্টিস না। তাও আপাতত MVVM এর মূল জিনিসটা বুঝার জন্য এভাবে কাজটা করছি। পরবর্তীতে আমরা দেখব ইনশাআল্লাহ dagger দিয়ে কিভাবে model এর রেফারেন্স ভিউমডেলের constructor এ inject করতে পারি। মেথড দুটির ভিতর model এর instance দিয়ে related দুটি মেথডে কল করে যথাক্রমে সিটি লিস্ট ও ওয়েদার ইনফো নিয়ে আসা হচ্ছে। যেই মেথডে কল করা হচ্ছে সেই মেথডের দ্বিতীয় প্যারামিটারে callback implement করা হয়েছে। এই কলব্যাক success ও error দুইটার জন্য আলাদা দুটি মেথড অভাররাইড করে। তাই success callback method এর ভিতর success LiveData আপডেট করা হয়েছে। error হলে নির্দিষ্ট error LiveData-গুলো আপডেট করে দেয়া হচ্ছে। ক্লাসটির দ্বিতীয় মেথডের ডেটা আসবে নেটওয়ার্ক কলের মাধ্যমে। তাই এখানে লোডার দেখানো লাগতে পারে। সেটাও দ্বিতীয় মেথডে হ্যান্ডেল করা হয়েছে।

লক্ষ্য করে দেখুন, পুরো ভিউমডেল ক্লাসে Android SDK এর কোনো কিছুই ব্যবহার করা হয় নাই। Activity এর context ব্যবহার করা হয় নাই। ফলে আমরা আমাদের এই ভিউমডেল ক্লাসটাকে চাইলে JVM এর মধ্যে ইউনিট টেস্টিং করতে পারব। এই ক্লাসটা টেস্ট করার জন্য Android platform specific কোনো কিছুই দরকার হবে না। আপাতত এই প্রোজেক্টে ইউনিট টেস্টিং নিয়ে কোনো আলোচনা করছি না। ভবিষ্যতে আল্লাহ তাওফিক দিলে ইউনিট টেস্টিংয়ের উপরও লিখব ইনশাআল্লাহ।

Warning: ViewModel এর ভিতর কোনো ক্রমেই View, lifecycle বা এমন কোনো ক্লাসের রেফারেন্স ব্যবহার করা যাবে না যেটা কোনো না কোনো ভাবে Activity context কে hold করে। যেমন Activity এর Context বা Activity instance ভিউমডেলে ইউজ করা যাবে না। যদি একান্তই context এর দরকার হয় সেক্ষেত্রে প্রয়োজন সাপেক্ষে application context ব্যবহার করা যেতে পারে। যেজন্য আমরা আমাদের ক্লাসটিকে ViewModel ক্লাস extend করে না বানিয়ে, AndroidViewModel ক্লাসকে extend করে বানাতে পারি। তাহলে application context কে access করতে পারব।

View layer – MVVM Android

View layer এর জন্য আমাদের এখানে শুধু ব্যবহৃত হচ্ছে একটা মাত্র Activity. Activity এর কোডটা নিচে তুলে ধরা হলোঃ

class MainActivity : AppCompatActivity() {

    private lateinit var model: WeatherInfoShowModel
    private lateinit var viewModel: WeatherInfoViewModel

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

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

        model = WeatherInfoShowModelImpl(applicationContext)
        // initialize ViewModel
        viewModel = ViewModelProviders.of(this).get(WeatherInfoViewModel::class.java)

        // set LiveData and View click listeners before the call for data fetching
        setLiveDataListeners()
        setViewClickListener()
       
        // call to fetch city list
        viewModel.getCityList(model)
    }

    private fun setViewClickListener() {
        // View Weather button click listener
        btn_view_weather.setOnClickListener {
            val selectedCityId = cityList[spinner.selectedItemPosition].id
            viewModel.getWeatherInfo(selectedCityId, model) // fetch weather info
        }
    }

    private fun setLiveDataListeners() {

        // This method will be triggered when city list fetched successfully
        viewModel.cityListLiveData.observe(this, object : Observer<MutableList<City>>{
            override fun onChanged(cities: MutableList<City>) {
                setCityListSpinner(cities)
            }
        })

        // This method will be triggered if city list failed to fetch or any error occurred
        viewModel.cityListFailureLiveData.observe(this, Observer { errorMessage ->
            Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show()
        })

        // This method will be triggered for handling ProgressBar
        viewModel.progressBarLiveData.observe(this, Observer { isShowLoader ->
            if (isShowLoader)
                progressBar.visibility = View.VISIBLE
            else
                progressBar.visibility = View.GONE
        })

        // This method will be triggered if weather info fetched successfully
        viewModel.weatherInfoLiveData.observe(this, Observer { weatherData ->
            setWeatherInfo(weatherData)
        })

       // This method will be triggered if weather info failed to fetch or any error occurred
        viewModel.weatherInfoFailureLiveData.observe(this, Observer { errorMessage ->
            output_group.visibility = View.GONE
            tv_error_message.visibility = View.VISIBLE
            tv_error_message.text = errorMessage
        })
    }

    private fun setCityListSpinner(cityList: MutableList<City>) {
        this.cityList = cityList

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

        spinner.adapter = arrayAdapter
    }

    private fun setWeatherInfo(weatherData: WeatherData) {
        output_group.visibility = View.VISIBLE
        tv_error_message.visibility = View.GONE

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

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

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

উপরের কোডে আমরা দেখতে পাচ্ছি onCreate() এর ভিতরে model ও ViewModel এর initialization করা হচ্ছে। যদিও আমরা জানি activity এর মধ্যে model এর initialization থাকাটা উচিত না। তবুও সহজবোধ্য  করার জন্য এরকম বিধিবিরুদ্ধ কাজ করেছি। পরবর্তীতে আমরা Dagger – Dependency Injection ইউজ করে এটাকে বাদ দিতে পারব। ViewModel এর ইনিশিয়ালাইজেশন কিন্তু সরাসরি constructor কল দিয়ে object creation না। নির্দিষ্ট ফরমেটে নির্দিষ্ট মেথড কল করে ViewModel বানানো হয়। ViewModelProviders.of(this) – এখানে this হচ্ছে ViewModel এর owner. অর্থাৎ ভিউমডেলটি যেই Activity এর লাইফসাইকেলকে মান্য করে করে চলবে সেই activity এর রেফারেন্স। এই activity পুরোপুরো finished না হওয়ার আগ পর্যন্ত এই ViewModel টি alive থাকবে।

setLiveDataListeners() মেথডের ভিতরে মূলতে LiveData-গুলোকে observe করার কাজ করা হয়েছে। প্রথম observe করা হচ্ছে সিটি লিস্ট fetch করার success LiveData-কে। এখানে viewModel object-টির cityListLiveData এর observe() মেথড কল করা হয়েছে। observe() মেথডের প্রথম parameter এ পাঠানো হয়েছে LifeCyclerOwner এর instance (এখানে activity instance). আর দ্বিতীয় প্যারামিটারে পাঠানো হয়েছে Observer নামক interface এর একটা inline implementation.

viewModel.cityListLiveData.observe(this, object : Observer<MutableList<City>>{
    override fun onChanged(cities: MutableList<City>?) {
          setCityListSpinner(cities)
    }
})

ViewModel থেকে যদি LiveData এর value কখনো change হয়, সাথে সাথে এই onChanged() মেথডটি triggered হবে। মেথডের প্যারামিটারে পাওয়া যাবে LiveData টি যেই ডেটা টাইপের, সেটা ডেটা টাইপের ডেটা। উপরের কোডব্লকে দেখা যাচ্ছে MutableList<City> পাওয়া যাচ্ছে। কারণ ViewModel class এ গেলে দেখতে পাবেন cityListLiveData এর type সেট করা আছে MutableList<City>. এই কোডের interface এর implementation এর পার্টটুকু lambda expression দিয়ে সংক্ষিপ্ত করে ফেলতে পারি। অন্যান্য লাইভডেটার observe() মেথডের ক্ষেত্রে কোডে lambda expression দিয়েই দেখানো হয়েছে। Android Studio দিয়ে প্রজেক্টটা ওপেন করলে সেখানেই suggestion দেখতে পাবেন কোড শর্ট করে lambda লেখার জন্য। Alt+Enter চেপে শর্ট করলে কোডের চেহারা দেখতে হবে এরকমঃ

viewModel.cityListLiveData.observe(this, Observer<MutableList<City>> { cities -> 
    setCityListSpinner(cities) 
})

বাকি লাইভডেটাগুলোর ক্ষেত্রেও লজিক একই। তাই আর আলাদা করে ওগুলো explain করলাম না।

Some other classes and codes

MVVM 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 করা সম্ভব। কিন্তু প্লেইন টেক্সটের চেয়ে একটু বেশি কঠিন।

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

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

15 thoughts on “MVVM Architectural Pattern in Android – (Weather App: Kotlin + ViewModel + LiveData + Retrofit)

  1. আসসালামু আলাইকুম ভাই, মাশা আল্লাহ অনেক অনেক সুন্দর লিখেছেন।অফিসের প্রজেক্টে MVVM ব্যবহার হচ্ছে। ঘাটাঘাটি করতে গিয়ে আপনার লিখাটা পেলাম, আলহামদুলিল্লাহ। বেশ কাজে দিবে আশা করছি ইন শা আল্লাহ।

    আল্লাহ যেন আপনার কাজে বরকত দেন, ভাই। আরো ভালো ভালো লিখা যেন পাই আপনার থেকে, আমীন 😉

  2. AlHamdulillah, vai onek valo post korasen. apner jonno duwa roilo. Apni o amar jonno duwa korben.

  3. কোন একটা কাস্টম ডাটা অবজেক্ট এর সাথে সার্ভারে ছবি পাঠানো এর প্রজেক্ট MVVM ডিজাইন প্যাটার্ন ফলো করে একটি টিউটোরিয়াল বানালে কৃতজ্ঞ থাকব। যেখানে ইমেজ এ ক্লিক করলে গ্যালারি উপেন হবে এবং আমি সিলেক্ট করা ছবির সাথে কিছু ডাটা সার্ভারে পাঠাতে পারব।
    ধন্যবা।

    1. সার্ভারে ফাইল আপলোড করা নিয়ে আলাদা পোস্ট রয়েছে। এখান থেকে দেখতে পারেন। এরপর দুইটা পোস্টের সমন্বয়ে ইনশাআল্লাহ নিজেই ইমপ্লিমেন্ট করে ফেলতে পারবেন। 🙂

  4. Thank you so much brother, I learnt lots of from this tutorial. Now
    Waiting for MVVM with dagger related tutorial.

  5. ভাই RX3 আর HILT দিয়ে ইমপ্লিমেন্টেশন নিয়ে একটি আর্টিকেল লিখে ফেলেন। আপনার লেখার স্টাইলটা অনেক সাবলীল এবং অনেক সহজেই বুঝে ফেলার মতন।ধন্যবাদ।

  6. Dagger নিয়ে এরকম একটি টিউটোরিয়াল পেলে উপকৃত হতাম স্যার।

Leave a Reply

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