কম্পিউটার টিউটোরিয়াল

Android-এ RESTful Services 습득: Retrofit, OkHttp, Gson, Glide এবং Coroutines

Kriptofolio app series — Part 5

আজকাল প্রায় সব Android অ্যাপ ইন্টারনেটে কানেক্ট থাকে ডেটা পেতে বা পাঠাতে। আপনাকে অবশ্যই RESTful Web Services হ্যান্ডল করার কৌশল শিখতে হবে, কারণ আধুনিক অ্যাপ তৈরি করার সময় এদের সঠিক ইমপ্লিমেন্টেশন মূল জ্ঞান।

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

সিরিজ কন্টেন্ট

  • Introduction: ২০১৮–২০১৯-এ একটি আধুনিক Android অ্যাপ বিল্ড করার রোডম্যাপ
  • Part 1: SOLID প্রিন্সিপালের একটি ইন্ট্রোডাকশন
  • Part 2: আপনার Android অ্যাপ বিল্ড শুরু করা: Mockups, UI, এবং XML লেআউট তৈরি করা
  • Part 3: আর্কিটেকচার নিয়ে সব কিছু: বিভিন্ন আর্কিটেকচার প্যাটার্ন এক্সপ্লোর করা এবং আপনার অ্যাপে সেগুলো কীভাবে ব্যবহার করবেন
  • Part 4: Dagger 2 দিয়ে আপনার অ্যাপে Dependency Injection ইমপ্লিমেন্ট করা
  • Part 5: Retrofit, OkHttp, Gson, Glide এবং Coroutines ব্যবহার করে RESTful Web Services হ্যান্ডল করা (আপনি এখানে আছেন)

Retrofit, OkHttp এবং Gson কী?

Retrofit Java এবং Android-র জন্য একটি REST Client। আমার মতামতে, এটি শিখার জন্য সবচেয়ে গুরুত্বপূর্ণ লাইব্রেরি, কারণ এটি মূল কাজটি করবে। এটি একটি REST-ভিত্তিক ওয়েব সার্ভিসের মাধ্যমে JSON (অথবা অন্যান্য স্ট্রাকচার্ড ডেটা) রিট্রিভ এবং আপলোড করা יחসামান্য সহজ করে দেয়।

Retrofit-এ আপনি কনফিগার করেন কোন কনভার্টার ডেটা সিরিয়ালাইজেশনের জন্য ব্যবহার করা হবে। সাধারণত অবজেক্টগুলো JSON-এ সিরিয়ালাইজ এবং ডিসিরিয়ালাইজ করার জন্য একটি ওপেন-সোর্স Java লাইব্রেরি — Gson ব্যবহার করা হয়। আপনার যদি প্রয়োজন হয়, তবে আপনি XML বা অন্যান্য প্রোটোকল প্রসেস করার জন্য Retrofit-এ কাস্টম কনভার্টার যোগ করতে পারেন।

HTTP রিকোয়েস্ট করার জন্য Retrofit OkHttp লাইব্রেরি ব্যবহার করে। OkHttp একটি পিউর HTTP/SPDY ক্লায়েন্ট যা যেকোনো লো-লেভেল নেটওয়ার্ক অপারেশন, ক্যাশিং, রিকোয়েস্ট এবং রেসপন্স ম্যানিপুলেশনের জন্য দায়ী। বিপরীতে, Retrofit OkHttp-এর ওপর তৈরি একটি হাই-লেভেল REST অ্যাবস্ট্রাকশন। Retrofit OkHttp-এর সাথে শক্তিশালীভাবে কাপল্ড এবং এর ইনটেনসিভ ব্যবহার করে।

এখন আপনি জানেন সব কিছু ঘনিষ্ঠভাবে সম্পর্কিত, আমরা এই ৩টি লাইব্রেরি একসাথে ব্যবহার করব। আমাদের প্রথম লক্ষ্য হল ইন্টারনেট থেকে Retrofit ব্যবহার করে সমস্ত ক্রিপ্টোকারেন্সির লিস্ট পাওয়া। আমরা CoinMarketCap API প্রমাণীকরণের জন্য একটি স্পেশাল OkHttp ইন্টারসেপ্টর ক্লাস ব্যবহার করব সার্ভারে কল করার সময়। আমরা একটি JSON ডেটা ফলাফল পাব এবং তারপর Gson লাইব্রেরি ব্যবহার করে এটি কনভার্ট করব।

Retrofit 2 দ্রুত সেটআপ শুধু ট্রাই করার জন্য

নতুন কিছু শিখার সময়, আমি সেটি যত দ্রুত সম্ভব প্র্যাকটিসে ট্রাই করতে পছন্দ করি। আমরা Retrofit 2-এ একই পদ্ধতি প্রয়োগ করব যাতে আপনি দ্রুত ভালোভাবে বুঝতে পারেন। এখন কোড কোয়ালিটি বা কোনো প্রোগ্রামিং প্রিন্সিপাল বা অপ্টিমাইজেশনের চিন্তা করবেন না — আমরা শুধু কিছু কোড লিখব Retrofit 2 আমাদের প্রজেক্টে কাজ করার জন্য এবং আলোচনা করব এটি কী করে।

My Crypto Coins অ্যাপ প্রজেক্টে Retrofit 2 সেটআপ করার জন্য এই ধাপগুলো ফলো করুন:

প্রথম, অ্যাপের জন্য INTERNET পারমিশন দিন

আমরা ইন্টারনেটের মাধ্যমে অ্যাক্সেসযোগ্য একটি সার্ভারে HTTP রিকোয়েস্ট এক্সিকিউট করব। এই পারমিশন দিন আপনার Manifest ফাইলে এই লাইনগুলো যোগ করে:

<manifest xmlns:android="https://schemas.android.com/apk/res/android" package="com.baruckis.mycryptocoins"> <uses-permission android:name="android.permission.INTERNET" /> ... </manifest>

তারপর লাইব্রেরি ডিপেন্ডেন্সি যোগ করুন

সর্বশেষ Retrofit ভার্সন খুঁজে বের করুন। আরো আপনাকে জানতে হবে যে Retrofit একটি ইন্টিগ্রেটেড JSON কনভার্টার নিয়ে আসে না। যেহেতু আমরা JSON ফরম্যাটে রেসপন্স পাব, আমাদের কনভার্টারটি ম্যানুয়ালি ডিপেন্ডেন্সিতে অন্তর্ভুক্ত করতে হবে। আমরা Google-এর সর্বশেষ JSON কনভার্টার Gson ভার্সন ব্যবহার করব। আপনার gradle ফাইলে এই লাইনগুলো যোগ করুন:

// 3rd party // HTTP client - Retrofit with OkHttp implementation "com.squareup.retrofit2:retrofit:$versions.retrofit" // JSON converter Gson for JSON to Java object mapping implementation "com.squareup.retrofit2:converter-gson:$versions.retrofit"

আপনি আমার কমেন্ট থেকে লক্ষ্য করেছেন, OkHttp ডিপেন্ডেন্সি ইতিমধ্যে Retrofit 2 ডিপেন্ডেন্সির সাথে শিপড। Versions শুধু সুবিধার জন্য একটি আলাদা gradle ফাইল:

def versions = [:] versions.retrofit = "2.4.0" ext.versions = versions

এরপর Retrofit ইন্টারফেস সেট আপ করুন

এটি একটি ইন্টারফেস যা আমাদের রিকোয়েস্ট এবং তাদের টাইপ ডিক্লেয়ার করে। এখানে আমরা ক্লায়েন্ট সাইডে API ডিফাইন করি।

/** * REST API access points. */ interface ApiService { // The @GET annotation tells retrofit that this request is a get type request. // The string value tells retrofit that the path of this request is // baseUrl + v1/cryptocurrency/listings/latest + query parameter. @GET("v1/cryptocurrency/listings/latest") // Annotation @Query is used to define query parameter for request. Finally the request url will // look like that https://sandbox-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?convert=EUR. fun getAllCryptocurrencies(@Query("convert") currency: String): Call<CryptocurrenciesLatest> // The return type for this function is Call with its type CryptocurrenciesLatest. }

এবং ডেটা ক্লাস সেট আপ করুন

ডেটা ক্লাস হল POJOs (Plain Old Java Objects) যা আমরা কল করার API রেসপন্স রিপ্রেজেন্ট করে।

/** * Data class to handle the response from the server. */ data class CryptocurrenciesLatest( val status: Status, val data: List<Data> ) { data class Data( val id: Int, val name: String, val symbol: String, val slug: String, // The annotation to a model property lets you pass the serialized and deserialized // name as a string. This is useful if you don't want your model class and the JSON // to have identical naming. @SerializedName("circulating_supply") val circulatingSupply: Double, @SerializedName("total_supply") val totalSupply: Double, @SerializedName("max_supply") val maxSupply: Double, @SerializedName("date_added") val dateAdded: String, @SerializedName("num_market_pairs") val numMarketPairs: Int, @SerializedName("cmc_rank") val cmcRank: Int, @SerializedName("last_updated") val lastUpdated: String, val quote: Quote ) { data class Quote( // For additional option during deserialization you can specify value or alternative // values. Gson will check the JSON for all names we specify and try to find one to // map it to the annotated property. @SerializedName(value = "USD", alternate = ["AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR"]) val currency: Currency ) { data class Currency( val price: Double, @SerializedName("volume_24h") val volume24h: Double, @SerializedName("percent_change_1h") val percentChange1h: Double, @SerializedName("percent_change_24h") val percentChange24h: Double, @SerializedName("percent_change_7d") val percentChange7d: Double, @SerializedName("market_cap") val marketCap: Double, @SerializedName("last_updated") val lastUpdated: String ) } } data class Status( val timestamp: String, @SerializedName("error_code") val errorCode: Int, @SerializedName("error_message") val errorMessage: String, val elapsed: Int, @SerializedName("credit_count") val creditCount: Int ) }

সার্ভারে কল করার সময় প্রমাণীকরণের জন্য একটি স্পেশাল ইন্টারসেপ্টর ক্লাস তৈরি করুন

এটি যেকোনো API-এর জন্য Besonder কেস যা প্রমাণীকরণ আশ্রয় করে একটি সফল রেসপন্স পেতে। ইন্টারসেপ্টরগুলো আপনার রিকোয়েস্ট কাস্টোমাইজ করার একটি শক্তিশালী পথ। আমরা আসল রিকোয়েস্টটি ইন্টারসেপ্ট করব এবং ইনডিভিজুয়াল রিকোয়েস্ট হেডার যোগ করব, যা CoinMarketCap Professional API Developer Portal-দ্বারা প্রদত্ত একটি API Key দিয়ে কলটি যাচাই করবে। আপনার পাওয়ার জন্য, আপনাকে সেখানে রেজিস্টার করতে হবে।

/** * Interceptor used to intercept the actual request and * to supply your API Key in REST API calls via a custom header. */ class AuthenticationInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val newRequest = chain.request().newBuilder() // TODO: Use your API Key provided by CoinMarketCap Professional API Developer Portal. .addHeader("X-CMC_PRO_API_KEY", "CMC_PRO_API_KEY") .build() return chain.proceed(newRequest) } }

শেষে, এই কোড আমাদের এক্টিভিটিতে যোগ করুন Retrofit কাজ করছে দেখতে

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

class AddSearchActivity : AppCompatActivity(), Injectable { private lateinit var listView: ListView private lateinit var listAdapter: AddSearchListAdapter ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ... // Later we will setup Retrofit correctly, but for now we do all in one place just for quick start. setupRetrofitTemporarily() } ... private fun setupRetrofitTemporarily() { // We need to prepare a custom OkHttp client because need to use our custom call interceptor. // to be able to authenticate our requests. val builder = OkHttpClient.Builder() // We add the interceptor to OkHttpClient. // It will add authentication headers to every call we make. builder.interceptors().add(AuthenticationInterceptor()) val client = builder.build() val api = Retrofit.Builder() // Create retrofit builder. .baseUrl("https://sandbox-api.coinmarketcap.com/") // Base url for the api has to end with a slash. .addConverterFactory(GsonConverterFactory.create()) // Use GSON converter for JSON to POJO object mapping. .client(client) // Here we set the custom OkHttp client we just created. .build().create(ApiService::class.java) // We create an API using the interface we defined. val adapterData: MutableList<Cryptocurrency> = ArrayList<Cryptocurrency>() val currentFiatCurrencyCode = "EUR" // Let's make asynchronous network request to get all latest cryptocurrencies from the server. // For query parameter we pass "EUR" as we want to get prices in euros. val call = api.getAllCryptocurrencies("EUR") val result = call.enqueue(object : Callback<CryptocurrenciesLatest> { // You will always get a response even if something wrong went from the server. override fun onFailure(call: Call<CryptocurrenciesLatest>, t: Throwable) { Snackbar.make(findViewById(android.R.id.content), // Throwable will let us find the error if the call failed. "Call failed! " + t.localizedMessage, Snackbar.LENGTH_INDEFINITE).show() } override fun onResponse(call: Call<CryptocurrenciesLatest>, response: Response<CryptocurrenciesLatest>) { // Check if the response is successful, which means the request was successfully // received, understood, accepted and returned code in range [200..300). if (response.isSuccessful) { // If everything is OK, let the user know that. Toast.makeText(this@AddSearchActivity, "Call OK.", Toast.LENGTH_LONG).show(); // Than quickly map server response data to the ListView adapter. val cryptocurrenciesLatest: CryptocurrenciesLatest? = response.body() cryptocurrenciesLatest!!.data.forEach { val cryptocurrency = Cryptocurrency(it.name, it.cmcRank.toShort(), 0.0, it.symbol, currentFiatCurrencyCode, it.quote.currency.price, 0.0, it.quote.currency.percentChange1h, it.quote.currency.percentChange7d, it.quote.currency.percentChange24h, 0.0) adapterData.add(cryptocurrency) } listView.visibility = View.VISIBLE listAdapter.setData(adapterData) } // Else if the response is unsuccessful it will be defined by some special HTTP // error code, which we can show for the user. else Snackbar.make(findViewById(android.R.id.content), "Call error with HTTP status code " + response.code() + "!", Snackbar.LENGTH_INDEFINITE).show() } }) } ... }

আপনি কোডটি এখানে এক্সপ্লোর করতে পারেন। মনে রাখবেন এটি শুধু আপনার আইডিয়া ভালো পেতে একটি_initial simplified implementation version।

Retrofit 2, OkHttp 3 এবং Gson-সঙ্গে চূড়ান্ত সঠিক সেটআপ

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

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

সুধার করার প্রথম ধাপ ছিল Dependency Injection ব্যবহার শুরু করা। পिछলে পার্ট থেকে মনে রাখবেন আমাদের প্রজেক্টে Dagger 2 ইতিমধ্যে সঠিকভাবে ইমপ্লিমেন্টেড। তাই আমি Retrofit সেটআপের জন্য এটিকে ব্যবহার করেছি।

/** * AppModule will provide app-wide dependencies for a part of the application. * It should initialize objects used across our application, such as Room database, Retrofit, Shared Preference, etc. */ @Module(includes = [ViewModelsModule::class]) class AppModule() { ... @Provides @Singleton fun provideHttpClient(): OkHttpClient { // We need to prepare a custom OkHttp client because need to use our custom call interceptor. // to be able to authenticate our requests. val builder = OkHttpClient.Builder() // We add the interceptor to OkHttpClient. // It will add authentication headers to every call we make. builder.interceptors().add(AuthenticationInterceptor()) // Configure this client not to retry when a connectivity problem is encountered. builder.retryOnConnectionFailure(false) // Log requests and responses. // Add logging as the last interceptor, because this will also log the information which // you added or manipulated with previous interceptors to your request. builder.interceptors().add(HttpLoggingInterceptor().apply { // For production environment to enhance apps performance we will be skipping any // logging operation. We will show logs just for debug builds. level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE }) return builder.build() } @Provides @Singleton fun provideApiService(httpClient: OkHttpClient): ApiService { return Retrofit.Builder() // Create retrofit builder. .baseUrl(API_SERVICE_BASE_URL) // Base url for the api has to end with a slash. .addConverterFactory(GsonConverterFactory.create()) // Use GSON converter for JSON to POJO object mapping. .addCallAdapterFactory(LiveDataCallAdapterFactory()) .client(httpClient) // Here we set the custom OkHttp client we just created. .build().create(ApiService::class.java) // We create an API using the interface we defined. } ... }

আপনি দেখতে পাচ্ছেন, Retrofit অ্যাক্টিভিটি ক্লাস থেকে আলাদা করা হয়েছে যেন должно। এটি শুধু একবার ইনিশিয়ালাইজ হবে এবং অ্যাপ-ওয়াইড ব্যবহার হবে।

আপনি লক্ষ্য করেছেন হতে পারে Retrofit builder ইনস্ট্যান্স তৈরি করার সময়, আমরা একটি স্পেশাল Retrofit কলস অ্যাডাপ্টার অ্যাড করেছি addCallAdapterFactory ব্যবহার করে। ডিফল্টভাবে, Retrofit একটি Call<T> রিটার্ন করে, কিন্তু আমাদের প্রজেক্টের জন্য আমাদের প্রয়োজন LiveData<T> টাইপ। এটি করার জন্য আমাদের LiveDataCallAdapter অ্যাড করতে হবে LiveDataCallAdapterFactory ব্যবহার করে।

/** * A Retrofit adapter that converts the Call into a LiveData of ApiResponse. * @param <R> </R> */ class LiveDataCallAdapter<R>(private val responseType: Type) : CallAdapter<R, LiveData<ApiResponse<R>>> { override fun responseType() = responseType override fun adapt(call: Call<R>): LiveData<ApiResponse<R>> { return object : LiveData<ApiResponse<R>>() { private var started = AtomicBoolean(false) override fun onActive() { super.onActive() if (started.compareAndSet(false, true)) { call.enqueue(object : Callback<R> { override fun onResponse(call: Call<R>, response: Response<R>) { postValue(ApiResponse.create(response)) } override fun onFailure(call: Call<R>, throwable: Throwable) { postValue(ApiResponse.create(throwable)) } }) } } } }
class LiveDataCallAdapterFactory : CallAdapter.Factory() { override fun get( returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit ): CallAdapter<*, *>? { if (CallAdapter.Factory.getRawType(returnType) != LiveData::class.java) { return null } val observableType = CallAdapter.Factory.getParameterUpperBound(0, returnType as ParameterizedType) val rawObservableType = CallAdapter.Factory.getRawType(observableType) if (rawObservableType != ApiResponse::class.java) { throw IllegalArgumentException("type must be a resource") } if (observableType !is ParameterizedType) { throw IllegalArgumentException("resource must be parameterized") } val bodyType = CallAdapter.Factory.getParameterUpperBound(0, observableType) return LiveDataCallAdapter<Any>(bodyType) } }

এখন আমরা ApiService ইন্টারফেসে ডিক্লেয়ার করা Retrofit সার্ভিস মেথড থেকে Call<T> এর বদলে LiveData<T> পাব।

এগিয়ে যাওয়ার আরেকটি গুরুত্বপূর্ণ ধাপ হল Repository প্যাটার্ন ব্যবহার শুরু করা। আমি পার্ট ৩-এ এটিকে নিয়ে কথা বলেছি। সেই পোস্ট থেকে আমাদের MVVM আর্কিটেকচার স্কিমা দেখে মনে করুন এটা কোথায় যায়।

Android-এ RESTful Services 습득: Retrofit, OkHttp, Gson, Glide এবং Coroutines

চিত্রে দেখতে পাচ্ছেন, Repository ডেটার জন্য একটি আলাদা লেয়ার। এটি ডেটা পাওয়া বা পাঠানোর জন্য আমাদের একক যোগাযোগের উৎস। Repository ব্যবহার করলে আমরা separation of concerns প্রিন্সিপ্যাল ফলো করি। আমাদের বিভিন্ন ডেটা সোর্স থাকতে পারে (যেমন আমাদের কেসে SQLite ডেটাবেস থেকে পারসিস্টেন্ট ডেটা এবং ওয়েব সার্ভিস থেকে ডেটা), কিন্তু Repository সবসময় অ্যাপ ডেটার জন্য একক সত্যের উৎস হবে।

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

/** * The class for managing multiple data sources. */ @Singleton class CryptocurrencyRepository @Inject constructor( private val context: Context, private val appExecutors: AppExecutors, private val myCryptocurrencyDao: MyCryptocurrencyDao, private val cryptocurrencyDao: CryptocurrencyDao, private val api: ApiService, private val sharedPreferences: SharedPreferences ) { // Just a simple helper variable to store selected fiat currency code during app lifecycle. // It is needed for main screen currency spinner. We set it to be same as in shared preferences. var selectedFiatCurrencyCode: String = getCurrentFiatCurrencyCode() ... // The Resource wrapping of LiveData is useful to update the UI based upon the state. fun getAllCryptocurrencyLiveDataResourceList(fiatCurrencyCode: String, shouldFetch: Boolean = false, callDelay: Long = 0): LiveData<Resource<List<Cryptocurrency>>> { return object : NetworkBoundResource<List<Cryptocurrency>, CoinMarketCap<List<CryptocurrencyLatest>>>(appExecutors) { // Here we save the data fetched from web-service. override fun saveCallResult(item: CoinMarketCap<List<CryptocurrencyLatest>>) { val list = getCryptocurrencyListFromResponse(fiatCurrencyCode, item.data, item.status?.timestamp) cryptocurrencyDao.reloadCryptocurrencyList(list) myCryptocurrencyDao.reloadMyCryptocurrencyList(list) } // Returns boolean indicating if to fetch data from web or not, true means fetch the data from web. override fun shouldFetch(data: List<Cryptocurrency>?): Boolean { return data == null || shouldFetch } override fun fetchDelayMillis(): Long { return callDelay } // Contains the logic to get data from the Room database. override fun loadFromDb(): LiveData<List<Cryptocurrency>> { return Transformations.switchMap(cryptocurrencyDao.getAllCryptocurrencyLiveDataList()) { data -> if (data.isEmpty()) { AbsentLiveData.create() } else { cryptocurrencyDao.getAllCryptocurrencyLiveDataList() } } } // Contains the logic to get data from web-service using Retrofit. override fun createCall(): LiveData<ApiResponse<CoinMarketCap<List<CryptocurrencyLatest>>>> = api.getAllCryptocurrencies(fiatCurrencyCode) }.asLiveData() } ... fun getCurrentFiatCurrencyCode(): String { return sharedPreferences.getString(context.resources.getString(R.string.pref_fiat_currency_key), context.resources.getString(R.string.pref_default_fiat_currency_value)) ?: context.resources.getString(R.string.pref_default_fiat_currency_value) } ... private fun getCryptocurrencyListFromResponse(fiatCurrencyCode: String, responseList: List<CryptocurrencyLatest>?, timestamp: Date?): ArrayList<Cryptocurrency> { val cryptocurrencyList: MutableList<Cryptocurrency> = ArrayList() responseList?.forEach { val cryptocurrency = Cryptocurrency(it.id, it.name, it.cmcRank.toShort(), it.symbol, fiatCurrencyCode, it.quote.currency.price, it.quote.currency.percentChange1h, it.quote.currency.percentChange7d, it.quote.currency.percentChange24h, timestamp) cryptocurrencyList.add(cryptocurrency) } return cryptocurrencyList as ArrayList<Cryptocurrency> } }

CryptocurrencyRepository ক্লাস কোডে আপনি লক্ষ্য করেছেন, আমি NetworkBoundResource অ্যাবস্ট্রাক্ট ক্লাস ব্যবহার করছি। এটি কী এবং কেন দরকার?

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

NetworkBoundResource ডেটাবেস পর্যবেক্ষণ করে রিসোর্সের জন্য শুরু করে। যখন ডেটাবেস থেকে প্রথমবার এন্ট্রি লোড হয়, এটি চেক করে রেজাল্ট কি ডিসপ্যাচ করার জন্য যথেষ্ট ভালো নাকি নেটওয়ার্ক থেকে পুনরায় ফেচ করা দরকার। নোট করুন যে এই দুটি সিচুয়েশন একই সময়ে ঘটতে পারে, যেহেতু আপনি সম্ভবত ক্যাশড ডেটা দেখাতে চান যখন নেটওয়ার্ক থেকে আপডেট होছে।

যদি নেটওয়ার্ক কল সফলভাবে সম্পন্ন হয়, তবে এটি রেসপন্স ডেটাবেসে সেভ করে এবং স্ট্রিম পুনরায় ইনিশিয়ালাইজ করে। যদি নেটওয়ার্ক রিকোয়েস্ট ফেইল করে, তবে NetworkBoundResource সরাসরি ফেইলার ডিসপ্যাচ করে।

/** * A generic class that can provide a resource backed by both the sqlite database and the network. * * * You can read more about it in the [Architecture * Guide](https://developer.android.com/arch). * @param <ResultType> - Type for the Resource data. * @param <RequestType> - Type for the API response. </RequestType></ResultType> */ // It defines two type parameters, ResultType and RequestType, // because the data type returned from the API might not match the data type used locally. abstract class NetworkBoundResource<ResultType, RequestType> @MainThread constructor(private val appExecutors: AppExecutors) { // The final result LiveData. private val result = MediatorLiveData<Resource<ResultType>>() init { // Send loading state to UI. result.value = Resource.loading(null) @Suppress("LeakingThis") val dbSource = loadFromDb() result.addSource(dbSource) { data -> result.removeSource(dbSource) if (shouldFetch(data)) { fetchFromNetwork(dbSource) } else { result.addSource(dbSource) { newData -> setValue(Resource.successDb(newData)) } } } } @MainThread private fun setValue(newValue: Resource<ResultType>) { if (result.value != newValue) { result.value = newValue } } // Fetch the data from network and persist into DB and then send it back to UI. private fun fetchFromNetwork(dbSource: LiveData<ResultType>) { val apiResponse = createCall() // We re-attach dbSource as a new source, it will dispatch its latest value quickly. result.addSource(dbSource) { newData -> setValue(Resource.loading(newData)) } // Create inner function as we want to delay it. fun fetch() { result.addSource(apiResponse) { response -> result.removeSource(apiResponse) result.removeSource(dbSource) when (response) { is ApiSuccessResponse -> { appExecutors.diskIO().execute { saveCallResult(processResponse(response)) appExecutors.mainThread().execute { // We specially request a new live data, // otherwise we will get immediately last cached value, // which may not be updated with latest results received from network. result.addSource(loadFromDb()) { newData -> setValue(Resource.successNetwork(newData)) } } } } is ApiEmptyResponse -> { appExecutors.mainThread().execute { // reload from disk whatever we had result.addSource(loadFromDb()) { newData -> setValue(Resource.successDb(newData)) } } } is ApiErrorResponse -> { onFetchFailed() result.addSource(dbSource) { newData -> setValue(Resource.error(response.errorMessage, newData)) } } } } } // Add delay before call if needed. val delay = fetchDelayMillis() if (delay > 0) { Handler().postDelayed({ fetch() }, delay) } else fetch() } // Called when the fetch fails. The child class may want to reset components // like rate limiter. protected open fun onFetchFailed() {} // Returns a LiveData object that represents the resource that's implemented // in the base class. fun asLiveData() = result as LiveData<Resource<ResultType>> @WorkerThread protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body // Called to save the result of the API response into the database. @WorkerThread protected abstract fun saveCallResult(item: RequestType) // Called with the data in the database to decide whether to fetch // potentially updated data from the network. @MainThread protected abstract fun shouldFetch(data: ResultType?): Boolean // Make a call to the server after some delay for better user experience. protected open fun fetchDelayMillis(): Long = 0 // Called to get the cached data from the database. @MainThread protected abstract fun loadFromDb(): LiveData<ResultType> // Called to create the API call. @MainThread protected abstract fun createCall(): LiveData<ApiResponse<RequestType>> }

হুডের নিচে, NetworkBoundResource ক্লাস MediatorLiveData এবং এর একাধিক LiveData সোর্স একসাথে পর্যবেক্ষণ করার ক্ষমতা ব্যবহার করে তৈরি করা হয়েছে। এখানে আমাদের দুটি LiveData সোর্স আছে: ডেটাবেস এবং নেটওয়ার্ক কল রেসপন্স। এই দুটি LiveData একটি MediatorLiveData-এ র‍্যাপ করা হয়েছে যা NetworkBoundResource-এ এক্সপোজ করা হয়েছে।

Android-এ RESTful Services 습득: Retrofit, OkHttp, Gson, Glide এবং Coroutines
NetworkBoundResource

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

যদি অ্যাপ তাজা ইনস্টলড হয় এবং এটি এর প্রথম লঞ্চ হয়, তবে লোকাল ডেটাবেসের ভিতরে কোনো ডেটা থাকবে না। কারণ দেখানোর জন্য কোনো ডেটা নেই, একটি লোডিং প্রগ্রেস বার UI দেখাবে। মাঝপথে অ্যাপ সার্ভারে একটি রিকোয়েস্ট কল করবে ওয়েব সার্ভিসের মাধ্যমে সমস্ত ক্রিপ্টোকারেন্সির লিস্ট পেতে।

যদি রেসপন্স আনসাক্সেসফুল হয় তবে এরর মেসেজ UI দেখাবে রিট্রাই কল করার জন্য একটি বাটন চাপা capacités সহ। যখন একটি রিকোয়েস্ট কল চূড়ান্ত সফল হয়, তবে রেসপন্স ডেটা একটি লোকাল SQLite ডেটাবেসে সেভ হবে।

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

আমাদের Repository এবং LiveDataCallAdapter-এ ব্যবহৃত আরেকটি ক্লাস যেখানে সব "জাদু" ঘটে সেটি ApiResponse। আসলে ApiResponse শুধু একটি সিম্পল কমন র‍্যাপার Retrofit2.Response ক্লাসের চারপাশে যা প্রতিটি রেসপন্সকে LiveData-এর একটি ইনস্ট্যান্সে কনভার্ট করে।

/** * Common class used by API responses. ApiResponse is a simple wrapper around the Retrofit2.Call * class that convert responses to instances of LiveData. * @param <CoinMarketCapType> the type of the response object </T> */ @Suppress("unused") // T is used in extending classes sealed class ApiResponse<CoinMarketCapType> { companion object { fun <CoinMarketCapType> create(error: Throwable): ApiErrorResponse<CoinMarketCapType> { return ApiErrorResponse(error.message ?: "Unknown error.") } fun <CoinMarketCapType> create(response: Response<CoinMarketCapType>): ApiResponse<CoinMarketCapType> { return if (response.isSuccessful) { val body = response.body() if (body == null || response.code() == 204) { ApiEmptyResponse() } else { ApiSuccessResponse(body = body) } } else { // Convert error response to JSON object. val gson = Gson() val type = object : TypeToken<CoinMarketCap<CoinMarketCapType>>() {}.type val errorResponse: CoinMarketCap<CoinMarketCapType> = gson.fromJson(response.errorBody()!!.charStream(), type) val msg = errorResponse.status?.errorMessage ?: errorResponse.message val errorMsg = if (msg.isNullOrEmpty()) { response.message() } else { msg } ApiErrorResponse(errorMsg ?: "Unknown error.") } } } } /** * Separate class for HTTP 204 resposes so that we can make ApiSuccessResponse's body non-null. */ class ApiEmptyResponse<CoinMarketCapType> : ApiResponse<CoinMarketCapType>() data class ApiSuccessResponse<CoinMarketCapType>(val body: CoinMarketCapType) : ApiResponse<CoinMarketCapType>() data class ApiErrorResponse<CoinMarketCapType>(val errorMessage: String) : ApiResponse<CoinMarketCapType>()

এই র‍্যাপার ক্লাসের ভিতরে, যদি আমাদের রেসপন্সে এরর থাকে, তবে আমরা Gson লাইব্রেরি ব্যবহার করে এররকে একটি JSON অবজেক্টে কনভার্ট করি। যাইহোক, যদি রেসপন্স সফল হয়, তবে JSON to POJO অবজেক্ট ম্যাপিং-এর জন্য Gson কনভার্টার ব্যবহার করা হয়। আমরা ইতিমধ্যে Dagger AppModule ফাংশন provideApiService-এ GsonConverterFactory দিয়ে Retrofit builder ইনস্ট্যান্স তৈরি করার সময় এটি যোগ করেছি।

Glide ছবি লোডিং জন্য

Glide কী? ডক্স থেকে:

Glide Android-র জন্য একটি ফাস্ট এবং ইফিশিয়েন্ট ওপেন সোর্স মিডিয়া ম্যানেজমেন্ট এবং ইমেজ লোডিং ফ্রেমওয়ার্ক যা মিডিয়া ডিকোডিং, মেমরি এবং ডিস্ক ক্যাশিং, এবং রিসোর্স পুলিং একটি সিম্পল এবং ইজি-টু-ইউজ ইন্টারফেসে মোড়ে দেয়।
Glide-এর প্রাথমিক ফোকাস হল যেকোনো ধরনের ইমেজ লিস্ট স্ক্রোলিং यथাসম্ভব স্মুথ এবং ফাস্ট করা, কিন্তু এটি প্রায় যেকোনো কেসে কার্যকর যেখানে আপনাকে রিমোট ইমেজ ফেচ, রিসাইজ, এবং ডিসপ্লে করতে হয়।

একটি জটিল লাইব্রেরির মতো শোনাচ্ছে যা অনেক উপকারী ফিচার অফার করে যা আপনি স 혼자 ডেভেলপ করতে চাইবেন না। My Crypto Coins অ্যাপে, আমাদের অনেক লিস্ট স্ক্রিন আছে যেখানে আমাদের একসাথে একাধিক ক্রিপ্টোকারেন্সি লোগো — ইন্টারনেট থেকে নেওয়া ছবি — দেখাতে হবে — এবং এখনো ব্যবহারকারীর জন্য একটি স্মুথ স্ক্রোলিং অভিজ্ঞতা নিশ্চিত করতে হবে। তাই এই লাইব্রেরি আমাদের প্রয়োজন পুরোপুরি পূরণ করে। এছাড়া এই লাইব্রেরি Android ডেভেলপারদের মধ্যে খুব জনপ্রিয়।

My Crypto Coins অ্যাপ প্রজেক্টে Glide সেটআপ করার ধাপগুলো:

ডিপেন্ডেন্সি ঘোষণা করুন

সর্বশেষ Glide ভার্সন পান। আবার versions একটি আলাদা ফাইল versions.gradle প্রজেক্টের জন্য।

// Glide implementation "com.github.bumptech.glide:glide:$versions.glide" kapt "com.github.bumptech.glide:compiler:$versions.glide" // Glide's OkHttp3 integration. implementation "com.github.bumptech.glide:okhttp3-integration:$versions.glide"+"@aar"

যেহেতু আমরা আমাদের প্রজেক্টে সব নেটওয়ার্ক অপারেশনের জন্য নেটওয়ার্কিং লাইব্রেরি OkHttp ব্যবহার করতে চাই, আমাদের স্পেসিফিক Glide ইন্টিগ্রেশন সেটি ইনক্লুড করতে হবে ডিফল্টটির বদলে। এছাড়া যেহেতু Glide ইন্টারনেটের মাধ্যমে একটি নেটওয়ার্ক রিকোয়েস্ট করবে ইমেজ লোড করার জন্য, আমাদের INTERNET পারমিশন আমাদের AndroidManifest.xml ফাইলে ইনক্লুড করতে হবে — কিন্তু আমরা Retrofit সেটআপের সাথে এটি ইতিমধ্যে করেছি।

AppGlideModule তৈরি করুন

Glide v4, যা আমরা ব্যবহার করব, অ্যাপ্লিকেশনের জন্য একটি জেনারেটেড API অফার করে। এটি একটি অ্যানোটেশন প্রসেসর ব্যবহার করে একটি API জেনারেট করে যা অ্যাপ্লিকেশনকে Glide-এর API এক্সটেন্ড করতে এবং ইন্টিগ্রেশন লাইব্রেরিগুলো দ্বারা প্রদত্ত কম্পোনেন্ট ইনক্লুড করতে দেয়। যেকোনো অ্যাপ জেনারেটেড Glide API অ্যাক্সেস করতে পারার জন্য, আমাদের একটি উপযুক্তভাবে অ্যানোটেটেড AppGlideModule ইমপ্লিমেন্টেশন ইনক্লুড করতে হবে। জেনারেটেড API-এর শুধু একটি ইমপ্লিমেন্টেশন থাকতে পারে এবং প্রতি অ্যাপ্লিকেশনে শুধু একটি AppGlideModule থাকতে পারে।

চলুন আপনার অ্যাপ প্রজেক্টে কোথাও একটি ক্লাস তৈরি করি AppGlideModule এক্সটেন্ড করে:

/** * Glide v4 uses an annotation processor to generate an API that allows applications to access all * options in RequestBuilder, RequestOptions and any included integration libraries in a single * fluent API. * * The generated API serves two purposes: * Integration libraries can extend Glide’s API with custom options. * Applications can extend Glide’s API by adding methods that bundle commonly used options. * * Although both of these tasks can be accomplished by hand by writing custom subclasses of * RequestOptions, doing so is challenging and produces a less fluent API. */ @GlideModule class AppGlideModule : AppGlideModule()

এমনকি আমাদের অ্যাপ্লিকেশন কোনো অ্যাডিশনাল সেটিংস পরিবর্তন করছে না বা AppGlideModule-এ কোনো মেথড ইমপ্লিমেন্ট করছে না, আমাদের এটিকে ব্যবহার করার জন্য এর ইমপ্লিমেন্টেশন থাকতে হবে। AppGlideModule-এ কোনো মেথড ইমপ্লিমেন্ট করতে আপনাকে বাধ্য নন API জেনারেট করার জন্য। আপনি ক্লাসটি খালি রেখে দিতে পারেন যতক্ষণ না এটি AppGlideModule এক্সটেন্ড করে এবং @GlideModule দিয়ে অ্যানোটেটেড থাকে।

Glide-জেনারেটেড API ব্যবহার করুন

AppGlideModule ব্যবহার করার সময়, অ্যাপ্লিকেশনগুলো API ব্যবহার করতে পারে সব লোড GlideApp.with() দিয়ে শুরু করে। এটি কোডটি যা দেখায় আমি কীভাবে Glide ব্যবহার করেছি অ্যাড ক্রিপ্টো কয়েন্স স্ক্রিনে সব ক্রিপ্টোকারেন্সি লিস্টে ক্রিপ্টোকারেন্সি লোগো লোড এবং দেখাতে।

class AddSearchListAdapter(val context: Context, private val cryptocurrencyClickCallback: ((Cryptocurrency) -> Unit)?) : BaseAdapter() { ... override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { ... val itemBinding: ActivityAddSearchListItemBinding ... // We make an Uri of image that we need to load. Every image unique name is its id. val imageUri = Uri.parse(CRYPTOCURRENCY_IMAGE_URL).buildUpon() .appendPath(CRYPTOCURRENCY_IMAGE_SIZE_PX) .appendPath(cryptocurrency.id.toString() + CRYPTOCURRENCY_IMAGE_FILE) .build() // Glide generated API from AppGlideModule. GlideApp // We need to provide context to make a call. .with(itemBinding.root) // Here you specify which image should be loaded by providing Uri. .load(imageUri) // The way you combine and execute multiple transformations. // WhiteBackground is our own implemented custom transformation. // CircleCrop is default transformation that Glide ships with. .transform(MultiTransformation(WhiteBackground(), CircleCrop())) // The target ImageView your image is supposed to get displayed in. .into(itemBinding.itemImageIcon.imageview_front) ... return itemBinding.root } ... }

আপনি দেখতে পাচ্ছেন, আপনি কেবল কয়েক লাইন কোড দিয়ে Glide ব্যবহার শুরু করতে পারেন এবং সেটিকে সকল কঠিন কাজ করতে দিতে পারেন। এটি খুব সোজা।

Kotlin Coroutines

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

মেইন থ্রেড একটি একক থ্রেড যা UI-এ সকল আপডেট হ্যান্ডেল করে। ডেভেলপারদের এটি ব্লক না করার জন্য বাধ্য করা হয় অ্যাপ ফ্রিজ বা Application Not Responding ডায়ালগের সাথে ক্র্যাশ হওয়া এড়াতে। Kotlin coroutines আমাদের জন্য এই সমস্যা সমাধান করবে মেইন থ্রেড সেফটি ইন্ট্রোডিউস করে। এটি শেষ মিসিং পিস যা আমরা My Crypto Coins অ্যাপে যোগ করতে চাই।

Coroutines হল একটি Kotlin ফিচার যা লং-রানিং টাস্কের জন্য অ্যাসিনক্রোনাস কলব্যাক, যেমন ডেটাবেস বা নেটওয়ার্ক অ্যাক্সেস, সিকোয়েনশিয়াল কোডে কনভার্ট করে। Coroutines-এ, আপনি অ্যাসিনক্রোনাস কোড লিখতে পারেন, যা ট্র্যাডিশনালি Callback প্যাটার্ন ব্যবহার করে লেখা হতো, একটি সিনক্রোনাস স্টাইল ব্যবহার করে। একটি ফাংশনের রিটার্ন ভ্যালু অ্যাসিনক্রোনাস কলের ফলাফল প্রদান করবে। সিকোয়েনশিয়ালি লেখা কোড সাধারণত সহজে পড়া যায়, এবং ল্যাঙ্গুয়েজ ফিচার যেমন এক্সেপশনও ব্যবহার করতে পারে।

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

প্রথমে প্রজেক্টে কোরুটিন যোগ করুন:

// Coroutines support libraries for Kotlin. // Dependencies for coroutines. implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines" // Dependency is for the special UI context that can be passed to coroutine builders that use // the main thread dispatcher to dispatch events on the main thread. implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines"

তারপর আমরা একটি অ‍্যাবস্ট্রাক্ট ক্লাস তৈরি করব যা কোনো ViewModel-এর জন্য বেস ক্লাস হবে যাকে কমন ফাংশনালিটি দরকার যেমন আমাদের কেসে কোরুটিনস:

abstract class BaseViewModel : ViewModel() { // In Kotlin, all coroutines run inside a CoroutineScope. // A scope controls the lifetime of coroutines through its job. private val viewModelJob = Job() // Since uiScope has a default dispatcher of Dispatchers.Main, this coroutine will be launched // in the main thread. val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) // onCleared is called when the ViewModel is no longer used and will be destroyed. // This typically happens when the user navigates away from the Activity or Fragment that was // using the ViewModel. override fun onCleared() { super.onCleared() // When you cancel the job of a scope, it cancels all coroutines started in that scope. // It's important to cancel any coroutines that are no longer required to avoid unnecessary // work and memory leaks. viewModelJob.cancel() } }

এখানে আমরা একটি স্পেসিফিক কোরুটিন স্কোপ তৈরি করেছি, যা এর জবের মাধ্যমে কোরুটিনের লাইফটাইম কন্ট্রোল করবে। আপনি দেখতে পাচ্ছেন, স্কোপ আপনাকে একটি ডিফল্ট ডিসপ্যাচার স্পেসিফাই করতে দেয় যা কন্ট্রোল করে কোন থ্রেড একটি কোরুটিন রান করে। ViewModel আর ব্যবহার না হলে, আমরা viewModelJob ক্যানسেল করি এবং তার সাথে uiScope-এ শুরু করা সকল কোরুটিনও ক্যানসেল হয়ে যায়।

শেষে, রিট্রাই ফাংশনালিটি ইমপ্লিমেন্ট করুন:

/** * The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. * The ViewModel class allows data to survive configuration changes such as screen rotations. */ // ViewModel will require a CryptocurrencyRepository so we add @Inject code into ViewModel constructor. class MainViewModel @Inject constructor(val context: Context, val cryptocurrencyRepository: CryptocurrencyRepository) : BaseViewModel() { ... val mediatorLiveDataMyCryptocurrencyResourceList = MediatorLiveData<Resource<List<MyCryptocurrency>>>() private var liveDataMyCryptocurrencyResourceList: LiveData<Resource<List<MyCryptocurrency>>> private val liveDataMyCryptocurrencyList: LiveData<List<MyCryptocurrency>> ... // This is additional helper variable to deal correctly with currency spinner and preference. // It is kept inside viewmodel not to be lost because of fragment/activity recreation. var newSelectedFiatCurrencyCode: String? = null // Helper variable to store state of swipe refresh layout. var isSwipeRefreshing: Boolean = false init { ... // Set a resource value for a list of cryptocurrencies that user owns. liveDataMyCryptocurrencyResourceList = cryptocurrencyRepository.getMyCryptocurrencyLiveDataResourceList(cryptocurrencyRepository.getCurrentFiatCurrencyCode()) // Declare additional variable to be able to reload data on demand. mediatorLiveDataMyCryptocurrencyResourceList.addSource(liveDataMyCryptocurrencyResourceList) { mediatorLiveDataMyCryptocurrencyResourceList.value = it } ... } ... /** * On retry we need to run sequential code. First we need to get owned crypto coins ids from * local database, wait for response and only after it use these ids to make a call with * retrofit to get updated owned crypto values. This can be done using Kotlin Coroutines. */ fun retry(newFiatCurrencyCode: String? = null) { // Here we store new selected currency as additional variable or reset it. // Later if call to server is unsuccessful we will reuse it for retry functionality. newSelectedFiatCurrencyCode = newFiatCurrencyCode // Launch a coroutine in uiScope. uiScope.launch { // Make a call to the server after some delay for better user experience. updateMyCryptocurrencyList(newFiatCurrencyCode, SERVER_CALL_DELAY_MILLISECONDS) } } // Refresh the data from local database. fun refreshMyCryptocurrencyResourceList() { refreshMyCryptocurrencyResourceList(cryptocurrencyRepository.getMyCryptocurrencyLiveDataResourceList(cryptocurrencyRepository.getCurrentFiatCurrencyCode())) } // To implement a manual refresh without modifying your existing LiveData logic. private fun refreshMyCryptocurrencyResourceList(liveData: LiveData<Resource<List<MyCryptocurrency>>>) { mediatorLiveDataMyCryptocurrencyResourceList.removeSource(liveDataMyCryptocurrencyResourceList) liveDataMyCryptocurrencyResourceList = liveData mediatorLiveDataMyCryptocurrencyResourceList.addSource(liveDataMyCryptocurrencyResourceList) { mediatorLiveDataMyCryptocurrencyResourceList.value = it } } private suspend fun updateMyCryptocurrencyList(newFiatCurrencyCode: String? = null, callDelay: Long = 0) { val fiatCurrencyCode: String = newFiatCurrencyCode ?: cryptocurrencyRepository.getCurrentFiatCurrencyCode() isSwipeRefreshing = true // The function withContext is a suspend function. The withContext immediately shifts // execution of the block into different thread inside the block, and back when it // completes. IO dispatcher is suitable for execution the network requests in IO thread. val myCryptocurrencyIds = withContext(Dispatchers.IO) { // Suspend until getMyCryptocurrencyIds() returns a result. cryptocurrencyRepository.getMyCryptocurrencyIds() } // Here we come back to main worker thread. As soon as myCryptocurrencyIds has a result // and main looper is available, coroutine resumes on main thread, and // [getMyCryptocurrencyLiveDataResourceList] is called. // We wait for background operations to complete, without blocking the original thread. refreshMyCryptocurrencyResourceList( cryptocurrencyRepository.getMyCryptocurrencyLiveDataResourceList (fiatCurrencyCode, true, myCryptocurrencyIds, callDelay)) } ... }

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

এছাড়া, একটি সাসপেন্ড ফাংশনে আমরা আরেকটি সাসপেন্ড ফাংশন কল করতে পারি। আপনি দেখতে পাচ্ছেন আমরা সেটি withContext দিয়ে করছি যেটি সাসপেন্ড ফাংশন হিসেবে চিহ্নিত এবং অন্য থ্রেডে এক্সিকিউট হয়।

এই সকল কোডের আইডিয়া হল আমরা একাধিক কল কম্বাইন করতে পারি সুন্দর সিকোয়েনশিয়াল কোড বানাতে। প্রথম আমরা লোকাল ডেটাবেস থেকে আমাদের স্বামित्ववाले ক্রিপ্টোকারেন্সির আইডিগুলো পাওয়ার জন্য রিকোয়েস্ট করি এবং রেসপন্সের জন্য অপেক্ষা করি। শুধু সেটি পাওয়ার পর আমরা রেসপন্স আইডিগুলো ব্যবহার করে Retrofit-এ একটি নতুন কল করি সেগুলোর আপডেটেড ক্রিপ্টোকারেন্সি ভ্যালু পেতে। এটি আমাদের রিট্রাই ফাংশনালিটি।

আমরা সফল হলাম! চূড়ান্ত চিন্তা, রিপোজিটরি, অ্যাপ এবং প্রেজেন্টেশন

অভিনন্দন, আমি খুশি হব যদি আপনি শেষ পর্যন্ত পৌঁছাতে পারেন। এই অ্যাপ তৈরি করার জন্য সবচেয়ে গুরুপূর্ণ পয়েন্টগুলো কভার করা হয়েছে। এই পার্টে অনেক নতুন জিনিস করা হয়েছে এবং 그중 অনেক এই নিবন্ধে কভার করা নেই, কিন্তু আমি আমার কোডে সব জায়গা খুব ভালোভাবে কমেন্ট করেছি তাই আপনি এটিতে হারাবেন না। এই পার্ট ৫-এর ফাইনাল কোড GitHub-এ দেখুন:

View Source On GitHub.

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

এই সকল আর্টিকেল "Kriptofolio" (পূর্বে "My Crypto Coins") অ্যাপের ভার্সন ১.০.০-র ওপর ভিত্তি করে যা আপনি একটি আলাদা APK ফাইল হিসেবে এখান থেকে ডাউনলোড করতে পারেন। কিন্তু আমি খুব খুশি হব যদি আপনি লেটেস্ট অ্যাপ ভার্সন স্টোর থেকে সরাসরি ইনস্টল এবং রেট করেন:

Get It On Google Play

এছাড়া অনুগ্রহ করে এই সিম্পল প্রেজেন্টেশন ওয়েবসাইট ভিজিট করুন যা আমি এই প্রজেক্টের জন্য বানিয়েছি:

Kriptofolio.app

Android-এ RESTful Services 습득: Retrofit, OkHttp, Gson, Glide এবং Coroutines

Ačiū! পড়ার জন্য ধন্যবাদ! আমি মূলত এই পোস্টটি আমার পারসোনাল ব্লগ www.baruckis.com-এ মে ১১, ২০১৯-এ পাবলিশ করেছিলাম।


  1. স্যামসাং মেম্বারস অ্যাপ দিয়ে স্যামসাং ফোনের সমস্যা ডায়াগনোজ ও ফিক্স করুন

  2. Edge Insider-এ PWA ইনস্টল ও ব্যবহার করার ধাপে ধাপে গাইড

  3. Outlook Web App-এ আউট-অফ-অফিস অটো-রিপ্লাই সেট করুন: দ্রুত, সহজ ধাপ

  4. জিমেইলের সাইডবার থেকে Google Meet আইকন সহজে সরান: ওয়েব ও মোবাইল