This library provides minimal infrastructure for android application building based on Model-View-ViewModel pattern and Google recommendations concerning Android application architecture, but with some improvements and less boilerplate that usually takes place, when you start MVVM app from scratch. MvvmCore will help to reduce some usual infrastructure routines dealing with:
-
- instantiation by dagger2
- sharing between views
- lifecycle management
- model-to-view notifications
First of all, MvvmCore is about interaction between view model and view. Let's have a look at how the library simplifies Activity/Fragment-ViewModel usage.
Generally, you should extend corresponding MvvmCore class when implementing Activity, Fragment or ViewModel. Classes with Bindable * prefix are used for databinding capabilities.
To go with Activity:
- Extend
ActivityCoreorBindableActivityCorefor databinding capabilities.
Note, that the first type-parameter of BindableActivityCore generic is ViewModel and the second - ViewDataBinding class autogenerated by Android Data binding engine for your Activity.
- Call MvvmCore
setContentView()or bind()method (in case ofBindableActivityCore) right from the start of theonCreate` callback.
These methods and their overloads commonly setup the following:
- layout resource id
NavHostimplementation id (optional)ViewModelclass
public class MainActivity extends BindableActivityCore<MainViewModel, ActivityMainBinding> {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bind(R.layout.activity_main, R.id.navHostFragment, MainViewModel.class);
}
}That's it.
With the code above you'll get:
ViewModelinstance created with necessary dependencies, injected to theActivityand accessible throughmodel()method of the correspondingActivity.- Layout elements accessible through
binding()Activitymethod. - Initialized
NavControlleraccessible throughnav()method ofActivity. - Option to subscribe and respond to any
ViewModel-issued eventwith the help ofsubscribeNotification()method.
Thus, in more complex case the code may be something like that:
public class MainActivity extends BindableActivityCore<MainViewModel, ActivityMainBinding> {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
bind(R.layout.activity_main, R.id.navHostFragment, MainViewModel.class);
//initializing recycler view adapter
binding().recyclerView.setAdapter(new Adapter());
//filling up view model with data from intent
String someId = getIntent().getStringExtra("someId");
model().setId(someId);
//subscribing to some events issued by view model
subscribeNotification(MainViewModel.MainViewModel.class,
notification -> nav().navigate(R.id.somethingFragment));
subscribeNotification(Finish.class, notification -> finish());
.....
}
}What if its necessary to process onActivityResult callback by a ViewModel? MvvmCore allows that by simply implementing IActivityResultHandler with no code bloated the Activity itself :
@ActivityResultHandler(100)
public class OpenDocumentHandler implements IActivityResultHandler {
@Inject
public OpenDocumentHandler() {}
@Override
public void onActivityResult(ActivityCore activity, int resultCode, @Nullable Intent data) {
IOpenDocumentTarget target = (IOpenDocumentTarget) activity.findImplementationOf(IOpenDocumentTarget.class);
if (target != null) {
if (resultCode == RESULT_OK) {
target.onDocumentReady(data.getData());
} else if (resultCode == RESULT_CANCELED) {
target.onDocumentCanceled();
}
}
}
public interface IOpenDocumentTarget {
void onDocumentReady(Uri uri);
void onDocumentCanceled();
}
}The example above shows, how to pass the result of document selection processed by Activity to a ViewModel:
-
The handler should be annotated with
@ActivityResultHandlerannotation and uniqueIntegerrequest code as a parameter. -
A
ViewModelshould implement custom interface (IOpenDocumentTargetin example case). -
The handling code is placed in
onActivityResult()callback of theActivityResultHandler. -
In order to get the necessary
ViewModel, the utility methodfindImplementationOf()can be used. If targetViewModelis owned byFragment, it will also be found by the method.
The library provides the same abilities for Fragments as for Activities. But there are slightly differences in preparation:
- Extend the one of two MvvmCore base classes -
FragmentCoreorBindableFragmentCore.
As with Activity, BindableFragmentCore generic class expects two type-params:
ViewModeltype corresponding to the Fragment*Bindingclass generated by Android databinding library.
- Call parent constructor with corresponding parameters:
- layout resource id
NavigationFragmentid (optional)ViewModelclass
like in the example below:
@ViewModelOwner
public class MyFragment extends BindableFragmentCore<MyViewModel, FragmentMyBinding> {
public MyFragment() {
super(R.layout.fragment_my, R.id.myFragment, MyViewModel.class);
}
@Override
protected void onBindingReady() {
...
}
}If your ViewModel lifecycle is controlled by a Fragment, it's always required to use @ViewModelOwner annotation. Otherwise, if ViewModel owner is an Activity that should share it's own ViewModel with the Fragment - skip the annotaion.
Methods like model(), nav() and subscribeNotification() become accessable with onActivityCreated Fragment lifecycle callback.
binding() method can be used starting from onBindingReady lifecycle callback, that is invoked betweenonActivityCreated and onStart lifecycle callbacks in Fragments extended from BindableFragmentCore.
MvvmCore ViewModel implements ViewModel from Android architecture components. To enable MvvmCore powered ViewModel, ViewModelCore base class must be extended.
As MvvmCore uses Dagger2 to provide ViewModels instances, annotate ViewModel constructor with @Inject:
public class MyViewModel extends ViewModelCore {
@Inject
public MyViewModel(...) {
...
}
}ViewModel constructor may have no arguments or declare any number of necessary dependencies, except Context or any View-specific objects references, because of architecture principles violation.
Being extended from ViewModelCore, your ViewModel becomes to be a subtype of androidx.lifecycle.ViewModel. It supports all androidx.lifecycle.ViewModel features and also implements androidx.databinding.Observable out of the box, so it is ready to provide Data bindable properties for its View like the following:
public class MyViewModel extends ViewModelCore {
...
private String login;
@Bindable
public String getLogin() {
return login;
}
public void setLogin(String login) {
if (!login.equals(this.login)) {
this.login = login;
notifyPropertyChanged(BR.login);
}
}
...
}and corresponding layout (some usual xml code is omitted for brevity):
<layout>
<data>
<variable
name="model"
type="com.example.view.MyViewModel" />
</data>
...
<EditText
android:id="@+id/login"
android:text="@={model.login}" />
...
</layoutMvvmCore provides additional way to broadcast notifications outside of ViewModel and handle them either by Activity/Fragment or by special NotificationHandler (usually, in case of global notifications).
For example, as ViewModel shouldn't have direct reference to Context, the one of the ways to finish Activity from ViewModel is to send corresponding notification to it. In terms of Android architecture components recomendations, this is usually done by introducing LiveData object as ViewModel public property, that is subscribed by Activity or Fragment. But when app grows, such implementation becomes boring and code - bloated.
So, it can be done easier with the help of MvvmCore ViewModel's notifyView() method, that accepts parameter of any type as notification content:
public class MyViewModel extends ViewModelCore {
...
public void onClose() {
...
notifyView(new Finish());
}
...
public static class Finish {
...
}
}and the Activity subscribed to the model notification:
public class MyActivity extends ActivityCore<MyViewModel> {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my, MyViewModel.class);
...
subscribeNotification(MyViewModel.Finish.class, notification -> finish());
}
}In the example above, MyActivity will fininsh itself as soon as MyViewModel object performs notifyView() method call, and in accordance with Activity lifecycle, MyViewModel will be similarly disposed.
Note, that subscribeNotification() method is also dependent upon Activity lifecycle. It's alive from onResume till onPause states of Activity. During other states it is automatically unsubscribed by the library and resubscribed again when onResume occurs. So, you shouldn't care about it by yourself. Just do subscribeNotification() at the moment of View creation, but after ViewModel initialization (for Activity it is onCreate() method, in case of Fragment - onActivityCreated() or onBindingReady() callbacks).
Sometimes, it's required to issue similar notifications by different ViewModels and handle them equally over all Views. Usually that is the case for common tasks like showing Dialog/Toast, open Document or quit app by ViewModel command. And that's a deal for custom NotificationHandler. All you have to do, is to implement INotificationHandler interface as the following:
public class QuitAppHandler implements INotificationHandler<QuitApp> {
private final Context context;
@Inject
public QuitAppHandler(Context context) {
this.context = context;
}
@Override
public void handle(ActivityCore activity, QuitApp notification) {
context.stopService(new Intent(context, AppService.class));
activity.finishAffinity();
}
}Note:
QuitAppis aViewModelcustom notification that may be called by anyViewModelwith the help ofnotifyView()method.- As your handler implementation is resolved by
Dagger2, it should either be decalared in correspondingDagger2module or have a constructor denoted with@Injectannotation like in the example above.
That's it. The rest is done automatically by MvvmCore prebuild processing.
- Check repository and include library dependencies:
repositories {
jcenter()
}
dependencies {
implementation 'com.gorgexec.mvvmcore:mvvmcore:1.0.7'
annotationProcessor 'com.gorgexec.mvvmcore:compiler:1.0.7'
}-
Include Dagger2 dependency to your project.
-
If you intend to use
Navigation component, add it to the project. -
Check additional gradle options in your app's module
build.gradlefile:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
dataBinding {
enabled = true
}In order MvvmCore to be properly used with your app, some settings must be fulfilled.
Your app should contain implementation of AppCoreConfig interface, that is, first of all, used to pass to MvvmCore references to BR resources generated by the Data binding library for your project. You can freely use this class for additional custom config data, if required.
Note, that not all of the requested BR resources might be used in project, so, in case some are not engaged, zero may be used as return, but at least getDefaultModelBR() must return actual value.
The config object is available through appConfig() method of MvvmCore Activity.
-
Your app must have at least one Dagger2 component.
-
Top-level Dagger2 component must be extended from
AppCoreComponentand contain componentFactorymethod accepting at leastContextandAppCoreConfigas parameters. -
The top-level Dagger2 component (if only one) or
Activityscope subcomponent (if there are multiple components are used) must be also extended fromActivityCoreComponentinterface and includeCoreBindingsModule. Note, thatCoreBindingsModuleis composed during compile time, thus at the first build it would not be found.
So, totally the component code may be like that:
@Component(modules = {CoreBindingsModule.class})
public interface AppComponent extends AppCoreComponent, ActivityCoreComponent {
@Component.Factory
interface Factory {
AppComponent create(@BindsInstance Context context, @BindsInstance AppCoreConfig appCoreConfig);
}
}-
Your app should have
Applicationclass extended fromAppCorewith top-level Dagger2 component as the type-parameter. -
Applicationclass must be annotated with@MvvmCoreApp. -
Your
Applicationclass must be registered inAndroidManifest.xmlunder theandroid:namefield of<application/>tag. -
Overrided
onCreatecalback of the extendedApplicationclass must invokesetAppComponent()method, that accepts initialized Dagger2 root component.
Generally, the code will be like the following:
@MvvmCoreApp
public class App extends AppCore<AppComponent> {
@Override
public void onCreate() {
super.onCreate();
setAppComponent(DaggerAppComponent.factory().create(this, new AppConfig()));
}
}- If your Dagger2 configuration consists of several components (subcomponents), it is required to additionally override
getActivityComponent()method of extendedApplicationclass, so that MvvmCore will know, how to get Activity scope related component:
@MvvmCoreApp
public class App extends AppCore<AppComponent> {
@Override
public ActivityCoreComponent getActivityComponent() {
return getAppComponent().activityComponentFactory().create();
}
}TBD