Android进阶之路Android开发经验谈Android开发

厌倦了MVP?不妨来看看Android View Compone

2019-08-06  本文已影响8人  881ef7b85f62

为什么要重构?

做什么?

先思考 => 什么架构

我应该用什么架构 MVP 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还是有很多坑)

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

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

怎么做?

先看一个图来解释

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

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

他们的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)

来点复杂的?

来看主页的图书馆模块

图书馆模块本身也是一个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,在页面的内部管理可观察数据流即可。
架构不是死的,思维也不是。大家还是要根据自己的业务场景适当发挥啊~

学习分享,共勉

题外话,我从事Android开发已经五年了,此前我指导过不少同行。但很少跟大家一起探讨,正好最近我花了一个多月的时间整理出来一份包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术,今天暂且开放给有需要的人,若有关于此方面可以转发+关注+点赞后领取,或者评论与我一起交流探讨。

资料免费领取方式:转发+关注+点赞后,加入点击链接加入群聊:Android高级开发交流群(818520403)即可获取免费领取方式!

重要的事说三遍,关注!关注!关注!

上一篇下一篇

猜你喜欢

热点阅读