写在前面的废话:
我姑且也算个 Android 程序员,虽然上次写安卓已经是两年前。这次临时把原来的混合开发计划更改为原生开发 ,而 Android 端编码的工作就交给了我。
我花了 1 个月的时间完全重写了原本用 ReactNative 和 H5 实现的逻辑。而在第二个星期接触了 Databinding 以及 Jetpack 体系后完全颠覆了原有的安卓编码习惯,于是回过头重构了所有的代码并且不断的修正之前错误的写法。
趁着刚写完,梳理一下撸码过程中的思路变迁顺便记录一下四个星期以来的踩坑经历,再顺便整理一下用到的组件和类,以防以后需要再写一遍。
那么,开始吧。
痛苦的 MVX
虽然前端或者移动端技术这几年发展迅速,但是工程师们做的事情却一直没怎么改变:无非是用动态的数据和静态的页面构建一个完整的视图,同时在这个过程中尽可能的降低两者的耦合度。
早期的 Android 体系还是沿用的服务端的 MVC 的思路,在 Controller 里实例化 view 层的组件并装配上相应的数据。这是一种简单粗暴而且(在交互简单的情况下)符合正常思维的办法,但缺点也很明显:作为 Controller 的 Activity 总是会无比臃肿。
即便是后来有了 ButterKnife 一类注解库,又进化出了所谓的 MVP,依然不能完全解决 MVX 体系的痛点:作为 view 层的 xml,永远只能是静态的。
事实上前端在 Angular 和 React 出来之前也有同样的尴尬,只能用 jq 不断的 $('div')
的进行实体 dom 操作($('div').onClick = function() {}
),然而现在无论是 Angular 还是 React 还是 Vue,都可以把变量写进 View层,把原本由 Controller 必须承担的 view 部分交还给了 view 自己。
上帝的归上帝,凯撒的归凯撒,视图层和数据层一下变的泾渭分明。
而 Android 开发者等了这么多年,才等来了现在的 databinding。
其实 2 年前我就已经开始用 db 了,但是那个时候一方面 db 还不是特别稳定,另一方面功能还没那么强大。当然最主要的是我第一次用 db 就整瘫了整个项目,组内的其他人同步了我的代码以后无论如何也编译不过去(都是 gradle 惹的祸)。然后我就被老大踹到前端组“帮忙”,一帮就是两年多。
啊哈,Databinding!
举一个例子来说明有没有 db 的巨大差别。
这是一个车卡的基本界面,上面是人物的基本信息,下面是人物基本属性,点击随机按钮以后可以用掷点法初始化。
如果没有 db,那么写法基本如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.recyclerView);
TextView tvName = findViewById(R.id.tv_name);
TextView tvRace = findViewById(R.id.tv_race);
TextView tvLevel = findViewById(R.id.tv_level);
TextView tvClass = findViewById(R.id.tv_class);
GridLayoutManager layoutManager = new GridLayoutManager(this, 2);
ArrayList<HeroAttr> arrayList = new ArrayList<>();
AttrAdapter attrAdapter = new AttrAdapter();
attrAdapter.initData(arrayList);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(attrAdapter);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
}
}
其实没必要仔细看,因为这是一个错误的示范(狗头)
在还没有进行任何数据层的逻辑编码时,Activity 里已经充斥了大量的 view 实例。把 View 层的东西拽到数据层实例化并且赋值,这便是 MVX 里最痛苦的地方。
但是 databinding 的实现方式和思维模式则刚好相反。
开启 databinding
打开 app 文件夹里的 build.gradle
,添加如下代码
compileSdkVersion 27
// ...
dataBinding {
enabled = true
}
// ...
defaultConfig {
}
不过貌似目前功能还不是特别稳定,对于 db 谷歌也有过几次改动,在新项目里用 db 的时候花了一天多的时候踩坑,报的最多的错就是:
Element: class com.intellij.psi.impl.source.xml.XmlFileImpl because: different providers:
这种错误或者其他的编译错误解决方法如下:
- 科学上网,起码可以解决 70% 上的 gradle 编译失败。
- 升级 Android Studio 的版本,最早用 2.x 的时候简直寸步难行,动不动 gradle 就一堆编译错误。升级到 3.1 的时候这种情况就少了很多,我现在用的 3.3 Canary,gradle 比较激进用的
'com.android.tools.build:gradle:3.3.0-alpha03'
,糟心的情况少了很多。 gradle.properties
文件中添加如下代码:
android.databinding.enableV2=true
stackoverflow
上的说法是现在 google 埋了两个版本的 databinding,互相之间不兼容,需要强制开启高版本。
有的时候
BR.xx
报错的话,仔细看的话会有 2 个 BR 文件,我一般用第二个结尾是adapter
的可以正常访问。
- build -> make project
- file -> invalit cache and restart
几个坑点:
- 编译报错。
有可能 xml 或者代码里写错了,现在 db 提示做的蛮良心的,按图索骥一般能顺利解掉。
- 使用 layout 标签包裹完 xml 以后,在代码里使用 databingUtils 没有 binding 类
解决方法:make 一下,不行 rebuild,还不行(排除 0 的错误)file -> invalit cache and restart
- 使用 varable 标签加了变量以后,binding 不提供对应的 set 方法。
也是 make 一下就好,但是也有可能没鸟用,可以用 setVariable(BR.xxx, obj) 代替。
java 8 与 lamda 表达式
java 8 里有很多令人惊喜的新特性,其中最让我心动的就是 lamda 表达式。写 lamda 表达式的时候让我有前端写箭头函数的熟悉感,也让 java 中的 oop 有了那么一丝函数式编程的味道。
比较好玩的是,我的前端同事们接触 lamda 表达式都是一眼就能看懂,而安卓同事反而觉得特别别扭。
简单的说对于接口 a,如果有且只有一个抽象方法,那就可以直接写成 lamda 表达式。(据说 java 8 里的接口除了写抽象方法也可以写具体方法了,面向过程的痕迹越来越重了)
即对于
public interface A {
int func(B b)
}
A a = new A () {
int func(B b) {
// ... 自有逻辑
}
}
等价于
A a = b -> {
//...
}
同时也意味着古典派的写法:
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// ...
}
});
等价于:
view.setOnClickListener(v -> {
// ...
});
当然,看起来好像只是简单的少写了一些代码,事实上也确实是这样 —— 匿名函数的简洁写法。但是再加上 databinding,却治好了一个我多年的编程误区。
卖个关子,后面说。
了不起的 databinding
使用 databinding 与 lamda 表达式重构以后,MainActivity 里的代码如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 对 layout 进行 databinding 化
ActivityMainBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
// 设置基本信息
Hero hero = new Hero("齐格飞", "人类", "1级", "法师");
viewDataBinding.setVariable(BR.hero, hero);
View.OnClickListener listener = v -> {
int[] values = new int[6];
for(int i=0; i< 6;i++) {
values[i] = getFinalValue();
}
// 每次点击以后随机给属性复制
viewDataBinding.setVariable(BR.heroAttrs, HeroAttrs.getHeroAttrs(values) );
};
// 设置监听器
viewDataBinding.setVariable(BR.listener, listener);
}
// 掷点法获得属性
private int getFinalValue() {
Double[] values = {
new Double(Math.floor(Math.random() * 6 + 1)),
new Double(Math.floor(Math.random() * 6 + 1)),
new Double(Math.floor(Math.random() * 6 + 1)),
new Double(Math.floor(Math.random() * 6 + 1))
};
ArrayList<Double> array = new ArrayList<>();
Collections.addAll(array, values);
Collections.sort(array);
array.remove(0);
int finalValue = 0;
for(int j = 0;j < array.size();j++) {
finalValue += array.get(j);
}
// 个人房规:单一属性不得小于 6
return finalValue > 6 ? finalValue : getFinalValue();
}
}
没了。
真的没了,带上业务逻辑,代码也就这么多,整个 Activity 里也完全见不到任何 view 的实例。
这便是 databinding 最了不起的地方。
我最开始用 db 的时候只是把它当做另一个 butterknife,写出来的代码是这个样子:
viewDataBinding.tvName.setText("齐格飞");
viewDataBinding.tvRace.setText("人类");
viewDataBinding.tvClass.setText("法师");
viewDataBinding.tvLevel.setText("1级");
但这还是一种老式的 MVC 思维,“把 view 的实例带进 Activity”。这么使用 databinding 不能说错,但完全是高射炮打蚊子。
从 db 的角度看,activity 里压根就不应该存在任何 view 的实例。如果出现了,说明封装的还不够彻底。
整个代码就做了三件事:
- 对 activity 进行 db 化:
ActivityMainBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
- 初始化人物卡的基本信息,扔进 xml 里去
viewDataBinding.setVariable(BR.hero, hero);
- 初始化给人物属性赋值的 listener,扔进 xml 里去
viewDataBinding.setVariable(BR.listener, listener);
- 每次随机获得属性后,扔到 xml 里去
viewDataBinding.setVariable(BR.heroAttrs, HeroAttrs.getHeroAttrs(values) );
完。
引入 databinding 以后 MainActivity 最大的变化,就是从数据处理者变成了数据提供者。以前的 Activity 又要初始化 view 的实例又要给实例赋值,赋值的逻辑还得亲自实现(没有 presenter 的情况),又当爹又当妈;现在 Activity 只是服务员,数据什么的经个手,扭头就扔出去了。
扔给谁呢?
xml。
XML 里的 databinding
databinding
的 xml 是要经过特殊处理的,和传统的 xml 有以下几点不同:
- 原本界面层的根布局外侧需要再加套一个
layout
标签。 - 如果要引入变量,需要增加
data
标签,data
标签和根布局同级,而且只能有一个。 data
标签内可以有很多variable
标签,必须有name
属性(xml 布局内唯一)和type
属性(xml 内可以不唯一,但是对泛型支持略差),用于确定唯一的变量。- xml 布局内可以用
@{}
直接引用 data 标签里定义的变量(此举似曾相识)。
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
>
<!-- 这里是变量定义 -->
<!--viewDataBinding.setVariable(BR.heroAttrs, xxx );-->
<!--BR. 的值和 variable 的 name 一一对应-->
<data>
<variable
name="heroAttrs"
type="android.powerword.siegfried.com.dnd_builder.model.HeroAttrs" />
<variable
name="hero"
type="android.powerword.siegfried.com.dnd_builder.model.Hero" />
<variable
name="listener"
type="android.view.View.OnClickListener" />
</data>
<!-- 具体的 view -->
<android.support.constraint.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorWhite"
tools:context=".MainActivity">
<RelativeLayout
android:id="@+id/rl_hero_short"
android:layout_width="match_parent"
android:layout_height="180dp"
android:background="@color/colorPrimaryDark"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<!-- 其实这个 linearlayout 也可以抽成组件 -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center_horizontal"
android:orientation="vertical">
<de.hdodenhof.circleimageview.CircleImageView
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_alignParentTop="true"
android:layout_centerHorizontal="true"
android:layout_marginTop="32dp"
android:elevation="10dp"
android:src="@drawable/ic_launcher_background" />
<LinearLayout
android:layout_marginTop="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{hero.name}"
android:textColor="@color/colorWhite" />
<TextView
android:id="@+id/tv_race"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="@{hero.race}"
android:textColor="@color/colorWhite" />
<TextView
android:id="@+id/tv_level"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="@{hero.level}"
android:textColor="@color/colorWhite" />
<TextView
android:id="@+id/tv_class"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:text="@{hero.clazz}"
android:textColor="@color/colorWhite" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
<android.powerword.siegfried.com.dnd_builder.custom.AttRecycleView
android:id="@+id/recyclerView"
style="@android:style/Widget.Material.TextView"
android:layout_width="match_parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rl_hero_short"
app:heroAttrs="@{heroAttrs}"
app:layout_constraintVertical_weight="1"
android:layout_height="wrap_content" />
<android.powerword.siegfried.com.dnd_builder.custom.DrawableButton
android:id="@+id/button"
style="@android:style/Widget.Material.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:text="随机"
android:drawableTint="@color/colorWhite"
android:drawableLeft="@drawable/baseline_apps_24"
android:textColor="@color/colorWhite"
android:onClick="@{listener}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/recyclerView"
app:layout_constraintVertical_bias="1.0" />
</android.support.constraint.ConstraintLayout>
</layout>
前端工程师和移动端工程师一直在做的事情,就是分离动态的数据和静态的 view,同时在这个过程中尽可能的降低两者的耦合。
没有 databinding 之前,只能通过在 controller 里实例化 view 并进行数据装配;有了 databinding 之后,动态的数据(以及操作数据的方法)可以直接提供至 view 层。Controller 里原本的 view 层部分彻底交还给了 view 自己,只需要专心提供 view 层所需要的数据就可以了。
据说这种做法被称作数据绑定。
刚开始用 db 写 xml 的时候我还有点恍惚,感觉像是在用 jade 模板画 vue —— 无非是
{{}}
变成了@{}
。
画一个组件,给动态数据留个坑,再画下一个组件,再留一个坑,周而复始。等界面画完,data 里面写数据,method 里面写方法,用数据和方法去填坑。
databinding 表达式
databinding 在 xml 是可以使用表达式的,比如 > < =
或者三元表达式,也可以进行一定程度的逻辑处理。不过写了一个多月,个人觉得以下三种写法最常用到。
1. 已有 set 方法的属性进行赋值
对于
<data>
<!--...-->
<variable
name="hero"
type="android.powerword.siegfried.com.dnd_builder.model.Hero" />
</data>
可以直接使用:"@{hero.name}"
进行数据的装配
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{hero.name}"
android:textColor="@color/colorWhite" />
同理
level
class
race
。
对于 android:text="@{hero.name}"
,其等价于 textView.setText(hero.name)
。db 会检测 TextView 组件中属性 text
的 set 方法,在找到以后判断参数是否是 String
(hero 对象的 name 属性为 String),如果以上条件都符合,db 会调用组件的 setText 方法,并把 hero.name 作为参数传递进去。
AKA,反射...
2. 对未有 set 方法或者未包含的属性进行赋值。
牵扯到 app:heroAttrs="@{heroAttrs}"
,情况就要复杂一点。
首先,装载 heroAttrs 数据的组件的是 RecycleView,而 RecycleView 并没有 heroAttrs
的属性更不可能有 setHeroAttrs
的方法。
其次,数据是通过 adapter 和 RecycleView 链接,adapter 不是组件。
一开始我是处于这个思维盲点,钻进牛角尖里出不来。然而等我以前端的观点思考的时候,答案却是显而易见的。
没有这么一个符合条件的 RecycleView,那就以 RecycleView 为基底写一个符合条件的组件呗。
public class AttRecycleView extends RecyclerView {
private AttrAdapter attrAdapter;
public AttRecycleView(Context context) {
this(context, null, 0);
}
public AttRecycleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public AttRecycleView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
initHeroAttrs();
}
private void init() {
// 初始化 RecycleView 后就设置 mapager
// 之前的编码误区之一,就是 adatper 和 manager 要写在 activity 里
// 事实上,manager 和 adatper 是跟着业务走的东西
// 完全应该封装进组件里
GridLayoutManager layoutManager = new GridLayoutManager(this.getContext(), 2);
attrAdapter = new AttrAdapter();
this.setLayoutManager(layoutManager);
this.setAdapter(attrAdapter);
}
public void setHeroAttrs(HeroAttrs heroAttrs) {
if(heroAttrs == null) {
return ;
}
this.setHeroAttrs(heroAttrs.attrs);
}
private void initHeroAttrs() {
HeroAttrs attrs = HeroAttrs.getInitHeroAttrs();
this.setHeroAttrs(attrs);
}
private void setHeroAttrs(ArrayList<HeroAttrItem> arrayList) {
this.attrAdapter.initData(arrayList);
}
}
也是之前的 mvc 的坏习惯,adapter 也好 manager 也好,都是习惯写在 Activity 里。但是实际上,adapter 和 manager 原本就是应该跟着 RecycleView 走的东西,RecycleView 对外暴露出的只是一个加载数据的方式,而已。
该怎么操作数据,你别管,只要把数据交给我 —— 据说这种思维现在被称为响应式。
响应式的英文称呼是 React,我才不信这是一个巧合。
3. 使用 lamda 表达式设置函数
MVX 的思想不仅包含需要操作的数据,也包含对数据的操作 —— 也即是所谓的函数。不过 java 并不是 js,不能直接操作函数(不能用函数当参数、返回值不能是函数 所以也没有高阶函数的存在)。因此在方法的传递上,比较通用的做法的是用一个类实现对应方法的接口,然后把接口当参数。
个人现在对类(class)和接口(interface)最大的感触,就是类是用来描述属性,interface 是用来描述方法。
比如经典的 View.OnClickListener
,最直接的做法是
View.OnClickListener listener = new View.OnClickListener(){
public void onClick(View view) {
....
}
}
<data>
<variable
name="listener"
type="android.view.View.OnClickListener" />
</data>
<Button
<!-- 其他操作 -->
android:onClick="@{listener}"
/>
当然这样写,不能说错,但是我还是更推崇 lamda 表达式的写法。
因为这样写起来和前端一毛一样。
如果有类 presenter 如下
public class Presenter {
private final ViewDataBinding viewDataBinding;
public Presenter(ViewDataBinding viewDataBinding) {
this.viewDataBinding = viewDataBinding;
}
public void onClick() {
int[] values = new int[6];
for(int i=0; i< 6;i++) {
values[i] = getFinalValue();
}
viewDataBinding.setVariable(BR.heroAttrs, HeroAttrs.getHeroAttrs(values) );
}
private int getFinalValue() {
Double[] values = {
new Double(Math.floor(Math.random() * 6 + 1)),
new Double(Math.floor(Math.random() * 6 + 1)),
new Double(Math.floor(Math.random() * 6 + 1)),
new Double(Math.floor(Math.random() * 6 + 1))
};
ArrayList<Double> array = new ArrayList<>();
Collections.addAll(array, values);
Collections.sort(array);
array.remove(0);
int finalValue = 0;
for(int j = 0;j < array.size();j++) {
finalValue += array.get(j);
}
return finalValue > 6 ? finalValue : getFinalValue();
}
}
那么
View.OnClickListener listener = new View.OnClickListener(){
public void onClick(View view) {
掷骰法....
}
}
等价于
View.OnClickListener listener = view -> {
// 掷骰法....
}
}
等价于
View.OnClickListener listener = view -> presenter.onClick(view);
那么
<data>
<variable
name="listener"
type="android.view.View.OnClickListener" />
</data>
<Button
<!-- 其他操作 -->
android:onClick="@{listener}"
/>
等价于
<data>
<variable
name="presenter"
type="Presenter" />
</data>
<Button
<!-- 其他操作 -->
android:onClick="@{ view -> presenter.onClick(view)}"
然后 MainActivity
里现在只有这么点东西:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
Hero hero = new Hero("齐格飞", "人类", "1级", "法师");
viewDataBinding.setVariable(BR.hero, hero);
Presenter presenter = new Presenter(viewDataBinding);
viewDataBinding.setVariable(BR.presenter, presenter);
}
}
逻辑都去哪儿了?
扔给 Presenter
了。
其实这也是一个错误的示范,因为从开发的角度来讲,presenter 里也是不应该有任何与 view 相关的东西,presenter 只负责处理数据,和 view 是隔绝的。负责连接数据和视图的,是
viewmodel
里的liveData
。
这么写的好处有二:
- 代码量少了,这是很明显的。(说实话也没少多少)。
- db 的这种写法逼着开发者把重逻辑放在 p 里,直接减少了 Activity 的体积(这点很关键)。
换句话说,db 逼着开发者至少要用到 MVP 的解耦水平。
在我以前的编码习惯中,在 listener
里写逻辑实在是中稀松平常的事情。一方面是省事(因为数据都在 activity
里),另一方面是懒(懒得优化逻辑)。
但是从开发的角度看,如果所有的数据处理逻辑随便的堆放在 Activity 很容易写成乱七八糟的面条代码;而把所有具体的逻辑归纳进 Presenter
—— Activity
仅作为一个堆放 P 容器 —— 的做法,则大大提高了业务逻辑的模块化。逻辑之间互相独立,任何一个逻辑有变化只要修改对应的 presenter 就可以了。
实话实说,我刚开始写 MVP 的时候完全不理解 P 是个什么东西,后来我写 Angular 的时候突然发现卧槽 Presenter 不就是 Service 嘛....
把数据和逻辑声明成变量放进 xml 里的最大好处,就是不用看代码翻翻 XML 就知道这个页面是干什么的。同时,接到需求以后简单的评估一下那些是动态的数据那些是处理数据的方法,Activity 和 XML 的写法就能猜个七八分。
4 bind adapter 写法
我并没有直接用到过,但是看了下文档其实解决的也是 2、3 类问题。(请高手指点下应用场景)
Databinding 与响应式
我姑且算个 Android 程序员,虽然我的编码习惯已经完全前端化了。这一个月在用 db 等 jetpack 模块开发 app 的时候完全没有什么太大的压力,甚至有种在写前端的错觉 —— 无非是语法换了换写法换了换,原理和核心思想却和 vue/react 们别无二致(甚至越写越像 Angular ,尤其是在我尝试 koltin 化的时候)。
两年多的前端编码工作带给我最大的转变是从 MVC 的思维变成了响应式(或许还有函数式)的习惯。编码时注重组件化(而不是像之前实例化 view 进行数据装配),并提供对外的调用接口,具体的逻辑放在组件内部实现。一个 Activity(或者 Fragment)就是一个 container,负责传递数据和传递处理数据的方法。
不过组件化开发的问题是如何进行优雅的组件间传值 —— 父子组件传值、平级组件传值,等等等等。
前端的处理方式比较统一 —— 创建一个/数个数据仓库(这个概念是在写前端的时候体会到的)。比如 React 的 redux/mobx,vue 的 vuex(Angular 里的 Service 其实也算半个)。组件可以链接数据仓库进行数据的读取和写入,从而完成值的传递。
Jetpack 也给 Android 提供了自己的数据仓库 -- ViewModel。