8

Android 声明式UI框架Jetpack Compose与React Hook的统一性

 3 years ago
source link: https://zhuanlan.zhihu.com/p/357885060
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Android 声明式UI框架Jetpack Compose与React Hook的统一性

58同城 Android工程师

Jetpack Compose是Google在2019 I/O大会上公布的声明式UI框架。近期Google也在不断的宣传Compose,举行开发挑战赛 Android 开发挑战赛 | 第 3 周: 速度比拼。鼓励更多的开发者去了解与使用它,当然现在不是很完善,但未来逐步使用Jetpack Compose这个趋势也会有可能的。Compose在设计理念与使用方式上很大程度上借鉴了Rect Hooks.如函数组件,纯函数副作用声明式渲染组件状态这些概念在Compose与Rect Hooks都是保持一致的。所以本片文章对React Hooks与Compose都会进行大体上的介绍。让大家有个整体上的认识。

React Hook是啥?

下面是官网的介绍

Hook是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性

通常情况下写一个React组件,需要声明一个class,在内部处理状态和声明周期等。如下

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

我们能不能简化这一过程呢?

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

我们通过使用函数组件完成了改造,但是如果遇到内部包含状态的组件怎么处理呢?这里就引入了副作用的概念。我们知道一个纯函数只要输入值相同,输出值也一定相同。就是我们给了组件相同的属性,展示应该一直保持一定。这就是幂等性的概念。

函数式编程将那些跟数据计算无关的操作(比如生成日志、储存数据、改变应用状态等等),都称为 "副效应"(side effect)。如果函数内部直接包含产生副效应的操作,就不再是纯函数了,我们称之为不纯的函数。

引入useState保存组件状态的副作用函数(钩子),来解决函数组件的状态问题。状态发生变化时同步更新UI

import React, { useState } from "react";

export default function  Button()  {
  //这个赋值在react中叫做解构赋值
  const  [buttonText, setButtonText] =  useState("Click me,   please");

  function handleClick()  {
    return setButtonText("Thanks, been clicked!");
  }

  return  <button  onClick={handleClick}>{buttonText}</button>;
}

useState()这个函数接受状态的初始值,作为参数,上例的初始值为按钮的文字。该函数返回一个数组,数组的第一个成员是一个变量(上例是buttonText),指向状态的当前值。第二个成员是一个函数,用来更新状态,约定是set前缀加上状态的变量名(上例是setButtonText)。

引入useEffect,给函数组件增加了操作副作用的能力。如文本变化时我们更新外部的标题

import React, { useState } from "react";

export default function  Button()  {
  const  [buttonText, setButtonText] =  useState("Click me,   please");
  useEffect(() => {
     document.title = ` ${buttonText} `;
  }, [buttonText])
  function handleClick()  {
    return setButtonText("Thanks, been clicked!");
  }

  return  <button  onClick={handleClick}>{buttonText}</button>;
}

通过介绍我们对Reat Hook有了大致的了解,对下面Jetpack的学习有很大的帮助,两者在使用上几乎是一样的。

Jetpack Compose 是啥?

从一个简单的TextView输入界面开始

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HelloScreen(HelloViewModel())
        }
    }

    @Composable
    fun HelloScreen(helloViewModel: HelloViewModel) {
        val name:String by helloViewModel.name.observeAsState(initial = "")
        Column {
            Text(text = name)
            TextField(
                value = name,
                onValueChange = { helloViewModel.onNameChanged(it) },
                label = { Text("Name") }
            )
        }
    }
}
class HelloViewModel : ViewModel() {

    private val _name = MutableLiveData("")
    val name: LiveData<String> = _name
    fun onNameChanged(newName: String) {
        _name.value = newName
    }
}

在开发过程中我们遇到这个场景其他页面也需要这个输入控件,需要我们抽离出来,不能耦合一些业务逻辑,我们怎么抽离呢?经过我们的一波头脑风暴后

    @Composable
    fun HelloScreen(helloViewModel: HelloViewModel) {
        val name:String by helloViewModel.name.observeAsState(initial = "")
        HellowInput(name = name, onNameChange = { helloViewModel.onNameChanged(it) })
    }
    /**
     * 状态提升,状态组件提升为无状态组件
     */
    @Composable
    fun HellowInput(name:String,onNameChange:(String)->Unit) {
        Column {
            Text(text = name)
            TextField(
                value = name,
                onValueChange = onNameChange,
                label = { Text("Name") }
            )
        }
    }

我们抽离出了HellowInput控件, 我们将控件依赖的属性和产生的事件回调抽离出来,由上层组件去设置,同时该组件实现不依赖任何状态。这个控件就是无状态组件。我们将状态组件抽离出无状态组件的过程就是状态提升,在这种模式下,通过将可组合项中的内部状态替换为参数和事件,将状态移至可组合项的调用方。

状态组件与无状态组件

状态组件:一言以蔽之,该组件的实现你可以找到状态变量。HelloScreen 是一个有状态可组合项,因为它依赖于最终类 HelloViewModel,该类可以直接更改 name 状态。

无状态组件:一言以蔽之,该组件的实现没有状态变量。如我们提出HellowInput组件,Compose内置Text,TextField等组件

Jetpack Compose中的副作用

状态操作:当name状态发送变化时同步更新文本UI

  @Composable
    fun HelloScreen(helloViewModel: HelloViewModel) {
        val name:String by helloViewModel.name.observeAsState(initial = "")
        HellowInput(name = name, onNameChange = { helloViewModel.onNameChanged(it) })
    }

LaunchedEffect副作用:当name发生变化是进行Toast提示

 @Composable
    fun HelloScreen(helloViewModel: HelloViewModel) {
        val name:String by helloViewModel.name.observeAsState(initial = "")
        LaunchedEffect(name){
            Toast.makeText(this@MainActivity,name,Toast.LENGTH_LONG).show()
        }
        HellowInput(name = name, onNameChange = { helloViewModel.onNameChanged(it) })
    }

通过对比使用方式上是不是大同小异。

React使用diff算法和VirtualDom实现的组件局部刷新,对VirtualDom进一步理解可以查看这篇文章 如何实现一个 Virtual DOM 算法,而Compose使用的Gap Buffer算法实现局部刷新。引入Compose包大小方面会增大许多,一个简单的Demo包大小大约10M,如图androidx.compose的代码量占17.9MB大小。另外使用官方的新闻列表页在滑动过程中也是肉眼可见的卡顿。

通过对比我们发现Compose与React Hooks都是使用函数组件,状态管理也是引入副作用函数,学会了React Hooks很容易入手Jetpack Compose。大前端统一技术栈在不远的未来应该就会实现了。

Jetpack Compose 生命周期

Split buffers

Gap Buffer--简洁有效的文本编辑算法

https://developer.android.com/jetpack/compose/lifecycle?hl=zh-cn

https://developer.android.com/jetpack/compose?hl=zh-cn

深入详解 Jetpack Compose | 实现原理

React Hooks 入门教程

轻松学会 React 钩子:以 useEffect() 为例

Hook 简介 – React

Jetpack Compose SideEffect:副作用及相关API


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK