可能是最全的kotlin协程+Jetpack入门教程


前言

最近一直在学习kotlin,为了能够尽快入门,所以我准备写一个简单的app练手,加上之前一直在研究Jetpack这套组件库,基于以上原因我打算用kotlin+jetpack搭建我的app框架,至于为啥用kotlin协程,之前在网上看了一篇博客Kotlin 协程 看这一篇就够了,既然是学习项目,所以把这个也加上,在查看了大量的文章,我发现大多数文章都是一个简单的例子,距离真正的项目,还有一定的差距,so这篇文章诞生了,希望能够给大家有所帮助。


一、协程是什么?

协程是什么很难用一句话简单的概括,我们先看一个例子,然后再来讲它到底是什么。

1.引入库

kotlin相关依赖库:

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.1"

2.官方例子

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在后台启动一个新的协程并继续
        delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒)
        println("World!") // 在延迟后打印输出
    }
    println("Hello,") // 协程已在等待时主线程还在继续
    Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活
}

运行结果如下:
在这里插入图片描述
简单说一下,上面这段代码,GlobalScope.launch开启了一个新的协程,大括号里面的代码就是协程体,结合我们的运行结果可以知道,delay不会阻塞当前主线程,这就是一个最简单的协程的用法,看到这里可能有点疑惑,我再举一个简单的例子来说明为什么要用协程。

我们平时在开发过程中,经常会有这样的需求,我们通过网络异步获取数据,然后显示在界面上:

    /**
     * 从网络拉取数据
     */
    private fun fetchDataFromWork(url: String, callback: NetWorkListener) {
        var data: String = ""
        // 模拟耗时任务
        delay(2000L)
        data = url + "ddup"

        callback.onSuccess(data)
    }

    /**
     * 展示数据
     */
    private fun showText(data: String) {
      tv_my_content.text = data
    }

调用如下:

        Thread() {
            fetchDataFromWork("www.ddup.com", object : NetWorkListener {
                override fun onSuccess(data: String) {
                    // 切换到主线程显示结果
                    Handler().post {
                        showText(data)
                    }
                }

                override fun onFail(error: String) {
                    // 异常处理
                }

            })
        }.start()

以上是我们用传统的方式实现的需求,我们再看看协程的方式,首先我们需要改造一下fetchDataFromWork()方法:

     /**
     * 从网络拉取数据
     */
    private suspend fun fetchDataFromWork(url: String): String {
        var data: String = ""

        withContext(Dispatchers.IO) {
            // 模拟耗时任务
            delay(2000L)
            data = url + "ddup"
        }

        return data;
    }

suspend是kotlin的一个关键字,用于标记挂起函数,代表当前方法是一个耗时方法,withContext另外一种创建协程的方式,与此类似的还有async,一般配合await一起使用。我们再看下利用协程实现上述需求的调用方式:

    GlobalScope.launch(Dispatchers.Main) {
            val data: String = fetchDataFromWork("www.ddup.com")
            showText(data)
        }

对比以前的实现方式是不是要简单很多,以前的方式需要我们手动切换线程,而协程内部已经帮我们处理了,自动切换线程。这样看可能不明显,当我们的需求修改为,当我们第一条数据显示完成后再接着显示第二条数据:
传统的实现方式:

        Thread() {
            fetchDataFromWork("www.ddup.com", object : NetWorkListener {
                override fun onSuccess(data: String) {
                    // 切换到主线程显示结果
                    Handler().post {
                        showText(data)
                        // 请求下一条数据
                        Thread() {
                          fetchDataFromWork2(
                          // 切换到主线程显示第二条数
                        )
                          }.start()
                    }
                }

                override fun onFail(error: String) {
                    // 切换到主线程显示异常
                    Handler().post {
                        showText(error)
                    }
                }

            })
        }.start()

协程的方式:

GlobalScope.launch(Dispatchers.Main) {
            val data: String = fetchDataFromWork("www.ddup.com")
            showText(data)
            val data2: String = fetchDataFromWork2("www.ddup.com")
            showText(data2)
        }

看到没有,协程只需要在后面添加请求第二条数据的方法,接着显示出来就行,相比之前的方式,不仅不会陷入嵌套回调地狱,而且让我写异步方法如同写同步方法一样。

回到一开始的协程是什么的问题,从上面的例子可以看出,协程本质上是一个轻量级的线程,协程内部帮我们自动切换线程,是用来帮我们简化异步执行代码,解决嵌套回调地狱的问题一种机制。

二、Jetpack是什么?

前面大概讲了下协程是什么,接下来说说Jetpack是什么,然后将协程和Jetpack结合起来使用。

1.Jetpack官方架构图

官方架构图
如上图所示,Jetpack是一套组件库的框架,帮助我们更快的开发app,减少模板代码,专注我们的业务开发。我们的app基于此Jetpack开发,activity/fragment主要用来显示UI的容器,ViewModel来管理我们的可观察的LiveData数据,通过数据仓库层Respository来管理远程服务端数据和本地可持续数据。

2.常用组件介绍

了解完大概的框架,我们来熟悉一下常见的组件。

DataBinding

DataBinding,使用过MVVM框架开发的人,应该知道是一个数据动态绑定框架,可双向绑定,看下最常见的用法。

使用之前先要将DataBinding启用,在app/build.gradle增加如下代码:

android {
    ...
    dataBinding {
        enabled = true

    }
}

在使用到DataBinding布局中增加layout布局

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <import type="android.view.View" />

        <import type="com.gxp.ddup_douban.bean.TopicBean.IssueListBean.ItemListBean" />

        <variable
            name="videoDataBean"
            type="ItemListBean" />
    </data>
    ...
    </layout>

其中data下面的type是我们引入的数据类型,控件可以通过@{}使用:

   <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="250dp">

        <ImageView
            android:id="@+id/iv_photo"
            android:layout_width="match_parent"
            android:layout_height="200dp"
            android:layout_alignParentTop="true"
            app:imgUrl="@{videoDataBean.data.cover.feed}" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_below="@id/iv_photo"
            android:orientation="horizontal">

            <ImageView
                android:id="@+id/iv_user"
                android:layout_width="50dp"
                android:layout_height="50dp"
                android:layout_gravity="center"
                android:padding="10dp"
                android:visibility="@{videoDataBean.data.author !=null ? View.VISIBLE : View.GONE}"
                app:imgUrl="@{videoDataBean.data.author.icon}" />

            <RelativeLayout
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="15dp">

                <TextView
                    android:id="@+id/tv_title"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentTop="true"
                    android:maxLines="1"
                    android:padding="5dp"
                    android:text="@{videoDataBean.data.title}"
                    android:textSize="15sp" />

                <TextView
                    android:id="@+id/tv_detail"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentBottom="true"
                    android:padding="5dp"
                    android:text='@{@string/topic_bean_publish+videoDataBean.data.category}' />
            </RelativeLayout>

        </LinearLayout>


    </RelativeLayout>

其中app:imgUrl需要我们定义一个@BindingAdapter注解来实现url变化后,控件里面的图片跟着变化

companion object {

        @BindingAdapter("app:imgUrl")
        @JvmStatic
        fun load(view: ImageView, url: String) {
            Glide.with(view.context).load(url).into(view)
        }
    }

下面是如何设置数据

        val mainBindingUtil =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main);
        mainBindingUtil.lifecycleOwner = this;
        mainBindingUtil.itembean = TopicBean.IssueListBean.ItemListBean()

以上是DataBinding基本用法,还有事件绑定、双向绑定这里就不介绍了。

Navigation

Navigation顾名思义导航,主要用来管理fragment,控制fragment之间跳转的框架。

首先引入依赖

    implementation "androidx.navigation:navigation-fragment-ktx:2.2.0"
    implementation "androidx.navigation:navigation-ui-ktx:2.2.0"

在res目录下创建navigation文件夹,新建nav_demo导航文件
新建navigation文件
编写fragment之间跳转的代码

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/nav_demo"
    app:startDestination="@id/homeFragment">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.jetpacklearn.HomeFragment"
        android:label="fragment_home"
        tools:layout="@layout/fragment_home">
        <action
            android:id="@+id/action_homeFragment_to_secondFragment"
            app:destination="@id/secondFragment"
            app:enterAnim="@anim/in_from_right"
            app:exitAnim="@anim/out_to_left" />
    </fragment>
    <fragment
        android:id="@+id/secondFragment"
        android:name="com.example.jetpacklearn.SecondFragment"
        android:label="fragment_second"
        tools:layout="@layout/fragment_second">
        <action
            android:id="@+id/action_secondFragment_to_thirdFragment"
            app:destination="@id/thirdFragment"
            app:enterAnim="@anim/in_from_right"
            app:exitAnim="@anim/out_to_left" />
    </fragment>
    <fragment
        android:id="@+id/thirdFragment"
        android:name="com.example.jetpacklearn.ThirdFragment"
        android:label="fragment_third"
        app:enterAnim="@anim/in_from_right"
        app:exitAnim="@anim/out_to_left"
        tools:layout="@layout/fragment_third" />
</navigation>

其中app:startDestination="@id/homeFragment"代表根Fragment,<fragment标签里面的id,跳转到指定fragment需要用到,<action标签里面的id用于执行跳转动作,destination是跳转目标的fragment。
我们也可以用可视化UI来编辑
navigation可视化编辑
使用之前创建的navi_demo.xml导航文件

<fragment
    android:id="@+id/demo_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:defaultNavHost="true"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:navGraph="@navigation/nav_demo" />

app:navGraph="@navigation/nav_demo"引用我们之前创建fragment跳转依赖,我们也可以用代码来执行指定的跳转动作

    // 这个id就是navigation里的action的id
    Navigation.findNavController(view).navigate(R.id.action_secondFragment_to_thirdFragment)

    // 回退到上一个页面
    Navigation.findNavController(view).popBackStack()

以上就是Navigation基本用法,想了解更多可以看下这篇文章Android开发 navigation入门详解

Lifecycle

Lifecycle生命周期感知组件,用来监听activity、fragment生命周期事件来执行相应的操作。我们平常在开发中,应该会经常遇到在activity、fragment onStart来执行开启的操作,onStop来执行对应的取消或关闭的操作。

我们先看看原来是怎么做的

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

        myLocationListener = MyLocationListener(context = this,callback = object :Callback{
            override fun updateUI() {
                // 更新UI操作
            }
        })
    class MyLocationListener : LocationListener {

        constructor(context: Context, callback: Callback) {
            // ...
        }

        override fun onStart() {
            print("onStart")
        }

        override fun onStop() {
            print("onStop")
        }
    }

    override fun onStart() {
        super.onStart()
        // 开启定位服务
        myLocationListener?.onStart()
    }

    override fun onStop() {
        super.onStop()
        // 结束定位服务
        myLocationListener?.onStop()
    }

    interface LocationListener {
        fun onStart()
        fun onStop()
    }
    }

这是我们常见的一种写法,当我们的组件越来越多,在activity生命周期里面的方法会变得越来越多,会变得很臃肿、难以维护,于是Lifecycle应运而生,用来维护activity、fragment的生命周期,使得每个类都拥有生命周期。
下面改造一下,看下Lifecycle实现方式

    class MyLocationListener : LifecycleObserver {

        constructor(context: Context, callback: Callback) {
            // ...
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_START)
        fun onStart(){
            print("onStart")
        }

        @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
        fun onStop(){
            print("onStop")
        }


    }
        override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        myLocationListener = MyLocationListener(context = this,callback = object :Callback{
            override fun updateUI() {
                // 更新UI操作
            }

        })

        lifecycle.addObserver(myLocationListener!!)


    }
        override fun onDestroy() {
        super.onDestroy()
        myLocationListener?.let { lifecycle.removeObserver(it) }
    }

我们只需要在需要用到activity、fragment生命周期的地方,实现LifecycleObserver接口,并在对应的event注解下实现相应的启动或停止,然后在activity、fragment生命周期下相应添加、移除观察对象,相比之前的方式是不是很简单,不需要我们自己在activity、fragment生命周期下添加对应的接口方法,只需要关注我们业务本身,而且在不需要的时候直接移除观察对象,做一些内存回收操作。

LiveData

LiveData可观察的数据类,相比其他的观察类来说,具有生命周期感知能力,而且仅更新处于活跃生命周期状态的应用组件观察者。
下面来说说它的用法,说LiveData之前,对比一下MutableLiveData,他们两个最大的区别:LiveData在实体类里可以通知指定某个字段的数据更新,而MutableLiveData则是完全是整个实体类或者数据类型变化后才通知.不会细节到某个字段。

来看代码

class DemoData : LiveData<DemoData?>() {
    private var age = 0
    private var name: String? = null
    fun getAge(): Int {
        return age
    }

    fun setAge(age: Int) {
        this.age = age
        postValue(this)
    }

    fun getName(): String? {
        return name
    }

    fun setName(name: String?) {
        this.name = name
        postValue(this)
    }
}
class DemoMutableData<T> : LiveData<T?>() {
    override fun postValue(value: T?) {
        super.postValue(value)
    }

    override fun setValue(value: T?) {
        super.setValue(value)
    }
}

既然是数据类,最重要是set、get两个方法,对应LiveData最重要的是通知数据更新即postValuesetValue,前者可以在子线程调用,后者只能在主线程调用。
我们下面看一下如何使用,我们先要创建一个ViewModel用来管理LiveData

class DemoViewModel : ViewModel() {
    private val mDemoData = DemoData()
    fun getDemoData(): DemoData {
        return mDemoData
    }
}

在activity、fragment调用

    private var demoViewModel: DemoViewModel? = null


    override fun initView() {
        demoViewModel = ViewModelProvider(this).get(DemoViewModel::class.java)
        demoViewModel!!.getDemoData().observe(this, Observer {
            print("数据更新了:" + (it?.getName()));
        })

        tv_my_content.setOnClickListener {
            demoViewModel!!.getDemoData().setName("ddup")
        }
    }

简单说一下上面的代码,先创建一个观察对象,如果数据更新的话会打印数据更新了: xxx,点击按钮更新数据,这里要说明一点的是ViewModelProvider当前如果是fragment的话,数据依赖当前fragment,如果是activity的话,可以在多个fragment里面使用,对应的生命周期依赖当前activity。MutableLiveData对应的使用在这里就不介绍了,感兴趣的可以看下这篇文章Android开发 LiveData与MutableLiveData详解,更详细的使用方法可以看官方LiveData介绍

ViewModel

上面讲LiveData的使用已经提到了ViewModel的使用,它主要就是用来管理LiveData的,注重生命周期的方式来管理和存储数据,并且在屏幕旋转等配置更改后保存(原理是ViewModel在Activity重新创建时,会在onCreate时判断当前是否由屏幕旋转引起,如果屏幕引起的不在重新创建实例)。这里我就不在重复写它的使用方法了。

Paging

字面的意思是页面,我们平时平常最常见的列表页面,上拉加载、下拉刷新这类的控件,在官方没有推出Paging之前,我们都是基于Recyclerview、Listview之上封装实现的,Paging相比这些可以实现无限滚动的效果,类似一次完全加载完数据的效果。类似下面的效果(图片来自Android官方架构组件Paging:分页库的设计美学
来自Android官方架构组件Paging:分页库的设计美学

在介绍Paging的使用之前,我简单介绍一下Paging一些基本概念

DataSource

DataSource<Key,Value>数据源相关的类,其中Key对应加载数据的条件信息,Value对应返回结果, 针对不同场景,Paging提供了三种Datasource:

  • PageKeyedDataSource<Key, Value> :适用于目标数据根据页信息请求数据的场景,即Key 字段是页相关的信息。比如请求的数据的参数中包含类似next/previous的信息。
  • ItemKeyedDataSource<Key, Value> :适用于目标数据的加载依赖特定item的信息, 即Key字段包含的是Item中的信息,比如需要根据第N项的信息加载第N+1项的数据,传参中需要传入第N项的ID时,该场景多出现于论坛类应用评论信息的请求。
  • PositionalDataSource:适用于目标数据总数固定,通过特定的位置加载数据,这里Key是Integer类型的位置信息,T即Value。 比如从数据库中的1200条开始加在20条数据。
PageList

将Datasource比作抽水泵,那PagedList就像是一个蓄水池,但不仅仅如此。PagedList是List的子类,支持所有List的操作, 除此之外它主要有五个成员:

  • mMainThreadExecutor: 一个主线程的Excutor, 用于将结果post到主线程。
  • mBackgroundThreadExecutor: 后台线程的Excutor。
  • BoundaryCallback:加载Datasource中的数据加载到边界时的回调。
  • Config: 配置PagedList从Datasource加载数据的方式, 其中包含以下属性:
    . pageSize:设置每页加载的数量 prefetchDistance:预加载的数量
    . initialLoadSizeHint:初始化数据时加载的数量
    . enablePlaceholders:当item为null是否使用PlaceHolder展示
  • PagedStorage: 用于存储加载到的数据,它是真正的蓄水池所在,它包含一个ArrayList<List> 对象mPages,按页存储数据。
PageListAdapter

PagedListAdapte是RecyclerView.Adapter的实现,用于展示PagedList的数据。它本身实现的更多是Adapter的功能,但是它有一个小伙伴PagedListAdapterHelper, PagedListAdapterHelper会负责监听PagedList的更新, Item数量的统计等功能。这样当PagedList中新一页的数据加载完成时, PagedAdapte就会发出加载完成的信号,通知RecyclerView刷新,这样就省略了每次loading后手动调一次notifyDataChanged().

以上内容引自Android.Arch.Paging: 分页加载的新选项

在介绍了相关概念后,结合我们的需求来选择合适的DataSource,首先说下我们的需求,我在github下载了一个开眼视频的项目,首页列表Api,第一页数据和下一页数据使用不同的请求接口,下一页请求依赖上一页的请求url的日期,结合上面所说,我们选用PageKeyedDataSource自定义DataSource。

下面引入Paging相关依赖包(这里使用的Paging2版本,最新的是Paging3):

    // paging相关依赖
    implementation "androidx.paging:paging-runtime:2.1.2"
    // 下拉刷新依赖
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
    

由于用到了下拉刷新功能,所以加入了SwipeRefreshLayout相关依赖。

1.创建数据源
class TopicRepository {

    /**
     * 获取首页数据
     */
    suspend fun getTopicData(baseUrl: String): TopicBean? {
        return RetrofitClient.getInstance(baseUrl).create(ApiService::class.java)
            ?.getTopicBean()
    }

    /**
     * 获取首页之后的数据
     */
    suspend fun getMoreTopicData(baseUrl: String, data: String): TopicBean? {
        return RetrofitClient.getInstance(baseUrl).create(ApiService::class.java)
            ?.getMoreTopicBean(data, "2")
    }


}
2.自定义DataSource
class TopicDataSource(
    private val coroutineScope: CoroutineScope,
    private val repository: TopicRepository
) : PageKeyedDataSource<String, TopicBean.IssueListBean.ItemListBean>() {
    override fun loadInitial(
        params: LoadInitialParams<String>,
        callback: LoadInitialCallback<String, TopicBean.IssueListBean.ItemListBean>
    ) {
        coroutineScope.launch {
            try {
                val response = repository.getTopicData(Constants.REQUEST_BASE_URL)
                val (date, mList) = getVideoData(response)
                callback.onResult(mList, null, date)
            } catch (e: Exception) {
                print(e.message)
            }
        }

    }

    override fun loadAfter(
        params: LoadParams<String>,
        callback: LoadCallback<String, TopicBean.IssueListBean.ItemListBean>
    ) {
        coroutineScope.launch {
            try {
                val response = repository.getMoreTopicData(Constants.REQUEST_BASE_URL, params.key)
                val (date, mList) = getVideoData(response)
                callback.onResult(mList, date)
            } catch (e: Exception) {
                print(e.message)
            }
        }

    }
   ...
   }

主要看这loadInitial、loadAfter两个方法一个是刚开始加载第一个页面的请求,另一个是加载下一页的数据。

3.创建PageAdapter
class TopicAdapter :
    PagedListAdapter<TopicBean.IssueListBean.ItemListBean, TopicViewHolder>(diffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopicViewHolder {
        return TopicViewHolder(
            DataBindingUtil.inflate(
                LayoutInflater.from(parent.context),
                R.layout.item_topic,
                parent,
                false
            )
        )
    }

    override fun onBindViewHolder(holder: TopicViewHolder, position: Int) {
        val binding: ViewDataBinding = holder.dataBinding
        binding.setVariable(BR.videoDataBean, getItem(position))
        binding.executePendingBindings()

    }

    companion object {
        private val diffCallback =
            object : DiffUtil.ItemCallback<TopicBean.IssueListBean.ItemListBean>() {
                override fun areItemsTheSame(
                    oldItem: TopicBean.IssueListBean.ItemListBean,
                    newItem: TopicBean.IssueListBean.ItemListBean
                ): Boolean =
                    oldItem.data == newItem.data

                override fun areContentsTheSame(
                    oldItem: TopicBean.IssueListBean.ItemListBean,
                    newItem: TopicBean.IssueListBean.ItemListBean
                ): Boolean =
                    oldItem.data?.id == newItem.data?.id
            }

    }

}

这里主要看一下diffcallback根据里面的值判断是否需要更新页面

4.使用
        val topicAdapter = TopicAdapter()
        rv_home.adapter = topicAdapter
        rv_home.layoutManager = LinearLayoutManager(context)
        homeViewModel = ViewModelProvider(this).get(TopicViewModel::class.java)

        homeViewModel.topicPagedList.observe(
            this, Observer {
                if (mIsRefresh) {
                    mIsRefresh = false;
                    refresh_layout_home.isRefreshing = false;
                }
                topicAdapter.submitList(it)
                Log.e(TAG, it.size.toString());
            })

          refresh_layout_home.setOnRefreshListener {
              if (!mIsRefresh) {
                  mIsRefresh = true
                  homeViewModel.invalidateDataSource()
              }

          }
5.效果

Paging效果图


三、结合使用

至此,kotlin协程+Jetpack相关基础知识介绍完毕,正如一开始所说的一样,本文不会简单写个demo,切合实际开发项目使用。

1.协程的进阶使用

前面简单的介绍了协程的基本使用,但在Jetpack项目中使用,实际用到的是viewModelScope,它是ViewModel扩展属性,在ViewModel onCleared 会自动取消协程任务,避免内存泄漏。

  viewModelScope.launch {

            try {
                val apiService =
                    RetrofitClient.getInstance(context, baseUrl).create(ApiService::class.java)
                if (apiService != null) {
                    val topicBean = apiService.getTopicBean();
                    topicLiveData.value = topicBean
                }
            } catch (e: Exception) {
            }

        }

我们可以看出上面出现异常情况,会直接捕获异常,不够优雅,也不便于定位问题,下面增加扩展方法封装。

fun ViewModel.launch(
    block: suspend CoroutineScope.() -> Unit,
    onError: (e: Throwable) -> Unit = {},
    onComplete: () -> Unit = {}

) {
    viewModelScope.launch(CoroutineExceptionHandler { _, e -> onError(e) }) {
        try {
            block.invoke(this)
        } finally {
            onComplete()
        }

    }

}

改造后的使用

   launch(
            {
                topicLiveData.value = repository.getTopicData(Constants.REQUEST_BASE_URL)
            },
            {
                Log.e(TAG, "getTopicBean error->" + it.message)
            },
            {
                Log.e(TAG, "getTopicBean onComplete...")
            }
        )

2.工程架构图

看一个项目最重要的是参考架构图,这里我分享一下,我目前工程的架构图
工程架构图

3.项目地址

目前项目只搭了个架子,后续会完成整个项目,现有问题下拉刷新会闪烁,目前还没找到解决办法,知道的可以告知一下。
项目github地址
如果觉得有用的麻烦给个star,谢谢。


总结

这篇文章前前后后,花了几天时间才写完,在一开始计划写的时候,没觉得那么费时间,越写越发现,从输入到输出的过程中,相差还是蛮大的,在写demo的时候,特别Paging,看了很多篇博客,都没有太懂,最好在不断的尝试下才弄好,其实这里面每个知识点都可以写一篇文章,但是作为入门的项目,主要还是给大家一个初步的印象,一个实际且完整的例子,最终还是需要自己敲代码一步步实现,最好结合自己的项目来做。

第一次用markdown写博客,不知道效果咋样。

最好,创作不易,觉得不错的话,请点赞、评论鼓励,谢谢。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值