美文网首页Android开发学习
React设计思维的启发 - Android View Comp

React设计思维的启发 - Android View Comp

作者: RetroX | 来源:发表于2017-10-30 23:26 被阅读856次

    Android View Component 架构设计思维

    重构记事

    为什么要重构?

    • 项目当前采用的DataBinding框架严重限制了编译速度,并且DataBinding框架存在着出错提示混乱的毛病,在出错的时候大幅度降低了开发效率(当然没错的时候还是很快的)
    • 在尝试为Freeline适配最新的DataBinding时候遇到了巨大的阻力,实现的可能性很低了,只能做到局部兼容,因此需要多长全量编译,开发效率低下
    • 为Freeline适配kotlin增量成功,因此开始使用kotlin语言开发(kapt不敢用除外),准备大规模迁移至kotlin开发语言
    • 一些之前的逻辑存在着混乱的毛病,模块间耦合关系有待进一步梳理

    做什么?

    • 使用自己的观察者框架代替Google自带的DataBinding实现数据流
    • 使用kotlin写重构代码,并且局部替换Java代码
    • 去除一些不痛不痒的注解处理框架,在大幅度应用之前去除AROUTER,Butterknife

    先思考 => 什么架构

    我应该用什么架构 MVP MVVM ?

    • MVP 作为android应用很火的架构,因为充分的解耦被业界广泛使用,蛋疼之处在于需要些大量的接口来规范每一层的行为,来进行进一步的解耦。接口也可以被用于单元测试,目前的项目中还并没有足够的精力去写单元测试,也不存在替换model或其他某层的需求,因此可以使用只抽象View接口版的MVP架构(如果你有MVP情节的话)
    • MVVM架构随着DataBinding架构的提出而在android上被慢慢推广,ViewModel作为数据渲染层,承接着讲model渲染到view上的重任,同时使用数据绑定的方式将其与view相关联,MVVM的设计原则是ViewModel层不持有View的引用,加之DataBinding功能有限和某些部分及其蛋疼,可以做到高效开发但是某些时候及其蛋疼,当然我个人而言是非常喜欢MVVM架构以及数据绑定思维的,所以它也是重构前微北洋(我的项目名字)主模块的架构

    那么两种架构都有自己蛋疼的地方,可不可以有一种新的架构设计方式呢

    前些时间接触了React设计思维,React框架将各个UI组件称为Component,每个Component内部维护了自己的view和一些逻辑,并且将状态而非是view暴露在外,通过改变Component的状态来实现这个组件UI和其他细节的改变,Component暴露在外的状态是可以确定全局状态的最小状态,即状态的最小集,Component的其他剩余状态都可以通过暴露状态的改变所推倒出来,当然这个推倒应该是响应式的,自动的。

    当然android上无法写类似于JSX的东西,如果最类似的话,那就是Anko的DSL布局框架了,这样子就可以将view写在Component里面。

    不过View写在Xml里面,然后在Component的初始化时候来find来那些view去操作也是ok的(因为anko的DSL写法依然是一种富有争议的布局方式,尽管我定制的Freeline已经可以做到kotlin修改的10s内增量编译,DSL还是有很多坑)

    说了这么多,这个架构到底具体是什么样子呢?

    • 所有的view组件抽象成Component
    • 每个Component内维护自己的view,对外暴露可以推倒出全局状态的最小状态集,view均为private,不可被外部访问到,只可以修改Component的状态而不可访问component的view
    • Component内部维护自己view与状态之间的关系,推荐使用响应式数据流的方式来进行绑定,某些数据发生变化的时候对应的view也发生自己的改变

    可见,Component本身是高内聚的,对外暴露最小状态,所以外部只需修改最小的状态(集)就可以完成Component行为/view的变化,因此外部调用极其方便而且也不存在逻辑之间的相互干扰

    怎么做?

    • Component怎么分?
    • Component需要传入什么?
    • Component放在哪里?
    • Component内部数据流怎么写?
    • Component对外暴露什么?怎么暴露?
    • Component内部状态怎么管理?

    先看一个图来解释

    Screen Shot 2017-10-30 at 11.32.46 AM.png

    图示部分的页面,是使用Recyclerview作为页面容器,里面的每个Item,就可以作为一个Component来对待

    Screen Shot 2017-10-30 at 11.34.29 AM.png

    进一步的,此Component里面的那几个图书详情item,又可以作为子Component来对待

    Screen Shot 2017-10-30 at 11.36.31 AM.png

    他们的xml布局因为极其简单就跳过不谈,Component的设计部分我们可以从最小的item说起

    因为它没有被放在Recyclerview里面,所以它继承ViewHolder与否都是随意的,但是为了统一性,就继承RecyclerView.ViewHolder好了(事实上是否继承它都是随意的)

    先来看这个Component对应的数据Model部分

    public class Book {
    
        /**
         * barcode : TD002424561
         * title : 设计心理学.2,与复杂共处,= Living with complexity
         * author : (美) 唐纳德·A·诺曼著
         * callno : TB47/N4(5) v.2
         * local : 北洋园工学阅览区
         * type : 中文普通书
         * loanTime : 2017-01-09
         * returnTime : 2017-03-23
         */
    
        public String barcode;
        public String title;
        public String author;
        public String callno;
        public String local;
        public String type;
        public String loanTime;
        public String returnTime;
    
        /**
         * 距离还书日期还有多少天
         * @return
         */
        public int timeLeft(){
            return BookTimeHelper.getBetweenDays(returnTime);
    //        return 20;
        }
    
        /**
         * 看是否超过还书日期
         * @return
         */
        public boolean isOverTime(){
            return this.timeLeft() < 0;
        }
    
        public boolean willBeOver(){
            return this.timeLeft() < 7 && !isOverTime();
        }
    }
    

    我们的需求是:在这个view里面有 书的名字,应还日期,书本icon的涂色方案随着距离还书日期的长短而变色

    首先声明用到的view和Context什么的

    class BookItemComponent(lifecycleOwner: LifecycleOwner,itemView: View) : RecyclerView.ViewHolder(itemView) {
        private val mContext = itemView.context
        private val cover: ImageView = itemView.findViewById(R.id.ic_item_book)
        private val name: TextView = itemView.findViewById(R.id.tv_item_book_name)
        private val returntimeText: TextView = itemView.findViewById(R.id.tv_item_book_return)
    }
    

    LifecycleOwner是来自Android Architecture Components的组件,用来管理android生命周期用,避免组件的内存泄漏问题 Android Architecture Components

    下来就是声明可观察的数据(也可以成为状态)

        private val bookData = MutableLiveData<Book>()
    

    因为此Component逻辑简单,只需要观测Book类即可推断确定其状态,因此它也是这个Component的最小状态集合

    插播一条补充知识:

    LiveData<T>,MutableLiveData<T>也都来自于Android Architecture Components的组件,是生命周期感知的可观测动态数据组件

    Sample:

    LiveData<BigDecimal> myPriceListener = ...;
            myPriceListener.observe(this, price -> {
                // Update the UI. 
            });
    

    当然用kotlin给它写了一个简单的函数式拓展

    /**
     * LiveData 自动绑定的kotlin拓展 再也不同手动指定重载了hhh
     */
    fun <T> LiveData<T>.bind(lifecycleOwner: LifecycleOwner, block : (T?) -> Unit) {
        this.observe(lifecycleOwner,android.arch.lifecycle.Observer<T>{
            block(it)
        })
    }
    

    好了,回到正题,然后我们就该把view和Component的可观测数据/状态绑定起来了

        init {
            bookData.bind(lifecycleOwner) {
                it?.apply {
                    name.text = this.title
                    setBookCoverDrawable(book = this)
                    returntimeText.text = "应还日期: ${this.returnTime}"
                }
            }
        }
    
    //这里是刚刚调用的函数 写了写动态涂色的细节   
    private fun setBookCoverDrawable(book: Book) {
            var drawable = ContextCompat.getDrawable(mContext, R.drawable.lib_book)
            val leftDays = book.timeLeft()
            when {
                leftDays > 20 -> DrawableCompat.setTint(drawable, Color.rgb(0, 167, 224)) //blue
                leftDays > 10 -> DrawableCompat.setTint(drawable, Color.rgb(42, 160, 74)) //green
                leftDays > 0 -> {
                    if (leftDays < 5) {
                        val act = mContext as? Activity
                        act?.apply {
                            Alerter.create(this)
                                    .setTitle("还书提醒")
                                    .setBackgroundColor(R.color.assist_color_2)
                                    .setText(book.title + "剩余时间不足5天,请尽快还书")
                                    .show()
                        }
                    }
                    DrawableCompat.setTint(drawable, Color.rgb(160, 42, 42)) //red
                }
                else -> drawable = ContextCompat.getDrawable(mContext, R.drawable.lib_warning)
            }
            cover.setImageDrawable(drawable)
        }
    

    通过观测LiveData<Book>来实现Component状态的改变,因此只需要修改Book就可以实现该Component的相关一系列改变

    然后我们只需要把相关函数暴露出来

        fun render(): View = itemView
    
        fun bindBook(book: Book){
            bookData.value = book
        }
    

    然后在需要的时候创建调用它就可以了

    val view = inflater.inflate(R.layout.item_common_book, bookContainer, false)
    val bookItem = BookItemComponent(life cycleOwner = lifecycleOwner, itemView = view)
    bookItem.bindBook(it)
    bookItemViewContainer.add(view)
    

    来点复杂的?

    来看主页的图书馆模块


    Screen Shot 2017-10-30 at 11.34.29 AM.png

    图书馆模块本身也是一个Component。

    需求:第二行的图标在刷新的时候显示progressbar,刷新成功后显示imageview(对勾),刷新错误的时候imageview显示错误的的图片

    1. 这个Item要放在Recyclerview里面,所以要继承ViewHolder

      class HomeLibItemComponent(private val lifecycleOwner: LifecycleOwner, itemView: View) : RecyclerView.ViewHolder(itemView) {
      }
      
    2. 声明该Component里面用到的view

      class HomeLibItemComponent(private val lifecycleOwner: LifecycleOwner, itemView: View) : RecyclerView.ViewHolder(itemView) {
          private val context = itemView.context
          private val stateImage: ImageView = itemView.findViewById(R.id.ic_home_lib_state)
          private val stateProgressBar: ProgressBar = itemView.findViewById(R.id.progress_home_lib_state)
          private val stateMessage: TextView = itemView.findViewById(R.id.tv_home_lib_state)
          private val bookContainer: LinearLayout = itemView.findViewById(R.id.ll_home_lib_books)
          private val refreshBtn: Button = itemView.findViewById(R.id.btn_home_lib_refresh)
          private val renewBooksBtn: Button = itemView.findViewById(R.id.btn_home_lib_renew)
          private val loadMoreBooksBtn: Button = itemView.findViewById(R.id.btn_home_lib_more)
      
      
    3. 声明Component里面的可观测数据流

      private val loadMoreBtnText = MutableLiveData<String>()
      private val loadingState = MutableLiveData<Int>()
      private val message = MutableLiveData<String>()
      private var isExpanded = false
      
    4. 声明一些其他的用到的东西

      //对应barcode和book做查询
      private val bookHashMap = HashMap<String, Book>()
      private val bookItemViewContainer = mutableListOf<View>() //缓存的LinearLayout里面的view 折叠提高效率用
      private val libApi = RetrofitProvider.getRetrofit().create(LibApi::class.java)
      
    5. 建立绑定关系

          init {
              //这里bind一下 解个耦
              message.bind(lifecycleOwner) { message ->
                  stateMessage.text = message
              }
      
              loadingState.bind(lifecycleOwner) { state ->
                  when (state) {
                      PROGRESSING -> {
                          stateImage.visibility = View.INVISIBLE
                          stateProgressBar.visibility = View.VISIBLE
                          message.value = "正在刷新"
      
                      }
                      OK -> {
                          stateImage.visibility = View.VISIBLE
                          stateProgressBar.visibility = View.INVISIBLE
                          Glide.with(context).load(R.drawable.lib_ok).into(stateImage)
      
                      }
                      WARNING -> {
                          stateImage.visibility = View.VISIBLE
                          stateProgressBar.visibility = View.INVISIBLE
                          Glide.with(context).load(R.drawable.lib_warning).into(stateImage)
                      }
                  }
              }
       
              loadMoreBtnText.bind(lifecycleOwner) {
                  loadMoreBooksBtn.text = it
                  if (it == NO_MORE_BOOKS) {
                      loadMoreBooksBtn.isEnabled = false
                  }
              }
          }
      
      
    6. 再写一个OnBindViewHolder的回调(到时候手动调用就可以了,会考虑使用接口规范这部分内容)

      fun onBind() {
              refreshBtn.setOnClickListener {
                  refresh(true)
              }
              refresh()
              renewBooksBtn.setOnClickListener {
                  renewBooksClick()
              }
              loadMoreBooksBtn.setOnClickListener { view: View ->
                  if (isExpanded) {
                      // LinearLayout remove的时候会数组顺延 所以要从后往前遍历
                      (bookContainer.childCount - 1 downTo 0)
                              .filter { it >= 3 }
                              .forEach { bookContainer.removeViewAt(it) }
                      loadMoreBtnText.value = "显示剩余(${bookItemViewContainer.size - 3})"
                      isExpanded = false
                  } else {
                      (0 until bookItemViewContainer.size)
                              .filter { it >= 3 }
                              .forEach { bookContainer.addView(bookItemViewContainer[it]) }
                      loadMoreBtnText.value = "折叠显示"
                      isExpanded = true
                  }
              }
          }
      
    7. 剩下的就是方法的具体实现了这个看个人喜欢的处理方式来处理,比如说我喜欢协程处理网络请求,然后用LiveData处理多种请求的映射

      比如说一个简单的网络请求以及缓存的封装

      object LibRepository {
          private const val USER_INFO = "LIB_USER_INFO"
          private val libApi = RetrofitProvider.getRetrofit().create(LibApi::class.java)
      
          fun getUserInfo(refresh: Boolean = false): LiveData<Info> {
              val livedata = MutableLiveData<Info>()
              async(UI) {
                  if (!refresh) {
                      val cacheData: Info? = bg { Hawk.get<Info>(USER_INFO) }.await()
                      cacheData?.let {
                          livedata.value = it
                      }
                  }
      
                  val networkData: Info? = bg { libApi.libUserInfo.map { it.data }.toBlocking().first() }.await()
                  networkData?.let {
                      livedata.value = it
                      bg { Hawk.put(USER_INFO, networkData) }
                  }
      
              }
              return livedata
          }
      
      }
      

    8.与其他Component的组合
    使用简单的方法即可相互集成,传入inflate好的view和对应的LifecycleOwener即可

       data?.books?.forEach {
         bookHashMap[it.barcode] = it
         val view = inflater.inflate(R.layout.item_common_book, bookContainer, false)
         val bookItem = BookItemComponent(lifecycleOwner = lifecycleOwner, itemView = view)
         bookItem.bindBook(it)
         bookItemViewContainer.add(view)
     }
    

    小总结:状态绑定,数据观测

    在图书馆的这个Component的开发中,只需要在发起各种任务以及处理任务返回信息的时候,改变相关的状态值和可观测数据流即可,便可实现Component一系列状态的改变,因为所有逻辑不依赖外部,所有目前该Component不对外暴露任何状态和view。实现了模块内的数据流和高内聚。
    模块内数据流可以大幅度简化代码,避免某种程度上对view直接操作所造成的混乱,例如异常处理方法

    private fun handleException(throwable: Throwable?) {
            //错误处理时候的卡片显示状况
            throwable?.let {
                Logger.e(throwable, "主页图书馆模块错误")
                when (throwable) {
                    is HttpException -> {
                        try {
                            val errorJson = throwable.response().errorBody()!!.string()
                            val errJsonObject = JSONObject(errorJson)
                            val errcode = errJsonObject.getInt("error_code")
                            val errmessage = errJsonObject.getString("message")
                            loadingState.value = WARNING
                            message.value = errmessage
                        } catch (e: IOException) {
                            e.printStackTrace()
                        } catch (e: JSONException) {
                            e.printStackTrace()
                        }
    
                    }
                    is SocketTimeoutException -> {
                        loadingState.value = WARNING
                        this.message.value = "网络超时...很绝望"
                    }
                    else -> {
                        loadingState.value = WARNING
                        this.message.value = "粗线蜜汁错误"
                    }
                }
            }
        }
    

    在收到相关错误码的时候,修改state和message的观测值,相关的数据流会根据最初的绑定关系自动通知到相关的view
    比如说loadingstate的观测:

            loadingState.bind(lifecycleOwner) { state ->
                when (state) {
                    PROGRESSING -> {
                        stateImage.visibility = View.INVISIBLE
                        stateProgressBar.visibility = View.VISIBLE
                        message.value = "正在刷新"
    
                    }
                    OK -> {
                        stateImage.visibility = View.VISIBLE
                        stateProgressBar.visibility = View.INVISIBLE
                        Glide.with(context).load(R.drawable.lib_ok).into(stateImage)
    
                    }
                    WARNING -> {
                        stateImage.visibility = View.VISIBLE
                        stateProgressBar.visibility = View.INVISIBLE
                        Glide.with(context).load(R.drawable.lib_warning).into(stateImage)
    
    
                    }
                }
            }
    

    最近更新的

    这个架构比较适合的场景就是,多个业务模块作为Card出现的时候。(或者说是Feed流里面的item,或者是你喜欢使用Recyclerview作为页面组件的容器)等等... 对于单页场景,其实一页就可以认为是一个Component,在页面的内部管理可观察数据流即可。
    架构不是死的,思维也不是。大家还是要根据自己的业务场景适当发挥啊~

    相关文章

      网友评论

        本文标题:React设计思维的启发 - Android View Comp

        本文链接:https://www.haomeiwen.com/subject/pihvpxtx.html