1. 程式人生 > >Android Architecture 1 -- Guide to App Architecture(App架構入門,示例程式碼演示)

Android Architecture 1 -- Guide to App Architecture(App架構入門,示例程式碼演示)

面臨的問題

  1. Android的各大元件可以隨意跳轉,可不遵守順序。比如QQ分享調相簿APP,相簿APP跳檔案管理APP。
  2. 還可能被系統殺死。所以最好
    you should not store any app data or state in your app components and your app components should not depend on each other.

Common architectural principles

If you can’t use app components to store app data and state, how should apps be structured?

  1. separation of concerns

Activity or a Fragment: Any code that does not handle a UI or operating system interaction should not be in these classes. minimize your dependency on them to provide a solid user experience.

  1. drive your UI from a model, preferably a persistent model

Keeping UI code simple and free of app logic makes it easier to manage.
Basing your app on model classes with well-defined responsibility of managing the data will make them testable and your app consistent.

推薦App 框架

假想你要構建一個UI展示使用者的簡介。從服務端用REST API拉取資料。

ViewModel

A ViewModel provides the data for a specific UI component, such as a fragment or activity, and handles the communication with the business part of data handling, such as calling other components to load the data or forwarding user modifications. The ViewModel does not know about the View and is not affected by configuration changes such as recreating an activity due to rotation.

ViewModel為特定的UI元件提供資料(如Fragment、Activity),並與業務模組中操作資料的邏輯 溝通,例如通知其他元件去 load data,更改資料。ViewModel不關心具體的View,且不受配置的更改(如旋轉、重新建立Activity)。

建立3個檔案
  • user_profile.xml: The UI definition for the screen.
  • UserProfileViewModel.java: The class that prepares the data for the UI.
  • UserProfileFragment.java: The UI controller that displays the data in the
    ViewModel and reacts to user interactions.
public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}
LiveData

After all, when the ViewModel’s user field is set, we need a way to inform the UI. This is where the LiveData class comes in.

LiveData is an observable data holder. It lets the components in your app observe LiveData objects for changes without creating explicit and rigid dependency paths between them. LiveData also respects the lifecycle state of your app components (activities, fragments, services) and does the right thing to prevent object leaking so that your app does not consume more memory.
Live data是持有資料 的被觀察者,使觀察LiveData的物件不需要 與其 建立顯式的、僵硬的(模板化的)依賴路徑。LiveData也遵守app元件的生命週期,防止記憶體洩露。

重要
If you are already using a library like RxJava or Agera, you can continue using them instead of LiveData. 但是要注意生命週期的控制。

將User替換成LiveData<User>, fragment就可以在User更新時收到通知。
LiveData最大的好處是 生命週期感知能力(Lifecycle aware),和在物件不需要時清理應用。

public class UserProfileViewModel extends ViewModel {
    ...
    //private User user;
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}

再修改UserProfileFragment去觀察data和更新UI

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    viewModel.getUser().observe(this, user -> {
      // update UI
    });
}

Live Data的callback只在Fragment處於active狀態(onStart後 onStop前)呼叫,LiveData將自動移除觀察者(Observer)在onDestroy後。

不需要處理Configure change(rotation),ViewModel將自動回覆當configure change,當Fragment 又回來時,會接受到相同的ViewModel例項,並被立即回撥callback傳遞現在的data。 這就是為什麼ViewModel不應該直接飲用Views,ViewModel比View的存活週期更長。ViewModel生命週期

Fetching Data 拉取資料

現在ViewModel連線了Fragment,但是ViewModel如何去拉取使用者資料。以下我們使用Retrofit去拉取服務端 REST API。

public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}

最直接的想法是 讓ViewModel直接呼叫Webservice拉取資料、賦值給User物件。可以,但是難以後期維護,違背了separation of concerns。另外ViewModel的scope(作用域)和Activity 或 Fragment綁定了。所以宣告週期走完時將lost data,糟糕的體驗。

替代方案, 我們將ViewModel這部分的工作代理給Repository module(倉庫模組)

Repository模組負責data處理,提供簡介的API。他們知道從哪裡獲取資料,更新資料呼叫什麼API。可以看做不同資料來源的中介者(持久化模型,web伺服器,快取等)

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        return data;
    }
}

即使Repository看起來沒必要,它服務於一個重要的目的。抽象了資料來源。現在ViewModel不知道資料是從WebService獲取的,意味著我們可以在必要時 更換別的實現。

Managing dependencies between components:

UserReposity使用Webservice,將會引入Webservice的依賴,使其變得複雜、重複的程式碼(別的地方也可能需要Webservice),而且Webservice是 重資源(resource heavy)

有2中模式解決:
- Dependency Injection 依賴注入:允許定義依賴,而不constructing them構建他們。在runtime(執行時),其他的類負責提供這些依賴。推薦使用Dagger2 根據dependency tree自動構建物件,提供編譯期程式碼生成。

  • Service Locator: Service Locator provides a registry where classes can obtain their dependencies instead of constructing them. It is relatively easier to implement than Dependency Injection (DI), so if you are not familiar with DI, use a Service Locator instead.

這些模式可以大幅減少程式碼,提供清晰的依賴管理(不增加複雜度、重複程式碼),最重要的是可以切換實現(如測試程式碼)

以下選用Dagger 2

Connecting ViewModel and the repository

Now we modify our UserProfileViewModel to use the repository.

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }

    public LiveData<User> getUser() {
        return this.user;
    }
}

Caching data

給UserRepository新增一個新的資料來源,將快取User到記憶體上

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}

Persisting data (持久化資料)

使用Room persistence library

Room is an object mapping library that provides local data persistence with minimal boilerplate code. At compile time, it validates each query against the schema, so that broken SQL queries result in compile time errors instead of runtime failures. Room abstracts away some of the underlying implementation details of working with raw SQL tables and queries. It also allows observing changes to the database data (including collections and join queries), exposing such changes via LiveData objects. In addition, it explicitly defines thread constraints that address common issues such as accessing storage on the main thread.

  1. object mapping library(可以用物件直接對映到資料庫)
  2. 驗證sql語句,如果錯誤編譯期就報錯了
  3. 抽象底層SQL邏輯
  4. 可以observing 觀察資料庫data變更
  5. 顯式的定義Thread約束,解決UI 執行緒訪問問題
標記table

使用@Entity標記table

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}
建立Database

繼承RoomDatabase來建立db:

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}

注意MyDatabase是abstract,Room自動implementation它。

insert data

建立DAO(data access object,讀取資料的物件)

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}

關聯DB

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

注意:load方法return LiveData<User>,Room知道db的更改,並會通知所有active的observers

Now 更改UserRepository,匯入Room 資料來源

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread
            // check if user was fetched recently
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database
                userDao.save(response.body());
            }
        });
    }
}

Testing

先不翻譯了
- User Interface & Interactions: This will be the only time you need an Android UI Instrumentation test. The best way to test UI code is to create an Espresso test. You can create the fragment and provide it a mock ViewModel. Since the fragment only talks to the ViewModel, mocking it will be sufficient to fully test this UI.

  • ViewModel: The ViewModel can be tested using a JUnit test. You need to mock only the UserRepository to test it.

  • UserRepository: You can test the UserRepository with a JUnit test as well. You need to mock the Webservice and the DAO. You can test that it makes the right web service calls, saves the result into the database and does not make any unnecessary requests if the data is cached and up to date. Since both Webservice and UserDao are interfaces, you can mock them or create fake implementations for more complex test cases..

  • UserDao: The recommended approach for testing DAO classes is using instrumentation tests. Since these instrumentation tests do not require any UI, they will still run fast. For each test, you can create an in-memory database to ensure that the test does not have any side effects (like changing the database files on the disk).

  • Room also allows specifying the database implementation so you can test it by providing it the JUnit implementation of the SupportSQLiteOpenHelper. This approach is usually not recommended because the SQLite version running on the device may differ from the SQLite version on your host machine.

  • Webservice: It is important to make tests independent from the outside world so even your Webservice tests should avoid making network calls to your backend. There are plenty of libraries that help with this. For instance, MockWebServer is a great library that can help you create a fake local server for your tests.

  • Testing Artifacts Architecture Components provides a maven artifact to control its background threads. Inside the android.arch.core:core-testing artifact, there are 2 JUnit rules:

    • InstantTaskExecutorRule: This rule can be used to force Architecture Components to instantly execute any background operation on the calling thread.
    • CountingTaskExecutorRule: This rule can be used in instrumentation tests to wait for background operations of the Architecture Components or connect it to Espresso as an idling resource.

The final architecture