JetPack 學習筆記:Databinding 與響應式

龍騎將楊影楓發表於2019-03-03

寫在前面的廢話:

我姑且也算個 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 的巨大差別。

image

這是一個車卡的基本介面,上面是人物的基本資訊,下面是人物基本屬性,點選隨機按鈕以後可以用擲點法初始化。

如果沒有 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:
複製程式碼

這種錯誤或者其他的編譯錯誤解決方法如下:

  1. 科學上網,起碼可以解決 70% 上的 gradle 編譯失敗。
  2. 升級 Android Studio 的版本,最早用 2.x 的時候簡直寸步難行,動不動 gradle 就一堆編譯錯誤。升級到 3.1 的時候這種情況就少了很多,我現在用的 3.3 Canary,gradle 比較激進用的 'com.android.tools.build:gradle:3.3.0-alpha03',糟心的情況少了很多。
  3. gradle.properties 檔案中新增如下程式碼:
android.databinding.enableV2=true
複製程式碼

stackoverflow 上的說法是現在 google 埋了兩個版本的 databinding,互相之間不相容,需要強制開啟高版本。

有的時候 BR.xx 報錯的話,仔細看的話會有 2 個 BR 檔案,我一般用第二個結尾是 adapter 的可以正常訪問。

  1. build -> make project
  2. 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 的例項。如果出現了,說明封裝的還不夠徹底。

整個程式碼就做了三件事:

  1. 對 activity 進行 db 化:ActivityMainBinding viewDataBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
  2. 初始化人物卡的基本資訊,扔進 xml 裡去 viewDataBinding.setVariable(BR.hero, hero);
  3. 初始化給人物屬性賦值的 listener,扔進 xml 裡去 viewDataBinding.setVariable(BR.listener, listener);
  4. 每次隨機獲得屬性後,扔到 xml 裡去 viewDataBinding.setVariable(BR.heroAttrs, HeroAttrs.getHeroAttrs(values) );

完。

引入 databinding 以後 MainActivity 最大的變化,就是從資料處理者變成了資料提供者。以前的 Activity 又要初始化 view 的例項又要給例項賦值,賦值的邏輯還得親自實現(沒有 presenter 的情況),又當爹又當媽;現在 Activity 只是服務員,資料什麼的經個手,扭頭就扔出去了。

image

扔給誰呢?

xml

XML 裡的 databinding

databinding 的 xml 是要經過特殊處理的,和傳統的 xml 有以下幾點不同:

  1. 原本介面層的根佈局外側需要再加套一個 layout 標籤。
  2. 如果要引入變數,需要增加 data 標籤,data 標籤和根佈局同級,而且只能有一個。
  3. data 標籤內可以有很多 variable 標籤,必須有 name 屬性(xml 佈局內唯一)和 type 屬性(xml 內可以不唯一,但是對泛型支援略差),用於確定唯一的變數。
  4. 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,同時在這個過程中儘可能的降低兩者的耦合

image

沒有 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

這麼寫的好處有二:

  1. 程式碼量少了,這是很明顯的。(說實話也沒少多少)。
  2. 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

相關文章