Android MVP模式 谷歌官方代碼解讀

来源:http://www.cnblogs.com/mengdd/archive/2016/10/22/5988104.html
-Advertisement-
Play Games

關於Android程式的構架, 當前(2016.10)最流行的模式即為MVP模式, Google官方提供了Sample代碼來展示這種模式的用法. 本文為閱讀官方sample代碼的閱讀筆記和分析. ...


Google官方MVP Sample代碼解讀

關於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.

mvp

基類

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得到數據.

todo-mvp-loaders

使用Loader的優勢:

  • 去掉了回調, 自動實現數據的非同步載入;
  • 當內容改變時回調出新數據;
  • 當應用因為configuration變化而重建loader時, 自動重連到上一個loader.

Diff with todo-mvp

既然是基於todo-mvp, 那麼之前說過的那些就不再重覆, 我們來看一下都有什麼改動:
git difftool -d todo-mvp

添加了兩個類:
TaskLoaderTasksLoader.

在Activity中new Loader類, 然後傳入Presenter的構造方法.

Contract中View介面刪掉了isActive()方法, Presenter刪掉了populateTask()方法.

數據獲取

添加的兩個新類是TaskLoaderTasksLoader, 都繼承於AsyncTaskLoader, 只不過數據的類型一個是單數, 一個是複數.

AsyncTaskLoader是基於ModernAsyncTask, 類似於AsyncTask,
把load數據的操作放在loadInBackground()里即可, deliverResult()方法會將結果返回到主線程, 我們在listener的onLoadFinished()裡面就可以接到返回的數據了, (在這個例子中是幾個Presenter實現了這個介面).

TasksDataSource介面的這兩個方法:

List<Task> getTasks();
Task getTask(@NonNull String taskId);

都變成了同步方法, 因為它們是在loadInBackground()方法里被調用.

Presenter中保存了LoaderLoaderManager, 在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.

todo-mvp-databinding

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層, 把應用分為了三層:
mvp-clean

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: TasksRepositoryModulemockprod目錄下各有一個.

對於每一個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中的數據.
mvp-contentproviders

使用Content Provider的優勢是:

  • 管理了結構化數據的訪問;
  • Content Provider是跨進程訪問數據的標準介面.

Diff with todo-mvp-loaders

註意這個例子是唯一一個不基於最基本的todo-mvp, 而是基於todo-mvp-loaders. (但是我覺得也可以認為是直接從todo-mvp轉化的.)
看diff: git difftool -d todo-mvp-loaders.

去掉了TaskLoaderTasksLoader. (回歸到了基本的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條件, 也是新增類.

LoaderManagerLoaderProvider都是由構造傳入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. 這樣方便測試.


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • webView.loadUrl("http://10.0.2.2:8080/assets/RealNetJSCallJavaActivity.htm"); js調用的Java文件中寫 點擊視頻可以選擇任意播放器 ...
  • 對於HTTPS我在網上找了一堆資料看了下, 各種協議和證書已經有點暈了 最後我現有的感覺是, 在HTTP伺服器上放一個證書, 在原本的HTTP訪問之前客戶端先檢查證書是否正確 如果客戶端證書檢查正確, 說明對 這個伺服器就是我要連接的那個server 不對, 就說明這個server是個假冒的 同樣也 ...
  •  ...
  • (六)與系統交互 6.1後臺通知 1.關於後臺通知,下麵展示6種樣式。值得一提的是,筆者的小米5只能顯示基本樣式,雷軍真是良心廠商啊。 2.首先上佈局xml 3.接著是完整代碼 6.3定時執行周期任務 1.AlarmManager用來管理和執行任務,可以在程式沒有運行的時候執行。並且有多種啟動和計算 ...
  • 清理單個應用緩存 通過反射調用方法 需要許可權:android.permission.DELETE_CACHE_FILES. 以上許可權為系統許可權,手機衛士應用為用戶應用,不能拿到此許可權 換思路: 調用系統清除緩存的界面,讓用戶在系統清除緩存界面實現清除邏輯 查找系統清除緩存的界面方法: ...
  • (四)實現設備硬體交互與媒體交互 4.6自定義攝像頭覆蓋層 1.將Camera中的內容實時的繪製到SurfaceView中 若要自定義拍攝界面,只要重新定義surface的界面即可 以下展示全部代碼 2.改變拍攝方向,調用setDisplayOrientation(90)後,拍攝方向方向才會垂直顯示 ...
  • 來自sqlite3源碼 /*** Compute a string length that is limited to what can be stored in** lower 30 bits of a 32-bit signed integer.**** The value returned w... ...
  • (1):Android體繫結構; 應用程式層(Applications), 應用程式框架層(Application Framework),系統運行庫層(Librarries),Linux內核層(Linux Kernel); (2)Android項目中文件夾的作用: 存放一些資源文件的信息,用於讀取文 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...