Androidアプリを開発しており、実行時に権限を要求する必要があります。Model-View-Presenterアーキテクチャを使用してそれを実装するための最良の方法について疑問に思っています。
私の最初の考えは、プレゼンターに権限を担当するコンポーネント(たとえば、PermissionHandler
)を呼び出し、それに応じてビューを更新することでした。
問題は、権限をチェックするコードがActivityクラスと緊密に結合されていることです。アクティビティまたはコンテキストを必要とする関連するメソッドのいくつかを次に示します。
ContextCompat.checkSelfPermission()
ActivityCompat.shouldShowRequestPermissionRationale()
ActivityCompat.requestPermissions()
onRequestPermissionsResult()
(コールバック)これは、アクティビティオブジェクトをプレゼンターに渡す必要があることを意味しますが、プレゼンターをAndroidコードから解放することはテストに適していると聞いたため、あまり好きではありませんでした。
そのため、ビューレベル(アクティビティ内)でアクセス許可を処理することを考えましたが、ビジネスロジックを使用せずに、ビューをUIの更新のみに責任を持たせるという目的に悪影響を与えると思います。
コードを可能な限り分離して保守可能に保つことに取り組むための最善のアプローチが何であるかはわかりません。何か案は?
私がすることは:
ビューは以下を実装します:
_public Activity getViewActivity();
_
プレゼンターは以下を実装します。
_public void requestPermissions();
public void onPermissionsResult();
_
requestPermissions
内で、プレゼンターは次のことを行います。getViewActivity().checkSelfPermission; getViewActivity.requestPermissions(); etc.
ビューは、onRequestPermissionsResult
コールバック内でpresenter.onPermissionsResult();
を呼び出します。
これにより、すべてのロジックがプレゼンター内に実装されます。
私の意見では、プレゼンターは分離されています。ビューの実装には依存しません(ビューのインターフェイスにのみ依存します)。
「プレゼンターをAndroidコードから解放するのはテストに適していると聞きました。」この部分はわかりません。コードが良ければ、問題なくテストできます。
それでもパーミッションアクセス/リクエストをモックできるようにしたい場合は、PermissionHandler
のようなものを作成しますが、それはビュークラス内でのみ参照します。例えば -
インターフェース:
_public interface PermissionsHandler {
boolean checkHasPermission(AppCompatActivity activity, String permission);
void requestPermission(AppCompatActivity activity, String[] permissions, int requestCode);
}
_
本番実装:
_public class PermissionsHandlerAndroid implements PermissionsHandler {
@Override
public boolean checkHasPermission(AppCompatActivity activity, String permission) {
return ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED;
}
@Override
public void requestPermission(AppCompatActivity activity, String[] permissions, int requestCode){
ActivityCompat.requestPermissions(activity, permissions, requestCode);
}
}
_
モッククラス(たとえば、アクティビティがonRequestPermissionsResult
を正しく処理することをテストおよび確認するため)
_public class PermissionsHandlerMocked implements PermissionsHandler {
@Override
public boolean checkHasPermission(AppCompatActivity activity, String permission) {
return false;
}
@Override
public void requestPermission(AppCompatActivity activity, String[] permissions, int requestCode){
int[] grantResults = new int[permissions.length];
for (int i = 0; i < permissions.length; i++) {
grantResults[i] = PackageManager.PERMISSION_GRANTED
}
activity.onRequestPermissionResult(requestCode, permissions, grantResults);
}
}
_
それからあなたの活動で:
_PermissionsHandler permissionsHandler;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
permissionsHandler = Injection.providePermissionsHandler();
//or however you choose to inject your production vs mocked handler.
}
//method from your view interface, to be called by your presenter
@Override
void requestLocationPermission() {
permissionsHandler.requestPermision((AppCompatActivity) this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, REQUEST_CODE_LOCATION};
}
_
fobo66 、いつでもビューにcheckLocationPermissionGranted()
やrequestLocationPermission()
などのより一般的なメソッドを実装させることができます。そうすれば、ビューの実装は必要に応じてアクティビティを参照でき、プレゼンターはアクティビティ参照に触れる必要がありません。
パーミッションのリクエストとステータスは、リクエストを行ったりパーミッションを付与したりするユーザーのアクションに依存するため、ビュー(フラグメントまたはアクティビティ)の責任です。私は次のようにMVPで権限を管理します(外部ストレージの例を読んでください):
私の契約
interface View {
...
void requestReadPermission();
boolean areReadPermissionGranted();
void showPermissionError();
void hidePermissionError();
...
}
interface Presenter {
...
void setReadPermissions(boolean grantedPermissions);
...
}
interface Model {
...
}
私のビューの実装。 (この場合のフラグメントですが、アクティビティなどの場合があります。プレゼンターは応答のみを期待します)。
public class MyView extends Fragment implements Contract.View {
...
Contract.Presenter presenter;
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
boolean grantedPermissions = (grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED);
presenter.setReadPermissions(grantedPermissions);
}
@Override
public void showPermissionError() {
// Show not permission message
}
@Override
public void hidePermissionError() {
// Hide not permission message
}
@Override
public void requestReadPermission() {
this.requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
}
@Override
public boolean areReadPermissionGranted() {
return ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
...
そしてプレゼンターの実装
public class MyPresenter implements Contract.Presenter {
...
Contract.View view;
public void doSomethingThatRequiresPermissions() {
...
if ( !view.areReadPermissionGranted() ) {
view.requestReadPermission();
view.showPermissionError();
} else {
view.hidePermissionError();
doSomethingWithPermissionsGranted();
}
...
}
@Override
public void setReadPermissions(boolean grantedPermissions) {
if( grantedPermissions ){
view.hidePermissionError();
doSomethingThatRequiresPermissions();
} else {
view.showPermissionError();
}
}
public void doSomethingWithPermissionsGranted(){
...
}
次に、次のようなユニットテストを行うことができます
Contract.View mockedView;
@Test
public void requestAlbumListWithoutPermissions() {
when(mockedView.areReadPermissionGranted()).thenReturn(false);
presenter.doSomethingWithPermissionsGranted();
verify(mockedView).showPermissionError();
verify(mockedView).requestReadPermission();
}