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
ActivityCore
orBindableActivityCore
for 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 of
BindableActivityCore) right from the start of the
onCreate` callback.
These methods and their overloads commonly setup the following:
- layout resource id
NavHost
implementation id (optional)ViewModel
class
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:
ViewModel
instance created with necessary dependencies, injected to theActivity
and accessible throughmodel()
method of the correspondingActivity
.- Layout elements accessible through
binding()
Activity
method. - Initialized
NavController
accessible throughnav()
method ofActivity
. - Option to subscribe and respond to any
ViewModel-issued event
with 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
@ActivityResultHandler
annotation and uniqueInteger
request code as a parameter. -
A
ViewModel
should implement custom interface (IOpenDocumentTarget
in 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 targetViewModel
is 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 -
FragmentCore
orBindableFragmentCore
.
As with Activity, BindableFragmentCore
generic class expects two type-params:
ViewModel
type corresponding to the Fragment*Binding
class generated by Android databinding library.
- Call parent constructor with corresponding parameters:
- layout resource id
NavigationFragment
id (optional)ViewModel
class
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}" />
...
</layout
MvvmCore 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:
QuitApp
is aViewModel
custom notification that may be called by anyViewModel
with the help ofnotifyView()
method.- As your handler implementation is resolved by
Dagger2
, it should either be decalared in correspondingDagger2
module or have a constructor denoted with@Inject
annotation 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.gradle
file:
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
AppCoreComponent
and contain componentFactory
method accepting at leastContext
andAppCoreConfig
as parameters. -
The top-level Dagger2 component (if only one) or
Activity
scope subcomponent (if there are multiple components are used) must be also extended fromActivityCoreComponent
interface and includeCoreBindingsModule
. Note, thatCoreBindingsModule
is 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
Application
class extended fromAppCore
with top-level Dagger2 component as the type-parameter. -
Application
class must be annotated with@MvvmCoreApp
. -
Your
Application
class must be registered inAndroidManifest.xml
under theandroid:name
field of<application/>
tag. -
Overrided
onCreate
calback of the extendedApplication
class 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 extendedApplication
class, 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