依賴注入實現元件化

非非白發表於2018-04-03

本文演示如何使用依賴注入的方式實現元件化,本文假設你對什麼是元件化已有一定認識,並且使用過 dagger2。

本文所說的元件均是指業務元件,包括有 UI 的業務元件和 UI 無關的業務元件,業務所依賴的基礎類庫均是指類庫,包括第三方的和公司內部的。

為了方便演示,本文所有的元件都放在同一個專案工程中,在同一個 git 倉庫中,實際的場景可能是專案獨有的元件放在同一個工程目錄中,以及在同一個 git 倉庫中,可以被多個專案共享的業務模組放在單獨的工程目錄以及 git 倉庫中。

本文配套的示範專案:android-modularization

元件劃分

依賴注入實現元件化

元件化的一個前提是劃分元件,如圖,在我們 demo 中,有一個 app 主工程,另有五個模組工程,它們之間的依賴關係如下:

依賴注入實現元件化

app 主工程負責組裝元件,它需要知道每一個元件

common-ui 是基礎的 UI 元件,它不依賴其它任何元件

business-a-ui 和 business-b-ui 是 UI 模組,它們都依賴於 common-ui,但彼此之間互不依賴

common-api 是抽象的 UI 無關的業務元件,它不依賴其它任何元件

business-c 是具體的 UI 無關的業務元件,它依賴 common-api

bisiness-a-ui 元件依賴 common-api,也就是說,UI 元件可以依賴純業務元件

所有的元件都不依賴主工程

依賴是指我們在模組工程的 build.gradle 檔案中有這樣的程式碼

implementation project(':common-ui')
複製程式碼

UI 元件

UI 元件是指有 UI 的業務元件

不管做什麼樣的 UI 介面,都離不開 Activity 或者 Fragment,想要從一個介面跳到另外一個介面,也只需要知道另外一個介面所屬的 Activity 或者 Fragment 即可。

通常,我們不會直接使用 Android Framework 為我們提供的 Activity、Fragment,而是搞一個基類,比如 BaseActivity,BaseFragment。

本文采用單 Activity 架構方式為大家演示如何使用依賴注入實現元件化。單 Activity 架構不是元件化必須的,這純粹是出於個人偏好。單 Activity 機構是指整個專案中基本只有一個 Activity,其餘介面全是 Fragment。

本文使用的單 Activity 架構類庫是 AndroidNavigation,它很好地解決了 fragment 巢狀,跳轉等 fragment 相關問題,同時解決了狀態列相關問題。

先來看看我們都有哪些 UI 元件

依賴注入實現元件化

我們在 common-ui 中定義了兩個基類: BaseActivity 和 BaseFragment

// common-ui/BaseActivity.java
// AwesomeActivity 是 AndroidNavigation 中的類
// HasSupportFragmentInjector 介面是 dagger2 的,用於依賴注入
public abstract class BaseActivity extends AwesomeActivity implements HasSupportFragmentInjector {
    @Inject
    DispatchingAndroidInjector<Fragment> supportFragmentInjector;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // activity 注入需要這一行
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
    }
    
    @Override
    public AndroidInjector<Fragment> supportFragmentInjector() {
        return supportFragmentInjector;
    }
}
複製程式碼
// common-ui/BaseFragment.java
// AwesomeFragment 是 AndroidNavigation 中的類
public abstract class BaseFragment extends AwesomeFragment {
    @Override
    public void onAttach(Context context) {
        // fragment 注入需要這一行
        AndroidSupportInjection.inject(this);
        super.onAttach(context);
    }
}
複製程式碼

同時還定義了一個介面,用於建立跨元件跳轉的 Fragment。

// common-ui/UIComponentFactory.java
public interface UIComponentFactory {
    BaseFragment createFragment(String moduleName);
}
複製程式碼

因為我們是單 Activity 架構,同時為了簡單演示,所以這裡只演示 Fragment 跳轉。實際工程中,即使是單 Activity 架構,除了 MainActivity,也會有少量的 Activity,比如 WebViewActivity,它可能執行在另外一個程式以避免某些問題,像這種情況這裡就不作演示了。

上面我們說到,business-a-ui 和 business-b-ui 彼此之間互不依賴,但如果 business-a-ui 中的頁面想要跳到 business-b-ui 中的頁面,怎麼辦呢? 讓我們從 UI 依賴的角度來看

依賴注入實現元件化

圖中,實心箭頭表示依賴,空心箭頭表示實現。

我們的 app 主工程,business-a-ui,business-b-ui 都依賴 common-ui

app 主工程實現了定義在 common-ui 中的 UIComponentFactory 這個介面。

具體流程如下:

明確 app 主工程依賴

// app/build.gradle
dependencies {
    implementation project(':common-ui')
    implementation project(':business-a-ui')
    implementation project(':business-b-ui')
}
複製程式碼

app 主工程定義一個類來註冊每個 UI 元件需要向外暴露的模組

// app/UIComponentRegistry.java
@Singleton
public class UIComponentRegistry {
    @Inject
    public UIComponentRegistry() {
        Log.w("Dagger", "UIComponentRegistry");
    }

    private HashMap<String, Class<? extends BaseFragment>> uiModules = new HashMap<>();

    public void registerModule(String moduleName, Class<? extends BaseFragment> clazz) {
        uiModules.put(moduleName, clazz);
    }

    public Class<? extends BaseFragment> moduleClassForName(String moduleName) {
        return uiModules.get(moduleName);
    }
}
複製程式碼

在應用啟動時,註冊模組

// app/MainApplication.java
import me.listenzz.businessa.AFragment;
import me.listenzz.businessb.EFragment;
import me.listenzz.businessb.FFragment;

public class MainApplication extends Application {
    @Inject
    UIComponentRegistry uiComponentRegistry;
    
    @Override
    public void onCreate() {
        super.onCreate();
        
        // business-a-ui 元件一共有 AFragment,BFragment,CFragment 三個 fragment
        // 在這裡,僅註冊一個 fragment 作為入口
        uiComponentRegistry.registerModule("A", AFragment.class);
        
        // business-b-ui 有兩個入口
        uiComponentRegistry.registerModule("E", EFragment.class);
        uiComponentRegistry.registerModule("F", FFragment.class);
    }
}
複製程式碼

app 主工程實現定義在 common-ui 中的 UIComponentFactory 這個介面

// app/UIComponentFactoryImpl.java
public class UIComponentFactoryImpl implements UIComponentFactory {
    private UIComponentRegistry uiComponentRegistry;

    @Inject
    public UIComponentFactoryImpl(UIComponentRegistry uiComponentRegistry) {
        this.uiComponentRegistry = uiComponentRegistry;
    }

    @Override
    public BaseFragment createFragment(String moduleName) {
        Class<? extends BaseFragment> fragmentClass = uiComponentRegistry.moduleClassForName(moduleName);
        if (fragmentClass == null) {
            // DEBUG 環境下崩潰,Release 環境下可返回 404 頁面
            throw new IllegalArgumentException("未能找到名為 " + moduleName + " 的模組,你是否忘了註冊?");
        }
        BaseFragment fragment = null;
        try {
            fragment = fragmentClass.newInstance();
        } catch (Exception e) {
            // ignore
        }
        return fragment;
    }
}
複製程式碼

在 dagger 模組中宣告實現和介面的關係

// app/AppModule.java
@Module
public abstract class AppModule {
    @Binds
    @Singleton
    abstract UIComponentFactory uiComponentFactory(UIComponentFactoryImpl uiComponentFactory);
}
複製程式碼

如果 AFragment(business-a-ui) 想要跳到 EFragment(business-b-ui)

// business-a-ui/AFragment.java
public class AFragment extends BaseFragment {
    @Inject
    UIComponentFactory uiComponentFactory;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View root = inflater.inflate(R.layout.a_fragment_a, container, false);
        root.findViewById(R.id.to_b_e).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 模組間跳轉,需要通過工廠方法來獲取目標頁面
                BaseFragment fragment = uiComponentFactory.createFragment("E");
                getNavigationFragment().pushFragment(fragment);
            }
        });
        return root;
    }
}

複製程式碼

在這個過程中,business-a-ui 不知道 business-b-ui,也根本無法知道 "E" 對應的是哪個類,是如何實現的,是原生介面?是 RN 介面?總之,除了知道對方遵從 BaseFragment 外,一無所知。

此外 business-a-ui 也對 UIComponentFactory 是如何實現的一無所知。app 主工程實現了 UIComponentFactory,但 business-a-ui 並不依賴主工程。

小結

UI 元件無需對外提供業務介面,它們只需要註冊入口模組即可

UI 元件的基類是特殊的介面,它有很多實現,我們通過工廠方法返回合適的實現

業務元件

業務元件是指 UI 無關的業務元件。

和 UI 元件不同的是,系統並沒有為我們的業務提供基類。因為我們的業務是唯一的獨特的,我們需要自定義介面

如果需要依賴業務元件,那麼依賴介面,而不是實現

當我們獲取一個 UI 模組時,我們得到的是基類的引用

當我們獲取一個業務模組時,我們得到的是一個介面的引用

實際面向的都是抽象

依賴注入實現元件化

業務元件不像 UI 元件那樣需要註冊,但它們需要定義介面

我們在 common-api 中,定義了一個業務介面,以及相關的一個 PO

// common-api/Account.java
// PO
public class Account {
    public Account(String username, String type) {
        this.username = username;
        this.type = type;
    }

    public String username;
    public String type;
}
複製程式碼
// common-api/AccountManager.java
public interface AccountManager {
    Account login(String username, String password);
    void invalidate();
}
複製程式碼

business-c 依賴 common-api 並實現了 AccountManager

// business-c/AccountManagerImpl.java
public class AccountManagerImpl implements AccountManager {
    @Inject
    public AccountManagerImpl() {
    }

    @Override
    public Account login(String username, String password) {
        return new Account(username, "password");
    }

    @Override
    public void invalidate() {
        //...
    }
}

複製程式碼

business-a-ui 依賴 common-api 並且想要使用 AccountManager, 可 AccountManager 只是個介面,怎麼辦呢?

還是需要主工程來組裝

business-c 先定義一個 dagger 模組,把實現和介面綁在一起

// business-c/CModule.java
@Module
public abstract class CModule {
    @Binds
    @Singleton
    abstract AccountManager provideAccountManager(AccountManagerImpl accountManager);
}
複製程式碼

app 主工程把該模組加入到依賴圖中

// app/AppComponent.java
@Singleton
@Component(modules = {
        AndroidSupportInjectionModule.class,
        AppModule.class,
        CModule.class, // 來自 business-c
})
public interface AppComponent extends AndroidInjector<MainApplication> {
    @Component.Builder
    abstract class Builder extends AndroidInjector.Builder<MainApplication> {
    }
}
複製程式碼

business-a-ui 在程式碼中宣告依賴

// business-a-ui/AFragment.java
public class AFragment extends BaseFragment {
    @Inject
    AccountManager accountManager;
    
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        setTitle("A 模組 A 頁面");
        Account account =  accountManager.login("listenzz", "123456");
        textView.setText("使用者名稱:" +account.username + "\n" + "登入方式:" + account.type);
    }
}
複製程式碼

就這樣,business-a-ui 用上了 AccountManager,但它對 business-c 一無所知。我們隨時可以用 business-d 來取代 business-c,而對 business-a-ui 毫無影響。

業務元件如何調起 UI 元件

業務元件是指 UI 無關的業務元件

UI元件是指有 UI 的業務元件

有些時候,有些業務元件是沒有 UI 的,它們可能執行在後臺,當某些事件發生時,可能需要調起 UI 介面以通知使用者。有兩種方式來處理這種業務元件需要調起 UI 元件的情況。一種是採用訂閱/釋出機制,具體的實現有 EventBus,LocalBroadcast 等等,另一種是使用代理(delegate)。

Delegate pattern, 就是遇著這事,我不知道怎麼辦,於是我找了個代理,將此事委派與他。

來看 PPT

依賴注入實現元件化

下面我們就來演示如何使用代理來實現業務元件調起 UI 元件

假設 business-c 這個 UI 無關的元件在登入已經過期無效的情況下,需要通知 UI 層,如何是好呢?

首先在 common-api 中定義一個介面

// common-api/AccountManagerDelegate.java
public interface AccountManagerDelegate {
    void onInvalidation();
}
複製程式碼

我們來看 common-api 和 business-c 中都有哪些類

依賴注入實現元件化

當 AccountManagerDelegate 和 AccountManager 放在一起時,已經表明了設計意圖,有經驗的工程師會知道在實現 AccountManager 的過程中,需要依賴 AccountManagerDelegate。

business-c 在實現 AccountManager 時宣告依賴 AccountManagerDelegate,並呼叫其中的方法

// business-c/AccountManagerImpl.java
public class AccountManagerImpl implements AccountManager {
    private AccountManagerDelegate delegate;

    @Inject
    public AccountManagerImpl(AccountManagerDelegate delegate) {
        this.delegate = delegate;
    }

    @Override
    public void invalidate() {
        this.delegate.onInvalidation();
    }
}
複製程式碼

app 主工程實現這一代理介面,跳到登入介面。

// app/AccountManagerDelegateImpl.java
@Singleton
public class AccountManagerDelegateImpl implements AccountManagerDelegate {
    private MainApplication application;

    @Inject
    public AccountManagerDelegateImpl(MainApplication application) {
        this.application = application;
    }

    @Override
    public void onInvalidation() {
        Log.w("Dagger", "onInvalidation");
        if (application.mainActivity != null) {
            NavigationFragment navigationFragment = new NavigationFragment();
            navigationFragment.setRootFragment(new LoginFragment());
            application.mainActivity.presentFragment(navigationFragment);
        } else {
            // do something
        }
    }
}
複製程式碼

實際開發中,登入介面不是由 app 主工程親自實現的,而是由其它 UI 元件實現,主工程在實現這一代理的過程中依賴其它 UI 元件即可。

app 主工程將這一實現和介面繫結

// app/AppModule.java
@Module
public abstract class AppModule {
    @Binds
    @Singleton
    abstract AccountManagerDelegate accountManagerDelegate(AccountManagerDelegateImpl delegate);
}
複製程式碼

就這樣,當 AccountManager 的 invalidate 方法被呼叫時,就會喚起一個 UI 介面,但是在這個過程中業務模組卻沒有依賴 UI 模組。

Dagger 幫助

如果某個元件宣告瞭依賴而又沒有組裝對應的實現,那麼是編譯不過去的,所以不用擔心只有介面沒有實現的情況。

如果在使用 dagger 的過程中編譯不過去,可以把 MainApplication 中建立 AppComponent 的程式碼先註釋掉,然後再次編譯,你會得到正確的提示

public class MainApplication extends Application implements HasActivityInjector, HasSupportFragmentInjector {
    @Override
    public void onCreate() {
        super.onCreate();
        // AppComponent.Builder builder = DaggerAppComponent.builder();
        // builder.seedInstance(this);
        // builder.build().inject(this);
    }
}
複製程式碼

總結

主工程負責組裝其它元件工程

UI 元件互不依賴,想要跳轉,通過工廠方法和模組名建立目標頁面例項

業務元件分離介面和實現

UI 元件可以依賴業務元件,但反過來不行

UI 元件不能直接依賴業務元件的實現,而應依賴其介面

UI 元件內部可以有自己獨立的業務類和 PO,但不對外公開

業務元件如果需要調起 UI,可以通過事件或代理的方式

元件內部可以有自己的分層結構,比如某 UI 元件使用 MVVM 模式

示例專案原始碼

相關文章