谷歌官方 Android MVP 模式程式碼解讀
關於Android程式的構架, 當前(2016.10)最流行的模式即為MVP模式, Google官方提供了Sample程式碼來展示這種模式的用法.
Repo地址: android-architecture.
本文為閱讀官方sample程式碼的閱讀筆記和分析.
官方Android Architecture Blueprints [beta]:
Android在如何組織和構架一個app方面提供了很大的靈活性, 但是同時這種自由也可能會導致app在測試, 維護, 擴充套件方面變得困難.
Android Architecture Blueprints展示了可能的解決方案. 在這個專案裡, 我們用各種不同的構架概念和工具實現了同一個應用(To Do App). 主要的關注點在於程式碼結構, 構架, 測試和維護性.
但是請記住, 用這些模式構架app的方式有很多種, 要根據你的需要, 不要把這些當做絕對的典範.
MVP模式概念
之前有一個MVC模式: Model-View-Controller.
MVC模式 有兩個主要的缺點: 首先, View持有Controller和Model的引用; 第二, 它沒有把對UI邏輯的操作限制在單一的類裡, 這個職能被Controller和View或者Model共享.
所以後來提出了MVP模式來克服這些缺點.
MVP(Model-View-Presenter)模式:
- Model: 資料層. 負責與網路層和資料庫層的邏輯互動.
- View: UI層. 顯示資料, 並向Presenter報告使用者行為.
- Presenter: 從Model拿資料, 應用到UI層, 管理UI的狀態, 決定要顯示什麼, 響應使用者的行為.
MVP模式的最主要優勢就是耦合降低, Presenter變為純Java的程式碼邏輯, 不再與Android Framework中的類如Activity, Fragment等關聯, 便於寫單元測試.
todo-mvp 基本的Model-View-Presenter架構
app中有四個功能:
- Tasks
- TaskDetail
- AddEditTask
- Statistics
每個功能都有:
- 一個定義View和Presenter介面的
Contract
介面; - 一個Activity用來管理fragment和presenter的建立;
- 一個實現了View介面的Fragment;
- 一個實現了Presenter介面的presenter.
基類
Presenter基類:
public interface BasePresenter { void start(); }
例子中這個start()
方法都在Fragment的onResume()
中呼叫.
View基類:
public interface BaseView<T> { void setPresenter(T presenter); }
View實現
- Fragment作為每一個View介面的實現, 主要負責資料顯示和在使用者互動時呼叫Presenter, 但是例子程式碼中也是有一些直接操作的部分, 比如點選開啟另一個Activity, 點選彈出選單(選單項的點選仍然是呼叫presenter的方法).
- View介面中定義的方法多為
showXXX()
方法. - Fragment作為View實現, 介面中定義了方法:
@Override public boolean isActive() { return isAdded(); }
在Presenter中資料回撥的方法中, 先檢查View.isActive()是否為true, 來保證對Fragment的操作安全.
Presenter實現
- Presenter的
start()
方法在onResume()
的時候呼叫, 這時候取初始資料; 其他方法均對應於使用者在UI上的互動操作. - New Presenter的操作是在每一個Activity的
onCreate()
裡做的: 先新增了Fragment(View), 然後把它作為引數傳給了Presenter. 這裡並沒有存Presenter的引用. - Presenter的建構函式有兩個引數, 一個是Model(Model類一般叫XXXRepository), 一個是View. 構造中先用guava的
checkNotNull()
檢查兩個引數是否為null, 然後賦值到欄位; 之後再呼叫View的setPresenter()
方法把Presenter傳回View中引用.
Model實現細節
- Model只有一個類, 即
TasksRepository
. 它還是一個單例. 因為在這個應用的例子中, 我們操作的資料就這一份.
它由手動實現的注入類Injection
類提供:
public class Injection { public static TasksRepository provideTasksRepository(@NonNull Context context) { checkNotNull(context); return TasksRepository.getInstance(FakeTasksRemoteDataSource.getInstance(), TasksLocalDataSource.getInstance(context)); } }
構造如下:
private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource, @NonNull TasksDataSource tasksLocalDataSource) { mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource); mTasksLocalDataSource = checkNotNull(tasksLocalDataSource); }
- 資料分為local和remote兩大部分. local部分負責資料庫的操作, remote部分負責網路. Model類中還有一個記憶體快取.
TasksDataSource
是一個介面. 介面中定義了Presenter查詢資料的回撥介面, 還有一些增刪改查的方法.
單元測試
MVP模式的主要優勢就是便於為業務邏輯加上單元測試.
本例子中的單元測試是給TasksRepository
和四個feature的Presenter加的.
Presenter的單元測試, Mock了View和Model, 測試呼叫邏輯, 如:
public class AddEditTaskPresenterTest { @Mock private TasksRepository mTasksRepository; @Mock private AddEditTaskContract.View mAddEditTaskView; private AddEditTaskPresenter mAddEditTaskPresenter; @Before public void setupMocksAndView() { MockitoAnnotations.initMocks(this); when(mAddEditTaskView.isActive()).thenReturn(true); } @Test public void saveNewTaskToRepository_showsSuccessMessageUi() { mAddEditTaskPresenter = new AddEditTaskPresenter("1", mTasksRepository, mAddEditTaskView); mAddEditTaskPresenter.saveTask("New Task Title", "Some Task Description"); verify(mTasksRepository).saveTask(any(Task.class)); // saved to the model verify(mAddEditTaskView).showTasksList(); // shown in the UI } ... }
todo-mvp-loaders 用Loader取資料的MVP
基於上一個例子todo-mvp, 只不過這裡改為用Loader來從Repository得到資料.
使用Loader的優勢:
- 去掉了回撥, 自動實現資料的非同步載入;
- 當內容改變時回撥出新資料;
- 當應用因為configuration變化而重建loader時, 自動重連到上一個loader.
Diff with todo-mvp
既然是基於todo-mvp, 那麼之前說過的那些就不再重複, 我們來看一下都有什麼改動:git difftool -d todo-mvp
新增了兩個類:TaskLoader
和TasksLoader
.
在Activity中new Loader類, 然後傳入Presenter的構造方法.
Contract
中View介面刪掉了isActive()
方法, Presenter刪掉了populateTask()
方法.
資料獲取
新增的兩個新類是TaskLoader
和TasksLoader
, 都繼承於AsyncTaskLoader
, 只不過資料的型別一個是單數, 一個是複數.
AsyncTaskLoader
是基於ModernAsyncTask
, 類似於AsyncTask
,把load資料的操作放在loadInBackground()
裡即可, deliverResult()
方法會將結果返回到主執行緒, 我們在listener的onLoadFinished()
裡面就可以接到返回的資料了, (在這個例子中是幾個Presenter實現了這個介面).
TasksDataSource
介面的這兩個方法:
List<Task> getTasks(); Task getTask(@NonNull String taskId);
都變成了同步方法, 因為它們是在loadInBackground()
方法裡被呼叫.
Presenter中儲存了Loader
和LoaderManager
, 在start()
方法裡initLoader
, 然後onCreateLoader
返回構造傳入的那個loader.
onLoadFinished()
裡面呼叫View的方法. 此時Presenter實現LoaderManager.LoaderCallbacks
.
資料改變監聽
TasksRepository
類中定義了observer的介面, 儲存了一個listener的list:
private List<TasksRepositoryObserver> mObservers = new ArrayList<TasksRepositoryObserver>(); public interface TasksRepositoryObserver { void onTasksChanged(); }
每次有資料改動需要重新整理UI時就呼叫:
private void notifyContentObserver() { for (TasksRepositoryObserver observer : mObservers) { observer.onTasksChanged(); } }
在兩個Loader裡註冊和登出自己為TasksRepository
的listener: 在onStartLoading()
裡add, onReset()
裡面remove方法.
這樣每次TasksRepository
有資料變化, 作為listener的兩個Loader都會收到通知, 然後force load:
@Override public void onTasksChanged() { if (isStarted()) { forceLoad(); } }
這樣onLoadFinished()
方法就會被呼叫.
todo-databinding
基於todo-mvp, 使用Data Binding library來顯示資料, 把UI和動作繫結起來.
說到ViewModel, 還有一種模式叫MVVM(Model-View-ViewModel)模式.
這個例子並沒有嚴格地遵循Model-View-ViewModel
模式或者Model-View-Presenter
模式, 因為它既用了ViewModel又用了Presenter.
Data Binding Library讓UI元素和資料模型繫結:
- layout檔案用來繫結資料和UI元素;
- 事件和action handler繫結;
- 資料變為可觀察的, 需要的時候可以自動更新.
Diff with todo-mvp
新增了幾個類:
StatisticsViewModel
;SwipeRefreshLayoutDataBinding
;TasksItemActionHandler
;TasksViewModel
;
從幾個View的介面可以看出方法數減少了, 原來需要多個showXXX()方法, 現在只需要一兩個方法就可以了.
資料繫結
以TasksDetailFragment
為例:
以前在todo-mvp裡需要這樣:
public void onCreateView(...) { ... mDetailDescription = (TextView) root.findViewById(R.id.task_detail_description); } @Override public void showDescription(String description) { mDetailDescription.setVisibility(View.VISIBLE); mDetailDescription.setText(description); }
現在只需要這樣:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.taskdetail_frag, container, false); mViewDataBinding = TaskdetailFragBinding.bind(view); ... } @Override public void showTask(Task task) { mViewDataBinding.setTask(task); }
因為所有資料繫結的操作都寫在了xml裡:
<TextView android:id="@+id/task_detail_description" ... android:text="@{task.description}" />
事件繫結
資料繫結省去了findViewById()
和setText()
, 事件繫結則是省去了setOnClickListener()
.
比如taskdetail_frag.xml
中的
<CheckBox android:id="@+id/task_detail_complete" ... android:checked="@{task.completed}" android:onCheckedChanged="@{(cb, isChecked) -> presenter.completeChanged(task, isChecked)}" />
其中Presenter是這時候傳入的:
@Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mViewDataBinding.setPresenter(mPresenter); }
資料監聽
在顯示List資料的介面TasksFragment
, 僅需要知道資料是否為空, 所以它使用了TasksViewModel
來給layout提供資訊, 當尺寸設定的時候, 只有一些相關的屬性被通知, 和這些屬性繫結的UI元素被更新.
public void setTaskListSize(int taskListSize) { mTaskListSize = taskListSize; notifyPropertyChanged(BR.noTaskIconRes); notifyPropertyChanged(BR.noTasksLabel); notifyPropertyChanged(BR.currentFilteringLabel); notifyPropertyChanged(BR.notEmpty); notifyPropertyChanged(BR.tasksAddViewVisible); }
其他實現細節
- Adapter中的Data Binding, 見
TasksFragment
中的TasksAdapter
.@Override public View getView(int i, View view, ViewGroup viewGroup) { Task task = getItem(i); TaskItemBinding binding; if (view == null) { // Inflate LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext()); // Create the binding binding = TaskItemBinding.inflate(inflater, viewGroup, false); } else { binding = DataBindingUtil.getBinding(view); } // We might be recycling the binding for another task, so update it. // Create the action handler for the view TasksItemActionHandler itemActionHandler = new TasksItemActionHandler(mUserActionsListener); binding.setActionHandler(itemActionHandler); binding.setTask(task); binding.executePendingBindings(); return binding.getRoot(); }
- Presenter可能會被包在ActionHandler中, 比如
TasksItemActionHandler
. - ViewModel也可以作為View介面的實現, 比如
StatisticsViewModel
. SwipeRefreshLayoutDataBinding
類定義的onRefresh()
動作繫結.
todo-mvp-clean
這個例子是基於Clean Architecture的原則: The Clean Architecture.
關於Clean Architecture, 還可以看這個Sample App: Android-CleanArchitecture.
這個例子在todo-mvp的基礎上, 加了一層domain層, 把應用分為了三層:
Domain: 盛放了業務邏輯, domain層包含use cases或者interactors, 被應用的presenters使用. 這些use cases代表了所有從presentation層可能進行的行為.
關鍵概念
和基本的mvp sample最大的不同就是domain層和use cases. 從presenters中抽離出來的domain層有助於避免presenter中的程式碼重複.
Use cases定義了app需要的操作, 這樣增加了程式碼的可讀性, 因為類名反映了目的.
Use cases對於操作的複用來說也很好. 比如CompleteTask
在兩個Presenter中都用到了.
Use cases的執行是在後臺執行緒, 使用command pattern. 這樣domain層對於Android SDK和其他第三方庫來說都是完全解耦的.
Diff with todo-mvp
每一個feature的包下都新增了domain層, 裡面包含了子目錄model和usecase等.
UseCase
是一個抽象類, 定義了domain層的基礎介面點.
UseCaseHandler
用於執行use cases, 是一個單例, 實現了command pattern.
UseCaseThreadPoolScheduler
實現了UseCaseScheduler
介面, 定義了use cases執行的執行緒池, 在後臺執行緒非同步執行, 最後把結果返回給主執行緒.
UseCaseScheduler
通過構造傳給UseCaseHandler
.
測試中用了UseCaseScheduler
的另一個實現TestUseCaseScheduler
, 所有的執行變為同步的.
Injection
類中提供了多個Use cases的依賴注入, 還有UseCaseHandler
用來執行use cases.
Presenter的實現中, 多個use cases和UsseCaseHandler
都由構造傳入, 執行動作, 比如更新一個task:
private void updateTask(String title, String description) { if (mTaskId == null) { throw new RuntimeException("updateTask() was called but task is new."); } Task newTask = new Task(title, description, mTaskId); mUseCaseHandler.execute(mSaveTask, new SaveTask.RequestValues(newTask), new UseCase.UseCaseCallback<SaveTask.ResponseValue>() { @Override public void onSuccess(SaveTask.ResponseValue response) { // After an edit, go back to the list. mAddTaskView.showTasksList(); } @Override public void onError() { showSaveError(); } }); }
todo-mvp-dagger
關鍵概念:
dagger2 是一個靜態的編譯期依賴注入框架.
這個例子中改用dagger2實現依賴注入. 這樣做的主要好處就是在測試的時候我們可以用替代的modules. 這在編譯期間通過flavors就可以完成, 或者在執行期間使用一些除錯皮膚來設定.
Diff with todo-mvp
Injection
類被刪除了.
新增了5個Component, 四個feature各有一個, 另外資料對應一個: TasksRepositoryComponent
, 這個Component被儲存在Application裡.
資料的module: TasksRepositoryModule
在mock
和prod
目錄下各有一個.
對於每一個feature的Presenter的注入是這樣實現的:
首先, 把Presenter的建構函式標記為@Inject, 然後在Activity中構造component並注入到欄位:
@Inject AddEditTaskPresenter mAddEditTasksPresenter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.addtask_act); ..... // Create the presenter DaggerAddEditTaskComponent.builder() .addEditTaskPresenterModule( new AddEditTaskPresenterModule(addEditTaskFragment, taskId)) .tasksRepositoryComponent( ((ToDoApplication) getApplication()).getTasksRepositoryComponent()).build() .inject(this); }
這個module裡provide了view和taskId:
@Module public class AddEditTaskPresenterModule { private final AddEditTaskContract.View mView; private String mTaskId; public AddEditTaskPresenterModule(AddEditTaskContract.View view, @Nullable String taskId) { mView = view; mTaskId = taskId; } @Provides AddEditTaskContract.View provideAddEditTaskContractView() { return mView; } @Provides @Nullable String provideTaskId() { return mTaskId; } }
注意原來構造方法裡呼叫的setPresenter方法改為用方法注入實現:
/** * Method injection is used here to safely reference {@code this} after the object is created. * For more information, see Java Concurrency in Practice. */ @Inject void setupListeners() { mAddTaskView.setPresenter(this); }
todo-mvp-contentproviders
這個例子是基於todo-mvp-loaders的, 用content provider來獲取repository中的資料.
使用Content Provider的優勢是:
- 管理了結構化資料的訪問;
- Content Provider是跨程式訪問資料的標準介面.
Diff with todo-mvp-loaders
注意這個例子是唯一一個不基於最基本的todo-mvp, 而是基於todo-mvp-loaders. (但是我覺得也可以認為是直接從todo-mvp轉化的.)
看diff: git difftool -d todo-mvp-loaders
.
去掉了TaskLoader
和TasksLoader
. (迴歸到了基本的todo-mvp).
TasksRepository
中的方法不是同步方法, 而是非同步加callback的形式. (迴歸到了基本的todo-mvp).
TasksLocalDataSource
中的讀方法都變成了空實現, 因為Presenter現在可以自動收到資料更新.
新增LoaderProvider
用來建立Cursor Loaders, 有兩個方法:
// 返回特定fiter下或全部的資料 public Loader<Cursor> createFilteredTasksLoader(TaskFilter taskFilter) // 返回特定id的資料 public Loader<Cursor> createTaskLoader(String taskId)
其中第一個方法的引數TaskFilter
, 用來指定過濾的selection條件, 也是新增類.
LoaderManager
和LoaderProvider
都是由構造傳入Presenter, 在回撥onTaskLoaded()
和onTasksLoaded()
中init loader.
在TasksPresenter
中還做了判斷, 是init loader還是restart loader:
@Override public void onTasksLoaded(List<Task> tasks) { // we don't care about the result since the CursorLoader will load the data for us if (mLoaderManager.getLoader(TASKS_LOADER) == null) { mLoaderManager.initLoader(TASKS_LOADER, mCurrentFiltering.getFilterExtras(), this); } else { mLoaderManager.restartLoader(TASKS_LOADER, mCurrentFiltering.getFilterExtras(), this); } }
其中initLoader()和restartLoader()時傳入的第二個引數是一個bundle, 用來指明過濾型別, 即是帶selection條件的資料庫查詢.
同樣是在onLoadFinshed()的時候做View處理, 以TaskDetailPresenter
為例:
@Override public void onLoadFinished(Loader<Cursor> loader, Cursor data) { if (data != null) { if (data.moveToLast()) { onDataLoaded(data); } else { onDataEmpty(); } } else { onDataNotAvailable(); } }
資料類Task中新增了靜態方法從Cursor轉為Task, 這個方法在Presenter的onLoadFinished()
和測試中都用到了.
public static Task from(Cursor cursor) { String entryId = cursor.getString(cursor.getColumnIndexOrThrow( TasksPersistenceContract.TaskEntry.COLUMN_NAME_ENTRY_ID)); String title = cursor.getString(cursor.getColumnIndexOrThrow( TasksPersistenceContract.TaskEntry.COLUMN_NAME_TITLE)); String description = cursor.getString(cursor.getColumnIndexOrThrow( TasksPersistenceContract.TaskEntry.COLUMN_NAME_DESCRIPTION)); boolean completed = cursor.getInt(cursor.getColumnIndexOrThrow( TasksPersistenceContract.TaskEntry.COLUMN_NAME_COMPLETED)) == 1; return new Task(title, description, entryId, completed); }
另外一些細節:
資料庫中的記憶體cache被刪了.
Adapter改為繼承於CursorAdapter
.
單元測試
新增了MockCursorProvider
類, 用於在單元測試中提供資料.
其內部類TaskMockCursor
mock了Cursor資料.
Presenter的測試中仍然mock了所有構造傳入的引數, 然後準備了mock資料, 測試的邏輯主要還是拿到資料後的view操作, 比如:
@Test public void loadAllTasksFromRepositoryAndLoadIntoView() { // When the loader finishes with tasks and filter is set to all when(mBundle.getSerializable(TaskFilter.KEY_TASK_FILTER)).thenReturn(TasksFilterType.ALL_TASKS); TaskFilter taskFilter = new TaskFilter(mBundle); mTasksPresenter.setFiltering(taskFilter); mTasksPresenter.onLoadFinished(mock(Loader.class), mAllTasksCursor); // Then progress indicator is hidden and all tasks are shown in UI verify(mTasksView).setLoadingIndicator(false); verify(mTasksView).showTasks(mShowTasksArgumentCaptor.capture()); }
todo-mvp-rxjava
關於這個例子, 之前看過作者的文章: Android Architecture Patterns Part 2:Model-View-Presenter,這個文章上過Android Weekly Issue #226.
這個例子也是基於todo-mvp, 使用RxJava處理了presenter和資料層之間的通訊.
MVP基本介面改變
BasePresenter介面改為:
public interface BasePresenter { void subscribe(); void unsubscribe(); }
View在onResume()
的時候呼叫Presenter的subscribe()
; 在onPause()的時候呼叫presenter的unsubscribe()
.
如果View介面的實現不是Fragment或Activity, 而是Android的自定義View, 那麼在Android View的onAttachedToWindow()
和onDetachedFromWindow()
方法裡分別呼叫這兩個方法.
Presenter中儲存了:
private CompositeSubscription mSubscriptions;
在subscribe()
的時候, mSubscriptions.add(subscription);
;
在unsubscribe()
的時候, mSubscriptions.clear();
.
Diff with todo-mvp
資料層暴露了RxJava的Observable
流作為獲取資料的方式, TasksDataSource
介面中的方法變成了這樣:
Observable<List<Task>> getTasks(); Observable<Task> getTask(@NonNull String taskId);
callback介面被刪了, 因為不需要了.
TasksLocalDataSource
中的實現用了SqlBrite, 從資料庫中查詢出來的結果很容易地變成了流:
@Override public Observable<List<Task>> getTasks() { ... return mDatabaseHelper.createQuery(TaskEntry.TABLE_NAME, sql) .mapToList(mTaskMapperFunction); }
TasksRepository
中整合了local和remote的data, 最後把Observable
返回給消費者(Presenters和Unit Tests). 這裡用了.concat()
和.first()
操作符.
Presenter訂閱TasksRepository的Observable, 然後決定View的操作, 而且Presenter也負責執行緒的排程.
簡單的比如AddEditTaskPresenter
中:
@Override public void populateTask() { if (mTaskId == null) { throw new RuntimeException("populateTask() was called but task is new."); } Subscription subscription = mTasksRepository .getTask(mTaskId) .subscribeOn(mSchedulerProvider.computation()) .observeOn(mSchedulerProvider.ui()) .subscribe(new Observer<Task>() { @Override public void onCompleted() { } @Override public void onError(Throwable e) { if (mAddTaskView.isActive()) { mAddTaskView.showEmptyTaskError(); } } @Override public void onNext(Task task) { if (mAddTaskView.isActive()) { mAddTaskView.setTitle(task.getTitle()); mAddTaskView.setDescription(task.getDescription()); } } }); mSubscriptions.add(subscription); }
StatisticsPresenter
負責統計資料的顯示, TasksPresenter
負責過濾顯示所有資料, 裡面的RxJava操作符運用比較多, 可以看到鏈式操作的特點.
關於執行緒排程, 定義了BaseSchedulerProvider
介面, 通過建構函式傳給Presenter, 然後實現用SchedulerProvider
, 測試用ImmediateSchedulerProvider
. 這樣方便測試.
相關文章
- 解讀Android官方MVP專案單元測試AndroidMVP
- Google官方MVP示例程式碼閱讀筆記GoMVP筆記
- Android Google MVP Demo TODO解讀AndroidGoMVP
- 解讀官方Android MediaPlayer API(1)AndroidAPI
- 【Android】一鍵生成MVP程式碼-DevMvp快速開發框架AndroidMVPdev框架
- 細說 Android 的 MVP 模式AndroidMVP模式
- 說說Android的MVP模式AndroidMVP模式
- Android MVP模式專案實戰AndroidMVP模式
- Android框架模式之MVC與MVPAndroid框架模式MVCMVP
- 聊聊Android開發中的MVP模式AndroidMVP模式
- mvp模式MVP模式
- Docker瞭解(官方解讀)Docker
- Android MVP模式--簡單實用示例 BMIAndroidMVP模式
- Android上實現MVP模式的途徑AndroidMVP模式
- 以太坊官方 Token 程式碼詳解
- Android MVP模式從入門到進門(一)AndroidMVP模式
- Android中RxJava+Retrofit2.0+MVP模式的整合AndroidRxJavaMVP模式
- kafka程式碼解讀Kafka
- Android 谷歌官方文件抓蟲計劃回顧 ?Android谷歌
- MVP設計模式MVP設計模式
- 讀解Android 系統原始碼?Android原始碼
- Android學習探索之運用MVP設計模式實現專案解耦AndroidMVP設計模式解耦
- Android谷歌官方語言Kotlin用法入門教程Android谷歌Kotlin
- CRF as RNN 程式碼解讀CRFRNN
- 讀懂 Android 中的程式碼混淆Android
- MVC模式和MVP模式的區別MVC模式MVP
- Android Mvp實踐AndroidMVP
- Android MVP 架構AndroidMVP架構
- Android MVP 實踐AndroidMVP
- Android開發中的MVP架構詳解AndroidMVP架構
- Linklist程式碼實現以及程式碼解讀
- 《JavaScript 模式》讀書筆記(6)— 程式碼複用模式2JavaScript模式筆記
- 《JavaScript 模式》讀書筆記(6)— 程式碼複用模式3JavaScript模式筆記
- 直播網站程式原始碼,【openpyxl】只讀模式、只寫模式網站原始碼模式
- Mybatis 原始碼解讀-設計模式總結MyBatis原始碼設計模式
- Android-Material Design風格MVP模式的新聞AppAndroidMaterial DesignMVP模式APP
- 谷歌終於開源BERT程式碼:3 億引數量,機器之心全面解讀谷歌
- 優化你的程式碼結構 --- MVP優化MVP