1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        這篇看完,Paging3還不會你捶我!

        共 28769字,需瀏覽 58分鐘

         ·

        2021-03-26 01:09

        作者:黃林晴

        https://huanglinqing.blog.csdn.net/

        Paging是什么?

        想想我們之前的業(yè)務(wù)中,實現(xiàn)分頁加載需要怎么處理?一般我們都是自己封裝RecycleView或者使用XRecycleView這種第三方庫去做,而Paging 就是Google為我們提供的分頁功能的標(biāo)準(zhǔn)庫,這樣我們就無須自己去基于RecycleView實現(xiàn)分頁功能,并且Paging為我們提供了許多可配置選項,使得分頁功能更加靈活。而Paging3是Paging庫當(dāng)前的最新版本,仍處于測試版本,相比較于Paging2的使用就簡潔多了。

        Paging的使用

        項目搭建

        首先我們新建項目,在gradle中引用paging庫如下:


        def paging_version = "3.0.0-alpha07"
        implementation "androidx.paging:paging-runtime:$paging_version"
        testImplementation "androidx.paging:paging-common:$paging_version"


        項目示例,我們使用Kotlin語言并且使用了協(xié)程和Flow,所以也需要添加協(xié)程的庫如下:


        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7-mpp-dev-11'


        項目示例

        在官方文檔中也給出了我們Paging在架構(gòu)中的使用圖。



        通過上圖我們也可以清晰的看出來,Paging在倉庫層、ViewModel和UI層都有具體的表現(xiàn),接下來我們通過一個示例來逐步講解Paging是如何在項目架構(gòu)中工作的。

        API接口準(zhǔn)備

        這里我們已經(jīng)寫好了RetrofitService類用于創(chuàng)建網(wǎng)絡(luò)請求的service代碼如下所示:


        object RetrofitService {


            /**
             * okhttp client
             */

            lateinit var okHttpClient: OkHttpClient

            /**
             * 主Url地址
             */

            private const val BASEAPI = "https://www.wanandroid.com/";


            /**
             * 創(chuàng)建service對象
             */

            fun <T> createService(mClass: Class<T>): T {
                val builder: OkHttpClient.Builder = OkHttpClient.Builder();
                okHttpClient = builder.build()
                val retrofit: Retrofit = Retrofit.Builder()
                    .baseUrl(BASEAPI)
                    .client(okHttpClient)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
                return retrofit.create(mClass) as T
            }

        }


        和 DataApi接口,這里我們將方法聲明為掛起函數(shù),便于在協(xié)程中調(diào)用。


        interface DataApi {

            /**
             * 獲取數(shù)據(jù)
             */

            @GET("wenda/list/{pageId}/json")
            suspend fun getData(@Path("pageId") pageId:Int): DemoReqData
        }

        定義數(shù)據(jù)源

        首先我們來定義數(shù)據(jù)源DataSource繼承自PagingSource,代碼如下所示:


        class DataSource():PagingSource<Int,DemoReqData.DataBean.DatasBean>(){
            override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DemoReqData.DataBean.DatasBean> {
                TODO("Not yet implemented")
            }
        }


        我們可以看到PagingSource中有兩個參數(shù)Key 和 Value,這里Key我們定義為Int類型Value DemoReqData 是接口返回數(shù)據(jù)對應(yīng)的實體類,這里的意思就是我們傳Int類型的值(如頁碼)得到返回的數(shù)據(jù)信息DemoReqData對象。


        這里需要提醒的是如果你使用的不是Kotlin 協(xié)程而是Java,則需要繼承對應(yīng)的PagingSource如RxPagingSource或ListenableFuturePagingSource。


        DataSource為我們自動生成了load方法,我們主要的請求操作就在load方法中完成。主要代碼如下所示:


        override suspend fun load(params: LoadParams<Int>): LoadResult<Int, DemoReqData.DataBean.DatasBean> {

            return try {

                //頁碼未定義置為1
                var currentPage = params.key ?: 1
                //倉庫層請求數(shù)據(jù)
                var demoReqData = DataRespority().loadData(currentPage)
                //當(dāng)前頁碼 小于 總頁碼 頁面加1
                var nextPage = if (currentPage < demoReqData?.data?.pageCount ?: 0) {
                    currentPage + 1
                } else {
                    //沒有更多數(shù)據(jù)
                    null
                }
                if (demoReqData != null) {
                    LoadResult.Page(
                        data = demoReqData.data.datas,
                        prevKey = null,
                        nextKey = nextPage
                    )
                } else {
                    LoadResult.Error(throwable = Throwable())
                }
            } catch (e: Exception) {
                LoadResult.Error(throwable = e)
            }
        }


        上面代碼我們可以看到在datasource中我們通過DataRespority()倉庫層,去請求數(shù)據(jù),如果沒有更多數(shù)據(jù)就返回null,最后使用 LoadResult.Page將結(jié)果返回,如果加載失敗則用LoadResult.Error返回,由于 LoadResult.Page中的data 必須是非空類型的,所以我們需要判斷返回是否為null。


        接下來我們看下DataRespority倉庫層的代碼,代碼比較簡單,如下所示:


        class DataRespority {

            private var netWork = RetrofitService.createService(
                DataApi::class.java
            )

            /**
             * 查詢護理數(shù)據(jù)
             */

            suspend fun loadData(
                pageId: Int
            )
        : DemoReqData? {
                return try {
                    netWork.getData(pageId)
                } catch (e: Exception) {
                    //在這里處理或捕獲異常
                    null
                }

            }
        }


        Load調(diào)用官方給出的流程如下所示:



        從上圖可以知道,load的方法 是我們通過Paging的配置自動觸發(fā)的,不需要我們每次去調(diào)用,那么我們?nèi)绾蝸硎褂肈ataSource呢?


        調(diào)用PagingSource

        The Pager object calls the load() method from the PagingSource object, providing it with the LoadParams object and receiving the LoadResult object in return.


        這句話翻譯過來的意思就是:Pager對象從PagingSource對象調(diào)用load()方法,為它提供LoadParams對象,并作為回報接收LoadResult對象。


        所以我們在創(chuàng)建viewModel對象,并創(chuàng)建pager對象從而調(diào)用PagingSource方法 ,代碼如下所示:


        class MainActivityViewModel : ViewModel() {

            /**
             * 獲取數(shù)據(jù)
             */

            fun getData() = Pager(PagingConfig(pageSize = 1)) {
                DataSource()
            }.flow
        }


        在viewmodel中我們定義了一個getData的方法,Pager中通過配置PagingConfig來實現(xiàn)特殊的定制,我們來看下PagingConfig中的參數(shù)如下:


        pageSize:定義從PagingSource一次加載的項目數(shù)。


        prefetchDistance:預(yù)取距離,簡單解釋就是 當(dāng)距離底部還有多遠(yuǎn)的時候自動加載下一頁,即自動調(diào)用load方法,默認(rèn)值和pageSize相等。


        enablePlaceholders:是否顯示占位符,當(dāng)網(wǎng)絡(luò)不好的時候,可以考到頁面的框架,從而提升用戶體驗。


        還有一些其他參數(shù)這里就不一一介紹了,從構(gòu)造方法的源碼中可以看出pageSize這個參數(shù)是必填的,其他的是可選項,所以我們這里傳了1。

        定義RecycleViewAdapter

        這一步,和我們平時定義普通的RecycleViewAdapter沒有太大的區(qū)別,只是我們繼承的是PagingDataAdapter,主要代碼如下所示:


        class DataRecycleViewAdapter :
            PagingDataAdapter<DemoReqData.DataBean.DatasBean, RecyclerView.ViewHolder>
        (object :
                DiffUtil.ItemCallback<DemoReqData.DataBean.DatasBean>() {

                override fun areItemsTheSame(
                    oldItem: DemoReqData.DataBean.DatasBean,
                    newItem: DemoReqData.DataBean.DatasBean
                )
        Boolean {
                    return oldItem.id == newItem.id
                }

                @SuppressLint("DiffUtilEquals")
                override fun areContentsTheSame(
                    oldItem: DemoReqData.DataBean.DatasBean,
                    newItem: DemoReqData.DataBean.DatasBean
                )
        Boolean {
                    return oldItem == newItem
                }
            }) {
            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                var dataBean = getItem(position)
                (holder as DataViewHolder).binding.demoReaData = dataBean
            }

            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder {

            return TestViewHolder(
                DataBindingUtil.inflate(
                    LayoutInflater.from(parent.context),
                    R.layout.health_item_test,
                    parent,
                    false
                )
            )
        }

            inner class DataViewHolder(private val dataBindingUtil: ItemDataBinding) :
                RecyclerView.ViewHolder(dataBindingUtil.root) {
                var binding = dataBindingUtil
            }


        }


        這里我們要提醒的是DiffUtil這個參數(shù),用于計算列表中兩個非空項目之間的差異的回調(diào)。無特殊情況一般都是固定寫法。

        View層數(shù)據(jù)請求并將結(jié)果顯示在View上

        到這里,基本工作已經(jīng)差不多了,當(dāng)然我們說的差不多了只是快能看到成果了,其中需要講解的地方還有很多,最后一步我們在view中請求數(shù)據(jù),并將結(jié)果綁定在adapter上。我們在View代碼中調(diào)用viewModel中的getData方法,代碼如下所示:


        val manager = LinearLayoutManager(this)
        rv_data.layoutManager = manager
        rv_data.adapter = dataRecycleViewAdapter
        btn_get.setOnClickListener {
            lifecycleScope.launch {
                mainActivityViewModel.getData().collectLatest {
                    dataRecycleViewAdapter.submitData(it)
                }
            }
        }


        我們在協(xié)程中調(diào)用getData方法,接收最新的數(shù)據(jù),通過PagingAdapter的submitData方法為adapter提供數(shù)據(jù),運行結(jié)果如下所示(忽略丑陋的UI.jpg)。



        當(dāng)我們往下滑動時,當(dāng)?shù)撞窟€剩1個(pageSize)數(shù)據(jù)的時候會自動加載下一頁。


        當(dāng)然對于這個接口不需要傳pageSize,所以返回的數(shù)據(jù)大小并不會受pageSize的影響,如此一來,我們就使用Paging3 完成了簡單的數(shù)據(jù)分頁請求。


        /   Paging的加載狀態(tài)   /


        Paging3 為我們提供了獲取Paging加載狀態(tài)的方法,其中包含添加監(jiān)聽事件的方式以及在adapter中直接顯示的方式,首先我們來看監(jiān)聽事件的方式。


        使用監(jiān)聽事件方式獲取加載狀態(tài)


        上面我們在Activity中創(chuàng)建了dataRecycleViewAdapter來顯示頁面數(shù)據(jù),我們可以使用addLoadStateListener方法添加加載狀態(tài)的監(jiān)聽事件,如下所示:


        dataRecycleViewAdapter.addLoadStateListener {
            when (it.refresh) {
                is LoadState.NotLoading -> {
                    Log.d(TAG, "is NotLoading")
                }
                is LoadState.Loading -> {
                    Log.d(TAG, "is Loading")
                }
                is LoadState.Error -> {
                    Log.d(TAG, "is Error")
                }
            }
        }


        這里的it是CombinedLoadStates數(shù)據(jù)類,有refresh、Append、Prepend 區(qū)別如下表格所示:


        refresh

        在初始化刷新的使用

        append

        在加載更多的時候使用

        prepend

        在當(dāng)前列表頭部添加數(shù)據(jù)的時候使用


        也就是說如果監(jiān)測的是it.refresh,當(dāng)加載第二頁第三頁的時候,狀態(tài)是監(jiān)聽不到的,這里只以it.refresh為例。


        LoadState的值有三種,分別是NotLoading:當(dāng)沒有加載動作并且沒有錯誤的時候。Loading和Error顧名思義即對應(yīng)為正在加載 和加載錯誤的時候,監(jiān)聽方式除了addLoadStateListener外,還可以直接使用loadStateFlow的方式,由于flow內(nèi)部是一個掛起函數(shù) 所以我們需要在協(xié)程中執(zhí)行,代碼如下所示:


        lifecycleScope.launch {
            dataRecycleViewAdapter.loadStateFlow.collectLatest {
                when (it.refresh) {
                    is LoadState.NotLoading -> {

                    }
                    is LoadState.Loading -> {

                    }
                    is LoadState.Error -> {

                    }
                }
            }
        }


        接下來我們運行上節(jié)的示例,運行成功后,點擊查詢按鈕,將數(shù)據(jù)顯示出來,我們看打印如下:


        2020-11-14 16:39:19.841 23729-23729/com.example.pagingdatademo D/MainActivity: is NotLoading
        2020-11-14 16:39:24.529 23729-23729/com.example.pagingdatademo D/MainActivity: 點擊了查詢按鈕
        2020-11-14 16:39:24.651 23729-23729/com.example.pagingdatademo D/MainActivity: is Loading
        2020-11-14 16:39:25.292 23729-23729/com.example.pagingdatademo D/MainActivity: is NotLoading


        首先是NotLoading 狀態(tài),因為我們什么都沒有操作,點擊了查詢按鈕后變成Loading狀態(tài)因為正在加載數(shù)據(jù),查詢結(jié)束后再次回到了NotLoading的狀態(tài),符合我們的預(yù)期,那這個狀態(tài)有什么用呢? 我們在Loading狀態(tài)顯示一個progressBar過渡提升用戶體驗等,當(dāng)然最重要的還是Error狀態(tài),因為我們需要Error狀態(tài)下告知用戶。


        我們重新打開App,斷開網(wǎng)絡(luò)連接,再次點擊查詢按鈕,打印日志如下:


        2020-11-14 16:48:25.943 26846-26846/com.example.pagingdatademo D/MainActivity: is NotLoading
        2020-11-14 16:48:27.218 26846-26846/com.example.pagingdatademo D/MainActivity: 點擊了查詢按鈕
        2020-11-14 16:48:27.315 26846-26846/com.example.pagingdatademo D/MainActivity: is Loading
        2020-11-14 16:48:27.322 26846-26846/com.example.pagingdatademo D/MainActivity: is Error


        這里要注意的是什么呢,就是這個Error的狀態(tài),不是Paging為我們自動返回的,而是我們在DataSource中捕獲異常后,使用LoadResult.Error方法告知的。


        我們也需要在Error狀態(tài)下監(jiān)聽具體的錯誤,無網(wǎng)絡(luò)的話就顯示無網(wǎng)絡(luò)UI 服務(wù)器異常的話 就提示服務(wù)器異常,代碼如下所示:


        is LoadState.Error -> {
            Log.d(TAG, "is Error:")
            when ((it.refresh as LoadState.Error).error) {
                is IOException -> {
                    Log.d(TAG, "IOException")
                }
                else -> {
                    Log.d(TAG, "others exception")
                }
            }
        }


        我們在斷網(wǎng)狀態(tài)下,點擊查詢,日志如下所示:


        2020-11-14 17:29:46.234 12512-12512/com.example.pagingdatademo D/MainActivity: 點擊了查詢按鈕
        2020-11-14 17:29:46.264 12512-12512/com.example.pagingdatademo D/MainActivity: 請求第1
        2020-11-14 17:29:46.330 12512-12512/com.example.pagingdatademo D/MainActivity: is Loading
        2020-11-14 17:29:46.339 12512-12512/com.example.pagingdatademo D/MainActivity: is Error:
        2020-11-14 17:29:46.339 12512-12512/com.example.pagingdatademo D/MainActivity: IOException

        在adapter中顯示

        Paging3為我們提供了添加底部、頭部adapter的方法,分別為withLoadStateFooter、withLoadStateHeader以及同時添加頭部和尾部方法withLoadStateHeaderAndFooter,這里我們以添加尾部方法為例。


        首先我們創(chuàng)建viewHolder LoadStateViewHolder綁定布局是底部顯示的布局,一個正在加載的顯示以及一個重試按鈕,xml布局如下所以:


        <layout>

            <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:app="http://schemas.android.com/apk/res-auto"
                android:layout_width="match_parent"
                android:layout_height="match_parent">



                <LinearLayout
                    android:id="@+id/ll_loading"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:gravity="center"
                    android:orientation="horizontal"
                    android:visibility="gone"
                    app:layout_constraintEnd_toEndOf="parent"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toTopOf="parent">


                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="正在加載數(shù)據(jù)... ..."
                        android:textSize="18sp" />


                    <ProgressBar
                        android:layout_width="20dp"
                        android:layout_height="20dp" />

                </LinearLayout>

                <Button
                    android:id="@+id/btn_retry"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="加載失敗,重新請求"
                    android:visibility="gone"
                    app:layout_constraintStart_toStartOf="parent"
                    app:layout_constraintTop_toBottomOf="@id/ll_loading" />


            </androidx.constraintlayout.widget.ConstraintLayout>
        </layout>


        正在加載提示和重新請求的布局默認(rèn)都是隱藏,LoadStateViewHolder代碼如下所示:


        class LoadStateViewHolder(parent: ViewGroup, var retry: () -> Void) : RecyclerView.ViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.item_loadstate, parent, false)
        ) {

            var itemLoadStateBindingUtil: ItemLoadstateBinding = ItemLoadstateBinding.bind(itemView)

            fun bindState(loadState: LoadState) {
                if (loadState is LoadState.Error) {
                    itemLoadStateBindingUtil.btnRetry.visibility = View.VISIBLE
                    itemLoadStateBindingUtil.btnRetry.setOnClickListener {
                        retry()
                    }
                } else if (loadState is LoadState.Loading) {
                    itemLoadStateBindingUtil.llLoading.visibility = View.VISIBLE
                }

            }

        }


        我們這里是和Adapter分為兩個類中的,所以我們要將adapter中的parent當(dāng)做參數(shù)傳過來,retry()是一個高階函數(shù),便于點擊重試后,在adapter中做重試邏輯。


        bindState即為設(shè)置數(shù)據(jù),根據(jù)State的狀態(tài)來顯示不同的UI。


        接著我們來創(chuàng)建LoadStateFooterAdapter 繼承自LoadStateAdapter,對應(yīng)的viewHolder即為LoadStateViewHolder,代碼如下所示:


        class LoadStateFooterAdapter(private val retry: () -> Void) :
            LoadStateAdapter<LoadStateViewHolder>() {

            override fun onBindViewHolder(holder: LoadStateViewHolder, loadState: LoadState) {
                (holder as LoadStateViewHolder).bindState(loadState)
            }

            override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): LoadStateViewHolder {
                return LoadStateViewHolder(parent, retry)
            }

        }


        這里的代碼比較簡單,就不作講解了,最后我們來添加這個adapter.


        rv_data.adapter =
            dataRecycleViewAdapter.withLoadStateFooter(footer = LoadStateFooterAdapter(retry = {
                dataRecycleViewAdapter.retry()
            }))


        這里要注意的是,應(yīng)該把withLoadStateFooter返回的adapter設(shè)置給recyclerview,如果你是這樣寫:dataRecycleViewAdapter.withLoadStateFooter后 在單獨設(shè)置recycleView的adapter,則會是沒有效果的。


        這里我們點擊重試dataRecycleViewAdapter的retry()方法即可,我們運行程序求救第一頁后,斷開網(wǎng)絡(luò),然后往下滾動,效果如下所示:



         如此,我們就在adapter中完成了數(shù)據(jù)加載狀態(tài)的顯示。


        除此之外,Paging3中還有一個比較重要的RemoteMediator,用來更好的加載網(wǎng)絡(luò)數(shù)據(jù)庫和本地數(shù)據(jù)庫,我們后續(xù)有機會再為大家單獨分享吧~


        2020年11月21日更新

        paging3的設(shè)計理念是不建議對列表數(shù)據(jù)直接修改;而是對數(shù)據(jù)源進(jìn)行操作,數(shù)據(jù)源的變化會自動更新到列表,看到評論區(qū)中很多朋友說如何操作item的刪除和修改,這里我們使用最簡單的方式即可。

        對單個item的修改

        我們都知道RecycleView中是沒有直接監(jiān)聽item的Api的,一般都是在onBindViewHolder中去操作,或者通過回調(diào)在View層操作,在這里回調(diào)也可以寫為一個高階函數(shù),我們這里回調(diào)到View層的原因是評論區(qū)中有伙伴評論說要操作viewModel,所以避免在將viewModel注入到adapter,我們直接使用一個高階函數(shù)回調(diào)即可。修改DataRecycleViewAdapter代碼如下所示:


        class DataRecycleViewAdapter(
            val itemUpdate: (Int, DemoReqData.DataBean.DatasBean?,DataRecycleViewAdapter) -> Unit
        ) :
            PagingDataAdapter<DemoReqData.DataBean.DatasBean, RecyclerView.ViewHolder>(object :
                DiffUtil.ItemCallback<DemoReqData.DataBean.DatasBean>() {

                override fun areItemsTheSame(
                    oldItem: DemoReqData.DataBean.DatasBean,
                    newItem: DemoReqData.DataBean.DatasBean
                )
        Boolean {
                    return oldItem.id == newItem.id
                }

                @SuppressLint("DiffUtilEquals")
                override fun areContentsTheSame(
                    oldItem: DemoReqData.DataBean.DatasBean,
                    newItem: DemoReqData.DataBean.DatasBean
                )
        Boolean {
                    return oldItem == newItem
                }
            }) {
            override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
                val dataBean = getItem(position)
                (holder as DataViewHolder).binding.demoReaData = dataBean
                holder.binding.btnUpdate.setOnClickListener {
                    itemUpdate(position, dataBean,this)
                }
            }

            override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
                val binding: ItemDataBinding =
                    DataBindingUtil.inflate(
                        LayoutInflater.from(parent.context),
                        R.layout.item_data,
                        parent,
                        false
                    )
                return DataViewHolder(binding)
            }


            inner class DataViewHolder(private val dataBindingUtil: ItemDataBinding) :
                RecyclerView.ViewHolder(dataBindingUtil.root) {
                var binding = dataBindingUtil
            }


        }


        為了便于演示我們這里在數(shù)據(jù)列表中新增了一個更新數(shù)據(jù)的按鈕,在Activity中聲明adapter的代碼修改如下:


        private var dataRecycleViewAdapter = DataRecycleViewAdapter { position, it, adapter ->
            it?.author = "黃林晴${position}"
            adapter.notifyDataSetChanged()
        }


        我們通過執(zhí)行高階函數(shù) 將作者的名字修改為黃林晴和當(dāng)前點擊的序號,然后調(diào)用notifyDataSetChanged即可,演示效果如下所示:



        對數(shù)據(jù)的刪除、新增

        我們都知道,在之前,我們給adapter設(shè)置一個List,如果需要刪除或者新增,我們只要改變List即可,但是在Paging3中好像沒有辦法,因為數(shù)據(jù)源是PagingSource ,看了下官網(wǎng)的介紹。


        A PagingSource / PagingData pair is a snapshot of the data set. A new PagingData / PagingData must be created if an update occurs, such as a reorder, insert, delete, or content update occurs. A PagingSource must detect that it cannot continue loading its snapshot (for instance, when Database query notices a table being invalidated), and call invalidate. Then a new PagingSource / PagingData pair would be created to represent data from the new state of the database query.


        大致意思就是如果數(shù)據(jù)發(fā)生變化 必須創(chuàng)建新的PagingData ,所以暫時我也不知道如何可以在不重新請求的情況下,在數(shù)據(jù)刪除、新增后來刷新,如果你有好的方案,歡迎賜教!



        ·················END·················

        推薦閱讀

        ? 耗時2年,Android進(jìn)階三部曲第三部《Android進(jìn)階指北》出版!

        ? 『BATcoder』做了多年安卓還沒編譯過源碼?一個視頻帶你玩轉(zhuǎn)!

        ? 『BATcoder』是時候下載Android11系統(tǒng)源碼和內(nèi)核源碼了!

        推薦我的技術(shù)博客

        推薦一下我的獨立博客: liuwangshu.cn ,內(nèi)含Android最強原創(chuàng)知識體系,一直在更新,歡迎體驗和收藏!

        BATcoder技術(shù)群,讓一部分人先進(jìn)大廠

        你好,我是劉望舒,百度百科收錄的騰訊云TVP,著有暢銷書《Android進(jìn)階之光》《Android進(jìn)階解密》《Android進(jìn)階指北》,蟬聯(lián)四屆電子工業(yè)出版社年度優(yōu)秀作者,谷歌開發(fā)者社區(qū)特邀講師。

        前華為面試官,現(xiàn)大廠技術(shù)負(fù)責(zé)人。


        歡迎添加我的微信 henglimogan ,備注:BATcoder,加入BATcoder技術(shù)群。


        明天見(??ω??)
        瀏覽 137
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            欧美人成人无码 | 91视频大香蕉 | 丰满女老板大胸老板bd高清 | 丁香五月婷婷五月天 | 好大好爽再深一点的动漫 | 免费看美女操逼 | 最新一区二区三区 | 色综合国产| 《桃色》无删减床戏日本 | 老外黄色一级片 |