从 React 的角度看 Android 的 Jetpack C

2024-04-23  本文已影响0人  头秃到底

最近为了开发一个小项目,学习了Jetpack Compose,API 设计的很不错。Jetpack Compose的API 非常丰富,正好我的 React 的知识可以发挥作用。也许这就是 React Native 开发者可以代替 Android 原生开发者的原因。

这两个框架的很多概念和方法虽然名称不同,但是工作原理却大同小异。以下是两者之间概念的对比和解释。

我们下面把 Jetpack Compose 简称为 JC。

Component 和 Composable

React 叫各个组成部分为 Component(组件)。

function Greeting(props) {
 return <span>Hello {props.name}!</span>;
}

Jetpack Compose 叫各个组成部分为 Composable(其实也是组件)。Composable 方法除了需要是一个方法意外,还需要一个@Composable注解。

@Composable
fun Greeting(name: String) {
 Text(text = "Hello $name!")
}

Render 和 Composition

当一个组件包含的数据发生改变,这些变化的数据需要以定义好的方式绘制到屏幕上。React 的叫做 render,Jetpack Compose 的叫做 Compose。

Reconciler 和 Composer

React 内部需要找到组件发生变更的地方才能对应的绘制出来。这个算法叫做Reconciler。JC 也包含着这样的算法,执行这个算法的叫做 Composer。

State 和 State

React 和 JC 都把他们的状态叫做 State

useState 和 State

React 使用useState来创建 state 变量。它会返回一个 tuple,一个是状态值,一个是这个状态的 setter。

const [count, setCount] = useState(0);

<button onClick={() => setCount(count + 1)}>You clicked {count} times</button>;

JC 使用mutableStateOf方法返回一个MutableState对象,这个对象还包含有一个属性和对应的 getter 和 setter。

val count = remember { mutableStateOf(0) }

Button(onClick = { count.value++ }) {
  Text("You clicked ${count.value} times")
}

MutableState可以像 React 的useState一样返回 value 和对应的 setter。

val (count, setCount) = remember { mutableStateOf(0) }

Button(onClick = { setCount(count + 1) }) {
  Text("You clicked ${count} times")
}

为了避免无效计算,remember经常和mutableStateOf一起使用。

setState 和 Snapshot

更新 React 的状态的时候可以使用一个方法来实现。如:

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  render() {
    <button
      onClick={() => this.setState((state) => ({ count: state.count + 1 }))}
    >
      You clicked {this.state.count} times
    </button>;
  }
}

在 JC 里,这个概念包含在一个叫做Snapshot的类里面。这个类里面有一个enter方法来调用更新后的回调。

Children Prop 和 Children Composable

React 和 JC 都把展示在其他 UI 组件里的组件叫做 Children。

React 是这样的:

function Container(props) {
  return <div>{props.children}</div>;
}

<Container>
  <span>Hello world!</span>
</Container>;

JC 是这样的:

@Composable
fun Container(children: @Composable () -> Unit) {
  Box {
    children()
  }
}

Container {
  Text("Hello world"!)
}

Context 和 CompositionLocal

数据知识沿着组件树传输有的时候过于繁琐。React 可以通过 Context 分享数据。JC 可以用CompositionLocal来实现同样的目的。

createContext 和 compositionLocal

React 使用createContext创建 Context 对象。JC 使用compositionLocalOfstaticCompositionLocalOf

如果一个compositionLocal的不太会改变的话可以使用staticCompositionLocalOf以获得性能的提升。

Provider 和 CompositionLocalProvider

<MyContext.Provider value={myValue}>
  <SomeChild />
</MyContext.Provider>

JC 的实现:

CompositionLocalProvider(MyLocal provides myValue) {
  SomeChild()
}

useContext 和 CompositionLocal.current

React:

const myValue = useContext(MyContext);

JC:

val myValue = MyLocal.current

总结一下 android 的写法:

使用composeLocalOf或者staticCompositionLocalOf(这个一般用于不怎么变化的值)创建一个对象。

val LocalColor = compositionLocalOf {Color.Red}

然后:

class MyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            CompositionLocalProvider(LocalColor provides colors) {
                // ... Content goes here ...
                ProviderReaderCompositable {
                    Text(text="Read compositable", modifier = Modifier.background(LocalColor.current))
                }
            }
        }
    }
}

LocalColor可以定义在MyActivity这个文件引用过来,也可以定义在本文件内部,通过CompositionLocalProvider把数据分享出去。

之后,在子组件中读取数据:

ProviderReaderCompositable {
    Text(text="Read compositable", modifier = Modifier.background(LocalColor.current))
}

Hooks, Effect

React允许开发这写自己的 hooks,这样可以把逻辑抽离出来达到重用的效果。这些 hooks 里也可以用其他的 hooks 比如useStateuseEffect

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
    };
  }, [friendID]);

  return isOnline;
}

JC 可以直接在@Composable方法里实现这个功能:

@Composable
fun friendStatus(friendID: String): State<Boolean?> {
  val isOnline = remember { mutableStateOf<Boolean?>(null) }

  DisposableEffect(friendID) {
    val handleStatusChange = { status: FriendStatus ->
      isOnline.value = status.isOnline
    }

    ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange)
    onDispose {
      ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange)
    }
  }

  return isOnline
}

useEffect 和 LaunchedEffect

副作用(side effect)都是作为回调方法执行的。React 和 Jetpack Compose 都是如此。

useEffect(() => {
  sideEffectRunEveryRender();
});

useEffect的功能比较丰富。JC 把这些功能做了细分。比如:DisposableEffectLaunchedEffectSideEffect

Clean-up 方法和 DisposableEffect

组件不再使用的时候需要回收副作用不再使用的资源。React会在useEffect里返回一个clean-up方法:

useEffect(() => {
  const subscription = source.subscribe(id);
  return () => {
    subscription.unsubscribe(id);
  };
}, [id]);

JC 则使用DisposableEffect

DisposableEffect(id) {
  val subscription = source.subscribe(id)
  onDispose {
    subscription.unsubscribe(id)
  }
}

useEffect(promise, deps) 和 LaunchedEffect

JS 使用async关键字创建异步方法。

useEffect(() => {
  async function asyncEffect() {
    await apiClient.fetchUser(id);
  }
  asyncEffect();
}, [id]);

上面的方法会在id这个依赖项发生改变的时候访问 API 获取数据。React 没有内置取消 promise 的方法,但是可以使用AbortController。如:

useEffect(() => {
  const controller = new AbortController();

  (async () => {
    // The abort signal will send abort events to the API client
    await apiClient.fetchUser(id, controller.signal);
  })();

  // Abort when id changes, or when the component is unmounted
  return () => controller.abort();
}, [id]);

在 JC 里,使用的是suspend方法和 coroutine。在LaunchedEffect可以接受传入的参数,他们和 useEffect 里的依赖是同样的作用。如:

LaunchedEffect(id) {
 apiClient.fetchUser(id)
}

useEffect(callback)和 SideEffect(callback)

useEffect没有依赖的话,会在每次绘制之后执行。

useEffect(() => {
  sideEffectRunEveryRender();
});

JC 使用SideEffect实现同样的效果。

SideEffect {
  sideEffectRunEveryComposition()
}

对于依赖的处理

总结以上,useEffect可以有带依赖useEffect(() => {}, [deps]), 可以不带依赖useEffect(() => {}),还有一个就是可以带一个空数组当做依赖useEffect(()=> {}, [])

对应的,在 JC 里可以有

LaunchedEffect(keys=listOf(deps)) {
    // Run when deps change
}

没有依赖:

SideEffect {
   // Something...
}

依赖为空数组:

LaunchedEffect(Unit) {
    // Run only once
}

在上面的一个例子中提到了自定义 hooks。如:

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);

  useEffect(() => {
    ChatAPI.subscribeToFriendStatus(friendID, (status) => {
      setIsOnline(status.isOnline);
    });

    return () => {
      ChatAPI.unsubscribeFromFriendStatus(friendID);
    };
  }, [friendID]);

  return isOnline;
}

在上例中,使用了 JC 的DisposableEffect。还可以选择另外一个方法:produceState。如:

@Composable
fun friendStatus(friendID: String): State<Boolean?> {
  return produceState(initialValue = null, friendID) {
    ChatAPI.subscribeToFriendStatus(friendID) { status ->
      value = status.isOnline
    }

    awaitDispose {
      ChatAPI.unsubscribeFromFriendStatus(friendID)
    }
  }
}

produceState里可以直接对value赋值,这样会调用他的 setter。读取的时候可以在返回值里读取。在produceState里执行的可以是一个 coroutine(协程),或者使用awaitDispose方法来清理资源。后者和useEffect返回一个 clean-up 方法的做法类似。

Key pros 和 key Composable

处理列表显示的时候,React和 JC 都需要用到 Key prop。这样才能在这列表里那个发生了,更改、添加或者是删除。Key必须使用唯一值来标记列表里的元素。

<ul>
  {todos.map((todo) => (
    <li key={todo.id}>{todo.text}</li>
  ))}
</ul>

JC有一个key composable 可以使用:

Column {
  for (todo in todos) {
    key(todo.id) { Text(todo.text) }
  }
}

.map 和 For 循环

React 经常使用 map 来显示一列组件:

function NumberList(props) {
  return (
    <ul>
      {props.numbers.map((number) => (
        <ListItem value={number} />
      ))}
    </ul>
  );
}

JC可以使用 for 循环,也可以使用 forEach,使用 map 也能达到效果:

@Composable
fun NumberList(numbers: List<Int>) {
  Column {
    for (number in numbers) {
      ListItem(value = number)
    }
  }
}

使用 forEach 和 map 的例子就不写了,各位可以在测试项目里试试。

useMemo 和 remember

React 可以使用useMemo来避免无效运算,只有在依赖发生变更的时候才执行运算。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

JS 使用 remember达到同样的效果。依赖可以作为参数传入:

val memoizedValue = remember(a, b) { computeExpensiveValue(a, b) }

条件绘制

React 里可以使用?:操作符,比如:const a = x === y ? b : c。再比如:

function Greeting(props) {
  return (
    <span>{props.name != null ? `Hello ${props.name}!` : 'Goodbye.'}</span>
  );
}

对应的,JC 可以使用 kotlin 的语法来实现:

@Composable
fun Greeting(name: String?) {
  Text(text = if (name != null) {
    "Hello $name!"
  } else {
    "Goodbye."
  })
}

预览

React 可以使用storybook来实现。

JC 可以这样:

@Composable
@Preview(showBackground = true)
fun SettingsScreensPreview() {
    MyTheme() {
        SettingsScreen(null)
    }
}

上一篇下一篇

猜你喜欢

热点阅读