实战安卓App,MVVM结构,仿开眼

先放开源代码:仿开眼App仓库地址,项目的git展示也在github中,大家自行查看

1、功能介绍

App使用BottomNavigationView + ViewPager2实现底部导航栏,点击切换界面。App部分页面设置沉浸式状态栏提升体验,RV列表添加动画,更加丝滑一些。对RV列表添加FloatingActionButton,可以在用户滑很远想回到上面的时候一键返回。整体结构借鉴了林潼学长的MVVM(比我之前的结构好…)

(1)首页

首页有两个页面,分别是推荐和日报,使用TabLayoutViewPager2实现页面的切换

(2)社区

社区有三个界面,广场、发现和人气。广场用PhotoView + ViewPager2实现图片的查看(第一次加载的时候有些慢),发现页面实现了Banner轮播图,分类等功能,人气为请求到的各种榜单。

(3)通知

通知没什么好说的…

(4)我的

我的页面也是体现本App一大特色的地方,实现了一键换肤、用户信息修改和收藏功能,前者通过全局的ViewModel实现,设置颜色主题后,通知所有界面主要颜色改变。后两个使用Room实现本地持久化存储。

(5)视频播放界面

视频播放调第三方库,可旋转全屏观看,在界面内可点击收藏,视频播放下面有相关视频推荐和评论。

(6)分类详情界面

视频内容点击更多可展开。

2、技术

(1)App换肤

实现该功能需要获取一个全局的ViewModel,供全局观察,当在设置好颜色之后,将appViewModel.appColor的值改变,在别的界面通过Observe观察其变化,发现其值发生改变,立即修改一些控件的颜色与主题一致。

Application中,自己定义一个工厂获取到Application的实例,并以此创建ViewModel可以在全局使用。将创建的ViewModel保存到自己的ViewModelStoreOwner中。

// 供全局引用
val appViewModel: AppViewModel by lazy {
    
     App.appViewModelInstance }

class App : Application(), ViewModelStoreOwner {
    
    

    private var mFactory: ViewModelProvider.Factory? = null
    private lateinit var mAppViewModelStore: ViewModelStore

    companion object {
    
    
        @SuppressLint("StaticFieldLeak")
        lateinit var context: Context
        lateinit var appViewModelInstance: AppViewModel
    }

    // 获取全局的 ViewModel
    private fun getAppViewModelProvider(): ViewModelProvider {
    
    
        return ViewModelProvider(this, this.getAppFactory())
    }

    // 自己定义 Factory
    private fun getAppFactory(): ViewModelProvider.Factory {
    
    
        if (mFactory == null) {
    
    
            mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(this)
        }
        return mFactory as ViewModelProvider.Factory
    }

    override fun onCreate() {
    
    
        super.onCreate()
        context = applicationContext
        mAppViewModelStore = ViewModelStore()
        appViewModelInstance = getAppViewModelProvider()[AppViewModel::class.java]
    }

    override fun getViewModelStore(): ViewModelStore {
    
    
        return mAppViewModelStore
    }
}

在其他界面使用该ViewModel,setUiTheme()是一个工具函数,用于修改控件颜色。

appViewModel.run {
    
    
    appColor.observe(this@DailyFragment) {
    
    
        setUiTheme(it, mDatabind.includeList.floatbtn, 	mDatabind.includeList.includeRecyclerview.swipeRefresh)
    }
}

但该实现方式有很大的缺点,就是可能忘记在某个界面设置颜色>_<,但目前只能想到这样实现换肤功能。

(2)扩展函数

我对一些可能经常复用的逻辑代码封装了很多扩展函数,比如初始化控件等。

// 初始化 ViewPager2
fun ViewPager2.init(
    fragment: Fragment,
    fragments: ArrayList<Fragment>,
    isUserInputEnabled: Boolean = true
): ViewPager2 {
    
    
    //是否可滑动
    this.isUserInputEnabled = isUserInputEnabled
    //设置适配器
    adapter = object : FragmentStateAdapter(fragment) {
    
    
        override fun createFragment(position: Int) = fragments[position]
        override fun getItemCount() = fragments.size
    }
    return this
}

// 初始化 FloatButton
fun RecyclerView.initFloatBtn(floatBtn: FloatingActionButton) {
    
    
    // 监听recyclerview滑动到顶部的时候,把向上返回顶部的按钮隐藏
    addOnScrollListener(object : RecyclerView.OnScrollListener() {
    
    
        @SuppressLint("RestrictedApi")
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
    
    
            super.onScrolled(recyclerView, dx, dy)
            if (!canScrollVertically(-1)) {
    
    
                floatBtn.visibility = View.INVISIBLE
            }
        }
    })
    floatBtn.backgroundTintList = ColorUtil.getOneColorStateList(App.context)
    floatBtn.setOnClickListener {
    
    
        val layoutManager = layoutManager as LinearLayoutManager
        // 如果当前recyclerview 最后一个视图位置的索引大于等于20,则迅速返回顶部,否则带有滚动动画效果返回到顶部
        if (layoutManager.findLastVisibleItemPosition() >= 30) {
    
    
            scrollToPosition(0) // 没有动画迅速返回到顶部
        } else {
    
    
            smoothScrollToPosition(0) // 有滚动动画返回到顶部
        }
    }
}

// 初始化BottomNavigationView,在init内即可实现其与ViewPager2的结合
fun BottomNavigationView.init(navigationItemSelectedAction: (Int) -> Unit) : BottomNavigationView {
    
    
    itemIconTintList = ColorUtil.getColorStateList(ColorUtil.getColor(App.context))
    itemTextColor = ColorUtil.getColorStateList(App.context)
    setOnNavigationItemSelectedListener {
    
    
        navigationItemSelectedAction.invoke(it.itemId)
        true
    }
    return this
}
......

(3)网络状态变化的监听

定义一个NetStateManager,提供实例给外部,在MainActivity中监听网络状态,可以管理多个界面网络状态的变化。

class NetStateManager private constructor() {
    
    

    val mNetworkStateCallback =
        EventLiveData<NetState>()

    companion object {
    
    
        val instance: NetStateManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
    
    
            NetStateManager()
        }
    }
}

// MainActivity中
private fun onNetworkStateChanged(netState: NetState) {
    
    
	if (netState.isSuccess) {
    
    
    	Toast.makeText(applicationContext, "我现在有网哦!", Toast.LENGTH_SHORT).show()
    } else {
    
    
        Toast.makeText(applicationContext, "我怎么断网了呀!", Toast.LENGTH_SHORT).show()
    }
}

(4)网络请求数据与本地存储数据结合

在本项目中,除了在网络上请求数据,还使用Room数据库框架实现本地数据持久化存储,主要存储我的喜欢以及用户的个人信息。二者数据都在仓库层中获取。

// 从网络获取数据
fun loadNotify() = fire(Dispatchers.IO) {
    
    
        val response = ApiLoad.loadNotify()
        val length = response.messageList.size
        val list = mutableListOf<NotifyData>()
        for (i in 0 until length) {
    
    
            val messageData = NotifyData(
                response.messageList[i].title,
                response.messageList[i].content,
                response.messageList[i].date,
                response.nextPageUrl
            )
            list.add(messageData)
        }

        Result.success(list)
    }

// 从数据库中获取数据
fun loadMyLike() : LiveData<List<VideoInfoBean>> {
    
    
    return videoDao.loadAllVideos()
}

(5)自定义悬浮按钮Behavior

实现悬浮按钮,用户滑动过多通过该按钮一键返回顶部,提升体验。

@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout,
                                   @NonNull FloatingActionButton child,
                                   @NonNull View directTargetChild,
                                   @NonNull View target,
                                   int nestedScrollAxes) {
    
    

    // 判断是否是垂直滚动,是则返回true
    return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
            super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}

@SuppressLint("RestrictedApi")
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child,
                           @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    
    

    super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);

    if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
    
    
        // 如果向上滑动则隐藏
        child.setVisibility(View.INVISIBLE);
    } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
    
    
        // 向下滑动则展示出来
        child.show();
    }
}

(6)RV部分

1. DiffUtil差分局部刷新

class DiffCallBack(private val oldList: MutableList<VideoInfoBean>, private val newList: MutableList<VideoInfoBean>) :
    DiffUtil.Callback() {
    
    

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    
    
        return oldList[oldItemPosition] === newList[newItemPosition]
    }

    override fun getOldListSize(): Int {
    
    
        return oldList.size
    }

    override fun getNewListSize(): Int {
    
    
        return newList.size
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    
    
        return oldList[oldItemPosition] == newList[newItemPosition]
    }

}

// 应用,在喜欢界面删除数据
fun removeData(position: Int) {
    
    
    val oldList = mList
    mList?.removeAt(position)
    val diffCallBack = DiffUtil.calculateDiff(DiffCallBack(oldList, mList))
    diffCallBack.dispatchUpdatesTo(this)
}

2. 自定义ItemTouchHelperCallBack实现侧换删除和长按拖拽

这部分在我的课件里有所讲述,这里就不多交代了。通过实现这四个接口来实现功能。

interface OnHelperCallBack {
    
    
    fun onMove(fromPosition: Int, targetPosition: Int)

    fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder, actionState: Int)

    fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder)

    fun remove(viewHolder: RecyclerView.ViewHolder, direction: Int, position: Int)
}

// 应用,在LikeActivity中
callBack = RecyclerTouchHelpCallBack(object : RecyclerTouchHelpCallBack.OnHelperCallBack {
    
    
    override fun onMove(fromPosition: Int, targetPosition: Int) {
    
    
        likeAdapter.mList?.let {
    
     it1 ->
            callBack.itemMove(likeAdapter,
                it1, fromPosition, targetPosition)
        }
    }

    override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder, actionState: Int) {
    
    
	// 选中修改Item样式
        viewHolder.itemView.alpha = 1f
        viewHolder.itemView.scaleX = 1.2f
        viewHolder.itemView.scaleY = 1.2f
    }

    override fun clearView(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder
    ) {
    
    
        likeAdapter.mList
        // 松手修改Item样式
        viewHolder.itemView.alpha = 1f
        viewHolder.itemView.scaleX = 1f
        viewHolder.itemView.scaleY = 1f
    }

    // 删除数据
    override fun remove(
        viewHolder: RecyclerView.ViewHolder,
        direction: Int,
        position: Int
    ) {
    
    
        // 在数据库中删除
        videoDao.deleteVideo(it[position])
        likeAdapter.removeData(position)
    }

})

callBack.edit = true
ItemTouchHelper(callBack).attachToRecyclerView(rvLike)

如果大家觉得有用,希望点个star~

猜你喜欢

转载自blog.csdn.net/m0_51276753/article/details/126717207