📜 ⬆️ ⬇️

Android Architecture Components. Part 4. ViewModel

image

The ViewModel component is designed to store and manage the data associated with the view, and at the same time, save us from the problem associated with the re-creation of activations during such operations as flipping the screen, etc. It should not be taken as a replacement for onSaveInstanceState, because after the system destroys our activations, for example, when we switch to another application, the ViewModel will also be destroyed and will not save its state. In general, the ViewModel component can be described as a singleton with a collection of instances of ViewModel classes, which guarantees that it will not be destroyed while there is an active instance of our activity and will free up resources after leaving it (everything is a bit more complicated, but it looks something like this). It is also worth noting that we can link any number of ViewModel to our Activity (Fragment).

The component consists of the following classes: ViewModel, AndroidViewModel, ViewModelProvider, ViewModelProviders, ViewModelStore, ViewModelStores . The developer will work only with the ViewModel, AndroidViewModel and to get the Spaniard from ViewModelProviders, but for a better understanding of the component, we will look at all the classes superficially.

The ViewModel class itself represents an abstract class, without abstract methods and with one protected onCleared () method. To implement our own ViewModel, we only need to inherit our class from ViewModel with a constructor without parameters, and that’s it. If we need to clear the resources, then we need to override the onCleared () method, which will be called when the ViewModel is not available for a long time and should be destroyed. As an example, you can recall the previous article about LiveData, and specifically about the observeForever (Observer) method, which requires explicit unsubscribe, and it is appropriate to implement it in the onCleared () method. It is worth adding that in order to avoid memory leaks, you do not need to refer directly to View or Context Activity from ViewModel. In general, the ViewModel should be completely isolated from the data view. In this case, the question arises: How can we notify the presentation (Activity / Fragment) of changes in our data? In this case, LiveData comes to the rescue, we must store all the editable data using LiveData, if we need, for example, to show and hide the ProgressBar, we can create MutableLiveData and store the show / hide logic in the ViewModel component. In general, it will look like this:
')
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;   } } 

To get a link to our instance of ViewModel, we must use 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(); } 

The AndroidViewModel class , is an extension of the ViewModel, with the only difference - there should be one Application parameter in the constructor. It is a rather useful extension in cases where we need to use the Location Service or another component that requires the Application Context. The only difference with it is that we inherit our ViewModel from ApplicationViewModel. In Activity / Fragment, we initialize it in the same way as the usual ViewModel.

The ViewModelProviders class is a four utility method that is called of and returns a ViewModelProvider. Adapted for working with Activity and Fragment, as well as, with the ability to substitute your implementation of ViewModelProvider.Factory, the default is DefaultFactory, which is a nested class in ViewModelProviders. So far, there are no other implementations given in the android.arch package.

The ViewModelProvider class, actually a class that returns our ViewModel instance. We will not go deep here, in general, it represents the role of an intermediary with the ViewModelStore, which stores and raises our intats ViewModel and returns it using the get method, which has two signatures get (Class) and get (String key, Class modelClass). The point is that we can bind several ViewModel to our Activity / Fragment, even of the same type. The get method returns them by a String key, which by default is configured as: "android.arch.lifecycle.ViewModelProvider.DefaultKey:" + canonicalName

The ViewModelStores class is a factory method, remember: The factory method is a pattern that defines the interface for creating an object, but leaves the subclasses a decision on which class to instantiate, in fact, allows the class to delegate instantiation to subclasses. At the moment, in the android.arch package there is both one interface and one subclass of ViewModelStore.

The ViewModelStore class, the class in which all magic resides, consists of put, get, and clear methods. You don’t have to worry about them, because we don’t have to work with them directly, but we can’t physically get and put, since they are declared as default (package-private) and are therefore visible only inside the package. But, for general education, consider the device of this class. The class itself stores the HashMap <String, ViewModel>, the methods get and put, respectively, return by key (according to the one that we form in the ViewModelProvider) or add the ViewModel. The clear () method will call the onCleared () method on all our ViewModel we added.

For an example of working with the ViewModel, let's implement a small application that allows the user to select a point on the map, set a radius and indicate whether a person is in this field or not. And also giving the opportunity to specify the WiFi network, if the user is connected to it, we will assume that he is in radius, regardless of the physical coordinates.


First, create two LiveData to track the location and name of the WiFi network:

 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;  } } 

Now let's go to the ViewModel, since we have a condition that depends on the data received from two LifeData, MediatorLiveData is ideal for us as a holder of the value itself, but since restarting services is not profitable for us, we will subscribe to MediatorLiveData without reference to the life cycle using observeForever. In the onCleared () method, we implement unsubscribe from it using removeObserver. In its turn, LiveData will notify about the change MutableLiveData, which will be signed by our presentation.

 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);  } } 

And our presentation:

 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);  } } 

In general, we subscribe to MutableLiveData using the getStatus () method from our ViewModel. We also work with it to initialize and save our data.

There are also several checks added here, such as RuntimePermission and a GPS status check. As you can see, the code in the Activity turned out to be quite extensive, in the case of a complex UI, Google recommends looking towards the creation of the presenter (but this may be overkill).

The example also used such libraries as:

 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' 

Full listing: here

Useful links: here and here

Android Architecture Components. Part 1. Introduction
Android Architecture Components. Part 2. Lifecycle
Android Architecture Components. Part 3. LiveData
Android Architecture Components. Part 4. ViewModel

Source: https://habr.com/ru/post/334942/


All Articles