আপনার Android অ্যাপে Kotlin Coroutines ব্যবহার করুন

এই কোডল্যাবে আপনি শিখবেন কিভাবে একটি Android অ্যাপে Kotlin Coroutines ব্যবহার করতে হয়—ব্যাকগ্রাউন্ড থ্রেড পরিচালনার একটি নতুন উপায় যা কলব্যাকের প্রয়োজনীয়তা কমিয়ে কোডকে সহজ করতে পারে। Coroutines হল একটি Kotlin বৈশিষ্ট্য যা ডাটাবেস বা নেটওয়ার্ক অ্যাক্সেসের মতো দীর্ঘ-চলমান কাজগুলির জন্য অ্যাসিঙ্ক কলব্যাকগুলিকে অনুক্রমিক কোডে রূপান্তর করে।

আপনি কি করবেন তার একটি ধারণা দিতে এখানে একটি কোড স্নিপেট রয়েছে৷

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

কলব্যাক-ভিত্তিক কোডটি কোরোটিন ব্যবহার করে অনুক্রমিক কোডে রূপান্তরিত হবে।

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

আপনি একটি বিদ্যমান অ্যাপ দিয়ে শুরু করবেন, যা আর্কিটেকচার কম্পোনেন্ট ব্যবহার করে তৈরি করা হয়েছে, যা দীর্ঘ-চলমান কাজের জন্য একটি কলব্যাক শৈলী ব্যবহার করে।

এই কোডল্যাবের শেষে নেটওয়ার্ক থেকে ডেটা লোড করার জন্য আপনার অ্যাপে কোরোটিন ব্যবহার করার জন্য আপনার যথেষ্ট অভিজ্ঞতা থাকবে এবং আপনি একটি অ্যাপে কোরোটিনগুলিকে একীভূত করতে সক্ষম হবেন। আপনি কোরোটিনগুলির জন্য সর্বোত্তম অনুশীলনের সাথে পরিচিত হবেন এবং কোরোটিনগুলি ব্যবহার করে এমন কোডের বিরুদ্ধে কীভাবে একটি পরীক্ষা লিখবেন।

পূর্বশর্ত

  • আর্কিটেকচার কম্পোনেন্টস ViewModel , LiveData , Repository এবং Room সাথে পরিচিতি।
  • এক্সটেনশন ফাংশন এবং ল্যাম্বডাস সহ কোটলিন সিনট্যাক্সের অভিজ্ঞতা।
  • প্রধান থ্রেড, ব্যাকগ্রাউন্ড থ্রেড এবং কলব্যাক সহ Android এ থ্রেড ব্যবহার করার একটি প্রাথমিক ধারণা।

আপনি কি করবেন

  • কোরোটিন সহ লিখিত কল কোড এবং ফলাফল প্রাপ্ত.
  • অ্যাসিঙ্ক কোড ক্রমিক করতে সাসপেন্ড ফাংশন ব্যবহার করুন।
  • কোড কীভাবে কার্যকর হয় তা নিয়ন্ত্রণ করতে launch এবং runBlocking ব্যবহার করুন।
  • সাসপেন্ডকরোটিন ব্যবহার করে বিদ্যমান এপিআইগুলিকে suspendCoroutine রূপান্তর করার কৌশলগুলি শিখুন৷
  • আর্কিটেকচার উপাদান সহ coroutines ব্যবহার করুন.
  • কোরোটিন পরীক্ষার জন্য সর্বোত্তম অনুশীলনগুলি শিখুন।

আপনি কি প্রয়োজন হবে

  • অ্যান্ড্রয়েড স্টুডিও 3. 5 (কোডল্যাব অন্যান্য সংস্করণের সাথে কাজ করতে পারে, তবে কিছু জিনিস অনুপস্থিত বা অন্যরকম দেখতে হতে পারে)।

আপনি যদি এই কোডল্যাবের মাধ্যমে কাজ করার সময় কোনো সমস্যায় পড়েন (কোড বাগ, ব্যাকরণগত ত্রুটি, অস্পষ্ট শব্দ, ইত্যাদি), তাহলে অনুগ্রহ করে কোডল্যাবের নীচের বাম কোণে একটি ভুল প্রতিবেদন করুন লিঙ্কের মাধ্যমে সমস্যাটি রিপোর্ট করুন৷

কোডটি ডাউনলোড করুন

এই কোডল্যাবের সমস্ত কোড ডাউনলোড করতে নিম্নলিখিত লিঙ্কে ক্লিক করুন:

জিপ ডাউনলোড করুন

... অথবা নিম্নলিখিত কমান্ড ব্যবহার করে কমান্ড লাইন থেকে GitHub সংগ্রহস্থল ক্লোন করুন:

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

প্রায়শই জিজ্ঞাসিত প্রশ্ন

প্রথমত, শুরুর নমুনা অ্যাপটি কেমন তা দেখা যাক। অ্যান্ড্রয়েড স্টুডিওতে নমুনা অ্যাপ খুলতে এই নির্দেশাবলী অনুসরণ করুন।

  1. আপনি যদি kotlin-coroutines zip ফাইলটি ডাউনলোড করে থাকেন, তাহলে ফাইলটি আনজিপ করুন।
  2. অ্যান্ড্রয়েড স্টুডিওতে coroutines-codelab প্রকল্পটি খুলুন।
  3. start অ্যাপ্লিকেশন মডিউল নির্বাচন করুন.
  4. ক্লিক করুন execute.png বোতাম চালান , এবং হয় একটি এমুলেটর চয়ন করুন বা আপনার অ্যান্ড্রয়েড ডিভাইসটি সংযুক্ত করুন, যা অবশ্যই Android ললিপপ চালানোর জন্য সক্ষম হতে হবে (সর্বনিম্ন SDK সমর্থিত 21)। Kotlin Coroutines পর্দা উপস্থিত হওয়া উচিত:

এই স্টার্টার অ্যাপ্লিকেশানটি থ্রেড ব্যবহার করে আপনি স্ক্রীন টিপানোর পরে অল্প বিলম্বের সংখ্যা বৃদ্ধি করতে। এটি নেটওয়ার্ক থেকে একটি নতুন শিরোনাম আনবে এবং এটি স্ক্রিনে প্রদর্শন করবে। এটি এখনই চেষ্টা করে দেখুন, এবং আপনি অল্প বিলম্বের পরে গণনা এবং বার্তা পরিবর্তন দেখতে পাবেন। এই কোডল্যাবে আপনি এই অ্যাপ্লিকেশনটিকে কোরোটিন ব্যবহার করতে রূপান্তর করবেন।

এই অ্যাপটি MainActivity এ UI MainViewModel এর অ্যাপ্লিকেশন লজিক থেকে আলাদা করতে আর্কিটেকচার উপাদান ব্যবহার করে। প্রকল্পের কাঠামোর সাথে নিজেকে পরিচিত করতে কিছুক্ষণ সময় নিন।

  1. MainActivity UI প্রদর্শন করে, ক্লিক শ্রোতাদের নিবন্ধন করে এবং একটি Snackbar প্রদর্শন করতে পারে। এটি MainViewModel এ ইভেন্ট পাস করে এবং MainViewModelLiveData এর উপর ভিত্তি করে স্ক্রীন আপডেট করে।
  2. MainViewModel এ ইভেন্ট পরিচালনা করে এবং onMainViewClicked ব্যবহার করে MainActivity এর সাথে যোগাযোগ করবে LiveData.
  3. Executors ব্যাকগ্রাউন্ড সংজ্ঞায়িত করে BACKGROUND, যা ব্যাকগ্রাউন্ড থ্রেডে জিনিস চালাতে পারে।
  4. TitleRepository নেটওয়ার্ক থেকে ফলাফল আনে এবং ডাটাবেসে সেভ করে।

একটি প্রকল্পে coroutines যোগ করা

Kotlin-এ coroutines ব্যবহার করতে, আপনাকে অবশ্যই আপনার প্রোজেক্টের build.gradle (Module: app) ফাইলে coroutines coroutines-core লাইব্রেরি অন্তর্ভুক্ত করতে হবে। কোডল্যাব প্রকল্পগুলি ইতিমধ্যে আপনার জন্য এটি করেছে, তাই কোডল্যাবটি সম্পূর্ণ করার জন্য আপনাকে এটি করার দরকার নেই।

অ্যান্ড্রয়েডে কোরোটিনগুলি একটি মূল লাইব্রেরি এবং অ্যান্ড্রয়েড নির্দিষ্ট এক্সটেনশন হিসাবে উপলব্ধ:

  • kotlinx-corountines-core — কোটলিনে কোরোটিন ব্যবহারের জন্য প্রধান ইন্টারফেস
  • kotlinx-coroutines-android — কোরোটিনে অ্যান্ড্রয়েড প্রধান থ্রেডের জন্য সমর্থন

স্টার্টার অ্যাপটি ইতিমধ্যেই build.gradle. একটি নতুন অ্যাপ প্রোজেক্ট তৈরি করার সময়, আপনাকে build.gradle (Module: app) খুলতে হবে এবং প্রোজেক্টে coroutines নির্ভরতা যোগ করতে হবে।

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

অ্যান্ড্রয়েডে, মূল থ্রেড ব্লক করা এড়ানো অপরিহার্য। প্রধান থ্রেড হল একটি একক থ্রেড যা UI-তে সমস্ত আপডেট পরিচালনা করে। এটি সেই থ্রেড যা সমস্ত ক্লিক হ্যান্ডলার এবং অন্যান্য UI কলব্যাককে কল করে। যেমন, এটি একটি দুর্দান্ত ব্যবহারকারীর অভিজ্ঞতার গ্যারান্টি দিতে মসৃণভাবে চালাতে হবে।

আপনার অ্যাপটি কোনো দৃশ্যমান বিরতি ছাড়াই ব্যবহারকারীর কাছে প্রদর্শনের জন্য, মূল থ্রেডটিকে প্রতি 16ms বা তার বেশি স্ক্রীন আপডেট করতে হবে, যা প্রতি সেকেন্ডে প্রায় 60 ফ্রেম। অনেক সাধারণ কাজ এর চেয়ে বেশি সময় নেয়, যেমন বড় JSON ডেটাসেট পার্স করা, ডেটাবেসে ডেটা লেখা বা নেটওয়ার্ক থেকে ডেটা আনা। অতএব, মূল থ্রেড থেকে এইরকম কলিং কোড অ্যাপটিকে বিরতি, তোতলাতে বা এমনকি হিমায়িত করতে পারে। এবং যদি আপনি প্রধান থ্রেডটি খুব দীর্ঘ সময়ের জন্য ব্লক করেন, তাহলে অ্যাপটি ক্র্যাশ হতে পারে এবং একটি অ্যাপ্লিকেশন নট রেসপন্সিং ডায়ালগ উপস্থাপন করতে পারে।

প্রধান-নিরাপত্তা প্রবর্তন করে Android-এ আমাদের জন্য কোরোটিন কীভাবে এই সমস্যার সমাধান করে তার একটি ভূমিকার জন্য নীচের ভিডিওটি দেখুন।

কলব্যাক প্যাটার্ন

প্রধান থ্রেড ব্লক না করে দীর্ঘ-চলমান কাজ সম্পাদনের জন্য একটি প্যাটার্ন হল কলব্যাক। কলব্যাক ব্যবহার করে, আপনি একটি ব্যাকগ্রাউন্ড থ্রেডে দীর্ঘ-চলমান কাজগুলি শুরু করতে পারেন। কাজটি শেষ হলে, মূল থ্রেডে ফলাফল সম্পর্কে আপনাকে জানানোর জন্য কলব্যাক ডাকা হয়।

কলব্যাক প্যাটার্নের একটি উদাহরণ দেখুন।

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

কারণ এই কোডটি @UiThread এর সাথে টীকা করা হয়েছে, এটি মূল থ্রেডে চালানোর জন্য যথেষ্ট দ্রুত চালাতে হবে। এর মানে, এটি খুব দ্রুত ফিরে আসতে হবে, যাতে পরবর্তী স্ক্রিন আপডেটে দেরি না হয়। যাইহোক, যেহেতু slowFetch সম্পূর্ণ হতে সেকেন্ড বা এমনকি মিনিট সময় লাগবে, তাই মূল থ্রেড ফলাফলের জন্য অপেক্ষা করতে পারে না। show(result) কলব্যাক slowFetch একটি ব্যাকগ্রাউন্ড থ্রেডে চালানোর অনুমতি দেয় এবং এটি প্রস্তুত হলে ফলাফলটি ফেরত দেয়।

কলব্যাকগুলি সরাতে কোরোটিন ব্যবহার করা

কলব্যাকগুলি একটি দুর্দান্ত প্যাটার্ন, তবে তাদের কয়েকটি ত্রুটি রয়েছে। যে কোডটি প্রচুর পরিমাণে কলব্যাক ব্যবহার করে তা পড়া কঠিন এবং যুক্তি করা কঠিন হতে পারে। উপরন্তু, কলব্যাক কিছু ভাষা বৈশিষ্ট্য ব্যবহার করার অনুমতি দেয় না, যেমন ব্যতিক্রম।

Kotlin coroutines আপনাকে কলব্যাক-ভিত্তিক কোডকে ক্রমিক কোডে রূপান্তর করতে দেয়। ক্রমানুসারে লেখা কোড সাধারণত পড়া সহজ, এবং এমনকি ভাষা বৈশিষ্ট্য যেমন ব্যতিক্রম ব্যবহার করতে পারে।

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

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

উদাহরণস্বরূপ নীচের কোডে, makeNetworkRequest() এবং slowFetch() উভয়ই suspend ফাংশন।

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

কলব্যাক সংস্করণের মতোই, makeNetworkRequest মূল থ্রেড থেকে ফিরে আসতে হবে কারণ এটি @UiThread চিহ্নিত করা হয়েছে। এর মানে হল যে সাধারণত এটি slowFetch মতো ব্লকিং পদ্ধতিগুলিকে কল করতে পারে না। এখানে suspend কীওয়ার্ড তার জাদু কাজ করে।

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

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

আপনি পরবর্তী বিভাগে নমুনা অ্যাপে কোরোটিনগুলি পরিচয় করিয়ে দেবেন।

এই অনুশীলনে আপনি বিলম্বের পরে একটি বার্তা প্রদর্শন করার জন্য একটি করুটিন লিখবেন। start করতে, নিশ্চিত করুন যে আপনার মডিউলটি Android স্টুডিওতে খোলা আছে।

CoroutineScope বোঝা

কোটলিনে, সমস্ত করটিন একটি CoroutineScope । একটি স্কোপ তার কাজের মাধ্যমে করোটিনের জীবনকাল নিয়ন্ত্রণ করে। আপনি যখন একটি সুযোগের কাজ বাতিল করেন, তখন এটি সেই সুযোগে শুরু হওয়া সমস্ত কোরোটিন বাতিল করে। অ্যান্ড্রয়েডে, আপনি সমস্ত চলমান কোরোটিন বাতিল করতে একটি সুযোগ ব্যবহার করতে পারেন যখন, উদাহরণস্বরূপ, ব্যবহারকারী একটি Activity বা Fragment থেকে দূরে নেভিগেট করেন৷ স্কোপগুলি আপনাকে একটি ডিফল্ট প্রেরণকারী নির্দিষ্ট করার অনুমতি দেয়। একটি প্রেরক নিয়ন্ত্রণ করে কোন থ্রেড একটি কোরোটিন চালায়।

UI দ্বারা শুরু করা কোরোটিনগুলির জন্য, এটি সাধারণত Dispatchers.Main শুরু করা সঠিক। মেইন যা Android এর প্রধান থ্রেড। Dispatchers.Main একটি কোরুটিন শুরু হয়েছে৷ স্থগিত থাকাকালীন মেইন মূল থ্রেডটিকে ব্লক করবে না৷ যেহেতু একটি ViewModel প্রায় সবসময়ই প্রধান থ্রেডে UI আপডেট করে, তাই মূল থ্রেডে coroutines শুরু করা আপনাকে অতিরিক্ত থ্রেড সুইচগুলি সংরক্ষণ করে। মূল থ্রেডে শুরু হওয়া একটি করুটিন এটি শুরু হওয়ার পরে যে কোনো সময় প্রেরকদের পরিবর্তন করতে পারে। উদাহরণস্বরূপ, এটি প্রধান থ্রেডের বাইরে একটি বড় JSON ফলাফল পার্স করতে অন্য প্রেরক ব্যবহার করতে পারে।

viewModelScope ব্যবহার করে

AndroidX lifecycle-viewmodel-ktx লাইব্রেরি ViewModels-এ একটি CoroutineScope যোগ করে যা UI-সম্পর্কিত coroutines শুরু করার জন্য কনফিগার করা হয়েছে। এই লাইব্রেরিটি ব্যবহার করতে, আপনাকে অবশ্যই এটিকে আপনার প্রকল্পের build.gradle (Module: start) ফাইলে অন্তর্ভুক্ত করতে হবে। কোডল্যাব প্রকল্পগুলিতে এই পদক্ষেপটি ইতিমধ্যে সম্পন্ন হয়েছে।

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

লাইব্রেরি ViewModel ক্লাসের এক্সটেনশন ফাংশন হিসাবে একটি viewModelScope যুক্ত করে। এই সুযোগটি Dispatchers.Main এর সাথে আবদ্ধ এবং ViewModel সাফ হয়ে গেলে স্বয়ংক্রিয়ভাবে বাতিল হয়ে যাবে।

থ্রেড থেকে কোরোটিনে স্যুইচ করুন

MainViewModel.kt এ এই কোড সহ পরবর্তী TODO খুঁজুন:

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

এই কোডটি ব্যাকগ্রাউন্ড এক্সিকিউটরসার্ভিস ব্যবহার করে ( util/Executor.kt BACKGROUND ExecutorService সংজ্ঞায়িত) একটি ব্যাকগ্রাউন্ড থ্রেডে চালানোর জন্য। যেহেতু sleep বর্তমান থ্রেডকে ব্লক করে, এটি মূল থ্রেডে কল করা হলে এটি UI হিমায়িত করবে। ব্যবহারকারী মূল দৃশ্যে ক্লিক করার এক সেকেন্ড পরে, এটি একটি স্ন্যাকবার অনুরোধ করে।

আপনি কোড থেকে BACKGROUND সরিয়ে এটি আবার চালানোর মাধ্যমে এটি ঘটতে দেখতে পারেন। লোডিং স্পিনার প্রদর্শিত হবে না এবং সবকিছু এক সেকেন্ড পরে চূড়ান্ত অবস্থায় "জাম্প" হবে।

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

updateTaps এই কোরোটিন ভিত্তিক কোড দিয়ে প্রতিস্থাপন করুন যা একই জিনিস করে। আপনাকে launch আমদানি করতে হবে এবং delay করতে হবে।

MainViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

এই কোডটি একই কাজ করে, একটি স্ন্যাকবার দেখানোর আগে এক সেকেন্ড অপেক্ষা করে। যাইহোক, কিছু গুরুত্বপূর্ণ পার্থক্য আছে:

  1. viewModelScope. launch ভিউমডেলস্কোপে একটি viewModelScope শুরু করবে। এর মানে হল যে কাজটি আমরা viewModelScope-এ পাস করেছি তা বাতিল হয়ে গেলে, এই চাকরি/স্কোপের সমস্ত viewModelScope বাতিল হয়ে যাবে। যদি ব্যবহারকারী delay ফিরে আসার আগে অ্যাক্টিভিটি ছেড়ে চলে যান, তাহলে ViewModel ধ্বংস করার জন্য onCleared বলা হলে এই coroutine স্বয়ংক্রিয়ভাবে বাতিল হয়ে যাবে।
  2. যেহেতু viewModelScopeDispatchers.Main এর একটি ডিফল্ট প্রেরক রয়েছে, এই coroutineটি মূল থ্রেডে চালু করা হবে। আমরা পরে দেখব কিভাবে বিভিন্ন থ্রেড ব্যবহার করতে হয়।
  3. ফাংশন delay একটি suspend ফাংশন। এটি অ্যান্ড্রয়েড স্টুডিওতে দেখানো হয়েছে বাম নর্দমায় আইকন। যদিও এই করোটিন মূল থ্রেডে চলে, delay এক সেকেন্ডের জন্য থ্রেডটিকে ব্লক করবে না। পরিবর্তে, প্রেরণকারী পরবর্তী বিবৃতিতে এক সেকেন্ডের মধ্যে পুনরায় শুরু করার জন্য কোরোটিন নির্ধারণ করবে।

এগিয়ে যান এবং এটি চালান. আপনি যখন মূল ভিউতে ক্লিক করেন তখন আপনি এক সেকেন্ড পরে একটি স্ন্যাকবার দেখতে পাবেন।

পরবর্তী বিভাগে আমরা বিবেচনা করব কিভাবে এই ফাংশনটি পরীক্ষা করা যায়।

এই অনুশীলনে আপনি যে কোডটি লিখেছেন তার জন্য একটি পরীক্ষা লিখবেন। kotlinx-coroutines-test লাইব্রেরি ব্যবহার করে Dispatchers.Main এ চলমান coroutines কিভাবে পরীক্ষা করবেন এই অনুশীলনটি আপনাকে দেখায়। পরে এই কোডল্যাবে আপনি একটি পরীক্ষা বাস্তবায়ন করবেন যা সরাসরি কোরোটিনের সাথে ইন্টারঅ্যাক্ট করে।

বিদ্যমান কোড পর্যালোচনা করুন

androidTest ফোল্ডারে MainViewModelTest.kt খুলুন।

MainViewModelTest.kt

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

একটি নিয়ম হল JUnit-এ পরীক্ষার আগে এবং পরে কোড চালানোর একটি উপায়। একটি অফ-ডিভাইস পরীক্ষায় আমাদের MainViewModel পরীক্ষা করার অনুমতি দেওয়ার জন্য দুটি নিয়ম ব্যবহার করা হয়:

  1. InstantTaskExecutorRule হল একটি JUnit নিয়ম যা প্রতিটি টাস্ক সিঙ্ক্রোনাসভাবে চালানোর জন্য LiveData কনফিগার করে
  2. MainCoroutineScopeRule হল এই কোডবেসের একটি কাস্টম নিয়ম যা Dispatchers.Main কে kotlinx-coroutines-test test থেকে একটি TestCoroutineDispatcher ব্যবহার করতে কনফিগার করে। এটি পরীক্ষাগুলিকে পরীক্ষার জন্য একটি ভার্চুয়াল-ঘড়ি অগ্রসর করার অনুমতি দেয় এবং ইউনিট পরীক্ষায় Dispatchers.Main ব্যবহার করার অনুমতি দেয়।

setup পদ্ধতিতে, টেস্টিং ফেক ব্যবহার করে MainViewModel এর একটি নতুন দৃষ্টান্ত তৈরি করা হয়েছে - এগুলি হল নেটওয়ার্ক এবং ডাটাবেসের জাল বাস্তবায়ন যা স্টার্টার কোডে প্রদত্ত প্রকৃত নেটওয়ার্ক বা ডাটাবেস ব্যবহার না করে পরীক্ষা লিখতে সাহায্য করার জন্য।

এই পরীক্ষার জন্য, জালগুলি শুধুমাত্র MainViewModel এর নির্ভরতা পূরণ করার জন্য প্রয়োজন। পরে এই কোড ল্যাবে আপনি coroutines সমর্থন করার জন্য জাল আপডেট করবেন।

কোরোটিন নিয়ন্ত্রণ করে এমন একটি পরীক্ষা লিখুন

একটি নতুন পরীক্ষা যোগ করুন যা নিশ্চিত করে যে মূল ভিউ ক্লিক করার পর ট্যাপগুলি এক সেকেন্ড আপডেট করা হয়েছে:

MainViewModelTest.kt

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

onMainViewClicked এ কল করার মাধ্যমে, আমরা এইমাত্র যে কোরোটিন তৈরি করেছি তা চালু করা হবে। এই পরীক্ষাটি পরীক্ষা করে যে ট্যাপ টেক্সটটি onMainViewClicked কল করার পরেই "0 ট্যাপস" থেকে যায়, তারপর 1 সেকেন্ড পরে এটি "1 ট্যাপস" এ আপডেট হয়।

এই পরীক্ষাটি onMainViewClicked দ্বারা চালু করা onMainViewClicked সম্পাদন নিয়ন্ত্রণ করতে ভার্চুয়াল-টাইম ব্যবহার করে। MainCoroutineScopeRule আপনাকে Dispatchers.Main এ লঞ্চ করা কোরোটিনগুলির সম্পাদনকে বিরতি, পুনরায় শুরু করতে বা নিয়ন্ত্রণ করতে দেয়। এখানে আমরা advanceTimeBy(1_000) কল করছি যার ফলে প্রধান প্রেরক অবিলম্বে 1 সেকেন্ড পরে পুনরায় শুরু করার জন্য নির্ধারিত কোরোটিনগুলি কার্যকর করবে৷

এই পরীক্ষাটি সম্পূর্ণরূপে নির্ধারক, যার মানে এটি সর্বদা একইভাবে সম্পাদন করবে। এবং, কারণ এটি Dispatchers.Main চালু করা কোরোটিনগুলির সম্পাদনের উপর সম্পূর্ণ নিয়ন্ত্রণ রাখে৷ প্রধান এটি মান সেট করার জন্য এক সেকেন্ড অপেক্ষা করতে হবে না৷

বিদ্যমান পরীক্ষা চালান

  1. একটি প্রসঙ্গ মেনু খুলতে আপনার সম্পাদকের MainViewModelTest ক্লাসের নামটিতে ডান ক্লিক করুন।
  2. প্রসঙ্গ মেনুতে নির্বাচন করুন execute.png 'MainViewModelTest' চালান
  3. ভবিষ্যতের রানের জন্য আপনি কনফিগারেশনের পাশের কনফিগারেশনে এই পরীক্ষা কনফিগারেশনটি নির্বাচন করতে পারেন execute.png টুলবারে বোতাম। ডিফল্টরূপে, কনফিগারেশনটিকে MainViewModelTest বলা হবে।

আপনি পরীক্ষা পাস দেখতে হবে! এবং এটি চালানোর জন্য এক সেকেন্ডের চেয়ে কিছুটা কম সময় নেওয়া উচিত।

পরবর্তী অনুশীলনে আপনি শিখবেন কিভাবে একটি বিদ্যমান কলব্যাক API থেকে coroutines ব্যবহার করতে রূপান্তর করতে হয়।

এই ধাপে, আপনি coroutines ব্যবহার করার জন্য একটি সংগ্রহস্থল রূপান্তর করা শুরু করবেন। এটি করার জন্য, আমরা ViewModel , Repository , Room এবং Retrofit এ coroutines যোগ করব।

স্থাপত্যের প্রতিটি অংশ কীসের জন্য দায়ী তা আমরা কোরোটিন ব্যবহারে স্যুইচ করার আগে বুঝে নেওয়া ভালো।

  1. MainDatabase রুম ব্যবহার করে একটি ডাটাবেস প্রয়োগ করে যা একটি Title সংরক্ষণ করে এবং লোড করে।
  2. MainNetwork একটি নেটওয়ার্ক API প্রয়োগ করে যা একটি নতুন শিরোনাম নিয়ে আসে। এটি শিরোনাম আনার জন্য Retrofit ব্যবহার করে। Retrofit এলোমেলোভাবে ত্রুটি বা উপহাস ডেটা ফেরত দেওয়ার জন্য কনফিগার করা হয়েছে, তবে অন্যথায় এমন আচরণ করে যেন এটি সত্যিকারের নেটওয়ার্ক অনুরোধ করছে।
  3. TitleRepository নেটওয়ার্ক এবং ডাটাবেস থেকে ডেটা একত্রিত করে শিরোনাম আনয়ন বা রিফ্রেশ করার জন্য একটি একক API প্রয়োগ করে।
  4. MainViewModel পর্দার অবস্থা উপস্থাপন করে এবং ঘটনাগুলি পরিচালনা করে। ব্যবহারকারী স্ক্রিনে ট্যাপ করলে এটি রিপোজিটরিকে শিরোনাম রিফ্রেশ করতে বলবে।

যেহেতু নেটওয়ার্ক অনুরোধটি UI-ইভেন্ট দ্বারা চালিত হয় এবং আমরা সেগুলির উপর ভিত্তি করে একটি coroutine শুরু করতে চাই, তাই coroutines ব্যবহার শুরু করার স্বাভাবিক জায়গাটি হল ViewModel এ।

কলব্যাক সংস্করণ

refreshTitle এর ঘোষণা দেখতে MainViewModel.kt খুলুন।

MainViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

ব্যবহারকারী যখনই স্ক্রিনে ক্লিক করে তখনই এই ফাংশনটিকে বলা হয় - এবং এটি রিপোজিটরিটিকে শিরোনামটি রিফ্রেশ করতে এবং ডাটাবেসে নতুন শিরোনাম লিখতে দেয়।

এই বাস্তবায়ন কিছু জিনিস করতে একটি কলব্যাক ব্যবহার করে:

  • এটি একটি ক্যোয়ারী শুরু করার আগে, এটি _spinner.value = true সহ একটি লোডিং স্পিনার প্রদর্শন করে
  • যখন এটি একটি ফলাফল পায়, এটি _spinner.value = false দিয়ে লোডিং স্পিনারকে সাফ করে
  • যদি এটি একটি ত্রুটি পায়, এটি একটি স্ন্যাকবারকে প্রদর্শন করতে বলে এবং স্পিনারটিকে পরিষ্কার করে৷

মনে রাখবেন যে onCompleted কলব্যাক title পাস করা হয় না। যেহেতু আমরা Room ডাটাবেসে সমস্ত শিরোনাম লিখি, তাই Room দ্বারা আপডেট করা LiveData পর্যবেক্ষণ করে UI বর্তমান শিরোনাম আপডেট করে।

কোরোটিনের আপডেটে, আমরা ঠিক একই আচরণ রাখব। UI কে স্বয়ংক্রিয়ভাবে আপ টু ডেট রাখতে Room ডাটাবেসের মতো একটি পর্যবেক্ষণযোগ্য ডেটা উত্স ব্যবহার করা এটি একটি ভাল প্যাটার্ন।

coroutines সংস্করণ

চলুন refreshTitle সঙ্গে refreshTitle আবার লিখুন!

যেহেতু আমাদের এখনই এটির প্রয়োজন হবে, আসুন আমাদের সংগ্রহস্থলে একটি খালি সাসপেন্ড ফাংশন তৈরি করি ( TitleRespository.kt )। একটি নতুন ফাংশন সংজ্ঞায়িত করুন যা suspend অপারেটর ব্যবহার করে Kotlin কে জানাতে যে এটি coroutines এর সাথে কাজ করে৷

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

যখন আপনি এই কোডল্যাবটির সাথে কাজ শেষ করবেন, আপনি একটি নতুন শিরোনাম আনার জন্য রেট্রোফিট এবং রুম ব্যবহার করার জন্য এটি আপডেট করবেন এবং কোরোটিন ব্যবহার করে ডাটাবেসে লিখবেন৷ আপাতত, এটি কাজ করার ভান করে 500 মিলিসেকেন্ড খরচ করবে এবং তারপর চালিয়ে যাবে।

MainViewModel এ, refreshTitle এর কলব্যাক সংস্করণটিকে এমন একটি দিয়ে প্রতিস্থাপন করুন যা একটি নতুন কোরোটিন চালু করে:

MainViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

আসুন এই ফাংশনটির মাধ্যমে ধাপে ধাপে যাই:

viewModelScope.launch {

ট্যাপ কাউন্ট আপডেট করার জন্য কোরোটিনের মতই, viewModelScope-এ একটি নতুন viewModelScope চালু করে শুরু করুন। এটি Dispatchers.Main ব্যবহার করবে যা ঠিক আছে। যদিও refreshTitle একটি নেটওয়ার্ক অনুরোধ এবং ডাটাবেস ক্যোয়ারী করবে এটি একটি প্রধান-নিরাপদ ইন্টারফেস প্রকাশ করতে coroutines ব্যবহার করতে পারে। এর মানে মূল থ্রেড থেকে এটি কল করা নিরাপদ হবে।

কারণ আমরা viewModelScope ব্যবহার করছি, যখন ব্যবহারকারী এই স্ক্রীন থেকে দূরে সরে যায় তখন এই কোরোটিন দ্বারা শুরু করা কাজটি স্বয়ংক্রিয়ভাবে বাতিল হয়ে যাবে৷ এর মানে এটি অতিরিক্ত নেটওয়ার্ক অনুরোধ বা ডাটাবেস কোয়েরি করবে না।

কোডের পরবর্তী কয়েকটি লাইন আসলে repository refreshTitle কল করে।

try {
    _spinner.value = true
    repository.refreshTitle()
}

এই কোরোটিন কিছু করার আগে এটি লোডিং স্পিনার শুরু করে - তারপর এটি একটি নিয়মিত ফাংশনের মতো refreshTitle বলে। যাইহোক, যেহেতু refreshTitle একটি সাসপেন্ডিং ফাংশন, এটি একটি সাধারণ ফাংশনের চেয়ে ভিন্নভাবে কার্যকর করে।

আমাদের কলব্যাক পাস করতে হবে না। refreshTitle দ্বারা পুনরায় চালু না হওয়া পর্যন্ত refreshTitle স্থগিত থাকবে। যদিও এটি দেখতে একটি নিয়মিত ব্লকিং ফাংশন কলের মতো, এটি স্বয়ংক্রিয়ভাবে মূল থ্রেড ব্লক না করে পুনরায় শুরু করার আগে নেটওয়ার্ক এবং ডাটাবেস ক্যোয়ারী সম্পূর্ণ না হওয়া পর্যন্ত অপেক্ষা করবে।

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

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

এবং, যদি আপনি একটি coroutine থেকে একটি ব্যতিক্রম ছুঁড়ে দেন - সেই coroutine ডিফল্টরূপে এটির অভিভাবক বাতিল করবে। তার মানে একসাথে একাধিক সম্পর্কিত কাজ বাতিল করা সহজ।

এবং তারপর, অবশেষে একটি ব্লকে, আমরা নিশ্চিত করতে পারি যে ক্যোয়ারী চালানোর পরে স্পিনার সবসময় বন্ধ থাকে।

স্টার্ট কনফিগারেশন নির্বাচন করে আবার টিপে অ্যাপ্লিকেশনটি চালান execute.png , আপনি যে কোনো জায়গায় ট্যাপ করার সময় একটি লোডিং স্পিনার দেখতে পাবেন। শিরোনাম একই থাকবে কারণ আমরা এখনও আমাদের নেটওয়ার্ক বা ডাটাবেস হুক আপ করিনি।

পরবর্তী অনুশীলনে আপনি আসলে কাজ করার জন্য সংগ্রহস্থলটি আপডেট করবেন।

এই অনুশীলনে আপনি শিখবেন কিভাবে TitleRepository-এর একটি কার্যকরী সংস্করণ বাস্তবায়নের জন্য একটি TitleRepository রান করা থ্রেডটি পরিবর্তন করতে হয়।

রিফ্রেশটাইটেলে বিদ্যমান কলব্যাক কোড পর্যালোচনা করুন

TitleRepository.kt খুলুন এবং বিদ্যমান কলব্যাক-ভিত্তিক বাস্তবায়ন পর্যালোচনা করুন।

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

TitleRepository.kt এ পদ্ধতি refreshTitleWithCallbacks একটি কলব্যাকের সাথে প্রয়োগ করা হয় যাতে লোডিং এবং ত্রুটির অবস্থা কলারের সাথে যোগাযোগ করা যায়।

রিফ্রেশ বাস্তবায়ন করার জন্য এই ফাংশনটি বেশ কয়েকটি জিনিস করে।

  1. BACKGROUND ExecutorService সহ অন্য থ্রেডে যান
  2. ব্লকিং execute() পদ্ধতি ব্যবহার করে fetchNextTitle নেটওয়ার্ক অনুরোধ চালান। এটি বর্তমান থ্রেডে নেটওয়ার্ক অনুরোধ চালাবে, এই ক্ষেত্রে BACKGROUND এর একটি থ্রেড।
  3. ফলাফল সফল হলে, insertTitle সহ ডাটাবেসে সংরক্ষণ করুন এবং onCompleted() পদ্ধতিতে কল করুন।
  4. যদি ফলাফল সফল না হয়, বা একটি ব্যতিক্রম আছে, ব্যর্থ রিফ্রেশ সম্পর্কে কলকারীকে জানাতে onError পদ্ধতিতে কল করুন।

এই কলব্যাক ভিত্তিক বাস্তবায়ন প্রধান-নিরাপদ কারণ এটি মূল থ্রেডকে ব্লক করবে না। কিন্তু, কাজ শেষ হলে কলারকে জানাতে এটি একটি কলব্যাক ব্যবহার করতে হবে। এটি BACKGROUND থ্রেডের কলব্যাকগুলিকেও কল করে যা এটি সুইচ করেছে৷

coroutines থেকে কল ব্লকিং কল

নেটওয়ার্ক বা ডাটাবেসে coroutines প্রবর্তন না করে, আমরা coroutines ব্যবহার করে এই কোডটিকে প্রধান-নিরাপদ করতে পারি। এটি আমাদের কলব্যাক থেকে পরিত্রাণ পেতে দেবে এবং আমাদেরকে প্রাথমিকভাবে যে থ্রেডটি বলা হয়েছিল সেখানে ফলাফলটি পাস করার অনুমতি দেবে।

আপনি যেকোন সময় এই প্যাটার্নটি ব্যবহার করতে পারেন আপনাকে ব্লকিং বা CPU নিবিড় কাজ করার জন্য একটি কোরোটিনের ভিতর থেকে যেমন একটি বড় তালিকা সাজানো এবং ফিল্টার করা বা ডিস্ক থেকে পড়া।

যেকোন প্রেরণকারীর মধ্যে স্যুইচ করতে, withContext ব্যবহার করে Context এর সাথে। কনটেক্সট দিয়ে কল করা শুধুমাত্র ল্যাম্বডার জন্য অন্য withContext সুইচ করে তারপর সেই ল্যাম্বডার ফলাফলের সাথে যে প্রেরককে ডেকেছিল তার কাছে ফিরে আসে।

ডিফল্টরূপে, Kotlin coroutines তিনটি প্রেরণকারী প্রদান করে: Main , IO , এবং Default । IO প্রেরণকারীকে IO কাজের জন্য অপ্টিমাইজ করা হয় যেমন নেটওয়ার্ক বা ডিস্ক থেকে পড়ার মতো, যখন ডিফল্ট প্রেরণকারীটি CPU নিবিড় কাজের জন্য অপ্টিমাইজ করা হয়।

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

এই বাস্তবায়ন নেটওয়ার্ক এবং ডাটাবেসের জন্য ব্লকিং কল ব্যবহার করে - কিন্তু এটি এখনও কলব্যাক সংস্করণের চেয়ে কিছুটা সহজ।

এই কোডটি এখনও ব্লকিং কল ব্যবহার করে। execute() এবং insertTitle(...) কল করা উভয়ই থ্রেডটিকে ব্লক করবে যেটি এই coroutine চলছে। তবে, withContext ব্যবহার করে Dispatchers.IO তে স্যুইচ করার মাধ্যমে, আমরা IO প্রেরণকারীর একটি থ্রেড ব্লক করছি। যে coroutine এটিকে কল করেছে, সম্ভবত Dispatchers.Main এ চলমান, withContext lambda সম্পূর্ণ না হওয়া পর্যন্ত স্থগিত করা হবে।

কলব্যাক সংস্করণের তুলনায়, দুটি গুরুত্বপূর্ণ পার্থক্য রয়েছে:

  1. withContext এটির ফলাফল ফেরত পাঠায় যে এটিকে ডেকেছে, এই ক্ষেত্রে Dispatchers.Main । কলব্যাক সংস্করণটি ব্যাকগ্রাউন্ড এক্সিকিউটর পরিষেবার একটি থ্রেডে BACKGROUND বলে।
  2. কলকারীকে এই ফাংশনে কলব্যাক পাস করতে হবে না। ফলাফল বা ত্রুটি পেতে তারা স্থগিত এবং পুনরায় শুরু করার উপর নির্ভর করতে পারে।

অ্যাপটি আবার চালান

আপনি যদি আবার অ্যাপটি চালান, তাহলে আপনি দেখতে পাবেন যে নতুন কোরোটিন-ভিত্তিক বাস্তবায়ন নেটওয়ার্ক থেকে ফলাফল লোড করছে!

পরবর্তী ধাপে আপনি রুম এবং রেট্রোফিটে কোরোটিন একত্রিত করবেন।

coroutines ইন্টিগ্রেশন চালিয়ে যেতে, আমরা রুম এবং রেট্রোফিটের স্থিতিশীল সংস্করণে সাসপেন্ড ফাংশনগুলির জন্য সমর্থন ব্যবহার করতে যাচ্ছি, তারপরে সাসপেন্ড ফাংশনগুলি ব্যবহার করে আমরা যে কোডটি যথেষ্ট পরিমাণে লিখেছি সেটিকে সরলীকরণ করব৷

রুমে corutines

প্রথমে MainDatabase.kt খুলুন এবং insertTitle একটি সাসপেন্ড ফাংশন করুন:

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

আপনি যখন এটি করবেন, রুম আপনার ক্যোয়ারীকে প্রধান-নিরাপদ করে তুলবে এবং এটি স্বয়ংক্রিয়ভাবে একটি ব্যাকগ্রাউন্ড থ্রেডে কার্যকর করবে। যাইহোক, এর মানে হল যে আপনি শুধুমাত্র একটি কোরাটিনের ভিতর থেকে এই ক্যোয়ারীটি কল করতে পারেন।

এবং - রুমে কোরোটিন ব্যবহার করার জন্য আপনাকে যা করতে হবে। বেশ নিফটি।

Retrofit মধ্যে Coroutines

এর পরে দেখা যাক রেট্রোফিটের সাথে কোরোটিনগুলিকে কীভাবে একীভূত করা যায়। MainNetwork.kt খুলুন এবং একটি সাসপেন্ড ফাংশনে fetchNextTitle পরিবর্তন করুন।

MainNetwork.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Retrofit এর সাথে সাসপেন্ড ফাংশন ব্যবহার করতে আপনাকে দুটি জিনিস করতে হবে:

  1. ফাংশনে একটি সাসপেন্ড মডিফায়ার যোগ করুন
  2. রিটার্ন টাইপ থেকে Call র্যাপার সরান। এখানে আমরা String ফেরত দিচ্ছি, তবে আপনি জটিল json-ব্যাকড টাইপও ফিরিয়ে দিতে পারেন। আপনি যদি এখনও রেট্রোফিটের সম্পূর্ণ ফলাফলে অ্যাক্সেস প্রদান করতে চান তবে আপনি সাসপেন্ড ফাংশন থেকে স্ট্রিংয়ের পরিবর্তে Result Result<String> String দিতে পারেন।

Retrofit স্বয়ংক্রিয়ভাবে সাসপেন্ড ফাংশনগুলিকে প্রধান-নিরাপদ করে তুলবে যাতে আপনি সরাসরি Dispatchers.Main থেকে তাদের কল করতে পারেন।

রুম এবং রেট্রোফিট ব্যবহার করা

এখন যেহেতু রুম এবং রেট্রোফিট ফাংশনগুলিকে সাসপেন্ড করে, আমরা সেগুলিকে আমাদের সংগ্রহস্থল থেকে ব্যবহার করতে পারি৷ TitleRepository.kt খুলুন এবং দেখুন কিভাবে সাসপেন্ডিং ফাংশন ব্যবহার করে যুক্তিকে অনেক সহজ করে দেয়, এমনকি ব্লকিং সংস্করণের তুলনায়:

শিরোনাম Repository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

বাহ, এটা অনেক ছোট। কি হলো? দেখা যাচ্ছে সাসপেন্ড এবং রিজুমের উপর নির্ভর করা কোডকে অনেক ছোট হতে দেয়। Retrofit আমাদের এখানে Call পরিবর্তে String বা User বস্তুর মত রিটার্ন প্রকার ব্যবহার করতে দেয়। এটি করা নিরাপদ, কারণ সাসপেন্ড ফাংশনের ভিতরে, রেট্রোফিট একটি ব্যাকগ্রাউন্ড থ্রেডে নেটওয়ার্ক অনুরোধ চালাতে সক্ষম হয় এবং কল সম্পূর্ণ হলে Retrofit পুনরায় শুরু করতে পারে।

আরও ভাল, আমরা withContext থেকে পরিত্রাণ পেয়েছি। যেহেতু রুম এবং রেট্রোফিট উভয়ই প্রধান-নিরাপদ সাসপেন্ডিং ফাংশন প্রদান করে, তাই Dispatchers.Main থেকে এই অ্যাসিঙ্ক কাজটি অর্কেস্ট্রেট করা নিরাপদ।

কম্পাইলার ত্রুটি ঠিক করা

coroutines এ সরানো ফাংশনের স্বাক্ষর পরিবর্তন জড়িত কারণ আপনি একটি নিয়মিত ফাংশন থেকে একটি সাসপেন্ড ফাংশন কল করতে পারবেন না। আপনি যখন এই ধাপে suspend মডিফায়ার যোগ করেন, তখন কিছু কম্পাইলার ত্রুটি তৈরি হয়েছিল যা দেখায় যে আপনি যদি একটি বাস্তব প্রকল্পে সাসপেন্ড করার জন্য একটি ফাংশন পরিবর্তন করেন তাহলে কী হবে।

প্রজেক্টের মাধ্যমে যান এবং তৈরি করা স্থগিত করতে ফাংশন পরিবর্তন করে কম্পাইলার ত্রুটিগুলি ঠিক করুন। এখানে প্রতিটির জন্য দ্রুত রেজোলিউশন রয়েছে:

TestingFakes.kt

নতুন সাসপেন্ড মডিফায়ারকে সমর্থন করতে টেস্টিং জাল আপডেট করুন।

শিরোনাম DaoFake

  1. উত্তরাধিকারসূত্রে সমস্ত ফাংশনে সাসপেন্ড মডিফায়ার যুক্ত করুন alt-enter টিপুন

মেইননেটওয়ার্ক ফেক

  1. উত্তরাধিকারসূত্রে সমস্ত ফাংশনে সাসপেন্ড মডিফায়ার যুক্ত করুন alt-enter টিপুন
  2. এই ফাংশন দিয়ে fetchNextTitle প্রতিস্থাপন করুন
override suspend fun fetchNextTitle() = result

MainNetwork Completable Fake

  1. উত্তরাধিকারসূত্রে সমস্ত ফাংশনে সাসপেন্ড মডিফায়ার যুক্ত করুন alt-enter টিপুন
  2. এই ফাংশন দিয়ে fetchNextTitle প্রতিস্থাপন করুন
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • refreshTitleWithCallbacks ফাংশনটি মুছুন কারণ এটি আর ব্যবহার করা হয় না।

অ্যাপটি চালান

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

Congratulations, you've completely swapped this app to using coroutines! To wrap up we'll talk a bit about how to test what we just did.

In this exercise, you'll write a test that calls a suspend function directly.

Since refreshTitle is exposed as a public API it will be tested directly, showing how to call coroutines functions from tests.

Here's the refreshTitle function you implemented in the last exercise:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Write a test that calls a suspend function

Open TitleRepositoryTest.kt in the test folder which has two TODOS.

Try to call refreshTitle from the first test whenRefreshTitleSuccess_insertsRows .

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

Since refreshTitle is a suspend function Kotlin doesn't know how to call it except from a coroutine or another suspend function, and you will get a compiler error like, "Suspend function refreshTitle should be called only from a coroutine or another suspend function."

The test runner doesn't know anything about coroutines so we can't make this test a suspend function. We could launch a coroutine using a CoroutineScope like in a ViewModel , however tests need to run coroutines to completion before they return. Once a test function returns, the test is over. Coroutines started with launch are asynchronous code, which may complete at some point in the future. Therefore to test that asynchronous code, you need some way to tell the test to wait until your coroutine completes. Since launch is a non-blocking call, that means it returns right away and can continue to run a coroutine after the function returns - it can't be used in tests. উদাহরণ স্বরূপ:

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

This test will sometimes fail. The call to launch will return immediately and execute at the same time as the rest of the test case. The test has no way to know if refreshTitle has run yet or not – and any assertions like checking that the database was updated would be flakey. And, if refreshTitle threw an exception, it will not be thrown in the test call stack. It will instead be thrown into GlobalScope 's uncaught exception handler.

The library kotlinx-coroutines-test has the runBlockingTest function that blocks while it calls suspend functions. When runBlockingTest calls a suspend function or launches a new coroutine, it executes it immediately by default. You can think of it as a way to convert suspend functions and coroutines into normal function calls.

In addition, runBlockingTest will rethrow uncaught exceptions for you. This makes it easier to test when a coroutine is throwing an exception.

Implement a test with one coroutine

Wrap the call to refreshTitle with runBlockingTest and remove the GlobalScope.launch wrapper from subject.refreshTitle().

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

This test uses the fakes provided to check that "OK" is inserted to the database by refreshTitle .

When the test calls runBlockingTest , it will block until the coroutine started by runBlockingTest completes. Then inside, when we call refreshTitle it uses the regular suspend and resume mechanism to wait for the database row to be added to our fake.

After the test coroutine completes, runBlockingTest returns.

Write a timeout test

We want to add a short timeout to the network request. Let's write the test first then implement the timeout. Create a new test:

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

This test uses the provided fake MainNetworkCompletableFake , which is a network fake that's designed to suspend callers until the test continues them. When refreshTitle tries to make a network request, it'll hang forever because we want to test timeouts.

Then, it launches a separate coroutine to call refreshTitle . This is a key part of testing timeouts, the timeout should happen in a different coroutine than the one runBlockingTest creates. By doing so, we can call the next line, advanceTimeBy(5_000) which will advance time by 5 seconds and cause the other coroutine to timeout.

This is a complete timeout test, and it will pass once we implement timeout.

Run it now and see what happens:

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

One of the features of runBlockingTest is that it won't let you leak coroutines after the test completes. If there are any unfinished coroutines, like our launch coroutine, at the end of the test, it will fail the test.

Add a timeout

Open up TitleRepository and add a five second timeout to the network fetch. You can do this by using the withTimeout function:

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Run the test. When you run the tests you should see all tests pass!

In the next exercise you'll learn how to write higher order functions using coroutines.

In this exercise you'll refactor refreshTitle in MainViewModel to use a general data loading function. This will teach you how to build higher order functions that use coroutines.

The current implementation of refreshTitle works, but we can create a general data loading coroutine that always shows the spinner. This might be helpful in a codebase that loads data in response to several events, and wants to ensure the loading spinner is consistently displayed.

Reviewing the current implementation every line except repository.refreshTitle() is boilerplate to show the spinner and display errors.

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Using coroutines in higher order functions

Add this code to MainViewModel.kt

MainViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Now refactor refreshTitle() to use this higher order function.

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

By abstracting the logic around showing a loading spinner and showing errors, we've simplified our actual code needed to load data. Showing a spinner or displaying an error is something that's easy to generalize to any data loading, while the actual data source and destination needs to be specified every time.

To build this abstraction, launchDataLoad takes an argument block that is a suspend lambda. A suspend lambda allows you to call suspend functions. That's how Kotlin implements the coroutine builders launch and runBlocking we've been using in this codelab.

// suspend lambda

block: suspend () -> Unit

To make a suspend lambda, start with the suspend keyword. The function arrow and return type Unit complete the declaration.

You don't often have to declare your own suspend lambdas, but they can be helpful to create abstractions like this that encapsulate repeated logic!

In this exercise you'll learn how to use coroutine based code from WorkManager.

What is WorkManager

There are many options on Android for deferrable background work. This exercise shows you how to integrate WorkManager with coroutines. WorkManager is a compatible, flexible and simple library for deferrable background work. WorkManager is the recommended solution for these use cases on Android.

WorkManager is part of Android Jetpack , and an Architecture Component for background work that needs a combination of opportunistic and guaranteed execution. Opportunistic execution means that WorkManager will do your background work as soon as it can. Guaranteed execution means that WorkManager will take care of the logic to start your work under a variety of situations, even if you navigate away from your app.

Because of this, WorkManager is a good choice for tasks that must complete eventually.

Some examples of tasks that are a good use of WorkManager:

  • Uploading logs
  • Applying filters to images and saving the image
  • Periodically syncing local data with the network

Using coroutines with WorkManager

WorkManager provides different implementations of its base ListanableWorker class for different use cases.

The simplest Worker class allows us to have some synchronous operation executed by WorkManager. However, having worked so far to convert our codebase to use coroutines and suspend functions, the best way to use WorkManager is through the CoroutineWorker class that allows to define our doWork() function as a suspend function.

To get started, open up RefreshMainDataWork . It already extends CoroutineWorker , and you need to implement doWork .

Inside the suspend doWork function, call refreshTitle() from the repository and return the appropriate result!

After you've completed the TODO, the code will look like this:

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

Note that CoroutineWorker.doWork() is a suspending function. Unlike the simpler Worker class, this code does NOT run on the Executor specified in your WorkManager configuration, but instead use the dispatcher in coroutineContext member (by default Dispatchers.Default ).

Testing our CoroutineWorker

No codebase should be complete without testing.

WorkManager makes available a couple of different ways to test your Worker classes, to learn more about the original testing infrastructure, you can read the documentation .

WorkManager v2.1 introduces a new set of APIs to support a simpler way to test ListenableWorker classes and, as a consequence, CoroutineWorker. In our code we're going to use one of these new API: TestListenableWorkerBuilder .

To add our new test, update the RefreshMainDataWorkTest file under the androidTest folder.

The content of the file is:

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

Before we get to the test, we tell WorkManager about the factory so we can inject the fake network.

The test itself uses the TestListenableWorkerBuilder to create our worker that we can then run calling the startWork() method.

WorkManager is just one example of how coroutines can be used to simplify APIs design.

In this codelab we have covered the basics you'll need to start using coroutines in your app!

We covered:

  • How to integrate coroutines to Android apps from both the UI and WorkManager jobs to simplify asynchronous programming,
  • How to use coroutines inside a ViewModel to fetch data from the network and save it to a database without blocking the main thread.
  • And how to cancel all coroutines when the ViewModel is finished.

For testing coroutine based code, we covered both by testing behavior as well as directly calling suspend functions from tests.

আরও জানুন

Check out the " Advanced Coroutines with Kotlin Flow and LiveData " codelab to learn more advanced coroutines usage on Android.

Kotlin coroutines have many features that weren't covered by this codelab. If you're interested in learning more about Kotlin coroutines, read the coroutines guides published by JetBrains. Also check out " Improve app performance with Kotlin coroutines " for more usage patterns of coroutines on Android.