Androidアーキテクチャコンポーネント。 パート4. ViewModel

画像



ViewModelコンポーネントは、ビューに関連付けられたデータを保存および管理すると同時に、画面の反転などの操作中にアクティビティを再作成する問題を軽減するように設計されています。 システムがアクティビティを破棄した後、たとえば別のアプリケーションに移動した場合、ViewModelも破棄され、その状態が保存されないため、onSaveInstanceStateの代わりとして使用しないでください。 一般に、ViewModelコンポーネントは、ViewModelクラスのインスタンスのコレクションを持つシングルトンとして記述できます。これにより、アクティビティのアクティブなインスタンスがある限り破棄されず、アクティビティを離れた後にリソースを解放します(すべてが少し複雑ですが、そのように見えます)。 また、ViewModelをいくつでもアクティビティ(フラグメント)にバインドできることにも注意してください。



コンポーネントは、 ViewModel、AndroidViewModel、ViewModelProvider、ViewModelProviders、ViewModelStore、ViewModelStoresのクラスで構成されています。 開発者はViewModel、AndroidViewModelのみを使用し、ViewModelProvidersで原告を取得しますが、コンポーネントをよりよく理解するために、すべてのクラスを表面的に検討します。



ViewModelクラス自体は、抽象メソッドを持たず、1つの保護されたメソッドonCleared()を持つ抽象クラスを表します。 独自のViewModelを実装するには、パラメーターなしのコンストラクターでViewModelクラスを継承する必要があります。それだけです。 リソースをクリアする必要がある場合、onCleared()メソッドを再定義する必要があります。このメソッドは、ViewModelが長時間利用できず、破棄する必要がある場合に呼び出されます。 例として、LiveData、特にobserveForever(Observer)メソッドに関する以前の記事を思い出すことができます。これは明示的な応答を必要とし、onCleared()メソッドで実装するのが適切です。 また、メモリリークを回避するために、ViewModelからViewまたはContextアクティビティを直接参照する必要がないことも追加する価値があります。 一般に、ViewModelはデータの表示から完全に分離されている必要があります。 この場合、問題が発生します。そして、データの変更をビュー(アクティビティ/フラグメント)にどのように通知するのでしょうか? この場合、LiveDataが助けになります。LiveDataを使用してすべての可変データを保存する必要がありますが、たとえばProgressBarを表示および非表示にする必要がある場合は、MutableLiveDataを作成し、ViewModelコンポーネントにshow \ hideロジックを保存できます。 一般的に、次のようになります。



public class MyViewModel extends ViewModel {   private MutableLiveData<Boolean> showProgress = new MutableLiveData<>();   //new thread   public void doSomeThing(){       showProgress.postValue(true);       ...       showProgress.postValue(false);   }    public MutableLiveData<Boolean> getProgressState(){       return showProgress;   } }
      
      





ViewModelのインスタンスへのリンクを取得するには、ViewModelProvidersを使用する必要があります。



 @Override protected void onCreate(Bundle savedInstanceState) {  super.onCreate(savedInstanceState);  setContentView(R.layout.activity_main);  final MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);  viewModel.getProgressState().observe(this, new Observer<Boolean>() {      @Override      public void onChanged(@Nullable Boolean aBoolean) {          if (aBoolean) {              showProgress();          } else {              hideProgress();          }      }  });  viewModel.doSomeThing(); }
      
      





AndroidViewModelクラスはViewModel拡張機能です。唯一の違いは、コンストラクターにApplicationパラメーターが1つあることです。 これは、ロケーションサービスまたはアプリケーションコンテキストを必要とする別のコンポーネントを使用する必要がある場合に非常に便利な拡張機能です。 それを扱う際の唯一の違いは、ApplicationViewModelからViewModelを継承することです。 Activity / Fragmentでは、通常のViewModelと同様に初期化します。



ViewModelProvidersクラスはViewModelProviderを呼び出して返す4つのユーティリティメソッドです。 アクティビティとフラグメント、およびViewModelProvider.Factoryの実装を置き換える機能と連携するように適合されたDefaultFactoryがデフォルトで使用されます。これは、ViewModelProvidersのネストされたクラスです。 これまでのところ、android.archパッケージにリストされている他の実装はありません。



ViewModelProviderクラス、実際にはViewModelインスタンスを返すクラス。 ここではあまり詳しく説明しませんが、一般的に、彼はViewModelStoreの仲介者として機能します。 ポイントは、複数のViewModelを1つのタイプのアクティビティ/フラグメントにバインドできることです。 getメソッドは、デフォルトで「android.arch.lifecycle.ViewModelProvider.DefaultKey:」+ canonicalNameの形式のStringキーでそれらを返します。



ViewModelStoresクラスはファクトリーメソッドです。思い出してください。ファクトリーメソッドは、オブジェクトを作成するためのインターフェイスを定義するパターンですが、サブクラスにインスタンス化するクラスに関する決定を残します。実際、クラスはインスタンス化をサブクラスに委任できます。 現在、android.archパッケージには、ViewModelStoreの1つのインターフェイスと1つのサブクラスの両方が含まれています。



ViewModelStoreクラス(すべての魔法を含むクラス)は、put、get、およびclearメソッドで構成されます。 それらを直接操作する必要がないため、それらについて心配する必要はありません。また、それらはパッケージ内でのみ表示されるデフォルト(パッケージプライベート)として宣言されているため、物理的に取得および配置することはできません。 ただし、一般教育では、このクラスのデバイスを検討してください。 クラス自体は、HashMap <String、ViewModel>、getおよびputメソッドをそれぞれ格納し、キー(ViewModelProviderで作成したもの)ごとに返すか、ViewModelを追加します。 clear()メソッドは、追加したすべてのViewModelでonCleared()メソッドを呼び出します。



ViewModelの使用例については、ユーザーがマップ上のポイントを選択し、半径を設定し、このフィールドに人がいるかどうかを表示できる小さなアプリケーションを実装してみましょう。 WiFiネットワークを指定する機能を提供するだけでなく、ユーザーがそれに接続している場合、物理座標に関係なく半径内にあると想定します。









まず、2つのLiveDataを作成して、WiFiネットワークの場所と名前を追跡します。



 public class LocationLiveData extends LiveData<Location> implements      GoogleApiClient.ConnectionCallbacks,      GoogleApiClient.OnConnectionFailedListener,      LocationListener {  private final static int UPDATE_INTERVAL = 1000;  private GoogleApiClient googleApiClient;  public LocationLiveData(Context context) {      googleApiClient =              new GoogleApiClient.Builder(context, this, this)                      .addApi(LocationServices.API)                      .build();  }  @Override  protected void onActive() {      googleApiClient.connect();  }  @Override  protected void onInactive() {      if (googleApiClient.isConnected()) {          LocationServices.FusedLocationApi.removeLocationUpdates(                  googleApiClient, this);      }      googleApiClient.disconnect();  }  @Override  public void onConnected(Bundle connectionHint) {          LocationRequest locationRequest = new LocationRequest().setInterval(UPDATE_INTERVAL).setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);          LocationServices.FusedLocationApi.requestLocationUpdates(                  googleApiClient, locationRequest, this);  }  @Override  public void onLocationChanged(Location location) {      setValue(location);  }  @Override  public void onConnectionSuspended(int cause) {      setValue(null);  }  @Override  public void onConnectionFailed(ConnectionResult connectionResult) {      setValue(null);  } }
      
      





 public class NetworkLiveData extends LiveData<String> {  private Context context;  private BroadcastReceiver broadcastReceiver;  public NetworkLiveData(Context context) {      this.context = context;  }  private void prepareReceiver(Context context) {      IntentFilter filter = new IntentFilter();      filter.addAction("android.net.wifi.supplicant.CONNECTION_CHANGE");      filter.addAction("android.net.wifi.STATE_CHANGE");      broadcastReceiver = new BroadcastReceiver() {          @Override          public void onReceive(Context context, Intent intent) {              WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);              WifiInfo wifiInfo = wifiMgr.getConnectionInfo();              String name = wifiInfo.getSSID();              if (name.isEmpty()) {                  setValue(null);              } else {                  setValue(name);              }          }      };      context.registerReceiver(broadcastReceiver, filter);  }  @Override  protected void onActive() {      super.onActive();      prepareReceiver(context);  }  @Override  protected void onInactive() {      super.onInactive();      context.unregisterReceiver(broadcastReceiver);      broadcastReceiver = null;  } }
      
      





2つのLifeDataから受信したデータに依存する条件があるため、ViewModelに進みましょう。MediatorLiveDataは値の所有者として理想的ですが、サービスを再開することは有益ではないため、observerForeverを使用してライフサイクルを参照せずにMediatorLiveDataにサブスクライブします。 onCleared()メソッドでは、removeObserverを使用してサブスクライブを解除します。 次に、LiveDataはMutableLiveDataの変更を通知し、これにビューがサブスクライブされます。



 public class DetectorViewModel extends AndroidViewModel { //   ,   Repository,      GitHub       private IRepository repository;  private LatLng point;  private int radius;  private LocationLiveData locationLiveData;  private NetworkLiveData networkLiveData;  private MediatorLiveData<Status> statusMediatorLiveData = new MediatorLiveData<>();  private MutableLiveData<String> statusLiveData = new MutableLiveData<>();  private String networkName;  private float[] distance = new float[1];  private Observer<Location> locationObserver = new Observer<Location>() {      @Override      public void onChanged(@Nullable Location location) {          checkZone();      }  };  private Observer<String> networkObserver = new Observer<String>() {      @Override      public void onChanged(@Nullable String s) {          checkZone();      }  };  private Observer<Status> mediatorStatusObserver = new Observer<Status>() {      @Override      public void onChanged(@Nullable Status status) {          statusLiveData.setValue(status.toString());      }  };  public DetectorViewModel(final Application application) {      super(application);      repository = Repository.getInstance(application.getApplicationContext());      initVariables();      locationLiveData = new LocationLiveData(application.getApplicationContext());      networkLiveData = new NetworkLiveData(application.getApplicationContext());      statusMediatorLiveData.addSource(locationLiveData, locationObserver);      statusMediatorLiveData.addSource(networkLiveData, networkObserver);      statusMediatorLiveData.observeForever(mediatorStatusObserver);  } //      LocationService  ,      WiFi network .  private void updateLocationService() {      if (isRequestedWiFi()) {          statusMediatorLiveData.removeSource(locationLiveData);      } else if (!isRequestedWiFi() && !locationLiveData.hasActiveObservers()) {          statusMediatorLiveData.addSource(locationLiveData, locationObserver);      }  } //     private void initVariables() {      point = repository.getPoint();      if (point.latitude == 0 && point.longitude == 0)          point = null;      radius = repository.getRadius();      networkName = repository.getNetworkName();  } //,              private void checkZone() {      updateLocationService();      if (isRequestedWiFi() || isInRadius()) {          statusMediatorLiveData.setValue(Status.INSIDE);      } else {          statusMediatorLiveData.setValue(Status.OUTSIDE);      }  }  public LiveData<String> getStatus() {      return statusLiveData;  } //          public void savePoint(LatLng latLng) {      repository.savePoint(latLng);      point = latLng;      checkZone();  }  public void saveRadius(int radius) {      this.radius = radius;      repository.saveRadius(radius);      checkZone();  }  public void saveNetworkName(String networkName) {      this.networkName = networkName;      repository.saveNetworkName(networkName);      checkZone();  }  public int getRadius() {      return radius;  }  public LatLng getPoint() {      return point;  }  public String getNetworkName() {      return networkName;  }  public boolean isInRadius() {      if (locationLiveData.getValue() != null && point != null) {          Location.distanceBetween(locationLiveData.getValue().getLatitude(), locationLiveData.getValue().getLongitude(), point.latitude, point.longitude, distance);          if (distance[0] <= radius)              return true;      }      return false;  }  public boolean isRequestedWiFi() {      if (networkLiveData.getValue() == null)          return false;      if (networkName.isEmpty())          return false;      String network = networkName.replace("\"", "").toLowerCase();      String currentNetwork = networkLiveData.getValue().replace("\"", "").toLowerCase();      return network.equals(currentNetwork);  }  @Override  protected void onCleared() {      super.onCleared();      statusMediatorLiveData.removeSource(locationLiveData);      statusMediatorLiveData.removeSource(networkLiveData);      statusMediatorLiveData.removeObserver(mediatorStatusObserver);  } }
      
      





そして私たちの見解:



 public class MainActivity extends LifecycleActivity {  private static final int PERMISSION_LOCATION_REQUEST = 0001;  private static final int PLACE_PICKER_REQUEST = 1;  private static final int GPS_ENABLE_REQUEST = 2;  @BindView(R.id.status)  TextView statusView;  @BindView(R.id.radius)  EditText radiusEditText;  @BindView(R.id.point)  EditText pointEditText;  @BindView(R.id.network_name)  EditText networkEditText;  @BindView(R.id.warning_container)  ViewGroup warningContainer;  @BindView(R.id.main_content)  ViewGroup contentContainer;  @BindView(R.id.permission)  Button permissionButton;  @BindView(R.id.gps)  Button gpsButton;  private DetectorViewModel viewModel;  private LatLng latLng;  @Override  protected void onCreate(Bundle savedInstanceState) {      super.onCreate(savedInstanceState);      setContentView(R.layout.activity_main);      ButterKnife.bind(this);      checkPermission();  }  @Override  public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {      super.onRequestPermissionsResult(requestCode, permissions, grantResults);      if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {          init();      } else {          showWarningPage(Warning.PERMISSION);      }  }  private void checkPermission() {      if (PackageManager.PERMISSION_GRANTED == checkSelfPermission(              Manifest.permission.ACCESS_FINE_LOCATION)) {          init();      } else {          requestPermissions(new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, PERMISSION_LOCATION_REQUEST);      }  }  private void init() {      viewModel = ViewModelProviders.of(this).get(DetectorViewModel.class);      if (Utils.isGpsEnabled(this)) {          hideWarningPage();          checkingPosition();          initInput();      } else {          showWarningPage(Warning.GPS_DISABLED);      }  }  private void initInput() {      radiusEditText.setText(String.valueOf(viewModel.getRadius()));      latLng = viewModel.getPoint();      if (latLng == null) {          pointEditText.setText(getString(R.string.chose_point));      } else {          pointEditText.setText(latLng.toString());      }      networkEditText.setText(viewModel.getNetworkName());  }  @OnClick(R.id.get_point)  void getPointClick(View view) {      PlacePicker.IntentBuilder builder = new PlacePicker.IntentBuilder();      try {          startActivityForResult(builder.build(MainActivity.this), PLACE_PICKER_REQUEST);      } catch (GooglePlayServicesRepairableException e) {          e.printStackTrace();      } catch (GooglePlayServicesNotAvailableException e) {          e.printStackTrace();      }  }  @OnClick(R.id.save)  void saveOnClick(View view) {      if (!TextUtils.isEmpty(radiusEditText.getText())) {          viewModel.saveRadius(Integer.parseInt(radiusEditText.getText().toString()));      }      viewModel.saveNetworkName(networkEditText.getText().toString());  }  @OnClick(R.id.permission)  void permissionOnClick(View view) {      checkPermission();  }  @OnClick(R.id.gps)  void gpsOnClick(View view) {      startActivityForResult(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS), GPS_ENABLE_REQUEST);  }  private void checkingPosition() {      viewModel.getStatus().observe(this, new Observer<String>() {          @Override          public void onChanged(@Nullable String status) {              updateUI(status);          }      });  }  private void updateUI(String status) {      statusView.setText(status);  }  protected void onActivityResult(int requestCode, int resultCode, Intent data) {      if (requestCode == PLACE_PICKER_REQUEST) {          if (resultCode == RESULT_OK) {              Place place = PlacePicker.getPlace(data, this);              updatePlace(place.getLatLng());          }      }      if (requestCode == GPS_ENABLE_REQUEST) {          init();      }  }  private void updatePlace(LatLng latLng) {      viewModel.savePoint(latLng);      pointEditText.setText(latLng.toString());  }  private void showWarningPage(Warning warning) {      warningContainer.setVisibility(View.VISIBLE);      contentContainer.setVisibility(View.INVISIBLE);      switch (warning) {          case PERMISSION:              gpsButton.setVisibility(View.INVISIBLE);              permissionButton.setVisibility(View.VISIBLE);              break;          case GPS_DISABLED:              gpsButton.setVisibility(View.VISIBLE);              permissionButton.setVisibility(View.INVISIBLE);              break;      }  }  private void hideWarningPage() {      warningContainer.setVisibility(View.GONE);      contentContainer.setVisibility(View.VISIBLE);  } }
      
      





一般に、ViewModelのgetStatus()メソッドを使用してMutableLiveDataをサブスクライブします。 また、彼と協力してデータを初期化して保存します。



RuntimePermissionやGPSステータスチェックなど、いくつかのチェックもここに追加されました。 ご覧のとおり、Activityのコードは非常に広範囲であることが判明しました。複雑なUIの場合、Googleはプレゼンターを作成する方向に目を向けることを推奨します(ただし、これはやり過ぎです)。



この例では、次のようなライブラリも使用しました。



 compile 'com.jakewharton:butterknife:8.6.0' compile 'com.google.android.gms:play-services-maps:11.0.2' compile 'com.google.android.gms:play-services-location:11.0.2' compile 'com.google.android.gms:play-services-places:11.0.2' annotationProcessor 'com.jakewharton:butterknife-compiler:8.6.0'
      
      





完全なリスト: こちら



便利なリンク: ここここ



Androidアーキテクチャコンポーネント。 パート1.はじめに

Androidアーキテクチャコンポーネント。 パート2.ライフサイクル

Androidアーキテクチャコンポーネント。 パート3. LiveData

Androidアーキテクチャコンポーネント。 パート4. ViewModel



All Articles