JetPack项目实战---音乐播放器
1更:作为前期搭建
-
图片准备:
- 图片就是网页的图片,都是可以换的;应该是加载的是图片的URL
-
概述:
-
JetPack全家桶+MVVM--->实现一个Activity+多个Fragment
- Activity只做控制
-
所有的布局文件交给DataBinding管理
-
涉及到的库:ViewModel,LiveData,LifeCycle,DataBinding,Navigation+MVVM
-
每一个Activity/Fragment对应一个ViewModel(保存数据的),再用ViewModel去调用仓库层
- MainActivityViewModel
- MainActivity对应的布局文件activity_main.xml交给DataBinding进行管理
-
-
Kotlin的弊端
-
Kotlin的注解不完善--->一般都是用java的注解
-
Kotlin的反射不完善
- 会额外开辟空间,1.6MB
- 学习成本高
- 比java慢,但是这个确实好用
-
-
布局设置:交给dataBinding 进行管理
-
概述:
- 任何一个布局都有一个Activity,对应一个ViewModel
-
-
项目架构:
- app 领域:项目主导的业务代码
- architecture:一些通用的支持,项目通用的工具类集合
具体流程
-
先写了MainActivity作为控制层,里面什么代码都没有
-
写布局文件activity_main.xml:交给DataBinding进行管理(开启开关,点击布局标签进行转换)
-
交给DataBinding进行管理
- 开启DataBinding的开关,布局要进行转换
- 这里就有一个ViewModel,它是来管理整个MainActivity中的数据来源的(一个布局至少有一个ViewModel)
-
抽屉控件
- 因为抽屉控件只能左右滑动:DrawerLayout(这个是主页面的与侧滑的)
- 但是需要上下滑动,所以就搞了:SlidingUpPanelLayout(这个是主页面里面的)
- 抽屉控件中就有ViewModel的值,允许抽屉打开/关闭+具体打开/关闭抽屉
-
引入了Fragment:遵循一个Activity多个Fragment的开发模式:
-
具体来说在activity_main.xml中引入了三个fragment标签
- 整个大的Fragment,包裹了RecycleView
- 下面那个播放条,点一下就会弹上去的
- 侧滑栏的Fragment:处理抽屉控件的
-
-
引入Navigation进行视图导航:Navigation本来就是用作Fragment导航使用的
-
三个fragment标签都具有下面这行语句,代表这三个fragment都是交给Navigation作导航
android:name="androidx.navigation.fragment.NavHostFragment"
-
每个Fragment都有各自的导航关系
- 代码示意:
app:navGraph="@navigation/nav_drawer"
-
具体的导航关系:扔到app/res/navigation 里面
-
在app/res/navigation 目录下加载的layout文件,这个才是拿来真正显示界面的
-
在app/res/navigation 目录下的布局文件中引入fragment标签,fragment标签引入布局文件(tools:layout="@layout/fragment_main"),这个布局文件才拿真正显示界面的
-
-
-
开始写首页的Fragment:MainFragment
-
创建相应的布局文件:fragment_main.xml
- 交给DataBinding进行管理
-
全盘使用DataBinding进行布局管理:
-
拿到首页布局:fragment_main.xml:
private var mainBinding: FragmentMainBinding? = null//就是fragment_main.xml了
-
-
创建首页的ViewModel(mainViewModel):每一个fragment都有一个ViewModel
-
首页的ViewModel(MainViewModel):用于管理首页的数据
位置:package com.xiangxue.puremusic.bridge.state private var mainViewModel : MainViewModel? = null // 首页Fragment的ViewModel todo Status ViewModel
总结一下:这个是有很深的嵌套关系的
MainActivity--->activity_main.xml--->抽屉控件(左右)--->抽屉控件(上下)--->首页的Fragment标签--->nav_main--->res/navigation/nav_main.xml--->fragment标签中的MainFragment(这个东西加载的layout文件才是真正拿来显示界面的,这个才是首页的Fragment)
有三个Fragment就要有三遍流程
再次总结:项目整体的页面逻辑
-
使用Navigation处理activity_main.xml中的导航工作
-
在相应的nav_XXX.xml中包含了多个Fragment
- 这些Fragment各自布局的文件处理页面具体显示内容(DataBinding)
- 这些Fragment都含有各自的ViewModel用于管理数据
-
项目中一共有4个Fragment,4个布局文件,4个ViewModel
-
-
-
引入基类:
-
原因:因为MainActivity继承Activity的话是不行的,Activity并没有实现相应的接口,导致无法使用LifeCycle的东西
-
解决:设计基类BaseActivity,BaseFragment;这两个基类会提供大量的工具函数,模板设计思路
-
BaseActivity:
-
继承AppCompatActivity:因为AppCompatActivity的爷爷类ComponentActivity实现了 LifeCycle接口,在开发中需要使用this指针,持有环境
-
open剔除final修饰:要拿给其他的类继承
open class BaseActivity : AppCompatActivity()
-
注册LifeCycle,因为后面的所有Activity都会继承BaseActivity,这些Activity作为被观察者
//这个BaseActivity为被观察者;NetworkStateManager为观察者 //眼睛是从后往前看的,有点像静态内部类实现单例模式 lifecycle.addObserver(NetworkStateManager.instance)
-
-
BaseFragment:所有Fragment的父类
- Fragment持有Activity环境:
-
-
引入工具类:
-
architecture里面的Utils:
- 有很多工具类:比如说换掉上面的Bar
-
architecture里面的binding包下面的东西
-
大量使用了@BindingAdapter注解:解决在控件中编写逻辑的问题
- 在Google官网上面使用DataBinding的时候,将逻辑写到了xml里面去了,导致耦合度较高:怎么能把逻辑写到控件里面去呢?
- 使用BindingAdapter的好处:减轻了DataBinding的压力
-
当对自定义的字段赋值为ViewModel的引用时:
- 会触发这个binding工具类,直接干到public static void setViewBackground,在这个函数中就可以写业务代码了
-
-
引入适配器:在项目中大量的使用了RecycleView
-
data/manager:使用LifeCycle进行整个项目的状态监听:作为扩展功能
-
比如说当页面可见并且网络等系统发生改变时,就可以用服务的手段进行提示或者处理
-
网络不给力,电量较低等
-
具体实现:依托LifeCycle;当界面不可见的时候,就不要浪费性能了
- 不可见,不提示
- 当界面可见的时候,注册广播;
- 当界面不可见的时候,移除广播;
-
-
-
丰富工具类剔除数据粘性:UnPeekLiveData
-
不解决的话,会发生数据倒灌;使用反射动态修改代码,让他的状态对齐
-
其实就是执行这个if语句就行了
if (observer.mLastVersion >= mVersion) { return; } observer.mLastVersion = mVersion; observer.mObserver.onChanged((T) mData);
-
-
引入mSharedViewModel:干掉EventBus;
-
因为EventBus会有很多的Bean类,带来可追逐性不强的问题
-
但是LiveData可追逐性非常强:
- 只有setValue,observe:在发生问题的时候,排查起来很好
-
另外,可以让所有的Activity/Fragment共享数据
-
只要动了这个mSharedViewModel所有与之关联的Activity/Fragment都会随之改变,解决一致性问题当界面可见就会更新,但是不可见就不操作;但是又可见的时候就还是会变;这个ViewModel会解决数据存储以及一致性问题;这个才是最关键的东西
- 会解决按下就会换掉封面,按下就会换歌曲的名字等 ·
-
保证mSharedViewModel单例:getAppViewModelProvider
-
这个还搭配了一个工厂方法:保证ViewModel独一份;
-
是怎么保证这个共享ViewModel独一份?
-
-
-
Kotlin的注解问题:
- 比java慢,有几兆的空间开辟,还有导包,还有Kotlin自己的API;但是呢,Kotlin的注解非常好用
ViewModel的设计
-
一个Fragment可能有多个ViewModel
-
任何一个Fragment必须有一个ViewModel
- 不会再写什么findViewById,setContentView布局文件全部交给DataBinding进行管理了
-
可能还会有多个的ViewModel:这个东西是要看需求的,有可能有七八个ViewModel
-
-
所以说这个就是Google标准架构设计模式了
2更
-
细化MainActivity:发送指令,子Fragment进行干活;当然它也可以接收指令(共享ViewModel驱动它干活)
-
处理共享ViewModel:取代EventBus(消息总线)
-
所有的Activity/Fragment都可以从这个里面拿数据
-
ViewModel的使用:
- setValue:改变
- obeserver:监听
- postValue:通过异步线程执行的,最终会调到setValue里面去的
- 所有ViewModel中的字段都是为了驱动干活的
-
所有ViewModel中的字段全部交给LiveData进行管理了
-
剔除掉数据粘性:防止数据倒灌产生的问题
- 不能直接使用MutableLiveData,需要使用工具类中那个剔除掉数据粘性的UnPeekLiveData
-
-
设置监听:控制上下滑动
// 是为了 sliding.addPanelSlideListener(new PlayerSlideListener(mBinding, sliding)); //通过这个东西(bool值):实现点击首页的播放条,弹出一个界面:就是那个上下滑动的东西 val timeToAddSlideListener = UnPeekLiveData<Boolean>()
-
设置监听:关掉弹上来的播放详情
// 为了关掉弹上来的菜单,点击系统Back键或者播放详情里面的那个v都可以关掉的 // 播放详情中 左手边滑动图标(点击的时候),与 MainActivity back 是 会set ----> 如果是扩大,也就是 详情页面展示了出来 val closeSlidePanelIfExpanded = UnPeekLiveData<Boolean>()
-
扩展功能:现在到底是个什么情况,记录Activity的活动
// 活动关闭的一些记录(实在没有发现他,到底有什么卵用) val activityCanBeClosedDirectly = UnPeekLiveData<Boolean>()
-
设置监听:左侧栏出现
// openMenu打开菜单的时候会 set触发---> 改变 openDrawer.setValue(aBoolean); 的值 val openOrCloseDrawer = UnPeekLiveData<Boolean>()
-
开启卡片的问题:此共享ViewModel中的LiveData与其他的结合
// 开启和关闭 卡片相关的状态,如果发生改变 会和 allowDrawerOpen 挂钩 val enableSwipeDrawer = UnPeekLiveData<Boolean>()
-
-
丰富MainActivity
-
添加眼睛:通过观察共享ViewModel中的字段作一定的过滤工作并借助中间的ViewModel实现对控件的间接管控
-
比如说:判断菜单是否打开
- 观察共享ViewModel中的数据有无改变:共享ViewModel中是有这个菜单的开闭情况(是有一个剔除掉粘性的UNPeekLiveData进行管控的)
- 过滤后,借助mainActivityViewModel来对mainActivityViewModel中相关字段设值,实现对mainActivityViewModel对应的布局(activity_main.xml)属性进行设置
// 间接的可以打开菜单 (观察)做很多的过滤检测工作,在通过mainActivityViewModel去工作,这个就是真实的项目开发,间接使用 mSharedViewModel.openOrCloseDrawer.observe(this, { aBoolean -> mainActivityViewModel!!.openDrawer.value = aBoolean // 触发,就会改变,---> 观察(打开菜单逻辑) }) // 间接的xxxx (观察) mSharedViewModel.enableSwipeDrawer.observe(this, { aBoolean -> mainActivityViewModel!!.allowDrawerOpen.value = aBoolean // 触发抽屉控件关联的值 })
-
重写Back键:点击后将播放详情页面放下来
/** * https://www.jianshu.com/p/d54cd7a724bc * Android中在按下back键时会调用到onBackPressed()方法, * onBackPressed相对于finish方法,还做了一些其他操作, * 而这些操作涉及到Activity的状态,所以调用还是需要谨慎对待。 */ override fun onBackPressed() { // super.onBackPressed(); mSharedViewModel.closeSlidePanelIfExpanded.value = true // 触发改变 }
-
判断当前的Activity真正呈现给用户了
-
重写这个函数:onWindowFocusChanged
/** * 详情看:https://www.cnblogs.com/lijunamneg/archive/2013/01/19/2867532.html * 这个onWindowFocusChanged指的是这个Activity得到或者失去焦点的时候 就会call。。 * 也就是说 如果你想要做一个Activity一加载完毕,就触发什么的话 完全可以用这个!!! * entry: onStart---->onResume---->onAttachedToWindow----------->onWindowVisibilityChanged--visibility=0---------->onWindowFocusChanged(true)-------> * @param hasFocus */ override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) if (!isListened) { mSharedViewModel.timeToAddSlideListener.value = true // 触发改变 isListened = true } }
-
-
-
处理MainActivityViewModel
-
管控的就是activity_main.xml这个布局文件中的属性
-
添加了注解:消除了openDrawer的getter函数(因为这个变量是val,是没有set函数的,如果是var类型的;会把get/set函数一起消掉),不
// 首页需要记录抽屉布局的情况 (响应的效果,都让 抽屉控件干了) @JvmField val openDrawer = MutableLiveData<Boolean>()
-
-
-
处理activity_main.xml中的一些小细节了
-
处理这个东西
-
这个allowDrawerOpen与isOpenDrawer是DataBinding中的东西了:抽象出BindingAdapter,BindingAdapter将功能抽出来了,抽出来了:独立的,可复用
-
要支持这个BindingAdapter:
- 需要在build.gradle中添加这个:kotlin-kapt(Kotlin中的注解处理器)
package com.xiangxue.puremusic.data.binding import androidx.core.view.GravityCompat import androidx.databinding.BindingAdapter import androidx.drawerlayout.widget.DrawerLayout /** * TODO 同学们一定要看哦,才能知道为什么,那么多同学一直编译不通过,各种错误,真正的原因是在这里哦,这里和布局建立绑定的呢 * 注意:这个类的使用,居然是和 activity_main.xml 里面的 allowDrawerOpen 和 openDrawer 挂钩的 */ object DrawerBindingAdapter { // 打开抽屉 与 关闭抽屉 @JvmStatic @BindingAdapter(value = ["isOpenDrawer"], requireAll = false) fun openDrawer(drawerLayout: DrawerLayout, isOpenDrawer: Boolean) { if (isOpenDrawer && !drawerLayout.isDrawerOpen(GravityCompat.START)) { drawerLayout.openDrawer(GravityCompat.START) } else { drawerLayout.closeDrawer(GravityCompat.START) } } // 允许抽屉打开 与 关闭 @JvmStatic @BindingAdapter(value = ["allowDrawerOpen"], requireAll = false) fun allowDrawerOpen(drawerLayout: DrawerLayout, allowDrawerOpen: Boolean) { drawerLayout.setDrawerLockMode(if (allowDrawerOpen) DrawerLayout.LOCK_MODE_UNLOCKED else DrawerLayout.LOCK_MODE_LOCKED_CLOSED) } }
-
-
关于界面中的图片:模拟json解析
-
-
现在处理这个导航图的问题
-
导航图中的子Fragment:MainFragment
-
寻找位置:activity_main.xml--->fragment:navGraph="@navigation/nav_main" --->nav_main.xml--->android:name="com.xiangxue.puremusic.ui.page.MainFragment"
-
处理ViewModel:由ViewModel的功能不同所以要写多个ViewModel,包都不同
// 我们操作布局,不去传统方式操作,全部使用Databindxxx private var mainBinding: FragmentMainBinding? = null//就是fragment_main.xml了 //mainViewModel只处理这个Fragment中的一些状态;比如说滑动等 private var mainViewModel : MainViewModel? = null // 首页Fragment的ViewModel todo Status ViewModel //musicRequestViewModel:这个ViewModel就是处理请求方面的 private var musicRequestViewModel: MusicRequestViewModel? = null // 音乐资源相关的VM todo Request ViewModel
-
界面使用RecycleView
- 适配器加进去
// 适配器 private var adapter: SimpleBaseBindingAdapter<TestMusic?, AdapterPlayItemBinding?>? = null
- 初始化ViewModel
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //初始化ViewModel mainViewModel = getFragmentViewModelProvider(this).get(MainViewModel::class.java) musicRequestViewModel = getFragmentViewModelProvider(this).get(MusicRequestViewModel::class.java) }
- DataBinding关联ViewModel
override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val view: View = inflater.inflate(R.layout.fragment_main, container, false) mainBinding = FragmentMainBinding.bind(view) mainBinding ?.click = ClickProxy() // 设置点击事件,布局就可以直接绑定 mainBinding ?.setVm(mainViewModel) // 设置VM,就可以实时数据变化 return view }
-
这个里面有个ClickProxy():统管所有的点击事件,如果说用ViewModel 代替他的话,Fragment的环境不好拿
inner class ClickProxy { // 当在首页点击 “菜单” 的时候,直接导航到 ---> 菜单的Fragment界面 fun openMenu() { sharedViewModel.openOrCloseDrawer.value = true } // 触发 // 当在首页点击 “搜索图标” 的时候,直接导航到 ---> 搜索的Fragment界面 fun search() = nav().navigate(R.id.action_mainFragment_to_searchFragment) }
-
-
-
现在就去处理MainFragment的布局文件
-
这个是交给DataBinding进行管理的,所以布局可以分为DataBinding区域与UI区域
-
DataBinding区域:
<data> <!-- 点击事件 --> <variable name="click" type="com.xiangxue.puremusic.ui.page.MainFragment.ClickProxy" /> <variable name="vm" type="com.xiangxue.puremusic.bridge.state.MainViewModel" /> </data>
-
处理UI区域:代码太多了
-
上面那个图片包含的区域
-
折叠工具栏:三个按钮
-
打开侧滑的菜单
-
打开搜索页面:依托Navigation进行导航
-
处理这个点击事件的执行:打开侧滑的菜单
-
现在页面的总控点击中进行注册(部分代码)其实就行了
inner class ClickProxy { // 当在首页点击 “菜单” 的时候,直接导航到 ---> 菜单的Fragment界面 fun openMenu() { sharedViewModel.openOrCloseDrawer.value = true } // 触发 }
-
会在MainActivity中有眼睛在看的,执行相应的逻辑
-
还有眼睛在看:才打开菜单
处理点击事件:打开搜索页面
-
在页面的监听总控中添加代码
inner class ClickProxy { // 当在首页点击 “搜索图标” 的时候,直接导航到 ---> 搜索的Fragment界面 fun search() = nav().navigate(R.id.action_mainFragment_to_searchFragment) }
-
在BaseFragment中进行跳转
/** * 为了给所有的 子fragment,导航跳转fragment的;封装了Navigation * @return */ protected fun nav(): NavController { return NavHostFragment.findNavController(this) }
-
需要注意跳转的Navigation的id是指定了的:来自NavigationR.id.action_mainFragment_to_searchFragment
-
-
ToolBar:它是有点击功能的,点左右两边会跳转到不同的界面的
-
有个ViewModel的
-
代码:
*/ class MainViewModel : ViewModel() { // ObservableBoolean or LiveData // ObservableBoolean 防止抖动,频繁改变,使用这个的场景 // LiveData 反之 // MainFragment初始化页面的标记 例如:“最近播放” 的记录 @JvmField//剔除set val initTabAndPage = ObservableBoolean() // MainFragment “最佳实践” 里面的 WebView需要加载的网页链接路径 @JvmField val pageAssetPath = ObservableField<String>() }
防止抖动:界面持续更新,例如下载的进度条
- 虽然没有加载图片,但是还是会有内存开销较大的问题 - 结论:当界面更新十分频繁时用Observable不用LiveData - 节约运行内存 - 因为LiveData开销很大 - Observable是属于DataBinding的,这个东西比LiveData出现早,优化历史长
-
-
ViewPager1:实现页面切换的
-
其他信息页面
<!-- 2.“其他信息区域” 其实就是 WebView展示网页信息而已 --> <androidx.core.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <WebView android:id="@+id/web_view" pageAssetPath="@{vm.pageAssetPath}" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#B4D9DD" android:clipToPadding="false" android:visibility="visible" /> </androidx.core.widget.NestedScrollView>
这个功能是如何实现的?
-
将其抽到BindingAdapter里面去了:大量使用DataBinding的优势
-
加载本地的页面:
//加载本地的页面 @JvmStatic @SuppressLint("SetJavaScriptEnabled") @BindingAdapter(value = ["pageAssetPath"], requireAll = false) fun loadAssetsPage(webView: WebView, assetPath: String) { webView.webViewClient = object : WebViewClient() {
-
加载网络的页面:
//加载网络的东西 @SuppressLint("SetJavaScriptEnabled") @BindingAdapter(value = ["loadPage"], requireAll = false) fun loadPage(webView: WebView, loadPage: String?) { webView.webViewClient = WebViewClient() webView.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
-
-
处理recyclerview:listitem
- 代码展示:
<androidx.recyclerview.widget.RecyclerView 1·汽修厂 android:id="@+id/rv" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:visibility="visible" 是的撒相亲网站 app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:listitem="@layout/adapter_play_item" />
-
实际上就是丢了一个布局文件过来的:还是要交给DataBinding进行管理(方便以后进行扩展使用)
-
添加TabLayout对应的DataBinding的东西
object TabPageBindingAdapter { @JvmStatic @BindingAdapter(value = ["initTabAndPage"], requireAll = false) fun initTabAndPage(tabLayout: TabLayout, initTabAndPage: Boolean) { if (initTabAndPage) { val count = tabLayout.tabCount val title = arrayOfNulls<String>(count) for (i in 0 until count) { val tab = tabLayout.getTabAt(i) if (tab != null && tab.text != null) { title[i] = tab.text.toString() } } val viewPager: ViewPager = tabLayout.rootView.findViewById(R.id.view_pager) if (viewPager != null) { viewPager.adapter = CommonViewPagerAdapter(count, false, title) tabLayout.setupWithViewPager(viewPager) } } } @BindingAdapter(value = ["tabSelectedListener"], requireAll = false) fun tabSelectedListener(tabLayout: TabLayout, listener: OnTabSelectedListener?) { tabLayout.addOnTabSelectedListener(listener) } }
-
-
处理MainFragment: -
-
添加适配器:处理recyclerview:listitem
// 适配器 private var adapter: SimpleBaseBindingAdapter<TestMusic?, AdapterPlayItemBinding?>? = null
-
将adapter_play_item.xml交给DataBinding进行管理,出来一个AdapterPlayItemBinding
// 设置设配器(item的布局 和 适配器的绑定) adapter = object : SimpleBaseBindingAdapter<TestMusic?, AdapterPlayItemBinding?>(context, R.layout.adapter_play_item) {
-
在onViewCreated进行初始化操作
-
触发Tablelayout:
mainViewModel!!.initTabAndPage.set(true)对应了Tablelayout
-
触发WebView:
// 触发,---> 还要加载WebView mainViewModel!!.pageAssetPath.set("JetPack之 WorkManager.html")
-
设置适配器(item的布局 和 适配器的绑定)
// 展示数据,适配器里面的的数据 展示出来 // 设置设配器(item的布局 和 适配器的绑定) adapter = object : SimpleBaseBindingAdapter<TestMusic?, AdapterPlayItemBinding?>(context, R.layout.adapter_play_item) { override fun onSimpleBindItem( binding: AdapterPlayItemBinding?, item: TestMusic?, holder: RecyclerView.ViewHolder? ) { binding ?.tvTitle ?.text = item ?.title // 通过DataBinding进行绑定:标题 binding ?.tvArtist ?.text = item ?.artist ?.name // 歌手 就是 艺术家 //使用Glide加载网络的东西 Glide.with(binding ?.ivCover!!.context).load(item ?.coverImg).into(binding.ivCover) // 左右边的图片 // 歌曲下标记录,为了以后的扩展 val currentIndex = PlayerManager.instance.albumIndex // 歌曲下标记录 // 播放的标记 binding.ivPlayStatus.setColor( if (currentIndex == holder ?.adapterPosition) resources.getColor(R.color.colorAccent) else Color.TRANSPARENT ) // 播放的时候,右变状态图标就是红色, 如果对不上的时候,就是没有 // 点击Item binding.root.setOnClickListener { v -> Toast.makeText(mContext, "播放音乐", Toast.LENGTH_SHORT).show() PlayerManager.instance.playAudio(holder !!.adapterPosition) } } } //将适配器帮到RecycleView上 mainBinding!!.rv.adapter = adapter
初始化一般都在重写的方法中去搞,不然this指针会有问题
-
-
添加musicRequestViewModel:处理请求的
-
按照功能的不同可以搞很多个ViewModel;但是StateViewModel就只有一份的
-
代码示例:
class MusicRequestViewModel : ViewModel() { var freeMusicsLiveData: MutableLiveData<TestAlbum>? = null get() { if (field == null) { field = MutableLiveData() } return field } //不准外界使用set private set fun requestFreeMusics() { HttpRequestManager.instance.getFreeMusic(freeMusicsLiveData) } }
-
-
引入仓库层:在真实开发中是很麻烦的
-
示意图:
-
部分代码展示:
//在这个里面,当服务器返回了什么东西,就可以直接干给LiveData用了 override fun getFreeMusic(liveData: MutableLiveData<TestAlbum>?) { // 处理json数据了模拟网络请求:将json数据搞成javaBean,再赋值给LiveData val gson = Gson() val type = object : TypeToken<TestAlbum?>() {}.type val testAlbum = gson.fromJson<TestAlbum>(Utils.getApp().getString(R.string.free_music_json), type) // TODO 在这里可以请求网络 // TODO 在这里可以请求网络 // TODO 在这里可以请求数据库 // ..... liveData!!.value = testAlbum }
-
总体来说:
RequestViewModel调用仓库,仓库中进行一系列操作(解析JSON数据,封装成JavaBean),交给LiveData;又因为仓库中的LiveData和RequestViewModel对应的LiveData是同一份;并且在进行生命周期监听时,监听的是requestViewModel,所以requestViewModel一改变就会刷新UI
-
3更
-
为什么不用模拟机
-
模拟器有个WebView加载失败的问题:找不到Class
- 模拟器加载中的WebView,Gradle兼容的问题
-
-
安卓原生的MediaPlayer的使用;
-
PlayManager:播放音乐时的一些细节
- PlayerCallHelper:在来电时自动协调和暂停音乐播放,Google 原生就不会做,其他的厂商就有
- PlayerReceiver:播放的广播,用于接收 某些改变(系统发出来的信息,断网了),对音乐做出对应操作
- PlayerService:实现后台播放
- PlayerManager:处理播放细节问题;
-
Navigation2:播放页面,也就是第二个Fragment:PlayerFragment;
-
初始化ViewModel:
// 初始化 VM playerViewModel = getFragmentViewModelProvider(this).get<PlayerViewModel>(PlayerViewModel::class.java)
-
将DataBinding与ViewModel进行绑定
//DataBnding+ViewModel override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { // 加载界面 val view: View = inflater.inflate(R.layout.fragment_player, container, false) // 绑定 Binding binding = FragmentPlayerBinding.bind(view) binding ?.click = ClickProxy()//上一首,暂停,下一首 binding ?.event = EventHandler()//拖动条的问题,拖到哪里播放到哪里 binding ?.vm = playerViewModel//ViewModel与布局绑定 return view }
-
添加眼睛:观察数据的变化
-
共享ViewModel监听,添加眼睛:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 观察 sharedViewModel.timeToAddSlideListener.observe(viewLifecycleOwner, { if (view.parent.parent is SlidingUpPanelLayout) { val sliding = view.parent.parent as SlidingUpPanelLayout // 添加监听(弹上来) sliding.addPanelSlideListener(PlayerSlideListener(binding!!, sliding)) } })
-
实现点击播放条,向上弹出一个页面
-
PlayerFragment中的onViewCreated方法
sharedViewModel.timeToAddSlideListener.observe(viewLifecycleOwner, { if (view.parent.parent is SlidingUpPanelLayout)
-
实际上就是在这个:SharedViewModel里面
-
实际上是在MainActivity中的
实现安卓原生未来趋势:JetPack全家桶+MVVM架构模式
-
-
PlayerFragment嵌套模式:activity_main.xml--->.SlidingUpPanelLayout这个控件包裹的fragment中的app:navGraph="@navigation/nav_slide" />--->fragment中的android:name="com.xiangxue.puremusic.ui.page.PlayerFragment"--->PlayerFragment
-
根据嵌套模式,进行添加监听:
// 观察到数据的变化,我就变化 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // 观察 sharedViewModel.timeToAddSlideListener.observe(viewLifecycleOwner, { //根据PlayerFragment的嵌套关系,对其进行监听; if (view.parent.parent is SlidingUpPanelLayout) { val sliding = view.parent.parent as SlidingUpPanelLayout // 添加监听(弹上来) sliding.addPanelSlideListener(PlayerSlideListener(binding!!, sliding)) } })
-
为PlayerFragment添加监听:实际上是对SlidingUpPanelLayout添加监听:class PlayerSlideListener(
-
添加眼睛:changeMusicLiveData
// 我是播放条,我要去变化,我成为观察者 -----> 播放相关的类PlayerManager PlayerManager.instance.changeMusicLiveData.observe(viewLifecycleOwner, { changeMusic -> // 例如 :理解 切歌的时候, 音乐的标题,作者,封面 状态等 改变 playerViewModel!!.title.set(changeMusic.title) playerViewModel!!.artist.set(changeMusic.summary) playerViewModel!!.coverImg.set(changeMusic.img) })
-
解决PlayerViewModel:ViewModel只做数据管理;
- 此时就不用LiveData使用ObservableField:因为我在不可见的时候,还是要播放歌曲
-
*/ class PlayerViewModel : ViewModel() { // 歌曲名称 @JvmField//剔除get函数 val title = ObservableField<String>()
-
添加眼睛:暂停播放
// 播放/暂停是一个控件 图标的true和false PlayerManager.instance.pauseLiveData.observe(viewLifecycleOwner, { aBoolean -> playerViewModel!!.isPlaying.set(!aBoolean!!) // 播放时显示暂停,暂停时显示播放 })
-
列表循环,单曲循环,随机播放:最后一行作为返回值,决定播放模式是三个中的哪一个
// 列表循环,单曲循环,随机播放 PlayerManager.instance.playModeLiveData.observe(viewLifecycleOwner, { anEnum -> val resultID: Int resultID = if (anEnum === PlayingInfoManager.RepeatMode.LIST_LOOP) { // 列表循环 playerViewModel!!.playModeIcon.set(MaterialDrawableBuilder.IconValue.REPEAT) R.string.play_repeat // 列表循环 } else if (anEnum === PlayingInfoManager.RepeatMode.ONE_LOOP) { // 单曲循环 playerViewModel!!.playModeIcon.set(MaterialDrawableBuilder.IconValue.REPEAT_ONCE) R.string.play_repeat_once // 单曲循环 } else { // 随机循环 playerViewModel!!.playModeIcon.set(MaterialDrawableBuilder.IconValue.SHUFFLE) R.string.play_shuffle // 随机循环 } // 真正的改变 if (view.parent.parent is SlidingUpPanelLayout) { val sliding = view.parent.parent as SlidingUpPanelLayout if (sliding.panelState == SlidingUpPanelLayout.PanelState.EXPANDED) {//当播放条展开的时候,才来:这个是封装好了的 // 这里一定会弹出:“列表循环” or “单曲循环” or “随机播放” showShortToast(resultID) } } })
-
当点击Back键的时候:观察这个东西closeSlidePanelIfExpanded,因为Back是做了两次的,拿给共享的ViewModel用,以后随便添加Fragment只是需要改变状态的
// 例如:场景 back 要不要做什么事情 sharedViewModel.closeSlidePanelIfExpanded.observe(viewLifecycleOwner, { if (view.parent.parent is SlidingUpPanelLayout) { val sliding = view.parent.parent as SlidingUpPanelLayout // 如果是扩大,也就是,详情页面展示出来的 if (sliding.panelState == SlidingUpPanelLayout.PanelState.EXPANDED) { sliding.panelState = SlidingUpPanelLayout.PanelState.COLLAPSED // 缩小了 } else { sharedViewModel.activityCanBeClosedDirectly.setValue(true) } } else { sharedViewModel.activityCanBeClosedDirectly.setValue(true) } })
-
监听的问题:
/** * 当我们点击的时候,我们要触发 */ inner class ClickProxy { /*public void playerMode() { PlayerManager.getInstance().changeMode(); }*/ fun previous() = PlayerManager.instance.playPrevious() operator fun next() = PlayerManager.instance.playNext() // 点击缩小的 fun slideDown() = sharedViewModel.closeSlidePanelIfExpanded.setValue(true) // 更多的 fun more() {} fun togglePlay() = PlayerManager.instance.togglePlay() fun playMode() = PlayerManager.instance.changeMode() fun showPlayList() = showShortToast("最近播放的细节,我没有搞...") }
-
专门更新 拖动条进度相关的:使用内部类(方便拿到Fragment的环境,在DataBinding的变量区域不仅可以放ViewModel还可以放内部类)
/** * 专门更新 拖动条进度相关的 */ inner class EventHandler : OnSeekBarChangeListener { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} override fun onStartTrackingTouch(seekBar: SeekBar) {} // 一拖动 松开手 把当前进度值 告诉PlayerManager override fun onStopTrackingTouch(seekBar: SeekBar) = PlayerManager.instance.setSeek(seekBar.progress) }
-
-
-
-
-
PlayerViewModel的东西:初始化操作,默认的播放界面
-
有很多个字段:
class PlayerViewModel : ViewModel() { // 歌曲名称 @JvmField//剔除get函数 val title = ObservableField<String>() // 歌手 @JvmField val artist = ObservableField<String>() // 歌曲图片的地址 htpp:xxxx/img0.jpg @JvmField val coverImg = ObservableField<String>() // 歌曲正方形图片 @JvmField val placeHolder = ObservableField<Drawable>() // 歌曲的总时长,会显示在拖动条后面 @JvmField val maxSeekDuration = ObservableInt() // 当前拖动条的进度值 @JvmField val currentSeekPosition = ObservableInt() // 播放按钮,状态的改变(播放和暂停) @JvmField val isPlaying = ObservableBoolean() // 这个是播放图标的状态,也都是属于状态的改变 @JvmField val playModeIcon = ObservableField<IconValue>()
-
初始化代码:
// 构造代码块,默认初始化 init { title.set(Utils.getApp().getString(R.string.app_name)) // 默认信息 artist.set(Utils.getApp().getString(R.string.app_name)) // 默认信息 placeHolder.set(Utils.getApp().resources.getDrawable(R.drawable.bg_album_default)) // 默认的播放图标,占位的 if (PlayerManager.instance.repeatMode === PlayingInfoManager.RepeatMode.LIST_LOOP) { // 如果等于“列表循环” playModeIcon.set(IconValue.REPEAT) } else if (PlayerManager.instance.repeatMode === PlayingInfoManager.RepeatMode.ONE_LOOP) { // 如果等于“单曲循环” playModeIcon.set(IconValue.REPEAT_ONCE) } else { playModeIcon.set(IconValue.SHUFFLE) // 随机播放 } }
-
-
处理PalyerFragment.xml
-
交给DataBinding进行管理:以后都是面向ViewModel进行编程了,而不是布局了
-
-
添加了自定义View:点击播放图标,转个圈圈的动画
-
开发模式:DataBinding+ViewModel+BindingAdapter(这个里面的函数就可以拿来用了,这个函数写到了xml中去)
-
异常的问题:刷新RecycleView,点一下的时候,那个红色的东西会进行刷新RecycleView:
-
刷新适配器:MainActivity中的,应用了这个LiveData的特性了
// 播放相关业务的数据(如果这个数据发生了变化,为了更好的体验) 盯着 PlayerManager.instance.changeMusicLiveData.observe(viewLifecycleOwner, { adapter ?.notifyDataSetChanged() // 更新及时 })
-
-
实现的细节:播放条的问题:
-
在fragment_player.xml中:就是有占位的问题
-
在BindingAdapter中的:这个地方是有两个字段的东西,这个BindingAdapter还是不会用啊;一定要去好好研究一下这个东西;
-
DataBinding中的东西,简书上面的四篇文章,要好好的学习
-
会极大的减少,Activity与Fragment和布局之间的压力;
-
-
4更
-
API处理:玩安卓的API
-
使用了RxJava实现:第二期的Derry里面的;晓不得嘛,先听着;
-
面向ViewModel编程
- 布局至少对应一个ViewModel
- 状态ViewModel只能有一个,功能有多个
-
添加登录功能:
-
概述:
-
布局全部交给DataBinding进行管理
-
数据按照功能交给相应的ViewModel处理
-
状态交给状态ViewModel处理
-
ViewModel交给LiveData管理:ViewModel中的字段都是LiveData管理的
-
监听全部交给注册类(RegisterActivity)中的内部类(监听类)处理
-
-
编写注册Activity:RegisterActivity
-
继承基类:BaseActivity
class RegisterActivity : BaseActivity()
-
重写onCreate
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
-
-
添加布局文件:activity_user_register.xml(交给DataBinding进行管理)
-
关注布局中的DataBinding区域:需要添加ViewModel:RegisterViewModel;需要添加监听器RegisterActivity.ClickClass
-
关注布局中的UI区域(部分):添加状态ViewModel(registerState)
-
-
处理状态ViewModel:RegisterViewModel
-
管理注册布局的状态:实现了单一原则,只管状态(是否登录成功)不管request
-
状态ViewModel是唯一的
-
代码示例:
// 管理布局的ViewModel 注册唯一的ViewModel class RegisterViewModel : ViewModel() { @JvmField // @JvmField消除了变量的getter方法 val userName = MutableLiveData<String>() val userPwd = MutableLiveData<String>() val registerState = MutableLiveData<String>() // 注册成功,注册失败 等等 // 默认初始化 init { userName.value = "" userPwd.value = "" registerState.value = "" } }
-
-
注册功能 请求服务器的 ViewModel:RequestRegisterViewModel
-
这个ViewModel需要交给LiveData进行管理:ViewModel中的字段全部是LiveData的
// 注册成功的状态 LiveData var registerData1 : MutableLiveData<LoginRegisterResponse> ? = null
-
对泛型类的说明:因为我们使用的是玩安卓的登录注册API,网页会返回一条JSON数据并且在使用LiveData的时候需要指定相应的泛型类,那么我们就需要将这个JSON数据转成 JavaBean类,并且在注册与登录中使用同一个javaBean类
-
对后面的那个问号的说明:我们在这个地方实现手动模拟懒加载:是可以用自带的懒加载,但是手写
- 在使用的时候才会加载(懒加载)
- 在使用的时候,会调用get方法
- 注册成功一定会拿到javaBean,注册失败就是返回一大堆字符串的
- 这里并不涉及多线程,就不加锁了
// 手写 模拟的 by lazy 懒加载功能(使用时 才会真正加载,这才是 懒加载) // 注册成功的状态 LiveData var registerData1 : MutableLiveData<LoginRegisterResponse> ? = null get() { if (field == null) { field = MutableLiveData() } return field } private set // 注册失败的状态 LiveData var registerData2 : MutableLiveData<String> ? = null get() { if (field == null) { field = MutableLiveData() } return field } private set
-
-
-
-
丰富RegisterActivity:
-
隐藏状态栏:
hideActionBar()//隐藏状态栏
-
初始化DataBinding:
registerViewModel = getActivityViewModelProvider(this).get(RegisterViewModel::class.java) // 状态VM requestRegisterViewModel = getActivityViewModelProvider(this).get(RequestRegisterViewModel::class.java) // 请求VM mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_user_register) // 初始化DB mainBinding ?.lifecycleOwner = this mainBinding ?.vm = registerViewModel // DataBinding绑定 ViewModel mainBinding ?.click = ClickClass() // 布局建立点击事件
-
添加眼睛:观察requestViewModel
// 一双眼睛 盯着 requestRegisterViewModel 监听 成功了吗 requestRegisterViewModel ?.registerData1 ?.observe(this, { registerSuccess(it) }) // 一双眼睛 盯着 requestRegisterViewModel 监听 失败了吗 requestRegisterViewModel ?.registerData2 ?.observe(this, { registerFailed(it) })
-
添加两个函数:实现数据驱动UI
-
在布局文件中有个这个:实现:requestRegisterViewModel ?.requestRegister(
-
当眼睛发现requestViewModel改变后就会调用相应的函数,函数就会改变相应数据的值(也就是ViewModel的值),布局中是有ViewModel的,就实现了数据驱动UI
fun registerSuccess(registerBean: LoginRegisterResponse?) { // Toast.makeText(this, "注册成功😀", Toast.LENGTH_SHORT).show() registerViewModel ?.registerState ?.value = "恭喜 ${registerBean ?.username} 用户,注册成功" // TODO 注册成功,直接进入的登录界面 同学们去写 startActivity(Intent(this, LoginActivity::class.java)) } fun registerFailed(errorMsg: String?) { // Toast.makeText(this, "注册失败 ~ 呜呜呜", Toast.LENGTH_SHORT).show() registerViewModel ?.registerState ?.value = "骚瑞 注册失败,错误信息是:${errorMsg}" }
-
-
添加监听内部类:处理布局中的所有监听
- 需要加上inner,Kotlin中默认是public
inner class ClickClass { // 点击事件,注册的函数 fun registerAction() { if (registerViewModel !!.userName.value.isNullOrBlank() || registerViewModel !!.userPwd.value.isNullOrBlank()) { registerViewModel ?.registerState ?.value = "用户名 或 密码 为空,请你好好检查" return } requestRegisterViewModel ?.requestRegister( this@RegisterActivity, registerViewModel !!.userName.value !!, registerViewModel !!.userPwd.value !!, registerViewModel !!.userPwd.value !! ) } }
-
回到 requestRegisterViewModel ?.requestRegister属于RequestRegisterViewModel
- 当ViewModel没有问题的话,就调用仓库
fun requestRegister(context: Context, username: String, userpwd: String, reuserpwd: String) { // TODO // 可以做很多的事情 // 可以省略很多代码 // 很多的校验 // .... // 没有任何问题后,直接调用仓库 HttpRequestManager.instance.register(context, username, userpwd, reuserpwd, registerData1 !!, registerData2 !!) }
-
调用仓库层:此时封装远程接口
-
接口示意:IRemoteRequest
-
封装细节:保证LiveData独一份
-
在ViewModel中使用了LiveData,在仓库中也使用了LiveData,那么我就需要保证所使用的LiveData是同一份
-
就RequestRegisterViewModel中的两个LiveData将传给仓库层
-
仓库层中的远程接口:这样子就保证是同一份了
-
-
-
当注册成功后,需要跳转页面:
fun registerSuccess(registerBean: LoginRegisterResponse?) { // Toast.makeText(this, "注册成功😀", Toast.LENGTH_SHORT).show() registerViewModel ?.registerState ?.value = "恭喜 ${registerBean ?.username} 用户,注册成功" // TODO 注册成功,直接进入的登录界面 同学们去写 startActivity(Intent(this, LoginActivity::class.java)) }
-
-
\
-
使用RxJava对网络模型进行二次封装:
-
为什么要封装:服务器可能存在乱搞现象
- 有时候返回正常的数据
- 有时候返回空数据
- 有时候甚至返回null
-
封装示意图:RxJava中的自定操作符实现了拦截与过滤
-
添加封装代码:完成了上图中的"内部实际通过OKHTTP请求",RxJava二次封装后在异步线程里面处理的
// 客户端API 可以访问 服务器的API interface WanAndroidAPI { /** * 登录API * username=Derry-vip&password=123456 */ @POST("/user/login") @FormUrlEncoded fun loginAction(@Field("username") username: String, @Field("password") password: String) : Observable<LoginRegisterResponseWrapper<LoginRegisterResponse>> // 返回值 /** * 注册的API */ @POST("/user/register") @FormUrlEncoded fun registerAction(@Field("username") username: String, @Field("password") password: String, @Field("repassword") repassword: String) : Observable<LoginRegisterResponseWrapper<LoginRegisterResponse>> // 返回值
-
下面就是解决RxJava2自定义操作符拦截过滤服务器内容了:将包装Bean流下去
-
实现接口:这个类就可以称作自定义操作了
abstract class APIResponse<T>(val context: Context) // 主 // LoginRegisterResponseWrapper<T> == 图 这个泛型就是包装Bean : Observer<LoginRegisterResponseWrapper<T>> {
-
启点分发的时候,RxJava中的函数,首次订阅的时候:实现一个弹框,表示正在工作,就是在注册的时候有一个圈圈在转
override fun onSubscribe(d: Disposable) { // 弹出 加载框 if (isShow) { LoadingDialog.show(context) } }
-
开始拦截服务器的数据
// 上游流下了的数据 我当前层 获取到了 上一层 流下来的 包装Bean == t: LoginRegisterResponseWrapper<T> override fun onNext(t: LoginRegisterResponseWrapper<T>) { if (t.data == null) { // 失败 failure("登录失败了,请检查原因:msg:${t.errorMsg}") } else { // 成功 success(t.data) } }
-
书写对应的函数:
// 成功 怎么办 abstract fun success(data: T ?) // 失败 怎么办 abstract fun failure(errorMsg: String ? ) // 上游流下了的错误 override fun onError(e: Throwable) { // 取消加载 LoadingDialog.cancel() failure(e.message) } // 停止 override fun onComplete() { // 取消加载 LoadingDialog.cancel() }
-
处理加载框:自定义View ,就是一个转的圈圈了
// 加载框 // object 没有主构造 也没有次构造 object LoadingDialog { // 内部生成的时候 根据INSTANCE 看起来感觉是 静态 因为可以 LoadingDialog.show fun show1() { } // 真正的static @JvmStatic fun show2() {} private var dialog: Dialog ? = null fun show(context: Context) { cancel() dialog = Dialog(context) dialog?.setContentView( R.layout.dialog_loading ) dialog?.setCancelable(false) dialog?.setCanceledOnTouchOutside(false) // ..... dialog?.show() } fun cancel() { dialog?.dismiss() } }
-
实现具体的登录流程:HttpRequestManager中的
// 登录的具体实现了 override fun login( context: Context, username: String, password: String, dataLiveData1: MutableLiveData<LoginRegisterResponse>, dataLiveData2: MutableLiveData<String>) { // RxJava封装网络模型 APIClient.instance.instanceRetrofit(WanAndroidAPI::class.java) .loginAction(username, password) .subscribeOn(Schedulers.io()) // 给上面的代码分配异步线程 .observeOn(AndroidSchedulers.mainThread()) // 给下面的代码分配 安卓的主线程,拿到数据后就在主线程中进行处理 // dataLiveData1.postValue(data) .subscribe(object : APIResponse<LoginRegisterResponse>(context) { override fun success(data: LoginRegisterResponse?) { // RxJava自定义操作符过滤后的 // MVP 模式 各种接口回调给外界 // callback.loginSuccess(data) dataLiveData1.value = data // MVVM,这个地方直接用setValue,因为上面已经分配了异步线程 } override fun failure(errorMsg: String?) { // RxJava自定义操作符过滤后的 dataLiveData2.value = errorMsg // MVVM } }) }
-
处理RegisterActivity中的监听问题:
- 布局交给DataBinding,其中维护了一个ViewModel
// 点击事件,注册的函数 fun registerAction() { if (registerViewModel !!.userName.value.isNullOrBlank() || registerViewModel !!.userPwd.value.isNullOrBlank()) { registerViewModel ?.registerState ?.value = "用户名 或 密码 为空,请你好好检查" return } requestRegisterViewModel ?.requestRegister( this@RegisterActivity, registerViewModel !!.userName.value !!, registerViewModel !!.userPwd.value !!, registerViewModel !!.userPwd.value !! ) } }
-
-
-
处理登录功能:LoginActivity
-
继承BaseActivity:
-
添加登录布局文件:activity_user_login.xml
-
有一个ViewModel管理数据,整个布局交给DataBinding进行管理
-
对于的布局中有相应的点击事件:
android:onClick="@{()->click.startToRegister()}"
-
-
拿到Binding与ViewModel
var mainBinding: ActivityUserLoginBinding? = null // 当前Register的布局 var loginViewModel: LoginViewModel? = null // ViewModel var requestLoginViewModel : RequestLoginViewModel? = null // TODO Reqeust ViewModel
-
编写ViewModel:LoginViewModel,属于状态ViewModel
// 登录的唯一ViewModel class LoginViewModel : ViewModel() { @JvmField // @JvmField消除了变量的getter方法 val userName = MutableLiveData<String>() val userPwd = MutableLiveData<String>() val loginState = MutableLiveData<String>() init { userName.value = "" userPwd.value = "" loginState.value = "" } }
-
编写ViewModel:RequestLoginViewModel
// 请求登录的ViewModel class RequestLoginViewModel : ViewModel() { //请求成功的ViewModel var registerData1: MutableLiveData<LoginRegisterResponse>? = null get() { if (field == null) { field = MutableLiveData<LoginRegisterResponse>() } return field } private set //请求失败的ViewModel var registerData2: MutableLiveData<String>? = null get() { if (field == null) { field = MutableLiveData<String>() } return field } private set
-
完善LoginActivity中的onCreate函数
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //隐藏状态栏 hideActionBar() //初始化操作: loginViewModel = getActivityViewModelProvider(this).get(LoginViewModel::class.java) // State ViewModel初始化 requestLoginViewModel = getActivityViewModelProvider(this).get(RequestLoginViewModel::class.java) // Request ViewModel初始化 mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_user_login) // DataBinding初始化 mainBinding ?.lifecycleOwner = this mainBinding ?.vm = loginViewModel // 绑定ViewModel与DataBinding关联 mainBinding ?.click = ClickClass() // DataBinding关联 的点击事件 // 登录成功 眼睛监听 成功,在跳转首页之前保存Session requestLoginViewModel ?.registerData1 ?.observe(this, { loginSuccess(it !!) }) // 登录失败 眼睛监听 失败 requestLoginViewModel ?.registerData2 ?.observe(this, { loginFialure(it !!) }) }
-
函数补充:
// 响应的两个函数 fun loginSuccess(registerBean: LoginRegisterResponse?) { // Toast.makeText(this@LoginActivity, "登录成功😀", Toast.LENGTH_SHORT).show() loginViewModel?.loginState?.value = "恭喜 ${registerBean?.username} 用户,登录成功" // 登录成功 在跳转首页之前,需要 保存 登录的会话 // 保存登录的临时会话信息 mSharedViewModel.session.value = Session(true, registerBean) // 跳转到首页 startActivity(Intent(this@LoginActivity, MainActivity::class.java)) } fun loginFialure(errorMsg: String?) { // Toast.makeText(this@LoginActivity, "登录失败 ~ 呜呜呜", Toast.LENGTH_SHORT).show() loginViewModel ?.loginState ?.value = "骚瑞 登录失败,错误信息是:${errorMsg}" }
-
在这个地方就有数据驱动UI,当登录失败的时候,loginViewModel ?.loginState ?.value 赋值,在activity_user_login.xml中的就有这个
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#f00" android:textSize="20dp" android:text="@{vm.loginState}"//就是这个啊,就可以实现数据驱动UI了 android:layout_gravity="center" />
-
-
添加点击函数:
inner class ClickClass { // 点击事件,登录的函数 fun loginAction() { if (loginViewModel !!.userName.value.isNullOrBlank() || loginViewModel !!.userPwd.value.isNullOrBlank()) { loginViewModel ?.loginState ?.value = "用户名 或 密码 为空,请你好好检查" return } // 非协程版本 /*requestLoginViewModel ?.requestLogin( this@LoginActivity, loginViewModel !!.userName.value!!, loginViewModel !!.userPwd.value!!, loginViewModel !!.userPwd.value!! )*/ // 协程版本 requestLoginViewModel ?.requestLoginCoroutine( this@LoginActivity, loginViewModel !!.userName.value!!, loginViewModel !!.userPwd.value!!) } // 跳转到 注册界面 fun startToRegister() = startActivity(Intent(this@LoginActivity, RegisterActivity::class.java)) }
-
添加requestLoginViewModel ?.requestLogin
// 非协程函数 fun requestLogin(context: Context, username: String, userpwd: String, reuserpwd: String) { // TODO // 可以做很多的事情 // 可以省略很多代码 // 很多的校验 // .... HttpRequestManager.instance.login(context, username, userpwd, registerData1!!, registerData2!!) }
此时需要在IRemoteRequest接口中定义一套登录标准
fun login( context: Context, username: String, password: String, dataLiveData1: MutableLiveData<LoginRegisterResponse>, dataLiveData2: MutableLiveData<String>)
在HttpRequestManager中进行登录的具体实现
// 登录的具体实现了 override fun login( context: Context, username: String, password: String, dataLiveData1: MutableLiveData<LoginRegisterResponse>, dataLiveData2: MutableLiveData<String>) { // RxJava封装网络模型 //拿到Retrofit的实例后进行loginAction(),将用户名密码传进去 APIClient.instance.instanceRetrofit(WanAndroidAPI::class.java) .loginAction(username, password)//返回一个Observable的RxJava包裹的起点,从这个起点向下流动,在流动中进行拦截 .subscribeOn(Schedulers.io()) // 给上面的代码分配异步线程 .observeOn(AndroidSchedulers.mainThread()) // 给下面的代码分配 安卓的主线程 // dataLiveData1.postValue(data) //通过自定义操作符进行过滤 .subscribe(object : APIResponse<LoginRegisterResponse>(context) { override fun success(data: LoginRegisterResponse?) { // RxJava自定义操作符过滤后的 // MVP 模式 各种接口回调 // callback.loginSuccess(data) dataLiveData1.value = data // MVVM } override fun failure(errorMsg: String?) { // RxJava自定义操作符过滤后的 dataLiveData2.value = errorMsg // MVVM } }) }
流程回顾:
-
调用:LoginActivity中的ClickClass中的requestLoginViewModel ?.requestLogin(,
-
调用:RequestLoginViewModel中的requestLogin函数进行校验,没有问题,调用到仓库中去:HttpRequestManager.instance.login(context, username, userpwd, registerData1!!, registerData2!!)
-
注意这个地方将这两个LiveData传过去的目的是共享,RequestLoginViewModel中的两个LiveData要和仓库中的()共享同两个
HttpRequestManager.override fun login里面的
-
当这两个LiveData改变的时候,LoginActivity中的眼睛就可以看到这个
-
-
-
保存登录成功的Session
// 保存登录信息的临时会话,把这个登录成功的Bean给他弄好,再给他添加一个字段 data class Session constructor(val isLogin: Boolean, val loginRegisterResponse: LoginRegisterResponse?)
-
如何在任何地方都能拿到Session,在共享ViewModel中添加字段
-
注意在这个地方是不能用去除粘性额LiveData 的,在共享领域的时候不能去掉粘性
-
先修改数据,后观察,那么你就收不到前面的数据了,这个即使剔除粘性所导致的;先修改数据后订阅就用了粘性数据了;
-
这个Session就是登陆后保存的东西
-
MainFragment中展示Session
细节:为什么在跳转的时候::class.java,因为我们需要跳转的东西是kotlin写的,而Intent的源码是java;并且Google承诺FrameWork层的源码是不会进行修改了;
-
-
-
RxJava需要弄
- 场景当服务器乱返回数据的时候,自定义RxJava操作符,去解决这个问题;