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
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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