Computer >> คอมพิวเตอร์ >  >> ระบบ >> Android

วิธีจัดการ RESTful web Services โดยใช้ Retrofit, OkHttp, Gson, Glide และ Coroutines

ชุดแอป Kriptofolio — ตอนที่ 5

ทุกวันนี้ แอพ Android เกือบทั้งหมดเชื่อมต่ออินเทอร์เน็ตเพื่อรับ/ส่งข้อมูล คุณควรเรียนรู้วิธีจัดการกับ RESTful Web Services อย่างแน่นอน เนื่องจากการใช้งานที่ถูกต้องคือความรู้หลักในขณะที่สร้างแอปที่ทันสมัย

ส่วนนี้จะซับซ้อน เราจะรวมหลายไลบรารีพร้อมกันเพื่อให้ได้ผลลัพธ์การทำงาน ฉันจะไม่พูดถึงวิธีดั้งเดิมของ Android ในการจัดการคำขอทางอินเทอร์เน็ตเพราะในโลกแห่งความเป็นจริงไม่มีใครใช้ ทุกแอปที่ดีไม่ได้พยายามสร้างวงล้อใหม่ แต่ใช้ไลบรารีของบุคคลที่สามที่ได้รับความนิยมมากที่สุดแทนเพื่อแก้ปัญหาทั่วไป คงจะซับซ้อนเกินไปที่จะสร้างฟังก์ชันการทำงานขึ้นมาใหม่ซึ่งไลบรารีที่สร้างมาอย่างดีเหล่านี้มีให้

เนื้อหาซีรีส์

  • บทนำ:แผนงานในการสร้างแอป Android ที่ทันสมัยในปี 2018–2019
  • ส่วนที่ 1:การแนะนำหลักการ SOLID
  • ส่วนที่ 2:วิธีเริ่มสร้างแอป Android:การสร้าง Mockups, UI และเลย์เอาต์ XML
  • ส่วนที่ 3:ทั้งหมดเกี่ยวกับสถาปัตยกรรมนั้น:สำรวจรูปแบบสถาปัตยกรรมต่างๆ และวิธีใช้งานในแอปของคุณ
  • ส่วนที่ 4:วิธีการใช้ Dependency Injection ในแอปของคุณด้วย Dagger 2
  • ส่วนที่ 5:จัดการ RESTful Web Services โดยใช้ Retrofit, OkHttp, Gson, Glide และ Coroutines (คุณอยู่ที่นี่)

Retrofit, OkHttp และ Gson คืออะไร

Retrofit เป็นไคลเอ็นต์ REST สำหรับ Java และ Android ในความคิดของฉัน ห้องสมุดนี้เป็นห้องสมุดที่สำคัญที่สุดในการเรียนรู้ เพราะห้องสมุดนี้จะทำหน้าที่หลัก ทำให้ง่ายต่อการดึงและอัปโหลด JSON (หรือข้อมูลที่มีโครงสร้างอื่นๆ) ผ่านเว็บเซอร์วิสที่ใช้ REST

ใน Retrofit คุณกำหนดค่าตัวแปลงที่ใช้สำหรับข้อมูลซีเรียลไลซ์เซชั่น โดยปกติในการทำให้เป็นอนุกรมและดีซีเรียลไลซ์ออบเจ็กต์เข้าและออกจาก JSON คุณใช้ไลบรารี Java แบบโอเพนซอร์ส — Gson นอกจากนี้ หากต้องการ คุณสามารถเพิ่มตัวแปลงแบบกำหนดเองลงใน Retrofit เพื่อประมวลผล XML หรือโปรโตคอลอื่นๆ

สำหรับการขอ HTTP Retrofit ใช้ไลบรารี OkHttp OkHttp เป็นไคลเอนต์ HTTP/SPDY แท้ ๆ ที่รับผิดชอบการทำงานของเครือข่ายระดับต่ำ การแคช คำขอ และการจัดการการตอบสนอง ในทางตรงกันข้าม Retrofit เป็นการสร้างนามธรรม REST ระดับสูงที่ด้านบนของ OkHttp ชุดติดตั้งเพิ่มเติมมาพร้อมกับ OkHttp และใช้งานอย่างเข้มข้น

ตอนนี้คุณรู้แล้วว่าทุกอย่างมีความเกี่ยวข้องกันอย่างใกล้ชิด เราจะใช้ไลบรารีทั้ง 3 นี้พร้อมกัน เป้าหมายแรกของเราคือการรับรายการ cryptocurrencies ทั้งหมดโดยใช้ Retrofit จากอินเทอร์เน็ต เราจะใช้คลาสตัวสกัดกั้น OkHttp พิเศษสำหรับการตรวจสอบความถูกต้องของ CoinMarketCap API เมื่อทำการเรียกไปยังเซิร์ฟเวอร์ เราจะได้ผลลัพธ์ข้อมูล JSON กลับมาแล้วแปลงโดยใช้ไลบรารี Gson

การตั้งค่าด่วนสำหรับ Retrofit 2 เพียงเพื่อลองใช้ก่อน

เมื่อเรียนรู้สิ่งใหม่ ๆ ฉันชอบที่จะลองปฏิบัติให้เร็วที่สุดเท่าที่จะทำได้ เราจะใช้วิธีเดียวกันกับ Retrofit 2 เพื่อให้คุณเข้าใจได้ดีขึ้นอย่างรวดเร็ว อย่าเพิ่งกังวลเกี่ยวกับคุณภาพของโค้ดหรือหลักการเขียนโปรแกรมหรือการเพิ่มประสิทธิภาพใดๆ เราจะเขียนโค้ดเพื่อให้ Retrofit 2 ทำงานในโครงการของเราและหารือเกี่ยวกับสิ่งที่ทำ

ทำตามขั้นตอนเหล่านี้เพื่อตั้งค่า Retrofit 2 ในโครงการแอป My Crypto Coins:

ขั้นแรก ให้อนุญาต INTERNET สำหรับแอป

เราจะดำเนินการตามคำขอ HTTP บนเซิร์ฟเวอร์ที่สามารถเข้าถึงได้ผ่านทางอินเทอร์เน็ต ให้สิทธิ์นี้โดยเพิ่มบรรทัดเหล่านี้ในไฟล์มานิเฟสต์ของคุณ:

<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 เราจึงต้องรวมตัวแปลงด้วยตนเองในการขึ้นต่อกันด้วย เราจะใช้ JSON converter Gson เวอร์ชันล่าสุดของ Google มาเพิ่มบรรทัดเหล่านี้ในไฟล์ 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 แล้ว เวอร์ชันเป็นเพียงไฟล์ 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.
}

และตั้งค่าคลาสข้อมูล

คลาสข้อมูลคือ POJO (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
    )
}

สร้างคลาส interceptor พิเศษสำหรับการพิสูจน์ตัวตนเมื่อทำการเรียกไปยัง เซิฟเวอร์

นี่เป็นกรณีเฉพาะสำหรับ API ใด ๆ ที่ต้องมีการรับรองความถูกต้องเพื่อให้ได้รับการตอบกลับที่ประสบความสำเร็จ Interceptors เป็นวิธีที่มีประสิทธิภาพในการปรับแต่งคำขอของคุณ เราจะสกัดกั้นคำขอจริงและเพิ่มส่วนหัวของคำขอแต่ละรายการ ซึ่งจะตรวจสอบการเรียกด้วยคีย์ API ที่จัดหาโดย CoinMarketCap Professional API Developer Portal คุณต้องลงทะเบียนที่นั่นเพื่อรับรางวัลของคุณ

/**
 * 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()

            }

        })

    }

   ...
}

คุณสามารถสำรวจรหัสได้ที่นี่ โปรดจำไว้ว่านี่เป็นเพียงเวอร์ชันเริ่มต้นที่ใช้งานง่ายเพื่อให้คุณได้รับแนวคิดที่ดีขึ้น

การตั้งค่าที่ถูกต้องขั้นสุดท้ายสำหรับ Retrofit 2 ด้วย OkHttp 3 และ Gson

หลังจากการทดลองอย่างรวดเร็ว ก็ถึงเวลาที่จะนำการติดตั้ง Retrofit นี้ไปใช้ในระดับต่อไป เราได้ข้อมูลเรียบร้อยแล้วแต่ไม่ถูกต้อง เราไม่มีสถานะเช่นการโหลด ข้อผิดพลาด และความสำเร็จ รหัสของเราผสมกันโดยไม่ต้องกังวลใจ เป็นข้อผิดพลาดทั่วไปในการเขียนโค้ดทั้งหมดของคุณในกิจกรรมหรือส่วนย่อย คลาสกิจกรรมของเราเป็นแบบ UI และควรมีเฉพาะตรรกะที่จัดการการโต้ตอบของ UI และระบบปฏิบัติการ

อันที่จริง หลังจากการตั้งค่าอย่างรวดเร็วนี้ ฉันทำงานหลายอย่างและทำการเปลี่ยนแปลงหลายอย่าง ไม่จำเป็นต้องใส่รหัสทั้งหมดที่มีการเปลี่ยนแปลงในบทความ คุณควรเรียกดู repo โค้ดส่วนที่ 5 สุดท้ายที่นี่แทน ฉันได้แสดงความคิดเห็นทุกอย่างเป็นอย่างดีและรหัสของฉันควรชัดเจนเพื่อให้คุณเข้าใจ แต่ฉันจะพูดถึงสิ่งที่สำคัญที่สุดที่ฉันทำและเหตุผลที่ฉันทำ

ขั้นตอนแรกในการปรับปรุงคือการเริ่มใช้ Dependency Injection จำจากส่วนก่อนหน้านี้ เรามี Dagger 2 ถูกใช้งานภายในโปรเจ็กต์อย่างถูกต้องแล้ว ดังนั้นฉันจึงใช้มันสำหรับการติดตั้งชุดติดตั้งเพิ่มเติม

/**
 * 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 พิเศษโดยใช้ 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)
    }
}

ตอนนี้เราจะได้ LiveData<T> แทน Call<T> เป็นประเภทการส่งคืนจากวิธีการบริการ Retrofit ที่กำหนดไว้ใน ApiService อินเทอร์เฟซ

ขั้นตอนสำคัญอีกประการหนึ่งที่ต้องทำคือการเริ่มใช้รูปแบบพื้นที่เก็บข้อมูล ฉันได้พูดถึงมันไปแล้วในตอนที่ 3 ลองดูสคีมาสถาปัตยกรรม MVVM ของเราจากโพสต์นั้นเพื่อจดจำว่ามันไปที่ไหน

วิธีจัดการ RESTful web Services โดยใช้ Retrofit, OkHttp, Gson, Glide และ Coroutines

ดังที่คุณเห็นในรูปภาพ Repository เป็นชั้นที่แยกจากกันสำหรับข้อมูล เป็นแหล่งติดต่อเดียวของเราในการรับหรือส่งข้อมูล เมื่อเราใช้ Repository เรากำลังปฏิบัติตามหลักการแยกข้อกังวล เราสามารถมีแหล่งข้อมูลที่แตกต่างกันได้ (เช่น ในกรณีของเรา ข้อมูลถาวรจากฐานข้อมูล SQLite และข้อมูลจากบริการเว็บ) แต่ Repository จะเป็นแหล่งความจริงแหล่งเดียวสำหรับข้อมูลแอปทั้งหมดเสมอ

แทนที่จะสื่อสารกับการติดตั้ง Retrofit ของเราโดยตรง เราจะใช้ 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 .

วิธีจัดการ RESTful web Services โดยใช้ Retrofit, OkHttp, Gson, Glide และ Coroutines
NetworkBoundResource

มาดูกันดีกว่าว่า NetworkBoundResource จะทำงานในแอปของเรา ลองนึกภาพว่าผู้ใช้จะเปิดแอปและคลิกที่ปุ่มการกระทำแบบลอยตัวที่มุมล่างขวา แอพจะเปิดหน้าจอเพิ่มเหรียญ crypto ตอนนี้เราสามารถวิเคราะห์ NetworkBoundResource การใช้งานภายใน

หากแอปได้รับการติดตั้งใหม่และเป็นการเปิดตัวครั้งแรก จะไม่มีข้อมูลใด ๆ จัดเก็บอยู่ในฐานข้อมูลภายในเครื่อง เนื่องจากไม่มีข้อมูลที่จะแสดง UI ของแถบความคืบหน้าในการโหลดจะปรากฏขึ้น ในขณะเดียวกันแอพจะทำการเรียกคำขอไปยังเซิร์ฟเวอร์ผ่านบริการเว็บเพื่อรับรายการ cryptocurrencies ทั้งหมด

หากการตอบสนองไม่สำเร็จ UI ข้อความแสดงข้อผิดพลาดจะแสดงพร้อมความสามารถในการลองโทรอีกครั้งโดยกดปุ่ม เมื่อการเรียกคำขอสำเร็จในที่สุด ข้อมูลการตอบกลับจะถูกบันทึกลงในฐานข้อมูล SQLite ในเครื่อง

หากเรากลับมาที่หน้าจอเดิมในครั้งต่อไป แอพจะโหลดข้อมูลจากฐานข้อมูลแทนการโทรเข้าอินเทอร์เน็ตอีกครั้ง แต่ผู้ใช้สามารถขออัปเดตข้อมูลใหม่ได้โดยใช้ฟังก์ชันดึงเพื่อรีเฟรช ข้อมูลเก่าจะแสดงขึ้นในขณะที่กำลังโทรผ่านเครือข่าย ทั้งหมดนี้ทำได้ด้วยความช่วยเหลือของ NetworkBoundResource .

คลาสอื่นที่ใช้ใน Repository และ LiveDataCallAdapter . ของเรา โดยที่ "เวทย์มนตร์" ทั้งหมดเกิดขึ้นคือ ApiResponse . จริงๆ แล้ว ApiResponse เป็นเพียง wrapper ทั่วไปรอบ ๆ 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>()

ภายในคลาส wrapper นี้ หากการตอบสนองของเรามีข้อผิดพลาด เราใช้ไลบรารี Gson เพื่อแปลงข้อผิดพลาดเป็นวัตถุ JSON อย่างไรก็ตาม หากการตอบสนองสำเร็จ จะใช้ตัวแปลง Gson สำหรับการแมปวัตถุ JSON เป็น POJO เราได้เพิ่มเข้าไปแล้วเมื่อสร้างอินสแตนซ์ตัวสร้างชุดติดตั้งเพิ่มเติมด้วย GsonConverterFactory ภายในกริช AppModule ฟังก์ชัน provideApiService .

เลื่อนเพื่อโหลดรูปภาพ

Glide คืออะไร? จากเอกสาร:

Glide คือการจัดการสื่อโอเพนซอร์สที่รวดเร็วและมีประสิทธิภาพและเฟรมเวิร์กการโหลดรูปภาพสำหรับ Android ที่รวมการถอดรหัสสื่อ การแคชหน่วยความจำและดิสก์ และการรวมทรัพยากรไว้ในอินเทอร์เฟซที่เรียบง่ายและใช้งานง่าย
จุดสนใจหลักของ Glide คือการทำให้การเลื่อนรายการรูปภาพทุกประเภทเป็นไปอย่างราบรื่นและรวดเร็วที่สุด แต่ยังมีประสิทธิภาพสำหรับเกือบทุกกรณีที่คุณต้องการดึง ปรับขนาด และแสดงรูปภาพระยะไกล

ดูเหมือนห้องสมุดที่ซับซ้อนซึ่งมีฟีเจอร์ที่มีประโยชน์มากมายที่คุณไม่อยากพัฒนาทั้งหมดด้วยตัวเอง ในแอพ My Crypto Coins เรามีหน้าจอรายการหลายหน้าจอที่เราจำเป็นต้องแสดงโลโก้ cryptocurrency หลายอัน — ภาพที่ถ่ายจากอินเทอร์เน็ตทั้งหมดในคราวเดียว — และยังคงให้ประสบการณ์การเลื่อนที่ราบรื่นสำหรับผู้ใช้ ดังนั้นห้องสมุดนี้จึงเหมาะกับความต้องการของเราอย่างสมบูรณ์แบบ นอกจากนี้ ห้องสมุดนี้ยังเป็นที่นิยมในหมู่นักพัฒนา Android

ขั้นตอนในการตั้งค่า Glide ในโครงการแอป My Crypto Coins:

ประกาศการพึ่งพา

รับ Glide เวอร์ชันล่าสุด อีกครั้งเวอร์ชันเป็นไฟล์แยกต่างหาก 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 ที่อนุญาตให้แอปพลิเคชันขยาย API ของ Glide และรวมส่วนประกอบที่มีให้โดยไลบรารีการผสานรวม สำหรับแอปใดๆ เพื่อเข้าถึง 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 เรายังต้องมีการนำไปปฏิบัติเพื่อใช้ Glide คุณไม่จำเป็นต้องใช้วิธีใดๆ ใน AppGlideModule เพื่อให้ API ถูกสร้างขึ้น คุณสามารถเว้นชั้นเรียนว่างไว้ได้ตราบเท่าที่ขยาย AppGlideModule และมีคำอธิบายประกอบด้วย @GlideModule .

ใช้ API ที่สร้างโดย Glide

เมื่อใช้ AppGlideModule , แอปพลิเคชันสามารถใช้ API ได้โดยเริ่มการโหลดทั้งหมดด้วย GlideApp.with() . นี่คือรหัสที่แสดงวิธีที่ฉันใช้ Glide เพื่อโหลดและแสดงโลโก้สกุลเงินดิจิทัลในหน้าจอเพิ่มเหรียญ crypto ในรายการ cryptocurrencies ทั้งหมด

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 คุณสามารถเขียนโค้ดแบบอะซิงโครนัส ซึ่งปกติแล้วจะเขียนโดยใช้รูปแบบการโทรกลับ โดยใช้รูปแบบซิงโครนัส ค่าที่ส่งคืนของฟังก์ชันจะให้ผลลัพธ์ของการเรียกแบบอะซิงโครนัส โดยทั่วไปแล้วโค้ดที่เขียนตามลำดับจะอ่านง่ายกว่า และสามารถใช้คุณลักษณะภาษา เช่น ข้อยกเว้นได้

ดังนั้นเราจะใช้ coroutines ทุกที่ในแอพนี้ซึ่งเราต้องรอจนกว่าผลลัพธ์จะพร้อมใช้งานจากงานที่ใช้เวลานานและดำเนินการต่อไป มาดูการใช้งานที่แน่นอนอย่างหนึ่งสำหรับ ViewModel ของเรา ซึ่งเราจะลองรับข้อมูลล่าสุดจากเซิร์ฟเวอร์สำหรับสกุลเงินดิจิตอลของเราที่แสดงบนหน้าจอหลักอีกครั้ง

ขั้นแรกให้เพิ่ม coroutines ให้กับโปรเจ็กต์:

// 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 ใด ๆ ที่จำเป็นต้องมีฟังก์ชันการทำงานทั่วไปเช่น coroutines ในกรณีของเรา:

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()
    }
}

ที่นี่เราสร้างขอบเขต coroutine เฉพาะ ซึ่งจะควบคุมอายุของ coroutines ผ่านงานของมัน อย่างที่คุณเห็น ขอบเขตช่วยให้คุณระบุโปรแกรมเลือกจ่ายงานเริ่มต้นที่ควบคุมว่าเธรดใดเรียกใช้ coroutine เมื่อเลิกใช้ ViewModel แล้ว เราจะยกเลิก viewModelJob และด้วยสิ่งนั้นทุก coroutine ที่เริ่มต้นโดย 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 ที่ทำงานบนเธรดอื่น

แนวคิดของโค้ดทั้งหมดนี้ก็คือ เราสามารถรวมการเรียกหลายๆ ครั้งเข้าด้วยกันเพื่อสร้างโค้ดตามลำดับที่ดูดีได้ ก่อนอื่นเราขอรหัสของ cryptocurrencies ที่เราเป็นเจ้าของจากฐานข้อมูลในเครื่องและรอการตอบกลับ หลังจากที่เราได้รับแล้ว เราจะใช้รหัสตอบกลับเพื่อโทรใหม่ด้วย Retrofit เพื่อรับค่าสกุลเงินดิจิทัลที่อัปเดตเหล่านั้น นั่นคือฟังก์ชันลองใหม่ของเรา

เราสร้างมันขึ้นมา! ความคิดสุดท้าย พื้นที่เก็บข้อมูล แอป และการนำเสนอ

ยินดีด้วย ฉันดีใจที่คุณทำสำเร็จจนสำเร็จ ทุกประเด็นที่สำคัญที่สุดสำหรับการสร้างแอพนี้ได้รับการคุ้มครอง ในส่วนนี้มีสิ่งใหม่ๆ มากมายที่ทำเสร็จแล้ว และมีหลายสิ่งที่ไม่ครอบคลุมในบทความนี้ แต่ฉันได้แสดงความคิดเห็นเกี่ยวกับโค้ดของฉันทุกที่เป็นอย่างดี ดังนั้นคุณไม่ควรหลงทาง ตรวจสอบรหัสสุดท้ายสำหรับส่วนที่ 5 นี้บน GitHub:

ดูแหล่งที่มาบน GitHub

ความท้าทายที่ยิ่งใหญ่ที่สุดสำหรับฉันเป็นการส่วนตัวไม่ใช่การเรียนรู้เทคโนโลยีใหม่ ไม่ใช่เพื่อพัฒนาแอพ แต่เพื่อเขียนบทความทั้งหมดเหล่านี้ อันที่จริงฉันมีความสุขมากกับตัวเองที่ได้ทำภารกิจท้าทายนี้สำเร็จ การเรียนรู้และพัฒนาเป็นเรื่องง่ายเมื่อเปรียบเทียบกับการสอนผู้อื่น แต่นั่นคือสิ่งที่คุณสามารถเข้าใจหัวข้อได้ดียิ่งขึ้น คำแนะนำของฉันหากคุณกำลังมองหาวิธีที่ดีที่สุดในการเรียนรู้สิ่งใหม่ ๆ คือการเริ่มสร้างบางสิ่งด้วยตนเองทันที ฉันสัญญาว่าคุณจะได้เรียนรู้มากมายและรวดเร็ว

บทความทั้งหมดเหล่านี้ใช้เวอร์ชัน 1.0.0 ของแอพ “Kriptofolio” (ก่อนหน้านี้คือ “My Crypto Coins”) ซึ่งคุณสามารถดาวน์โหลดเป็นไฟล์ APK แยกต่างหากได้ที่นี่ แต่จะยินดีเป็นอย่างยิ่งหากคุณติดตั้งและให้คะแนนแอปเวอร์ชันล่าสุดจากสโตร์โดยตรง:

ดาวน์โหลดบน Google Play

นอกจากนี้ โปรดเยี่ยมชมเว็บไซต์การนำเสนอง่ายๆ ที่ฉันทำขึ้นสำหรับโครงการนี้:

Kriptofolio.app

วิธีจัดการ RESTful web Services โดยใช้ Retrofit, OkHttp, Gson, Glide และ Coroutines

อาชิอู! ขอบคุณที่อ่าน! ตอนแรกฉันเผยแพร่โพสต์นี้สำหรับบล็อกส่วนตัวของฉัน www.baruckis.com เมื่อวันที่ 11 พฤษภาคม 2019