以下內容為原創,歡迎轉載,轉載請註明 來自天天博客: 使用Dagger 2來構建UserScope 原文: 在Dagger 2中自定義scopes可以在不尋常存活時間(與Application和界面生命周期不同的)的依賴上給我帶來更好的控制。但是在Android app中正確地實現它需要記住幾個事情 ...
以下內容為原創,歡迎轉載,轉載請註明
來自天天博客:http://www.cnblogs.com/tiantianbyconan/p/6237731.html
使用Dagger 2來構建UserScope
原文:http://frogermcs.github.io/building-userscope-with-dagger2/
在Dagger 2中自定義scopes可以在不尋常存活時間(與Application和界面生命周期不同的)的依賴上給我帶來更好的控制。但是在Android app中正確地實現它需要記住幾個事情:scope不能存活比application進程更長的周期,進程可以被系統殺死並且在更多的對象實例的用戶流中恢復過來。今天我們將介紹所有這些,並嘗試實現可用於生產的UserScope。
在我的其中一篇博客中寫了關於自定義scopes和Subcomponents。作為一個例子,我使用了UserScope
,只要用戶是登錄狀態就應該存活。一個scopes生命周期的例子:
雖然它看起來非常簡單,它的實現在那篇文章中已經展示,但是它的代碼是脫離與生產環境的。這就是為什麼我想要又一次深入這個話題 - 但是會有更多實現的上下文細節。
Example app
我們將構建3個界面的應用,它可以從Github API獲取用戶詳情(讓我們假設這在生產環境app中作為一個認證的事件)。
App將會有3個scopes:
- (Application scope, **@Singleton**) - 只要application存活依賴就存活。
- **@UserScope** - 只要用戶會話是激活狀態依賴就存活(在單個應用程式中啟動)。重要的是:這個scope存活時間不會超過application本身。每一個新的app實例都會創建一個新的@UserScope(甚至app不同的啟動間用戶會話沒有關閉)。
- **@ActivityScope** - 只要Activity界面存活依賴就存活。
它怎麼工作的呢?
當app從Github API得到特定用戶名的數據,新的界面會被打開(UserDetails)。在內部,LoginActivityPresenter
訪問UserManager
來開啟一個會話(從Github API獲取數據)。當操作成功,用戶保存到UserDataStore
,並且UserManager
創建UserComponent
。
//UserManager
public Observable<User> startSessionForUser(String username) {
return githubApiService.getUser(username)
.map(User.UserResponseToUser())
.doOnNext(new Action1<User>() {
@Override
public void call(User user) {
userDataStore.createUser(user);
startUserSession();
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
private boolean startUserSession() {
User user = userDataStore.getUser();
if (user != null) {
Timber.i("Session started, user: %s", user);
userComponent = userComponentBuilder.sessionModule(new UserModule(user)).build();
return true;
}
return false;
}
UserManager是一個單例,所以它的存活時間與Applicaiton一致,而且它對生命周期與用戶會話一樣的UserComponet
負責。當用戶決定去關閉會話,component會被移除,這樣所有@UserScope
註解了的對象應該準備被GC回收。
//UserManager
public void closeUserSession() {
Timber.i("Close session for user: %s", userDataStore.getUser());
userComponent.logoutManager().startLogoutProcess();
userDataStore.clearUser();
userComponent = null;
}
Components的層次結構
在我們的app中所有的subcomponents使用了一個AppComponent
作為一個根component。顯示用戶相關內容的Subcomponents使用保持了帶@Userscope
註解的對象(一個用戶會話一個實例)的UserComponent
。
UserDetailsActivityComponent
組件層次結構示例如下所示:
// AppComponent.java
@Singleton
@Component(modules = {
AppModule.class,
GithubApiModule.class
})
public interface AppComponent {
UserComponent.Builder userComponentBuilder();
}
// UserComponent.java
@UserScope
@Subcomponent(modules = UserModule.class)
public interface UserComponent {
@Subcomponent.Builder
interface Builder {
UserComponent.Builder sessionModule(UserModule userModule);
UserComponent build();
}
UserDetailsActivityComponent plus(UserDetailsActivityComponent.UserDetailsActivityModule module);
}
// UserDetailsActivityComponent.java
@ActivityScope
@Subcomponent(modules = UserDetailsActivityComponent.UserDetailsActivityModule.class)
public interface UserDetailsActivityComponent {
UserDetailsActivity inject(UserDetailsActivity activity);
}
對於UserComponent
,我們使用Subcomponent builder模式來讓我們的代碼更加整潔,有可能註入這個Builder到UserModule
(UserComponent.Builder
用於創建組件UserModule.startUserSession
方法如上所示)。
在app的多次啟動中恢復UserScope
就像我之前提到的UserScope的存活時間不能超過application進程。所以如果我們假設用戶會話可以在app的多次啟動之間保存,我們需要為我們的UserScope
去處理狀態恢復操作。有兩種場景我們需要記住:
用戶從頭開始啟動程式
這是最常見的情況,應該很簡單地去處理。用戶從頭開始啟動app(比如,通過點擊app icon)。Application
對象被創建,然後第一個Activity
(LAUNCHER)被啟動。我們需要提供簡單的邏輯檢查我們是否有保存的用戶在我們的數據存儲中。如果是則將用戶傳送到UserComponent自動創建的正確的界面(在我們案例中是UserDetailsActivity
)。
用戶進程被系統kill
但是還有一種情況我們經常會忘記。Application進程可以被系統殺死(比如,因為記憶體不足)。這意味著所有application數據被銷毀(application,activities,static fields)。不幸的是這不能被很好的處理 - 我們在Applicaiton生命周期中沒有任何回調。令它更複雜的是,android保存了activity棧。也就是說,當用戶決定啟動之前被殺死於流中間的應用程式時,系統將試圖帶用戶回到此界面。
對於我們而言我們需要準備在任何被使用的屏幕中去恢復UserComponent
。
讓我們考慮這個例子:
- 用戶在
UserDetailsActivity
界面最小化app。 - 整個app被系統殺死(稍後我會展示如何模擬它)
- 用戶打開任務切換欄,點擊我們的app界面。
- 系統創建了一個新的
Application
實例。也就是說有一個新的AppComponent
被創建。然後系統並不會打開LoginActivity
(我們的啟動activity),而是立即打開UserDetailsActivity
。
對於我們而言,UserComponent
被恢復回來(新的實例被創建)。然後這是我們的責任。例子的解決方案看起來如下:
public abstract class BaseUserActivity extends BaseActivity {
@Inject
UserManager userManager;
@Override
protected void setupActivityComponent(AppComponent appComponent) {
appComponent.inject(this);
setupUserComponent();
}
private void setupUserComponent() {
isUserSessionStarted = userManager.isUserSessionStartedOrStartSessionIfPossible();
onUserComponentSetup(userManager.getUserComponent());
//This screen cannot work when user session is not started.
if (!isUserSessionStarted) {
finish();
}
}
protected abstract void onUserComponentSetup(UserComponent userComponent);
}
每一個使用了UserComponent的Activity都繼承了我們的BaseUserActivity
類(setupActivityComponent()
方法在BaseActivity
的onCreate()
方法中調用)。
UserManager
從Application創建的AppComponent
中被註入。會話通過這種方式開啟:
public boolean isUserSessionStartedOrStartSessionIfPossible() {
return userComponent != null || startUserSession();
}
private boolean startUserSession() {
//This method was described earlier
}
如果用戶不再存在怎麼辦?
這裡有另外一種方式來處理 - 如果用戶登出(比如,通過SyncAdapter
),然後UserComponent
不能被創建怎麼辦?這就是為什麼在我們的BaseUserActivity
中有這幾行:
private void setupUserComponent() {
//...
//This screen cannot work when user session is not started.
if (!isUserSessionStarted) {
finish();
}
}
但是這裡有一個情況時當UserComponent不能被創建時我們必須要記住依賴註入將不會發生。這就是為什麼我每次需要在onCreate()
方法中檢查來防止來自依賴註入的NullPointerExceptions。
//UserDetailsActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_details);
tvUser = (TextView) findViewById(R.id.tvUser);
tvUserUrl = (TextView) findViewById(R.id.tvUserUrl);
//This check has to be called. Otherwise, when user is not logged in, presenter won't be injected and this line will cause NPE
if (isUserSessionStarted()) {
presenter.onCreate();
}
}
怎麼去模擬應用進程被系統殺死
- 打開你想測試的用戶界面
- 通過系統Home鍵最小化app。
- 在Android Studio,Android Monitor 選中你的應用,然後點擊Terminate
- 現在在你的設備上打開任務切換欄,找到你的app(你應該仍然能在最後一個可見的界面上預覽到)。
- 你的app被啟動,新的Application實例被創建,並且Activity被恢復。
源碼
例子中展示怎麼樣去創建和使用UserComponent
的可用的源碼已經在Github上:Dagger 2 recipes - UserScope。
感謝閱讀!
作者
Head of Mobile Development @ Azimo
[Android]使用Dagger 2依賴註入 - DI介紹(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5092083.html
[Android]使用Dagger 2依賴註入 - API(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5092525.html
[Android]使用Dagger 2依賴註入 - 自定義Scope(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5095426.html
[Android]使用Dagger 2依賴註入 - 圖表創建的性能(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5098943.html
[Android]Dagger2Metrics - 測量DI圖表初始化的性能(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/5193437.html
[Android]使用Dagger 2進行依賴註入 - Producers(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/6234811.html
[Android]在Dagger 2中使用RxJava來進行非同步註入(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/6236646.html
[Android]使用Dagger 2來構建UserScope(翻譯):
http://www.cnblogs.com/tiantianbyconan/p/6237731.html