前言
Android Jetpack是Google在18年IO大会上推荐的一整套组件库,它的出现填补了之前Android中自带的一些缺陷,例如Handler的内存泄露、Camera的不易用性、后台调度难以管理等等。所以我打算把整个架构组件系统性的学习一下,在这里和大家分享,希望能帮助到其他学习者。本系列文章包含十篇:
- Android Jetpack全家桶(一)之Jetpack介绍
- Android Jetpack全家桶(二)之Lifecycle生命周期感知
- Android Jetpack全家桶(三)之ViewModel控制器
- Android Jetpack全家桶(四)之LiveData数据维持
- Android Jetpack全家桶(五)之Room ORM库
- Android Jetpack全家桶(六)之Paging分页库
- Android Jetpack全家桶(七)之WorkManager工作管理
- Android Jetpack全家桶(八)之Navigation导航
- Android Jetpack全家桶(九)之DataBinding数据绑定
- Android Jetpack全家桶(十)之从0到1写一个Jetpack项目
项目介绍
因为拓意阅读之前有写过flutter版本,所以这次打算结合原有的api和交互效果写一款Jetpack版本,旨在结合博客内容加深学习印象,和大家一起真正的使用Jetpack组件。
该应用数据主要来源于开眼api。采用了QMUI + Jetpack MVVM的架构。使用QMUI也是一种尝试,不过在实际用过之后感觉该UI库不符合预期,并不推荐大家在商业化项目中使用。
项目架构
架构上以MVVM架构模式打通了整套Jetpack体系流程。详见下图:
项目结构
结构上简单来说就是功能模块与UI模块划分后,再次在UI模块内做业务性的功能划分,这样的划分形式很清晰展现出了各个模块职能。详见下图:
项目展示
以下是拓意阅读Jetpack版项目截图。
项目下载
以下是拓意阅读Jetpack版下载地址。
项目说明(以日报模块为例)
主体思路:
-
1,视图(生成ViewDataBinding,关联ViewModel)
-
2,数据来源(配置Paging组件,利用DataSource控制数据)
-
3,交互(PagedList建立关联,填充Adapter数据)
具体实现:
fragment_daily.xml
<layout>
<data></data>
<com.qmuiteam.qmui.widget.QMUIWindowInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.qmuiteam.qmui.widget.QMUIEmptyView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/qmui_config_color_white"
android:fitsSystemWindows="true"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout
android:id="@+id/pull_to_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
</com.qmuiteam.qmui.widget.pullRefreshLayout.QMUIPullRefreshLayout>
</FrameLayout>
</com.qmuiteam.qmui.widget.QMUIWindowInsetLayout>
</layout>
DailyFragment.kt
class DailyFragment : BaseFragment<FragmentDailyBinding>(){
private val mDailyAdapter: DailyAdapter by lazy { DailyAdapter() }
private val mViewModel: DailyViewModel by lazy(LazyThreadSafetyMode.NONE) {
ViewModelProviders.of(this,DailyModelFactory(DailyRepository()))[DailyViewModel::class.java]
}
override fun getLayoutId(): Int = R.layout.fragment_daily
override fun initView(view : View) {
mBinding.emptyView.show(true)
mBinding.rvCoordinator.layoutManager = LinearLayoutManager(context)
mBinding.rvCoordinator.isNestedScrollingEnabled = true
(mBinding.rvCoordinator.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
mBinding.rvCoordinator.adapter = mDailyAdapter
mBinding.rvCoordinator.addItemDecoration(SpacesItemDecoration(10))
}
override fun initData() {
mViewModel.fetchResult()
mViewModel.result?.observe(this, Observer<PagedList<HomeDailyItemListBean>>{
mDailyAdapter.submitList(it)
})
}
override fun initListener() {
mDailyAdapter.setOnItemListener(OnItemClickListener { position, view ->
val data = mDailyAdapter.currentList!![(position-1)]
if (data != null) {
openWebView(data.data.content.data.title,data.data.content.data.webUrl.raw)
val browseRecordBean = BrowseRecordBean(
data.id.toString(),
data.data.content.data.title, data.data.content.data.description,
data.data.content.data.webUrl.raw, data.data.content.data.cover.feed
)
doAsync {
ERApplication.db.browseRecordDao().insert(browseRecordBean)
}
}
})
mBinding.pullToRefresh.setOnPullListener(object : QMUIPullRefreshLayout.OnPullListener {
override fun onMoveTarget(offset: Int) {}
override fun onMoveRefreshView(offset: Int) {}
override fun onRefresh() {
initData()
mBinding.pullToRefresh.postDelayed( { mBinding.pullToRefresh.finishRefresh() }, 500)
}
})
CoroutineBus.register(this.javaClass.simpleName, UI, EventMessage::class.java) {
when {
it.tag == DailyFragment::class.java.name + ERAppConfig.PAGE_DATA_INIT ->
mBinding.emptyView.show(false)
it.tag == DailyFragment::class.java.name + ERAppConfig.PAGE_DATA_LOAD_START ->
mDailyAdapter.isLoadMore = 1
it.tag == DailyFragment::class.java.name + ERAppConfig.PAGE_DATA_LOAD_END ->
mDailyAdapter.isLoadMore = -1
}
mDailyAdapter.notifyDataSetChanged()
}
}
override fun onDestroy() {
super.onDestroy()
CoroutineBus.unregister(this.javaClass.simpleName)
}
}
DailyPaging.kt
class DailyRepository{
suspend fun getHomeDailyList(page: Int): List<HomeDailyItemListBean>? = withContext(Dispatchers.IO){
val result = RetrofitManager.apiService.getHomeDailyList(System.currentTimeMillis().toString()).itemList
val filterData = result.filter {
it.type == "followCard"
}
filterData
}
}
class DailyDataSource(private val repository: DailyRepository) : PageKeyedDataSource<Int, HomeDailyItemListBean>(), CoroutineScope by MainScope(){
override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, HomeDailyItemListBean>) {
safeLaunch{
val result = repository.getHomeDailyList(1)
result?.let {
callback.onResult(it,1,2)
CoroutineBus.post(
EventMessage(
DailyFragment::class.java.name
+ ERAppConfig.PAGE_DATA_INIT,
null
)
)
}
}
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, HomeDailyItemListBean>) {
CoroutineBus.post(
EventMessage(
DailyFragment::class.java.name
+ ERAppConfig.PAGE_DATA_LOAD_END,
null
)
)
}
override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, HomeDailyItemListBean>) {
}
override fun invalidate() {
super.invalidate()
cancel()
}
}
class DailyDataSourceFactory(private val repository: DailyRepository) : DataSource.Factory<Int, HomeDailyItemListBean>() {
override fun create(): DataSource<Int, HomeDailyItemListBean> = DailyDataSource(repository)
}
DailyViewModel.kt
class DailyViewModel(private val repository: DailyRepository) : ViewModel() {
var result: LiveData<PagedList<HomeDailyItemListBean>>? = null
fun fetchResult() {
result = LivePagedListBuilder(
DailyDataSourceFactory(repository),
PagedList.Config.Builder()
.setPageSize(ERAppConfig.PAGE_SIZE)
.setEnablePlaceholders(ERAppConfig.ENABLE_PLACEHOLDERS)
.setInitialLoadSizeHint(ERAppConfig.PAGE_SIZE_HINT)
.setPrefetchDistance((ERAppConfig.PAGE_SIZE/2))
.build()
).build()
}
}
class DailyModelFactory(private val repository: DailyRepository) : ViewModelProvider.NewInstanceFactory() {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T = DailyViewModel(repository) as T
}
DailyAdapter.kt
class DailyAdapter : PagedListAdapter<HomeDailyItemListBean, RecyclerView.ViewHolder>(diffCallback) {
internal var isLoadMore = 0
private lateinit var itemListener: OnItemClickListener
fun setOnItemListener(listener: OnItemClickListener) {
this.itemListener = listener
}
override fun getItemViewType(position: Int): Int =
when (position) {
0 -> ITEM_TYPE_HEADER
itemCount - 1 -> ITEM_TYPE_FOOTER
else -> super.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
when (viewType) {
ITEM_TYPE_HEADER -> HeaderViewHolder(parent)
ITEM_TYPE_FOOTER -> FooterViewHolder(parent)
else -> DailyViewHolder(parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> if(!currentList.isNullOrEmpty()) holder.bindsHeader(currentList!![0])
is FooterViewHolder -> holder.bindsFooter(isLoadMore)
is DailyViewHolder -> getDataItem(position)?.let { holder.bindTo(it,itemListener) }
}
}
private fun getDataItem(position: Int): HomeDailyItemListBean? =
getItem(position-1)
override fun getItemCount(): Int =
super.getItemCount() + 2
override fun registerAdapterDataObserver(observer: RecyclerView.AdapterDataObserver) {
super.registerAdapterDataObserver(BasePagedListAdapterObserver(observer, 1))
}
companion object {
private val diffCallback = object : DiffUtil.ItemCallback<HomeDailyItemListBean>() {
override fun areItemsTheSame(oldItem: HomeDailyItemListBean, newItem: HomeDailyItemListBean): Boolean =
oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: HomeDailyItemListBean, newItem: HomeDailyItemListBean): Boolean =
oldItem == newItem
}
private const val ITEM_TYPE_HEADER = 99
private const val ITEM_TYPE_FOOTER = 100
}
}
class DailyViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.view_list_item_daily, parent, false)) {
private lateinit var mBinding : ViewListItemDailyBinding
fun bindTo(data: HomeDailyItemListBean,listener: OnItemClickListener) {
mBinding = initViewBindingImpl(itemView) as ViewListItemDailyBinding
if(data.type == "textCard"){
mBinding.rlDailyLayout.layoutParams.height = 0
}else{
mBinding.rlDailyLayout.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
data.data.header.iconType = mBinding.root.resources.getString(R.string.str_release)+":"
data.data.header.issuerName = data.data.content.data.author.name
data.data.header.icon = data.data.content.data.author.icon
data.data.header.iconType = data.data.content.data.author.description
}
mBinding.item = data
mBinding.rlDailyLayout.setOnClickListener {
listener.onItemClick(layoutPosition,it)
}
}
}
internal class HeaderViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.view_list_item_header, parent, false)) {
private lateinit var mHeaderBinding : ViewListItemHeaderBinding
fun bindsHeader(item: HomeDailyItemListBean?) {
mHeaderBinding = initViewBindingImpl(itemView) as ViewListItemHeaderBinding
mHeaderBinding.headerText.visibility = View.GONE
}
}
internal class FooterViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.view_list_item_footer, parent, false)) {
private lateinit var mFooterBinding : ViewListItemFooterBinding
fun bindsFooter(isLoadMore: Int) {
mFooterBinding = initViewBindingImpl(itemView) as ViewListItemFooterBinding
when(isLoadMore){
0 -> mFooterBinding.tvBanner.text = ""
1 -> mFooterBinding.tvBanner.text = mFooterBinding.root.resources.getString(R.string.str_loading)
-1 -> mFooterBinding.tvBanner.text = mFooterBinding.root.resources.getString(R.string.str_not_more)
}
}
}
private fun initViewBindingImpl(itemView: View): ViewDataBinding? =
if (null == DataBindingUtil.getBinding(itemView)) {
DataBindingUtil.bind(itemView)
} else {
DataBindingUtil.getBinding(itemView)
}
结语
以上是项目模块的实现分析,如果理解不透可以参考学习一下整体项目。 —»> 进入项目
项目的开发主要为了加深学习印象和与实战相结合。希望对大家学习和了解Jetpack组件有所帮助。