
Компонент 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
