Android Architecture Components. Часть 4. ViewModel

    image

    Компонент ViewModel — предназначен для хранения и управления данными, связанными с представлением, а заодно, избавить нас от проблемы, связанной с пересозданием активити во время таких операций, как переворот экрана и т.д. Не стоит его воспринимать, как замену onSaveInstanceState, поскольку, после того как система уничтожит нашу активити, к примеру, когда мы перейдем в другое приложение, ViewModel будет также уничтожен и не сохранит свое состояние. В целом же, компонент ViewModel можно охарактеризовать как синглтон с колекцией экземпляров классов ViewModel, который гарантирует, что не будет уничтожен пока есть активный экземпляр нашей активити и освободит ресурсы после ухода с нее (все немного сложнее, но выглядит как-то так). Стоит также отметить, что мы можем привязать любое количество ViewModel к нашей Activity(Fragment).

    Компонент состоит из таких классов: ViewModel, AndroidViewModel, ViewModelProvider, ViewModelProviders, ViewModelStore, ViewModelStores. Разработчик будет работать только с  ViewModel, AndroidViewModel и для получения истанца с ViewModelProviders, но для лучшего понимания компонента, мы поверхностно рассмотрим все классы.

    Класс ViewModel, сам по себе представляет абстрактный класс, без абстрактных методов и с одним protected методом onCleared(). Для реализации собственного ViewModel, нам всего лишь необходимо унаследовать свой класс от ViewModel с конструктором без параметров и это все. Если же нам нужно очистить ресурсы, то необходимо переопределить метод onCleared(), который будет вызван когда ViewModel долго не доступна и должна быть уничтожена. Как пример, можно вспомнить предыдущую статью про LiveData, а конкретно о методе observeForever(Observer), который требует явной отписки, и как раз в методе onCleared() уместно ее реализовать. Стоит еще добавить, что во избежания утечки памяти, не нужно ссылаться напрямую на View или Context Activity из ViewModel. В целом, ViewModel должна быть абсолютно изолированная от представления данных. В таком случае появляется вопрос: А каким же образом нам уведомить представление (Activity/Fragment) об изменениях в наших данных? В этом случае на помощь нам приходит LiveData, все изменяемые данные мы должны хранить с помощью LiveData, если же нам необходимо, к примеру, показать и скрыть ProgressBar, мы можем создать MutableLiveData и хранить логику показать\скрыть в компоненте ViewModel. В общем это будет выглядеть так:

    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. Является довольно полезным расширением в случаях, когда нам нужно использовать Location Service или другой компонент, требующий Application Context. В работе с ним единственное отличие, это то что мы наследуем наш ViewModel от ApplicationViewModel. В Activity/Fragment инициализируем его точно также, как и обычный ViewModel.

    Класс ViewModelProviders, являет собой четыре метода утилиты, которые, называются of и возвращают ViewModelProvider. Адаптированные для работы с Activity и Fragment, а также, с возможностью подставить свою реализацию ViewModelProvider.Factory, по умолчанию используется DefaultFactory, которая является вложенным классом в ViewModelProviders. Пока что других реализаций приведенных в пакете android.arch нет.

    Класс ViewModelProvider, собственно говоря класс, который возвращает наш инстанс ViewModel. Не будем особо углубляться здесь, в общих чертах он являет роль посредника с ViewModelStore, который, хранит и поднимает наш интанс ViewModel и возвращает его с помощью метода get, который имеет две сигнатуры get(Class) и get(String key, Class modelClass). Смысл заключается в том, что мы можем привязать несколько ViewModel к нашему Activity/Fragment даже одного типа. Метод get возвращает их по String key, который по умолчанию формируется как: «android.arch.lifecycle.ViewModelProvider.DefaultKey:» + canonicalName

    Класс ViewModelStores, являет собой фабричный метод, напомню: Фабричный метод — паттерн, который определяет интерфейс для создания объекта, но оставляет подклассам решение о том, какой класс инстанцировать, по факту, позволяет классу делегировать инстанцирование подклассам. На данный момент, в пакете android.arch присутствует как один интерфейс, так и один подкласс ViewModelStore.

    Класс ViewModelStore, класс в котором и находится вся магия, состоит из методов put, get и clear. Про них не стоит беспокоится, поскольку работать напрямую мы с ними не должны, а с get и put и физически не можем, так как они объявлены как default (package-private), соответственно видны только внутри пакета. Но, для общего образования, рассмотрим устройство этого класса. Сам класс хранит в себе HashMap<String, ViewModel>, методы get и put, соответственно, возвращают по ключу (по тому самому, который мы формируем во ViewModelProvider) или добавляют ViewModel. Метод clear(), вызовет метод onCleared() у всех наших ViewModel которые мы добавляли.

    Для примера работы с ViewModel давайте реализуем небольшое приложение, позволяющее выбрать пользователю точку на карте, установить радиус и показывающее, находится человек в этом поле или нет. А также дающее возможность указать WiFi network, если пользователь подключен к нему, будем считать что он в радиусе, вне зависимости от физических координат.


    Для начала создадим две 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;
      }
    }
    

    Теперь перейдем к ViewModel, поскольку у нас есть условие, которое зависит от полученных данных с двух LifeData, нам идеально подойдет MediatorLiveData как холдер самого значения, но поскольку перезапускать сервисы нам невыгодно, поэтому подпишемся к MediatorLiveData без привязки к жизненному циклу с помощью observeForever.  В методе 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);
      }
    }

    В общих чертах мы подписываемся на MutableLiveData, с помощью меnода getStatus() из нашего ViewModel. А также работаем с ним для инициализации и сохранения наших данных.

    Здесь также добавлено несколько проверок, таких как RuntimePermission и проверка на состояние GPS. Как можно заметить, код в Activity получился довольно обширный, в случае сложного UI, гугл рекомендует посмотреть в сторону создания презентера(но это может быть излишество).

    В примере также использовались такие библиотеки как:

    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'

    Полный листинг: here

    Полезные ссылки: here и here

    Android Architecture Components. Часть 1. Введение
    Android Architecture Components. Часть 2. Lifecycle
    Android Architecture Components. Часть 3. LiveData
    Android Architecture Components. Часть 4. ViewModel
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.