项目开发日志

520 阅读29分钟

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:一些通用的支持,项目通用的工具类集合

具体流程

  1. 先写了MainActivity作为控制层,里面什么代码都没有

  2. 写布局文件activity_main.xml:交给DataBinding进行管理(开启开关,点击布局标签进行转换)

    1. 交给DataBinding进行管理

      • 开启DataBinding的开关,布局要进行转换
      • 这里就有一个ViewModel,它是来管理整个MainActivity中的数据来源的(一个布局至少有一个ViewModel)
    2. 抽屉控件

      • 因为抽屉控件只能左右滑动:DrawerLayout(这个是主页面的与侧滑的)
      • 但是需要上下滑动,所以就搞了:SlidingUpPanelLayout(这个是主页面里面的)
      • 抽屉控件中就有ViewModel的值,允许抽屉打开/关闭+具体打开/关闭抽屉
    1. 引入了Fragment:遵循一个Activity多个Fragment的开发模式:

      • 具体来说在activity_main.xml中引入了三个fragment标签

      1. 整个大的Fragment,包裹了RecycleView
      2. 下面那个播放条,点一下就会弹上去的
      3. 侧滑栏的Fragment:处理抽屉控件的
    2. 引入Navigation进行视图导航:Navigation本来就是用作Fragment导航使用的

      • 三个fragment标签都具有下面这行语句,代表这三个fragment都是交给Navigation作导航

         android:name="androidx.navigation.fragment.NavHostFragment"
        
      • 每个Fragment都有各自的导航关系

        • 代码示意:
         app:navGraph="@navigation/nav_drawer" 
        
        • 具体的导航关系:扔到app/res/navigation 里面

          image-20220215224841558

      • 在app/res/navigation 目录下加载的layout文件,这个才是拿来真正显示界面的

      • 在app/res/navigation 目录下的布局文件中引入fragment标签,fragment标签引入布局文件(tools:layout="@layout/fragment_main"),这个布局文件才拿真正显示界面的

        image-20220215225057834

  3. 开始写首页的Fragment:MainFragment

    1. 创建相应的布局文件:fragment_main.xml

      • 交给DataBinding进行管理
    2. 全盘使用DataBinding进行布局管理:

      • 拿到首页布局:fragment_main.xml:

         private var mainBinding: FragmentMainBinding? = null//就是fragment_main.xml了
        
    3. 创建首页的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就要有三遍流程

        再次总结:项目整体的页面逻辑

        1. 使用Navigation处理activity_main.xml中的导航工作

        2. 在相应的nav_XXX.xml中包含了多个Fragment

          • 这些Fragment各自布局的文件处理页面具体显示内容(DataBinding)
          • 这些Fragment都含有各自的ViewModel用于管理数据

      项目中一共有4个Fragment,4个布局文件,4个ViewModel

  4. 引入基类:

    • 原因:因为MainActivity继承Activity的话是不行的,Activity并没有实现相应的接口,导致无法使用LifeCycle的东西

    • 解决:设计基类BaseActivity,BaseFragment;这两个基类会提供大量的工具函数,模板设计思路

    1. BaseActivity:

      • 继承AppCompatActivity:因为AppCompatActivity的爷爷类ComponentActivity实现了 LifeCycle接口,在开发中需要使用this指针,持有环境

      • open剔除final修饰:要拿给其他的类继承

         open class BaseActivity : AppCompatActivity() 
        
      • 注册LifeCycle,因为后面的所有Activity都会继承BaseActivity,这些Activity作为被观察者

         //这个BaseActivity为被观察者;NetworkStateManager为观察者 
         //眼睛是从后往前看的,有点像静态内部类实现单例模式
         lifecycle.addObserver(NetworkStateManager.instance)
        
    2. BaseFragment:所有Fragment的父类

      • Fragment持有Activity环境:
  5. 引入工具类:

    • architecture里面的Utils:

      • 有很多工具类:比如说换掉上面的Bar
    • architecture里面的binding包下面的东西

      • 大量使用了@BindingAdapter注解:解决在控件中编写逻辑的问题

        • 在Google官网上面使用DataBinding的时候,将逻辑写到了xml里面去了,导致耦合度较高:怎么能把逻辑写到控件里面去呢?
        • 使用BindingAdapter的好处:减轻了DataBinding的压力
      • 当对自定义的字段赋值为ViewModel的引用时:

        • 会触发这个binding工具类,直接干到public static void setViewBackground,在这个函数中就可以写业务代码了
    • 引入适配器:在项目中大量的使用了RecycleView

    • data/manager:使用LifeCycle进行整个项目的状态监听:作为扩展功能

      • 比如说当页面可见并且网络等系统发生改变时,就可以用服务的手段进行提示或者处理

      • 网络不给力,电量较低等

      • 具体实现:依托LifeCycle;当界面不可见的时候,就不要浪费性能了

        • 不可见,不提示
        • 当界面可见的时候,注册广播;
        • 当界面不可见的时候,移除广播;
  6. 丰富工具类剔除数据粘性:UnPeekLiveData

    • 不解决的话,会发生数据倒灌;使用反射动态修改代码,让他的状态对齐

    • 其实就是执行这个if语句就行了

       if (observer.mLastVersion >= mVersion) {
           return;
       }
       observer.mLastVersion = mVersion;
       observer.mObserver.onChanged((T) mData);
      
  7. 引入mSharedViewModel:干掉EventBus;

    • 因为EventBus会有很多的Bean类,带来可追逐性不强的问题

    • 但是LiveData可追逐性非常强:

      • 只有setValue,observe:在发生问题的时候,排查起来很好
    • 另外,可以让所有的Activity/Fragment共享数据

      • 只要动了这个mSharedViewModel所有与之关联的Activity/Fragment都会随之改变,解决一致性问题当界面可见就会更新,但是不可见就不操作;但是又可见的时候就还是会变;这个ViewModel会解决数据存储以及一致性问题;这个才是最关键的东西

        • 会解决按下就会换掉封面,按下就会换歌曲的名字等 ·
      • 保证mSharedViewModel单例:getAppViewModelProvider

      • 这个还搭配了一个工厂方法:保证ViewModel独一份;

      • 是怎么保证这个共享ViewModel独一份?

  8. Kotlin的注解问题:

    • 比java慢,有几兆的空间开辟,还有导包,还有Kotlin自己的API;但是呢,Kotlin的注解非常好用

ViewModel的设计

  • 一个Fragment可能有多个ViewModel

    • 任何一个Fragment必须有一个ViewModel

      • 不会再写什么findViewById,setContentView布局文件全部交给DataBinding进行管理了
    • 可能还会有多个的ViewModel:这个东西是要看需求的,有可能有七八个ViewModel

  • 所以说这个就是Google标准架构设计模式了

2更

  1. 细化MainActivity:发送指令,子Fragment进行干活;当然它也可以接收指令(共享ViewModel驱动它干活)

    1. 处理共享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>()
        
    2. 丰富MainActivity

      • 添加眼睛:通过观察共享ViewModel中的字段作一定的过滤工作并借助中间的ViewModel实现对控件的间接管控

      • 比如说:判断菜单是否打开

        1. 观察共享ViewModel中的数据有无改变:共享ViewModel中是有这个菜单的开闭情况(是有一个剔除掉粘性的UNPeekLiveData进行管控的)
        2. 过滤后,借助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
               }
           }
          
    3. 处理MainActivityViewModel

      • 管控的就是activity_main.xml这个布局文件中的属性

      • 添加了注解:消除了openDrawer的getter函数(因为这个变量是val,是没有set函数的,如果是var类型的;会把get/set函数一起消掉),不

         // 首页需要记录抽屉布局的情况 (响应的效果,都让 抽屉控件干了)
         @JvmField
         val openDrawer = MutableLiveData<Boolean>()
        
  2. 处理activity_main.xml中的一些小细节了

    • 处理这个东西

      image-20220207150500702

    • 这个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解析

      image-20220207153317766

  1. 现在处理这个导航图的问题

    1. 导航图中的子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)
         }
        
  2. 现在就去处理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区域:代码太多了

        1. 上面那个图片包含的区域

          1. 折叠工具栏:三个按钮

            • 打开侧滑的菜单

              image-20220207161326081

            • 打开搜索页面:依托Navigation进行导航

              image-20220207161538155

          处理这个点击事件的执行:打开侧滑的菜单

          1. 现在页面的总控点击中进行注册(部分代码)其实就行了

                 inner class ClickProxy {
                     // 当在首页点击 “菜单” 的时候,直接导航到 ---> 菜单的Fragment界面
                     fun openMenu() { sharedViewModel.openOrCloseDrawer.value = true } // 触发
                 }
            
          2. 会在MainActivity中有眼睛在看的,执行相应的逻辑

            image-20220207161116975

          3. 还有眼睛在看:才打开菜单

          处理点击事件:打开搜索页面

          1. 在页面的监听总控中添加代码

             inner class ClickProxy {
                 // 当在首页点击 “搜索图标” 的时候,直接导航到 ---> 搜索的Fragment界面
                 fun search() = nav().navigate(R.id.action_mainFragment_to_searchFragment)
             }
            
          2. 在BaseFragment中进行跳转

             /**
              * 为了给所有的 子fragment,导航跳转fragment的;封装了Navigation
              * @return
              */
             protected fun nav(): NavController {
                 return NavHostFragment.findNavController(this)
             }
            
          3. 需要注意跳转的Navigation的id是指定了的:来自NavigationR.id.action_mainFragment_to_searchFragment

            image-20220207161949127

        2. 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出现早,优化历史长
            
        3. ViewPager1:实现页面切换的

        4. 其他信息页面

                <!-- 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的优势

            image-20220207163825267

          • 加载本地的页面:

             //加载本地的页面
             @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
            
        5. 处理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进行管理(方便以后进行扩展使用)

            image-20220207164456421

        6. 添加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:

  3. 添加适配器:处理recyclerview:listitem

         // 适配器
         private var adapter: SimpleBaseBindingAdapter<TestMusic?, AdapterPlayItemBinding?>? = null
    
  4. 将adapter_play_item.xml交给DataBinding进行管理,出来一个AdapterPlayItemBinding

     // 设置设配器(item的布局 和 适配器的绑定)
     adapter = object : SimpleBaseBindingAdapter<TestMusic?, AdapterPlayItemBinding?>(context, R.layout.adapter_play_item) {
    
  5. 在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指针会有问题

  6. 添加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)
           }
       }
      
  7. 引入仓库层:在真实开发中是很麻烦的

    • 示意图:

      image-20220207184420976

    • 部分代码展示:

       //在这个里面,当服务器返回了什么东西,就可以直接干给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:播放音乐时的一些细节

    1. PlayerCallHelper:在来电时自动协调和暂停音乐播放,Google 原生就不会做,其他的厂商就有
    2. PlayerReceiver:播放的广播,用于接收 某些改变(系统发出来的信息,断网了),对音乐做出对应操作
    3. PlayerService:实现后台播放
    4. PlayerManager:处理播放细节问题;
  • Navigation2:播放页面,也就是第二个Fragment:PlayerFragment;

    1. 初始化ViewModel:

        // 初始化 VM
           playerViewModel = getFragmentViewModelProvider(this).get<PlayerViewModel>(PlayerViewModel::class.java)
       ​
      
    2. 将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
           }
      
    3. 添加眼睛:观察数据的变化

      • 共享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))
                 }
             })
        
        • 实现点击播放条,向上弹出一个页面

          1. PlayerFragment中的onViewCreated方法

             sharedViewModel.timeToAddSlideListener.observe(viewLifecycleOwner, {
                     if (view.parent.parent is SlidingUpPanelLayout)
            
          2. 实际上就是在这个:SharedViewModel里面

            image-20220209200247719

          3. 实际上是在MainActivity中的

            image-20220209200352434

            实现安卓原生未来趋势: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(

          image-20220209201606762

        • 添加眼睛: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>()
            
        • 添加眼睛:暂停播放

               // 播放/暂停是一个控件  图标的truefalse
                   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

    1. 交给DataBinding进行管理:以后都是面向ViewModel进行编程了,而不是布局了

      image-20220209204311920

  • 添加了自定义View:点击播放图标,转个圈圈的动画

  • 开发模式:DataBinding+ViewModel+BindingAdapter(这个里面的函数就可以拿来用了,这个函数写到了xml中去)

  • 异常的问题:刷新RecycleView,点一下的时候,那个红色的东西会进行刷新RecycleView:

    • 刷新适配器:MainActivity中的,应用了这个LiveData的特性了

              // 播放相关业务的数据(如果这个数据发生了变化,为了更好的体验) 盯着
               PlayerManager.instance.changeMusicLiveData.observe(viewLifecycleOwner, {
                   adapter ?.notifyDataSetChanged() // 更新及时
               })
      
  • 实现的细节:播放条的问题:

    • 在fragment_player.xml中:就是有占位的问题

      image-20220209211637451

    • 在BindingAdapter中的:这个地方是有两个字段的东西,这个BindingAdapter还是不会用啊;一定要去好好研究一下这个东西;

      image-20220209211703126

    • DataBinding中的东西,简书上面的四篇文章,要好好的学习

      • 会极大的减少,Activity与Fragment和布局之间的压力;

4更

  • API处理:玩安卓的API

  • 使用了RxJava实现:第二期的Derry里面的;晓不得嘛,先听着;

  • 面向ViewModel编程

    • 布局至少对应一个ViewModel
    • 状态ViewModel只能有一个,功能有多个
  • 添加登录功能:

    • 概述:

      • 布局全部交给DataBinding进行管理

      • 数据按照功能交给相应的ViewModel处理

      • 状态交给状态ViewModel处理

      • ViewModel交给LiveData管理:ViewModel中的字段都是LiveData管理的

      • 监听全部交给注册类(RegisterActivity)中的内部类(监听类)处理

    1. 编写注册Activity:RegisterActivity

      1. 继承基类:BaseActivity

         class RegisterActivity : BaseActivity() 
        
      2. 重写onCreate

             override fun onCreate(savedInstanceState: Bundle?) {
                 super.onCreate(savedInstanceState)
        
    2. 添加布局文件:activity_user_register.xml(交给DataBinding进行管理)

      • 关注布局中的DataBinding区域:需要添加ViewModel:RegisterViewModel;需要添加监听器RegisterActivity.ClickClass

        image-20220210154005149

      • 关注布局中的UI区域(部分):添加状态ViewModel(registerState)

        image-20220210154125606

    3. 处理状态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 = ""
             }
         }
        
    4. 注册功能 请求服务器的 ViewModel:RequestRegisterViewModel

      1. 这个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
          
      1. 丰富RegisterActivity:

      2. 隐藏状态栏:

         hideActionBar()//隐藏状态栏
        
      3. 初始化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() // 布局建立点击事件
        
      4. 添加眼睛:观察requestViewModel

         // 一双眼睛 盯着 requestRegisterViewModel 监听 成功了吗
         requestRegisterViewModel ?.registerData1 ?.observe(this, {
             registerSuccess(it)
         })
         ​
         // 一双眼睛 盯着 requestRegisterViewModel 监听 失败了吗
         requestRegisterViewModel ?.registerData2 ?.observe(this, {
             registerFailed(it)
         })
        
      5. 添加两个函数:实现数据驱动UI

        • 在布局文件中有个这个:实现:requestRegisterViewModel ?.requestRegister(

          image-20220210161641291

        • 当眼睛发现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}"
         }
        
      6. 添加监听内部类:处理布局中的所有监听

        • 需要加上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 !!
                 )
             }
         }
        
      7. 回到 requestRegisterViewModel ?.requestRegister属于RequestRegisterViewModel

        • 当ViewModel没有问题的话,就调用仓库
         fun requestRegister(context: Context, username: String, userpwd: String, reuserpwd: String) {
             // TODO
             // 可以做很多的事情
             // 可以省略很多代码
             // 很多的校验
             // ....
         ​
             // 没有任何问题后,直接调用仓库
             HttpRequestManager.instance.register(context, username, userpwd, reuserpwd, registerData1 !!, registerData2 !!)
         }
        
      8. 调用仓库层:此时封装远程接口

        • 接口示意:IRemoteRequest

        • 封装细节:保证LiveData独一份

          • 在ViewModel中使用了LiveData,在仓库中也使用了LiveData,那么我就需要保证所使用的LiveData是同一份

          • 就RequestRegisterViewModel中的两个LiveData将传给仓库层

            image-20220210163431521

          • 仓库层中的远程接口:这样子就保证是同一份了

            image-20220210163724058

      9. 当注册成功后,需要跳转页面:

         fun registerSuccess(registerBean: LoginRegisterResponse?) {
             // Toast.makeText(this, "注册成功😀", Toast.LENGTH_SHORT).show()
             registerViewModel ?.registerState ?.value = "恭喜 ${registerBean ?.username} 用户,注册成功"
         ​
             // TODO 注册成功,直接进入的登录界面  同学们去写
             startActivity(Intent(this, LoginActivity::class.java))
         }
        

\

  1. 使用RxJava对网络模型进行二次封装:

    • 为什么要封装:服务器可能存在乱搞现象

      • 有时候返回正常的数据
      • 有时候返回空数据
      • 有时候甚至返回null
    • 封装示意图:RxJava中的自定操作符实现了拦截与过滤

      image-20220210165358099

    • 添加封装代码:完成了上图中的"内部实际通过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流下去

      1. 实现接口:这个类就可以称作自定义操作了

         abstract class APIResponse<T>(val context: Context)  // 主
         ​
             // LoginRegisterResponseWrapper<T> == 图 这个泛型就是包装Bean
             : Observer<LoginRegisterResponseWrapper<T>> {
        
      2. 启点分发的时候,RxJava中的函数,首次订阅的时候:实现一个弹框,表示正在工作,就是在注册的时候有一个圈圈在转

         override fun onSubscribe(d: Disposable) {
             // 弹出 加载框
             if (isShow) {
                 LoadingDialog.show(context)
             }
         }
        
      3. 开始拦截服务器的数据

         // 上游流下了的数据   我当前层 获取到了 上一层 流下来的 包装Bean == t: LoginRegisterResponseWrapper<T>
         override fun onNext(t: LoginRegisterResponseWrapper<T>) {
         ​
             if (t.data == null) {
                 // 失败
                 failure("登录失败了,请检查原因:msg:${t.errorMsg}")
             } else {
                 // 成功
                 success(t.data)
             }
         }
        
      4. 书写对应的函数:

         // 成功 怎么办
         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()
             }
        
      5. 处理加载框:自定义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()
             }
         ​
         }
        
      6. 实现具体的登录流程: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
                     }
                 })
         }
        
      7. 处理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 !!
                 )
             }
         }
        
  2. 处理登录功能:LoginActivity

    1. 继承BaseActivity:

    2. 添加登录布局文件:activity_user_login.xml

      • 有一个ViewModel管理数据,整个布局交给DataBinding进行管理

      • 对于的布局中有相应的点击事件:

         android:onClick="@{()->click.startToRegister()}"
        
    3. 拿到Binding与ViewModel

       var mainBinding: ActivityUserLoginBinding? = null // 当前Register的布局
       var loginViewModel: LoginViewModel? = null // ViewModel
       ​
          var requestLoginViewModel : RequestLoginViewModel? = null // TODO Reqeust ViewModel
      
    4. 编写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 = ""
           }
       }
      
    5. 编写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
      
    6. 完善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 !!)
           })
       }
      
    7. 函数补充:

       // 响应的两个函数
       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"
             />
        
    8. 添加点击函数:

          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))
           }
       ​
      
    9. 添加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
                       }
                   })
           }
      

      流程回顾:

      1. 调用:LoginActivity中的ClickClass中的requestLoginViewModel ?.requestLogin(,

      2. 调用:RequestLoginViewModel中的requestLogin函数进行校验,没有问题,调用到仓库中去:HttpRequestManager.instance.login(context, username, userpwd, registerData1!!, registerData2!!)

        • 注意这个地方将这两个LiveData传过去的目的是共享,RequestLoginViewModel中的两个LiveData要和仓库中的()共享同两个

          HttpRequestManager.override fun login里面的

          image-20220216011620947

        • 当这两个LiveData改变的时候,LoginActivity中的眼睛就可以看到这个

          image-20220216011819303

    1. 保存登录成功的Session

       // 保存登录信息的临时会话,把这个登录成功的Bean给他弄好,再给他添加一个字段
       data class Session constructor(val isLogin: Boolean, val loginRegisterResponse: LoginRegisterResponse?)
      
    2. 如何在任何地方都能拿到Session,在共享ViewModel中添加字段

      image-20220216012453067

      • 注意在这个地方是不能用去除粘性额LiveData 的,在共享领域的时候不能去掉粘性

      • 先修改数据,后观察,那么你就收不到前面的数据了,这个即使剔除粘性所导致的;先修改数据后订阅就用了粘性数据了;

      • 这个Session就是登陆后保存的东西

      • MainFragment中展示Session

        image-20220216013215462

        细节:为什么在跳转的时候::class.java,因为我们需要跳转的东西是kotlin写的,而Intent的源码是java;并且Google承诺FrameWork层的源码是不会进行修改了;

  • RxJava需要弄

    • 场景当服务器乱返回数据的时候,自定义RxJava操作符,去解决这个问题;