diff --git a/CHANGELOG.md b/CHANGELOG.md index 868946c..e30c163 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,74 @@ +## 1.4.0(2019-02-13) + +- Migrate to AndroidX + +## 1.3.5(2018-03-07) + +- Updated dependencies (Support library, build tools, Gradle). + +## 1.3.4(2017-11-02) + +- Update Gradle plugin. Fix Nullable annotation in getBinding(). + +## 1.3.3(2017-08-21) + +- Fix issue where viewmodels with generic types would not work correctly. + +## 1.3.2(2017-08-21) + +- Updated dependencies (Support library, build tools, gradle) + +## 1.3.1(2017-02-20) + +- Critical issue fixed that was introduced two days ago in 1.3.0 - please update to 1.3.1 (issue is related to the new getViewOptional() method). + +## 1.3.0(2017-02-18) + +- Added ``getViewOptional()`` method which is guaranteed to be non-null. It will return a dummy implemenation in case the View is not null. +- Removed the need to override ```getViewModelClass```, the ViewModel class is now automatically extracted from the ViewModel class definition. + +## 1.2.3(2017-01-4) + +- Fix ProGuard settings. + +## 1.2.2(2017-01-02) + +- Remove wrong jetbrains annotations import. + +## 1.2.1(2016-12-08) + +- Added default (null) implementation of ```getViewModelConfig()``` to the ```ViewModelBaseActivity``` so you don't need to implement it for your Activities. + +## 1.2.0(2016-11-24) + +- Added better support for FragmentStatePagerAdapter by adding [ViewModelStatePagerAdapter.java](library/src/main/java/eu/inloop/viewmodel/support/ViewModelStatePagerAdapter.java). +- Breaking change: Added ```removeViewModel()``` to [IView](library/src/main/java/eu/inloop/viewmodel/IView.java). You don't need to make any changes if you are using the default ViewModelBaseFragment. Otherwise you need to implement this method and return null in case you don't need data binding. + +## 1.1.0(2016-11-24) + +- Added support for data binding +- Breaking change: Added ```getViewModelConfig()``` to [IView](library/src/main/java/eu/inloop/viewmodel/IView.java). You don't need to make any changes if you are using the default ViewModelBaseFragment. Otherwise you need to implement this method and return null in case you don't need data binding. + +## 1.0.1(2016-9-14) + + - Updated dependencies (gradle, build tools, support library). + +## 1.0.0(2016-05-02) + + - We decided it's time for 1.0.0 release after a lot of use in production projects. + - Made getViewModel() as NonNull to prevent a lot of unncessary null checks. It will now throw an IllegalStateException in case it's null (should not happen under normal conditions. Only if you call it too soon - before Activity.onCreate or Fragment.onCreate). + - Breaking change: AbstractViewModel method saveState was renamed to onSaveInstanceState, bindView to onBindView and onModelRemoved to onDestroy. You may need to update your Models if you are overriding those methods. + +## 0.4.1(2016-01-22) + + - Added ViewModelBaseEmptyActivity - which you can extend in case you don't need a ViewModel in your activity (but your fragments have ViewModels). + - Added a sanity check if setModelView() was called - an error will be logged in case you forget to call it. + +## 0.4.0(2016-01-19) + + - The ViewModel instances are now kept within the Activity (onRetainCustomNonConfigurationInstance). All ViewModels in the activity (including fragment ViewModels) will be cleared if you leave the activity. + - If you are not extending ViewModelBaseActivity and instead you are using your own implementation, then you need to update your base Activity. The ViewModelBaseActivity.onCreate() changed and the activity now implements IViewModelProver interface. + ## 0.3.2(2015-05-18) Breaking changes: diff --git a/README.md b/README.md index 30b935b..d7dee4a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ AndroidViewModel ================ +Important notice: Deprecated +-------- +This library served it's purpose for over 3 years. We believe that Google's Android [Architecture Components](https://developer.android.com/topic/libraries/architecture/index.html) are the preferred setup now for new projects. +[INLOOPX](http://www.inloopx.com) is dedicated to continue maintaining this library (no deadline on support end). So rest assured that your existing projects don't need be migrated from AndroidViewModel because of this deprecation. We are only stopping new feature development and don't recommend using it for new projects. + + Separating data and state handling from Fragments or Activities without lots of boilerplate-code. Reducing them to simple dumb views. Basic idea behind this library. @@ -8,7 +14,6 @@ An instance of a ViewModel class is assigned to your Fragment or Activity during You can execute asynchronous tasks in this ViewModel instance and this class is not destroyed during orientation change. All data handling and state logic should be placed inside this class. The Fragment or Activity is just a "dumb" view. -![](website/static/viewmodel_architecture.png) How to implement -------- @@ -16,10 +21,10 @@ How to implement 1. Create an interface for your View by extending [IView](library/src/main/java/eu/inloop/viewmodel/IView.java). We will call it IUserListView for this example. ```java - - public interface IUserListView extends IView { - public void showUsers(List users); - } + public interface IUserListView extends IView { + public void showUsers(List users); + } + ``` 2. Create your ViewModel class by extending [AbstractViewModel](library/src/main/java/eu/inloop/viewmodel/AbstractViewModel.java). For example:
@@ -28,17 +33,12 @@ How to implement .... } ``` -3. Each Fragment or Activity that you would like to associate with a ViewModel will need either to extend [ViewModelActivityBase](library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseActivity.java)/[ViewModelBaseFragment](library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseFragment.java) or copy the implementation from these classes to your base activity/fragment class (in case you can't inherit directly). Override ```getViewModelClass()``` to return the corresponding ViewModel class. For example:
+3. Each Fragment or Activity that you would like to associate with a ViewModel will need either to extend [ViewModelActivityBase](library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseActivity.java)/[ViewModelBaseFragment](library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseFragment.java) or copy the implementation from these classes to your base activity/fragment class (in case you can't inherit directly). For example:
```java public class UserListFragment extends ViewModelBaseFragment implements IUserListView { - @Override - public Class getViewModelClass() { - return UserListViewModel.class; - } - } ``` @@ -64,6 +64,13 @@ You can forward user interaction from the View into the ViewModel simply by call The same goes for the opposite direction, when your asynchronous operation in the ViewModel finished and you would like to forward data to the View to show a list for example: + ```java + getViewOptional().showUsers(userList); + ``` + +The ```getViewOptional()``` method will never return null. It will return a dummy implementation in case the View is null at the moment (e.g. Fragment already destroyed, or between orientation change). +You can also check if the View is not null in case you need to: + ```java if (getView() != null) { getView().showUsers(userList); @@ -77,46 +84,60 @@ Your Fragment argument Bundle and Activity intent Bundle is forwarded to the Vie long userId = arguments.getInt("user_id", -1); } ``` + -How does it work? +Data binding support +-------- +Data binding is supported - extend [ViewModelBaseBindingFragment.java](library/src/main/java/eu/inloop/viewmodel/binding/ViewModelBaseBindingFragment.java) instead of ViewModelBaseFragment and implement ```getViewModelBindingConfig()``` in your Fragment. -A unique global ID is generated for the first time your Fragment or Activity is shown. This ID is passed on during orientation changes. Opening another instance of the same Fragment or Activity will result in a different ID. The ID is unique screen identifier. A ViewModel class is created and bound to this ID. The corresponding ViewModel instance is attached to your Fragment or Activity after an orientation change or if you return to the fragment in the back stack. -The ViewModel is discarded once the Fragment/Activity is not reachable anymore (activity is finished or fragment permanently removed). + ``` java + @Override + public ViewModelBindingConfig getViewModelBindingConfig() { + return new ViewModelBindingConfig(R.layout.fragment_sample_binding, requireActivity()); + } + ``` -Why no controller layer? +That's it. You can then directly use ObservableField in your ViewModels. See [example](sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/SampleBindingViewModel.java). -This is not a strict MVC/MVP architecture - simply because we felt that having another layer between the "model" and the view does not bring enough advantages. So to further reduce the code this was simplified, where the Model is talking to the View over an interface. In mobile application most of the code is about interaction with the UI (getting data from API/DB, showing the data, manipulating, saving) so a more direct connection between the layers felt appropriate. +Special handling for FragmentStatePagerAdapter +-------- +The Android implementation of [FragmentStatePagerAdapter](https://developer.android.com/reference/android/support/v4/app/FragmentStatePagerAdapter.html) is removing Fragments and storing their state. This is in contrast with [FragmentPagerAdapter](https://developer.android.com/reference/android/support/v4/app/FragmentPagerAdapter.html) where the Fragments are just detached but not removed. +We should be also removing ViewModels and storing their state to be consistent with this behaviour. -Sample Workflow: +Use [ViewModelStatePagerAdapter](library/src/main/java/eu/inloop/viewmodel/support/ViewModelStatePagerAdapter.java) instead of the default FragmentStatePagerAdapter. This class is only overriding the ```destroyItem()``` method and making sure that ViewModel is removed. The state is stored/restored automatically. +You can also use the standard FragmentStatePagerAdapter - in that case ViewModels will be kept in memory and removed only when you leave the screen (Activity finished or Fragment removed). -1. Fragment is shown to user. A ViewModel is assigned. -2. Fragment notifies the View that it's ready. -3. ViewModel starts the async task to load data. Tells the view to show progress. -4. User rotates the display. The ViewModel continues with the loading part. -5. The Fragment is recreated after the orientation change is assigned the same ViewModel instance. -6. Recreated Fragment tells the ViewModel that it's ready. ViewModel tells the UI to show loading, because it's still loading the data. -7. ViewModel finishes the async task and tells the Fragment to show the data. -8. User leaves the Activity, the Fragment is destroyed and the ViewModel is removed. +How does it work? +-------- -[![](website/static/lifecycle_thumb.png)](website/static/lifecycle.png) +A unique global ID is generated for the first time your Fragment or Activity is shown. This ID is passed on during orientation changes. Opening another instance of the same Fragment or Activity will result in a different ID. The ID is unique screen identifier. A ViewModel class is created and bound to this ID. The corresponding ViewModel instance is attached to your Fragment or Activity after an orientation change or if you return to the fragment in the back stack. +The ViewModel is discarded once the Fragment/Activity is not reachable anymore (activity is finished or fragment permanently removed). Download -------- -Grab via Gradle: ```groovy -compile 'eu.inloop:androidviewmodel:0.3.2' +compile 'eu.inloop:androidviewmodel:1.4.0' ``` -or Maven: -```xml - - eu.inloop - androidviewmodel - 0.3.2 - + +## Android Studio Template +For faster creating new screens, you can use [Android Studio Template](/template/AVM_Inloop) + +![Android Studio Template Window](/template/template-preview.png) + +### Install template +#### Manually: +Copy the template folder to Android Studio templates folder (`/Applications/Android Studio.app/Contents/plugins/android/lib/templates/others` on Mac) +#### Automatically: +Run the following command to download and install the template automatically (Mac only) ``` -Build and study sample application from source code or download from Google Play.
-[![](website/static/google_play.png)](https://play.google.com/store/apps/details?id=eu.inloop.viewmodel.sample) +curl -o androidviewmodel.zip -Lk https://github.com/inloop/AndroidViewModel/archive/master.zip && unzip androidviewmodel.zip && cp -af AndroidViewModel-master/template/AVM_Inloop/. "/Applications/Android Studio.app/Contents/plugins/android/lib/templates/other/AVM_Inloop" && rm -r AndroidViewModel-master && rm androidviewmodel.zip +``` +Don't forget to restart the Android Studio. + +### Usage +In the Android Studio right click inside the Projet window and select `File > New > AndroidViewModel Inloop > AVM Fragment` + -Development status: Used internally at company on some production applications. Library is under development and API changes might occur anytime. But it should be usuable at this point without any big issues (like memory leaks). +![Android Studio New Template](/template/create-new-template-preview.png) diff --git a/build.gradle b/build.gradle index c06351a..51a9613 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,17 @@ buildscript { repositories { jcenter() + google() } dependencies { - classpath 'com.android.tools.build:gradle:1.2.3' + classpath 'com.android.tools.build:gradle:3.3.1' } } allprojects { repositories { jcenter() + google() } group = 'eu.inloop' diff --git a/gradle.properties b/gradle.properties index 78510d3..f9ed904 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,4 +17,6 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=0.3.2 \ No newline at end of file +android.enableJetifier=true +android.useAndroidX=true +VERSION_NAME=1.4.0 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0bffd23..ab096e7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Wed Feb 13 17:35:05 CET 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/library/build.gradle b/library/build.gradle index 575d900..87a4ceb 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -2,29 +2,35 @@ apply plugin: 'com.android.library' apply plugin: 'maven' android { - compileSdkVersion 22 - buildToolsVersion '22.0.1' + compileSdkVersion 28 defaultConfig { minSdkVersion 15 - targetSdkVersion 22 + targetSdkVersion 28 versionCode 1 versionName VERSION_NAME + consumerProguardFiles 'proguard-rules.pro' + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } + } + + dataBinding { + enabled = true } } dependencies { - compile 'com.android.support:appcompat-v7:22.1.1' + implementation 'androidx.fragment:fragment:1.0.0' + implementation 'androidx.appcompat:appcompat:1.0.2' + + androidTestImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.2-alpha01' + androidTestImplementation 'androidx.test:rules:1.1.2-alpha01' } task androidJavadocs(type: Javadoc) { @@ -44,7 +50,6 @@ task androidSourcesJar(type: Jar) { artifacts { archives androidSourcesJar - archives androidJavadocsJar } uploadArchives { diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro index 9e42513..e32145a 100644 --- a/library/proguard-rules.pro +++ b/library/proguard-rules.pro @@ -15,3 +15,5 @@ #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} +-dontwarn android.databinding.** +-dontwarn eu.inloop.viewmodel.binding.** \ No newline at end of file diff --git a/library/src/androidTest/AndroidManifest.xml b/library/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..2e06fd1 --- /dev/null +++ b/library/src/androidTest/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/library/src/androidTest/java/eu/inloop/viewmodel/ViewModelActivityTest.java b/library/src/androidTest/java/eu/inloop/viewmodel/ViewModelActivityTest.java new file mode 100644 index 0000000..15eb73f --- /dev/null +++ b/library/src/androidTest/java/eu/inloop/viewmodel/ViewModelActivityTest.java @@ -0,0 +1,157 @@ +package eu.inloop.viewmodel; + + +import android.content.pm.ActivityInfo; +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.MediumTest; +import androidx.test.filters.SmallTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Map; + +import eu.inloop.viewmodel.fixture.activity.VMTestActivity; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + + +@RunWith(AndroidJUnit4.class) +public final class ViewModelActivityTest { + + @Rule + public final ActivityTestRule mActivityTestRule = + new ActivityTestRule<>(VMTestActivity.class, false, false); + + @SmallTest + @Test + public void viewModelActivity_onBindView_test() { + mActivityTestRule.launchActivity(VMTestActivity.makeIntent(InstrumentationRegistry.getContext(), true)); + + assertThat(mActivityTestRule.getActivityResult().getResultCode(), is(VMTestActivity.RESULT_CODE_OK)); + } + + @SmallTest + @Test + public void viewModelActivity_getViewModel_getView_test() { + mActivityTestRule.launchActivity(VMTestActivity.makeIntent(InstrumentationRegistry.getContext(), false)); + mActivityTestRule.getActivity().getViewModel().loadData(); + + assertThat(mActivityTestRule.getActivityResult().getResultCode(), is(VMTestActivity.RESULT_CODE_OK)); + } + + @SmallTest + @Test + public void viewModelActivity_getViewModel_getViewOptional_test() { + mActivityTestRule.launchActivity(VMTestActivity.makeIntent(InstrumentationRegistry.getContext(), false)); + mActivityTestRule.getActivity().getViewModel().loadDataOptional(); + + assertThat(mActivityTestRule.getActivityResult().getResultCode(), is(VMTestActivity.RESULT_CODE_OK)); + } + + @SmallTest + @Test + public void viewModelActivity_clearView_test() { + mActivityTestRule.launchActivity(VMTestActivity.makeIntent(InstrumentationRegistry.getContext(), false)); + mActivityTestRule.getActivity().getViewModel().clearView(); + + assertThat(mActivityTestRule.getActivity().getViewModel().getView(), is(nullValue())); + } + + @SmallTest + @Test + public void viewModelActivity_uniqueIdentifier_test() { + mActivityTestRule.launchActivity(VMTestActivity.makeIntent(InstrumentationRegistry.getContext(), false)); + String uniqueIdentifier = mActivityTestRule.getActivity().getViewModel().getUniqueIdentifier(); + + assertThat(uniqueIdentifier, is(notNullValue())); + } + + @SmallTest + @Test + public void viewModelActivity_fragment_getView_test() { + mActivityTestRule.launchActivity(VMTestActivity.makeIntent(InstrumentationRegistry.getContext(), false)); + + mActivityTestRule.getActivity().getTestFragment().getViewModel().loadData(); + + assertThat(mActivityTestRule.getActivityResult().getResultCode(), is(VMTestActivity.RESULT_CODE_OK)); + } + + @SmallTest + @Test + public void viewModelActivity_fragment_remove_test() { + mActivityTestRule.launchActivity(VMTestActivity.makeIntent(InstrumentationRegistry.getContext(), false)); + + String uniqueIdentifierActivity = mActivityTestRule.getActivity().getViewModel().getUniqueIdentifier(); + String uniqueIdentifierFragment = mActivityTestRule.getActivity().getTestFragment().getViewModel().getUniqueIdentifier(); + + Map> viewModels = + mActivityTestRule.getActivity().getViewModelProvider().getViewModels(); + + assertThat(viewModels.containsKey(uniqueIdentifierActivity), is(true)); + assertThat(viewModels.containsKey(uniqueIdentifierFragment), is(true)); + + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + mActivityTestRule.getActivity().removeTestFragment(); + } + }); + + //Check If ViewModel is removed after removing fragment + viewModels = mActivityTestRule.getActivity().getViewModelProvider().getViewModels(); + + assertThat(viewModels.containsKey(uniqueIdentifierActivity), is(true)); + assertThat(viewModels.containsKey(uniqueIdentifierFragment), is(false)); + } + + @SmallTest + @Test + public void viewModelActivity_fragment_model_state_test() { + mActivityTestRule.launchActivity(VMTestActivity.makeIntent(InstrumentationRegistry.getContext(), false)); + + final int stateValue = 1; + mActivityTestRule.getActivity().getTestFragment().getViewModel().setStateValue(stateValue); + + rotateScreen(1); + + int actualStateValue = mActivityTestRule.getActivity().getTestFragment().getViewModel().getStateValue(); + + assertThat(stateValue, is(actualStateValue)); + } + + @MediumTest + @Test + public void viewModelActivity_instance_count_test() { + mActivityTestRule.launchActivity(VMTestActivity.makeIntent(InstrumentationRegistry.getContext(), false)); + + String uniqueIdentifierActivity = mActivityTestRule.getActivity().getViewModel().getUniqueIdentifier(); + String uniqueIdentifierFragment = mActivityTestRule.getActivity().getTestFragment().getViewModel().getUniqueIdentifier(); + + rotateScreen(5); + + Map> viewModels = + mActivityTestRule.getActivity().getViewModelProvider().getViewModels(); + + assertThat(viewModels.size(), is(2)); //activity + fragment + + assertThat(viewModels.containsKey(uniqueIdentifierActivity), is(true)); + assertThat(viewModels.containsKey(uniqueIdentifierFragment), is(true)); + } + + private void rotateScreen(int numOfTimes) { + for (int i = 0; i < numOfTimes; i++) { + mActivityTestRule.getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + mActivityTestRule.getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + } + +} diff --git a/library/src/androidTest/java/eu/inloop/viewmodel/fixture/activity/IVMTestActivityView.java b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/activity/IVMTestActivityView.java new file mode 100644 index 0000000..0d8b0d9 --- /dev/null +++ b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/activity/IVMTestActivityView.java @@ -0,0 +1,9 @@ +package eu.inloop.viewmodel.fixture.activity; + +import eu.inloop.viewmodel.IView; + +public interface IVMTestActivityView extends IView { + + void onLoadData(boolean loaded); + +} diff --git a/library/src/androidTest/java/eu/inloop/viewmodel/fixture/activity/VMTestActivity.java b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/activity/VMTestActivity.java new file mode 100644 index 0000000..5e828f3 --- /dev/null +++ b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/activity/VMTestActivity.java @@ -0,0 +1,72 @@ +package eu.inloop.viewmodel.fixture.activity; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import android.widget.LinearLayout; + +import eu.inloop.viewmodel.base.ViewModelBaseActivity; +import eu.inloop.viewmodel.fixture.fragment.VMTestFragment; + +public class VMTestActivity extends ViewModelBaseActivity implements IVMTestActivityView { + + public static final int RESULT_CODE_OK = 1; + public static final String EXTRA_CALL_ON_BIND = "EXTRA_CALL_ON_BIND"; + + @NonNull + public static Intent makeIntent(@NonNull Context context, boolean callOnBindModel) { + Intent intent = new Intent(context, VMTestActivity.class); + intent.putExtra(EXTRA_CALL_ON_BIND, callOnBindModel); + + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + LinearLayout view = new LinearLayout(this); + view.setId(android.R.id.content); + setContentView(view); + + if (savedInstanceState == null) { + addTestFragment(); + } + + setModelView(this); + } + + public void addTestFragment() { + getSupportFragmentManager() + .beginTransaction() + .add(android.R.id.content, new VMTestFragment()) + .commitNow(); + } + + public void removeTestFragment() { + getSupportFragmentManager() + .beginTransaction() + .remove(getTestFragment()) + .commitNow(); + } + + @NonNull + public VMTestFragment getTestFragment() { + for (Fragment fragment : getSupportFragmentManager().getFragments()) { + if (fragment instanceof VMTestFragment) { + return (VMTestFragment) fragment; + } + } + throw new AssertionError("Fragment not found"); + } + + @Override + public void onLoadData(boolean loaded) { + setResult(RESULT_CODE_OK); + finish(); + } + +} diff --git a/library/src/androidTest/java/eu/inloop/viewmodel/fixture/activity/VMTestActivityViewModel.java b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/activity/VMTestActivityViewModel.java new file mode 100644 index 0000000..129d230 --- /dev/null +++ b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/activity/VMTestActivityViewModel.java @@ -0,0 +1,39 @@ +package eu.inloop.viewmodel.fixture.activity; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import eu.inloop.viewmodel.AbstractViewModel; + +public class VMTestActivityViewModel extends AbstractViewModel { + + private boolean mCallOnBind; + + @Override + public void onCreate(@Nullable Bundle arguments, @Nullable Bundle savedInstanceState) { + super.onCreate(arguments, savedInstanceState); + if (arguments == null) { + throw new AssertionError("Arguments must be set for this ViewModel"); + } + mCallOnBind = arguments.getBoolean(VMTestActivity.EXTRA_CALL_ON_BIND); + } + + @Override + public void onBindView(@NonNull IVMTestActivityView view) { + super.onBindView(view); + + if (mCallOnBind) { + loadData(); + } + } + + public void loadData() { + getView().onLoadData(true); + } + + public void loadDataOptional() { + getViewOptional().onLoadData(true); + } + +} diff --git a/library/src/androidTest/java/eu/inloop/viewmodel/fixture/fragment/IVMTestFragmentView.java b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/fragment/IVMTestFragmentView.java new file mode 100644 index 0000000..f8844d4 --- /dev/null +++ b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/fragment/IVMTestFragmentView.java @@ -0,0 +1,9 @@ +package eu.inloop.viewmodel.fixture.fragment; + +import eu.inloop.viewmodel.IView; + +public interface IVMTestFragmentView extends IView { + + void onLoadData(boolean loaded); + +} diff --git a/library/src/androidTest/java/eu/inloop/viewmodel/fixture/fragment/VMTestFragment.java b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/fragment/VMTestFragment.java new file mode 100644 index 0000000..ca603af --- /dev/null +++ b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/fragment/VMTestFragment.java @@ -0,0 +1,34 @@ +package eu.inloop.viewmodel.fixture.fragment; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import eu.inloop.viewmodel.base.ViewModelBaseFragment; +import eu.inloop.viewmodel.fixture.activity.VMTestActivity; + +public class VMTestFragment extends ViewModelBaseFragment + implements IVMTestFragmentView { + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return new LinearLayout(getContext()); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setModelView(this); + } + + @Override + public void onLoadData(boolean loaded) { + requireActivity().setResult(VMTestActivity.RESULT_CODE_OK); + requireActivity().finish(); + } +} diff --git a/library/src/androidTest/java/eu/inloop/viewmodel/fixture/fragment/VMTestFragmentViewModel.java b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/fragment/VMTestFragmentViewModel.java new file mode 100644 index 0000000..e8710d7 --- /dev/null +++ b/library/src/androidTest/java/eu/inloop/viewmodel/fixture/fragment/VMTestFragmentViewModel.java @@ -0,0 +1,42 @@ +package eu.inloop.viewmodel.fixture.fragment; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import eu.inloop.viewmodel.AbstractViewModel; + +public class VMTestFragmentViewModel extends AbstractViewModel { + + private static final String STATE_INT = "STATE_INT"; + + private int mStateValue; + + @Override + public void onCreate(@Nullable Bundle arguments, @Nullable Bundle savedInstanceState) { + super.onCreate(arguments, savedInstanceState); + + if (savedInstanceState != null) { + mStateValue = savedInstanceState.getInt(STATE_INT); + } + } + + @Override + public void onSaveInstanceState(@NonNull Bundle bundle) { + super.onSaveInstanceState(bundle); + bundle.putInt(STATE_INT, mStateValue); + } + + public void setStateValue(int value) { + mStateValue = value; + } + + public int getStateValue() { + return mStateValue; + } + + public void loadData() { + getView().onLoadData(true); + } + +} diff --git a/library/src/androidTest/java/sample/viewmodel/inloop/eu/viewmodelsample/ApplicationTest.java b/library/src/androidTest/java/sample/viewmodel/inloop/eu/viewmodelsample/ApplicationTest.java deleted file mode 100644 index 3844758..0000000 --- a/library/src/androidTest/java/sample/viewmodel/inloop/eu/viewmodelsample/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package sample.viewmodel.inloop.eu.viewmodelsample; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/library/src/main/java/eu/inloop/viewmodel/AbstractViewModel.java b/library/src/main/java/eu/inloop/viewmodel/AbstractViewModel.java index 2e410e1..560b4c3 100644 --- a/library/src/main/java/eu/inloop/viewmodel/AbstractViewModel.java +++ b/library/src/main/java/eu/inloop/viewmodel/AbstractViewModel.java @@ -3,20 +3,32 @@ import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; - -import eu.inloop.viewmodel.IView; +import androidx.annotation.CallSuper; +import androidx.annotation.CheckResult; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import android.util.Log; +import android.view.View; public abstract class AbstractViewModel { + @Nullable private String mUniqueIdentifier; @Nullable private T mView; - void setUniqueIdentifier(String uniqueIdentifier) { + @Nullable + private final Class mClassType; + + private boolean mBindViewWasCalled; + + public AbstractViewModel() { + mClassType = ProxyViewHelper.getGenericType(getClass(), IView.class); + } + + void setUniqueIdentifier(@NonNull final String uniqueIdentifier) { mUniqueIdentifier = uniqueIdentifier; } @@ -25,6 +37,8 @@ void setUniqueIdentifier(String uniqueIdentifier) { * @return An app unique identifier for the current viewmodel instance (will be kept during orientation * change). This identifier will be reset in case the corresponding activity is killed. */ + @SuppressWarnings("unused") + @Nullable public String getUniqueIdentifier() { return mUniqueIdentifier; } @@ -35,38 +49,69 @@ public String getUniqueIdentifier() { * {@link Activity#getIntent()}.{@link Intent#getExtras()} * @param savedInstanceState bundle with saved state, will be not null * only in case the system is killed due to low memory - * and restored (and {@link #saveState(Bundle)} returned a non-null bundle. + * and restored (and {@link #onSaveInstanceState(Bundle)} returned a non-null bundle. */ - @SuppressWarnings("EmptyMethod") + @SuppressWarnings({"EmptyMethod", "UnusedParameters"}) public void onCreate(@Nullable Bundle arguments, @Nullable Bundle savedInstanceState) { } - public void bindView(@NonNull T view) { + /** + * This method is an equivalent of {@link Fragment#onViewCreated(View, Bundle)} or {@link Activity#onCreate(Bundle)}. + * At this point, the View is ready and you can initialise it. + * @param view - View assigned to this ViewModel + */ + @CallSuper + public void onBindView(@NonNull T view) { + mBindViewWasCalled = true; mView = view; } + @CheckResult + @Nullable public T getView() { return mView; } + /** + * Alternative to {@link #getView()}. This method will never return a null view - not even in case the current Fragment or + * Activity is already destroyed or between orientation change. It will return a dummy + * implementation in that case. + * @return the View instance which implements {@link T}. It's never null. + */ + @CheckResult + @NonNull + public T getViewOptional() { + if (mView != null) { + return mView; + } else { + if (mClassType == null) { + throw new IllegalStateException("Your view must implement IView"); + } + return ProxyViewHelper.init(mClassType); + } + } + + @CallSuper public void clearView() { mView = null; } - @SuppressWarnings("EmptyMethod") - public void saveState(Bundle bundle) { + @SuppressWarnings({"EmptyMethod", "UnusedParameters"}) + public void onSaveInstanceState(@NonNull final Bundle bundle) { } - @SuppressWarnings("EmptyMethod") + @SuppressWarnings({"EmptyMethod", "WeakerAccess"}) public void onStop() { } - @SuppressWarnings("EmptyMethod") + @SuppressWarnings({"EmptyMethod", "WeakerAccess"}) public void onStart() { - + if (mView == null && !mBindViewWasCalled) { + Log.e("AndroidViewModel", this.getClass().getSimpleName() + " - no view associated. You probably did not call setModelView() in your Fragment or Activity"); + } } /** @@ -74,7 +119,7 @@ public void onStart() { * This is a good place to empty any planned tasks that are useless without a UI. */ @SuppressWarnings("EmptyMethod") - public void onModelRemoved() { + public void onDestroy() { } } diff --git a/library/src/main/java/eu/inloop/viewmodel/IView.java b/library/src/main/java/eu/inloop/viewmodel/IView.java index 300c4b3..7bd2054 100644 --- a/library/src/main/java/eu/inloop/viewmodel/IView.java +++ b/library/src/main/java/eu/inloop/viewmodel/IView.java @@ -1,5 +1,31 @@ package eu.inloop.viewmodel; +import android.app.Activity; +import androidx.annotation.Nullable; + +import eu.inloop.viewmodel.base.ViewModelBaseActivity; +import eu.inloop.viewmodel.base.ViewModelBaseFragment; +import eu.inloop.viewmodel.binding.ViewModelBindingConfig; + +/** + * Any Activity or Fragment that needs a ViewModel needs to implement this interface. + * You don't need to implement it yourself - use {@link ViewModelBaseActivity} and + * {@link ViewModelBaseFragment} instead. + */ public interface IView { + /** + * This method is used for Data Binding to bind correct layout and variable automatically + * Can return null value in case that Data Binding is not used. + * + * @return defined ViewModelBinding Config for a specific screen. + */ + @Nullable + ViewModelBindingConfig getViewModelBindingConfig(); + /** + * Implement this method to remove the ViewModel associated with the Fragment or Activity. + * This is usually implemented by calling {@link ViewModelHelper#removeViewModel(Activity)}, + * see {@link ViewModelBaseActivity#removeViewModel()} and {@link ViewModelBaseFragment#removeViewModel()}. + */ + void removeViewModel(); } diff --git a/library/src/main/java/eu/inloop/viewmodel/IViewModelProvider.java b/library/src/main/java/eu/inloop/viewmodel/IViewModelProvider.java new file mode 100644 index 0000000..76db555 --- /dev/null +++ b/library/src/main/java/eu/inloop/viewmodel/IViewModelProvider.java @@ -0,0 +1,17 @@ +package eu.inloop.viewmodel; + +import androidx.annotation.Nullable; + +/** + * Your {@link android.app.Activity} must implement this interface if + * any of the contained Fragments the {@link eu.inloop.viewmodel.ViewModelHelper} + */ +public interface IViewModelProvider { + + /** + * See {@link eu.inloop.viewmodel.base.ViewModelBaseActivity} on how to implement. + * @return the {@link ViewModelProvider}. + */ + @Nullable + ViewModelProvider getViewModelProvider(); +} \ No newline at end of file diff --git a/library/src/main/java/eu/inloop/viewmodel/ProxyViewHelper.java b/library/src/main/java/eu/inloop/viewmodel/ProxyViewHelper.java new file mode 100644 index 0000000..94d8ec4 --- /dev/null +++ b/library/src/main/java/eu/inloop/viewmodel/ProxyViewHelper.java @@ -0,0 +1,52 @@ +package eu.inloop.viewmodel; + + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; + +public class ProxyViewHelper { + + private static final class ProxyDummyClass { + } + + private static final ProxyDummyClass sDummyClass = new ProxyDummyClass(); + private static final Class[] sInterfaces = new Class[1]; + + private ProxyViewHelper() { + } + + @SuppressWarnings("unchecked") + @NonNull + static T init(@NonNull Class in) { + sInterfaces[0] = in; + return (T) Proxy.newProxyInstance(sDummyClass.getClass().getClassLoader(), sInterfaces, sInvocationHandler); + } + + @Nullable + public static Class getGenericType(@NonNull Class in, @NonNull Class whichExtends) { + final Type genericSuperclass = in.getGenericSuperclass(); + if (genericSuperclass instanceof ParameterizedType) { + final Type[] typeArgs = ((ParameterizedType) genericSuperclass).getActualTypeArguments(); + for (Type arg : typeArgs) { + if (arg instanceof ParameterizedType) { + arg = ((ParameterizedType) arg).getRawType(); + } + if (arg instanceof Class) { + final Class argClass = (Class) arg; + if (whichExtends.isAssignableFrom(argClass)) { + return argClass; + } + } + } + } + return null; + } + + private static final InvocationHandler sInvocationHandler = (proxy, method, args) -> null; + +} diff --git a/library/src/main/java/eu/inloop/viewmodel/ViewModelHelper.java b/library/src/main/java/eu/inloop/viewmodel/ViewModelHelper.java index a368ffa..b00234d 100644 --- a/library/src/main/java/eu/inloop/viewmodel/ViewModelHelper.java +++ b/library/src/main/java/eu/inloop/viewmodel/ViewModelHelper.java @@ -2,31 +2,49 @@ import android.app.Activity; import android.content.Intent; +import androidx.databinding.DataBindingUtil; +import androidx.databinding.ViewDataBinding; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import android.util.Log; +import android.view.LayoutInflater; import java.util.UUID; +import eu.inloop.viewmodel.binding.ViewModelBindingConfig; + public class ViewModelHelper> { + @NonNull + private static final String STATE_STRING_SCREEN_IDENTIFIER = ViewModelHelper.class + ".state.string.identifier"; //NON-NLS + + @Nullable private String mScreenId; + + @Nullable private R mViewModel; + + @Nullable + private ViewDataBinding mBinding; + private boolean mModelRemoved; private boolean mOnSaveInstanceCalled; /** * Call from {@link android.app.Activity#onCreate(android.os.Bundle)} or - * {@link android.support.v4.app.Fragment#onCreate(android.os.Bundle)} + * {@link androidx.core.app.Fragment#onCreate(android.os.Bundle)} + * + * @param activity parent activity * @param savedInstanceState savedInstance state from {@link Activity#onCreate(Bundle)} or * {@link Fragment#onCreate(Bundle)} - * @param viewModelClass the {@link Class} of your ViewModel - * @param arguments pass {@link Fragment#getArguments()} or - * {@link Activity#getIntent()}.{@link Intent#getExtras() getExtras()} + * @param viewModelClass the {@link Class} of your ViewModel + * @param arguments pass {@link Fragment#getArguments()} or + * {@link Activity#getIntent()}.{@link Intent#getExtras() getExtras()} */ - public void onCreate(@Nullable Bundle savedInstanceState, + public void onCreate(@NonNull Activity activity, + @Nullable Bundle savedInstanceState, @Nullable Class> viewModelClass, @Nullable Bundle arguments) { // no viewmodel for this fragment @@ -39,42 +57,87 @@ public void onCreate(@Nullable Bundle savedInstanceState, if (savedInstanceState == null) { mScreenId = UUID.randomUUID().toString(); } else { - mScreenId = savedInstanceState.getString("identifier"); + mScreenId = savedInstanceState.getString(STATE_STRING_SCREEN_IDENTIFIER); + if (null == mScreenId) { + throw new IllegalStateException("Bundle from onSaveInstanceState() didn't contain screen identifier. " + //NON-NLS + "Did you call ViewModelHelper.onSaveInstanceState?"); //NON-NLS + } + mOnSaveInstanceCalled = false; } // get model instance for this screen - final ViewModelProvider.ViewModelWrapper viewModelWrapper = ViewModelProvider.getInstance().getViewModel(mScreenId, viewModelClass); + final ViewModelProvider viewModelProvider = getViewModelProvider(activity).getViewModelProvider(); + if (null == viewModelProvider) { + throw new IllegalStateException("ViewModelProvider for activity " + activity + " was null."); //NON-NLS + } + + final ViewModelProvider.ViewModelWrapper viewModelWrapper = viewModelProvider.getViewModel(mScreenId, viewModelClass); //noinspection unchecked mViewModel = (R) viewModelWrapper.viewModel; if (viewModelWrapper.wasCreated) { // detect that the system has killed the app - saved instance is not null, but the model was recreated if (BuildConfig.DEBUG && savedInstanceState != null) { - Log.d("model", "Fragment recreated by system - restoring viewmodel"); + Log.d("model", "Fragment recreated by system or ViewModelStatePagerAdapter - restoring viewmodel"); //NON-NLS } mViewModel.onCreate(arguments, savedInstanceState); } } /** - * Call from {@link android.support.v4.app.Fragment#onViewCreated(android.view.View, android.os.Bundle)} + * Call from {@link androidx.core.app.Fragment#onViewCreated(android.view.View, android.os.Bundle)} * or {@link android.app.Activity#onCreate(android.os.Bundle)} - * @param view + * + * @param view view */ - public void setView(@NonNull T view) { + public void setView(@NonNull final T view) { if (mViewModel == null) { //no viewmodel for this fragment return; } - mViewModel.bindView(view); + mViewModel.onBindView(view); + } + + public void performBinding(@NonNull final IView bindingView) { + // skip if already create + if (mBinding != null) { + return; + } + + // get ViewModelBinding config + final ViewModelBindingConfig viewModelConfig = bindingView.getViewModelBindingConfig(); + // if fragment not providing ViewModelBindingConfig, do not perform binding operations + if (viewModelConfig == null) { + return; + } + + // perform Data Binding initialization + final ViewDataBinding viewDataBinding; + if (bindingView instanceof Activity) { + viewDataBinding = DataBindingUtil.setContentView(((Activity) bindingView), viewModelConfig.getLayoutResource()); + } else if (bindingView instanceof Fragment) { + viewDataBinding = DataBindingUtil.inflate(LayoutInflater.from(viewModelConfig.getContext()), viewModelConfig.getLayoutResource(), null, false); + } else { + throw new IllegalArgumentException("View must be an instance of Activity or Fragment (support-v4)."); + } + + // bind all together + if (!viewDataBinding.setVariable(viewModelConfig.getViewModelVariableName(), getViewModel())) { + throw new IllegalArgumentException("Binding variable wasn't set successfully. Probably viewModelVariableName of your " + + "ViewModelBindingConfig of " + bindingView.getClass().getSimpleName() + " doesn't match any variable in " + + viewDataBinding.getClass().getSimpleName()); + } + + mBinding = viewDataBinding; } /** - * Use in case this model is associated with an {@link android.support.v4.app.Fragment} - * Call from {@link android.support.v4.app.Fragment#onDestroyView()}. Use in case model is associated + * Use in case this model is associated with an {@link androidx.core.app.Fragment} + * Call from {@link androidx.core.app.Fragment#onDestroyView()}. Use in case model is associated * with Fragment - * @param fragment + * + * @param fragment fragment */ public void onDestroyView(@NonNull Fragment fragment) { if (mViewModel == null) { @@ -83,50 +146,55 @@ public void onDestroyView(@NonNull Fragment fragment) { } mViewModel.clearView(); if (fragment.getActivity() != null && fragment.getActivity().isFinishing()) { - removeViewModel(); + removeViewModel(fragment.getActivity()); } + mBinding = null; } /** - * Use in case this model is associated with an {@link android.support.v4.app.Fragment} - * Call from {@link android.support.v4.app.Fragment#onDestroy()} - * @param fragment + * Use in case this model is associated with an {@link androidx.core.app.Fragment} + * Call from {@link androidx.core.app.Fragment#onDestroy()} + * + * @param fragment fragment */ - public void onDestroy(@NonNull Fragment fragment) { + public void onDestroy(@NonNull final Fragment fragment) { if (mViewModel == null) { //no viewmodel for this fragment return; } - if (fragment.getActivity().isFinishing()) { - removeViewModel(); + if (fragment.requireActivity().isFinishing()) { + removeViewModel(fragment.requireActivity()); } else if (fragment.isRemoving() && !mOnSaveInstanceCalled) { // The fragment can be still in backstack even if isRemoving() is true. // We check mOnSaveInstanceCalled - if this was not called then the fragment is totally removed. if (BuildConfig.DEBUG) { - Log.d("mode", "Removing viewmodel - fragment replaced"); + Log.d("mode", "Removing viewmodel - fragment replaced"); //NON-NLS } - removeViewModel(); + removeViewModel(fragment.requireActivity()); } + mBinding = null; } /** * Use in case this model is associated with an {@link android.app.Activity} * Call from {@link android.app.Activity#onDestroy()} - * @param activity + * + * @param activity activity */ - public void onDestroy(@NonNull Activity activity) { + public void onDestroy(@NonNull final Activity activity) { if (mViewModel == null) { //no viewmodel for this fragment return; } mViewModel.clearView(); if (activity.isFinishing()) { - removeViewModel(); + removeViewModel(activity); } + mBinding = null; } /** - * Call from {@link android.app.Activity#onStop()} or {@link android.support.v4.app.Fragment#onStop()} + * Call from {@link android.app.Activity#onStop()} or {@link androidx.core.app.Fragment#onStop()} */ public void onStop() { if (mViewModel == null) { @@ -137,7 +205,7 @@ public void onStop() { } /** - * Call from {@link android.app.Activity#onStart()} ()} or {@link android.support.v4.app.Fragment#onStart()} ()} + * Call from {@link android.app.Activity#onStart()} ()} or {@link androidx.core.app.Fragment#onStart()} ()} */ public void onStart() { if (mViewModel == null) { @@ -148,30 +216,60 @@ public void onStart() { } - @Nullable + /** + * Returns the current ViewModel instance associated with the Fragment or Activity. + * Throws an {@link IllegalStateException} in case the ViewModel is null. This can happen + * if you call this method too soon - before {@link Activity#onCreate(Bundle)} or {@link Fragment#onCreate(Bundle)} + * or this {@link ViewModelHelper} is not properly setup. + * + * @return {@link R} + */ + @NonNull public R getViewModel() { + if (null == mViewModel) { + throw new IllegalStateException("ViewModel is not ready. Are you calling this method before Activity/Fragment onCreate?"); //NON-NLS + } return mViewModel; } /** * Call from {@link android.app.Activity#onSaveInstanceState(android.os.Bundle)} - * or {@link android.support.v4.app.Fragment#onSaveInstanceState(android.os.Bundle)}. + * or {@link androidx.core.app.Fragment#onSaveInstanceState(android.os.Bundle)}. * This allows the model to save its state. - * @param bundle + * + * @param bundle bundle */ public void onSaveInstanceState(@NonNull Bundle bundle) { - bundle.putString("identifier", mScreenId); + bundle.putString(STATE_STRING_SCREEN_IDENTIFIER, mScreenId); if (mViewModel != null) { - mViewModel.saveState(bundle); + mViewModel.onSaveInstanceState(bundle); mOnSaveInstanceCalled = true; } } - private void removeViewModel() { - if (!mModelRemoved) { - ViewModelProvider.getInstance().remove(mScreenId); - mViewModel.onModelRemoved(); + @Nullable + public ViewDataBinding getBinding() { + return mBinding; + } + + public void removeViewModel(@NonNull final Activity activity) { + if (mViewModel != null && !mModelRemoved) { + final ViewModelProvider viewModelProvider = getViewModelProvider(activity).getViewModelProvider(); + if (null == viewModelProvider) { + throw new IllegalStateException("ViewModelProvider for activity " + activity + " was null."); //NON-NLS + } + viewModelProvider.remove(mScreenId); + mViewModel.onDestroy(); mModelRemoved = true; + mBinding = null; + } + } + + @NonNull + private IViewModelProvider getViewModelProvider(@NonNull Activity activity) { + if (!(activity instanceof IViewModelProvider)) { + throw new IllegalStateException("Your activity must implement IViewModelProvider"); //NON-NLS } + return ((IViewModelProvider) activity); } } diff --git a/library/src/main/java/eu/inloop/viewmodel/ViewModelProvider.java b/library/src/main/java/eu/inloop/viewmodel/ViewModelProvider.java index f6a1026..ab07ecb 100644 --- a/library/src/main/java/eu/inloop/viewmodel/ViewModelProvider.java +++ b/library/src/main/java/eu/inloop/viewmodel/ViewModelProvider.java @@ -1,36 +1,67 @@ package eu.inloop.viewmodel; -import android.support.annotation.NonNull; -import android.util.SparseArray; +import android.app.Activity; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.FragmentActivity; +import java.util.Collections; import java.util.HashMap; +import java.util.Map; /** * Create and keep this class inside your Activity. Store it - * in {@link android.support.v4.app.FragmentActivity#onRetainCustomNonConfigurationInstance() - * and restore in {@link android.support.v4.app.FragmentActivity#onCreate(android.os.Bundle)} before + * in {@link androidx.core.app.FragmentActivity#onRetainCustomNonConfigurationInstance() + * and restore in {@link androidx.core.app.FragmentActivity#onCreate(android.os.Bundle)} before * calling the super implemenentation. */ public class ViewModelProvider { - private static final ViewModelProvider sInstance = new ViewModelProvider(); - + @NonNull private final HashMap> mViewModelCache; + @NonNull + public static ViewModelProvider newInstance(@NonNull final FragmentActivity activity) { + if (activity.getLastCustomNonConfigurationInstance() == null) { + return new ViewModelProvider(); + } else { + return (ViewModelProvider) activity.getLastCustomNonConfigurationInstance(); + } + } + + @SuppressWarnings({"deprecation", "unused"}) + @NonNull + @Deprecated + public static ViewModelProvider newInstance(@NonNull final Activity activity) { + if (activity.getLastNonConfigurationInstance() == null) { + return new ViewModelProvider(); + } else { + return (ViewModelProvider) activity.getLastNonConfigurationInstance(); + } + } + private ViewModelProvider() { mViewModelCache = new HashMap<>(); } - public static ViewModelProvider getInstance() { - return sInstance; + public synchronized void remove(@Nullable String modeIdentifier) { + mViewModelCache.remove(modeIdentifier); } - public synchronized void remove(String modeIdentifier) { - mViewModelCache.remove(modeIdentifier); + public synchronized void removeAllViewModels() { + mViewModelCache.clear(); + } + + @VisibleForTesting + @NonNull + Map> getViewModels() { + return Collections.unmodifiableMap(mViewModelCache); } @SuppressWarnings("unchecked") @NonNull - public synchronized ViewModelWrapper getViewModel(String modelIdentifier, @NonNull Class> viewModelClass) { + public synchronized ViewModelWrapper getViewModel(@NonNull final String modelIdentifier, + @NonNull final Class> viewModelClass) { AbstractViewModel instance = (AbstractViewModel) mViewModelCache.get(modelIdentifier); if (instance != null) { return new ViewModelWrapper<>(instance, false); @@ -38,20 +69,20 @@ public synchronized ViewModelWrapper getViewModel(String mo try { instance = viewModelClass.newInstance(); - instance.setUniqueIdentifier(modelIdentifier); - mViewModelCache.put(modelIdentifier, instance); - return new ViewModelWrapper<>(instance, true); - } catch (Exception ex) { + } catch (final Exception ex) { throw new RuntimeException(ex); } + instance.setUniqueIdentifier(modelIdentifier); + mViewModelCache.put(modelIdentifier, instance); + return new ViewModelWrapper<>(instance, true); } - public static class ViewModelWrapper { + final static class ViewModelWrapper { @NonNull - public final AbstractViewModel viewModel; - public final boolean wasCreated; + final AbstractViewModel viewModel; + final boolean wasCreated; - private ViewModelWrapper(@NonNull AbstractViewModel mViewModel, boolean mWasCreated) { + private ViewModelWrapper(@NonNull AbstractViewModel mViewModel, final boolean mWasCreated) { this.viewModel = mViewModel; this.wasCreated = mWasCreated; } diff --git a/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseActivity.java b/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseActivity.java index b77bfec..f7b895a 100644 --- a/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseActivity.java +++ b/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseActivity.java @@ -1,59 +1,94 @@ package eu.inloop.viewmodel.base; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v7.app.AppCompatActivity; +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import eu.inloop.viewmodel.AbstractViewModel; import eu.inloop.viewmodel.IView; +import eu.inloop.viewmodel.ProxyViewHelper; import eu.inloop.viewmodel.ViewModelHelper; +import eu.inloop.viewmodel.binding.ViewModelBindingConfig; -public abstract class ViewModelBaseActivity> extends AppCompatActivity implements IView { +public abstract class ViewModelBaseActivity> extends ViewModelBaseEmptyActivity implements IView { + @NonNull private final ViewModelHelper mViewModeHelper = new ViewModelHelper<>(); + @CallSuper @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mViewModeHelper.onCreate(savedInstanceState, getViewModelClass(), getIntent().getExtras()); + + Class> viewModelClass = getViewModelClass(); + // try to extract the ViewModel class from the implementation + if (viewModelClass == null) { + //noinspection unchecked + viewModelClass = (Class>) ProxyViewHelper.getGenericType(getClass(), AbstractViewModel.class); + } + mViewModeHelper.onCreate(this, savedInstanceState, viewModelClass, getIntent().getExtras()); } /** * Call this after your view is ready - usually on the end of {@link android.app.Activity#onCreate(Bundle)} - * @param view + * @param view view */ - public void setModelView(@NonNull T view) { + @SuppressWarnings("unused") + public void setModelView(@NonNull final T view) { mViewModeHelper.setView(view); } - public abstract Class getViewModelClass(); + @Nullable + public Class getViewModelClass() { + return null; + } + @CallSuper @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); mViewModeHelper.onSaveInstanceState(outState); } + @CallSuper @Override public void onStart() { super.onStart(); mViewModeHelper.onStart(); } + @CallSuper @Override public void onStop() { super.onStop(); mViewModeHelper.onStop(); } + @CallSuper @Override public void onDestroy() { mViewModeHelper.onDestroy(this); super.onDestroy(); } + /** + * @see ViewModelHelper#getViewModel() + */ @SuppressWarnings("unused") + @NonNull public R getViewModel() { return mViewModeHelper.getViewModel(); } + + @Override + public void removeViewModel() { + mViewModeHelper.removeViewModel(this); + } + + @Nullable + @Override + public ViewModelBindingConfig getViewModelBindingConfig() { + return null; + } } diff --git a/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseEmptyActivity.java b/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseEmptyActivity.java new file mode 100644 index 0000000..f98db59 --- /dev/null +++ b/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseEmptyActivity.java @@ -0,0 +1,52 @@ +package eu.inloop.viewmodel.base; + +import android.os.Bundle; +import androidx.annotation.CallSuper; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import eu.inloop.viewmodel.IViewModelProvider; +import eu.inloop.viewmodel.ViewModelProvider; + +/** + * All your activities must extend this activity - even in case your activity has no viewmodel. The fragment viewmodels are using the {@link IViewModelProvider} + * interface to get the {@link ViewModelProvider} from the current activity. + * You can copy this implementation in case you don't want to extend this class. + */ +public abstract class ViewModelBaseEmptyActivity extends AppCompatActivity implements IViewModelProvider { + + @Nullable + private ViewModelProvider mViewModelProvider; + + @CallSuper + @Override + protected void onCreate(@Nullable final Bundle savedInstanceState) { + //This code must be execute prior to super.onCreate() + mViewModelProvider = ViewModelProvider.newInstance(this); + super.onCreate(savedInstanceState); + } + + @Override + @Nullable + public Object onRetainCustomNonConfigurationInstance() { + return mViewModelProvider; + } + + @CallSuper + @Override + public void onStop() { + super.onStop(); + if (isFinishing()) { + if (null == mViewModelProvider) { + throw new IllegalStateException("ViewModelProvider for activity " + this + " was null."); //NON-NLS + } + mViewModelProvider.removeAllViewModels(); + } + } + + @Nullable + @Override + public ViewModelProvider getViewModelProvider() { + return mViewModelProvider; + } +} diff --git a/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseFragment.java b/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseFragment.java index 654b2b6..19d57b9 100644 --- a/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseFragment.java +++ b/library/src/main/java/eu/inloop/viewmodel/base/ViewModelBaseFragment.java @@ -1,68 +1,109 @@ package eu.inloop.viewmodel.base; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import android.view.View; import eu.inloop.viewmodel.AbstractViewModel; import eu.inloop.viewmodel.IView; +import eu.inloop.viewmodel.ProxyViewHelper; import eu.inloop.viewmodel.ViewModelHelper; +import eu.inloop.viewmodel.binding.ViewModelBindingConfig; public abstract class ViewModelBaseFragment> extends Fragment implements IView { - private final ViewModelHelper mViewModeHelper = new ViewModelHelper<>(); + @NonNull + private final ViewModelHelper mViewModelHelper = new ViewModelHelper<>(); + @CallSuper @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mViewModeHelper.onCreate(savedInstanceState, getViewModelClass(), getArguments()); - } - - public abstract Class getViewModelClass(); - /** - * Call this after your view is ready - usually on the end of {@link Fragment#onViewCreated(View, Bundle)} - * @param view - */ - protected void setModelView(@NonNull T view) { - mViewModeHelper.setView(view); + Class> viewModelClass = getViewModelClass(); + // try to extract the ViewModel class from the implementation + if (viewModelClass == null) { + //noinspection unchecked + viewModelClass = (Class>) ProxyViewHelper.getGenericType(getClass(), AbstractViewModel.class); + } + getViewModelHelper().onCreate(requireActivity(), savedInstanceState, viewModelClass, getArguments()); } + @CallSuper @Override - public void onSaveInstanceState(Bundle outState) { + public void onSaveInstanceState(@NonNull final Bundle outState) { super.onSaveInstanceState(outState); - mViewModeHelper.onSaveInstanceState(outState); + getViewModelHelper().onSaveInstanceState(outState); } + @CallSuper @Override public void onStart() { super.onStart(); - mViewModeHelper.onStart(); + getViewModelHelper().onStart(); } + @CallSuper @Override public void onStop() { super.onStop(); - mViewModeHelper.onStop(); + getViewModelHelper().onStop(); } + @CallSuper @Override public void onDestroyView() { - mViewModeHelper.onDestroyView(this); + getViewModelHelper().onDestroyView(this); super.onDestroyView(); } + @CallSuper @Override public void onDestroy() { - mViewModeHelper.onDestroy(this); + getViewModelHelper().onDestroy(this); super.onDestroy(); } + @Nullable + public Class getViewModelClass() { + return null; + } + + /** + * @see ViewModelHelper#getViewModel() + */ + @NonNull @SuppressWarnings("unused") public R getViewModel() { - return mViewModeHelper.getViewModel(); + return getViewModelHelper().getViewModel(); } + @Nullable + @Override + public ViewModelBindingConfig getViewModelBindingConfig() { + return null; + } + + @NonNull + public ViewModelHelper getViewModelHelper() { + return mViewModelHelper; + } + + @Override + public void removeViewModel() { + mViewModelHelper.removeViewModel(requireActivity()); + } + + /** + * Call this after your view is ready - usually on the end of {@link + * Fragment#onViewCreated(View, Bundle)} + * + * @param view view + */ + protected void setModelView(@NonNull final T view) { + getViewModelHelper().setView(view); + } } diff --git a/library/src/main/java/eu/inloop/viewmodel/binding/ViewModelBaseBindingFragment.java b/library/src/main/java/eu/inloop/viewmodel/binding/ViewModelBaseBindingFragment.java new file mode 100644 index 0000000..4fabe9e --- /dev/null +++ b/library/src/main/java/eu/inloop/viewmodel/binding/ViewModelBaseBindingFragment.java @@ -0,0 +1,48 @@ +package eu.inloop.viewmodel.binding; + +import androidx.databinding.ViewDataBinding; +import android.os.Bundle; +import androidx.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import eu.inloop.viewmodel.AbstractViewModel; +import eu.inloop.viewmodel.IView; +import eu.inloop.viewmodel.base.ViewModelBaseFragment; + +public abstract class ViewModelBaseBindingFragment, B extends ViewDataBinding> + extends ViewModelBaseFragment + implements IView { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getViewModelHelper().performBinding(this); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + getViewModelHelper().performBinding(this); + final ViewDataBinding binding = getViewModelHelper().getBinding(); + if (binding != null) { + return binding.getRoot(); + } else { + throw new IllegalStateException("Binding cannot be null. Perform binding before calling getBinding()"); + } + } + + @SuppressWarnings("unused") + @Nullable + public B getBinding() { + try { + return (B) getViewModelHelper().getBinding(); + } catch (ClassCastException ex) { + throw new IllegalStateException("Method getViewModelBindingConfig() has to return same " + + "ViewDataBinding type as it is set to base Fragment"); + } + } +} diff --git a/library/src/main/java/eu/inloop/viewmodel/binding/ViewModelBindingConfig.java b/library/src/main/java/eu/inloop/viewmodel/binding/ViewModelBindingConfig.java new file mode 100644 index 0000000..4cb750c --- /dev/null +++ b/library/src/main/java/eu/inloop/viewmodel/binding/ViewModelBindingConfig.java @@ -0,0 +1,59 @@ +package eu.inloop.viewmodel.binding; + +import android.content.Context; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; + +import eu.inloop.viewmodel.BR; + +/** + * Use this to define a ViewModelBinding Config for a specific screen. + *

+ * Config contains layout resource ID, Context, ViewModel binding variable name + */ +public class ViewModelBindingConfig { + + @LayoutRes + private final int mLayoutResource; + private final int mViewModelVariableName; + @NonNull + private final Context mContext; + + /** + * Create a ViewModelBinding Config object for an Activity/Fragment + * This constructor should be used if the binding variable is named differently + * + * @param layoutResource Layout resource ID + * @param viewModelVariableName Data Binding variable name for injecting the ViewModel - use + * generated id (e.g. BR.mViewModel) + */ + public ViewModelBindingConfig(@LayoutRes int layoutResource, int viewModelVariableName, @NonNull Context context) { + mLayoutResource = layoutResource; + mViewModelVariableName = viewModelVariableName; + mContext = context; + } + + /** + * Create a ViewModelBinding Config object for an Activity/Fragment + * Use this constructor if the binding variable is named viewModel + * + * @param layoutResource Layout resource ID + */ + public ViewModelBindingConfig(@LayoutRes int layoutResource, @NonNull Context context) { + this(layoutResource, BR.viewModel, context); + } + + @LayoutRes + public int getLayoutResource() { + return mLayoutResource; + } + + public int getViewModelVariableName() { + return mViewModelVariableName; + } + + @NonNull + public Context getContext() { + return mContext; + } +} diff --git a/library/src/main/java/eu/inloop/viewmodel/support/ViewModelStatePagerAdapter.java b/library/src/main/java/eu/inloop/viewmodel/support/ViewModelStatePagerAdapter.java new file mode 100644 index 0000000..c632fac --- /dev/null +++ b/library/src/main/java/eu/inloop/viewmodel/support/ViewModelStatePagerAdapter.java @@ -0,0 +1,39 @@ +package eu.inloop.viewmodel.support; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import android.util.Log; +import android.view.ViewGroup; + +import eu.inloop.viewmodel.BuildConfig; +import eu.inloop.viewmodel.IView; + +/** + * This class extends {@link FragmentStatePagerAdapter}. It removes the ViewModel once the + * pager item is destroyed ({@link #destroyItem(ViewGroup, int, Object)}). The ViewModel state + * is stored and then restored once you return back to this pager item and {@link #instantiateItem(ViewGroup, int)} + * is called. + */ +public abstract class ViewModelStatePagerAdapter extends FragmentStatePagerAdapter { + + public ViewModelStatePagerAdapter(@NonNull final FragmentManager fm) { + super(fm); + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + super.destroyItem(container, position, object); + final Fragment fragment = (Fragment) object; + + if (fragment instanceof IView) { + IView viewModelBaseFragment = (IView) fragment; + viewModelBaseFragment.removeViewModel(); + } else { + if (BuildConfig.DEBUG) { + Log.w("model", "Fragment " + fragment + " in FragmentStatePagerAdapter " + this + " doesn't implent IView"); //NON-NLS + } + } + } +} diff --git a/library/src/main/res/layout/binding_variable_placeholder.xml b/library/src/main/res/layout/binding_variable_placeholder.xml new file mode 100644 index 0000000..32835f4 --- /dev/null +++ b/library/src/main/res/layout/binding_variable_placeholder.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/sample/build.gradle b/sample/build.gradle index 0c6cb6a..228c3a8 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -1,21 +1,20 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 22 - buildToolsVersion '22.0.1' + compileSdkVersion 28 defaultConfig { applicationId 'eu.inloop.viewmodel.sample' minSdkVersion 15 - targetSdkVersion 22 + targetSdkVersion 28 versionCode 1 versionName '1.0' } compileOptions { encoding "UTF-8" - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } buildTypes { release { @@ -23,12 +22,19 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + dataBinding { + enabled = true + } } dependencies { - compile 'com.android.support:appcompat-v7:22.1.1' - debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3' - releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3' - compile 'com.jakewharton:butterknife:5.1.2' - compile project(':library') + implementation 'androidx.fragment:fragment:1.0.0' + implementation 'androidx.appcompat:appcompat:1.0.2' + + debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.5.1' + releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' + implementation 'com.jakewharton:butterknife:10.0.0' + annotationProcessor 'com.jakewharton:butterknife-compiler:10.0.0' + implementation project(':library') } diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 6c53763..50923b4 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -4,19 +4,21 @@ + android:label="@string/app_name"> + + diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/SampleApplication.java b/sample/src/main/java/eu/inloop/viewmodel/sample/SampleApplication.java index 22452b3..0fe00cb 100644 --- a/sample/src/main/java/eu/inloop/viewmodel/sample/SampleApplication.java +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/SampleApplication.java @@ -10,7 +10,8 @@ public class SampleApplication extends Application { private RefWatcher refWatcher; - @Override public void onCreate() { + @Override + public void onCreate() { super.onCreate(); refWatcher = LeakCanary.install(this); } diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/activity/MainActivity.java b/sample/src/main/java/eu/inloop/viewmodel/sample/activity/MainActivity.java index 7e54dc9..5d4ed8a 100644 --- a/sample/src/main/java/eu/inloop/viewmodel/sample/activity/MainActivity.java +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/activity/MainActivity.java @@ -3,27 +3,22 @@ import android.os.Bundle; import butterknife.ButterKnife; -import eu.inloop.viewmodel.base.ViewModelBaseActivity; +import eu.inloop.viewmodel.base.ViewModelBaseEmptyActivity; import eu.inloop.viewmodel.sample.R; import eu.inloop.viewmodel.sample.fragment.UserListFragment; -public class MainActivity extends ViewModelBaseActivity { +public class MainActivity extends ViewModelBaseEmptyActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - ButterKnife.inject(this); + ButterKnife.bind(this); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction().replace(R.id.root_content, new UserListFragment(), "user-list-fragment").commit(); } } - @Override - public Class getViewModelClass() { - return null; - } - } diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/activity/SampleBindingActivity.java b/sample/src/main/java/eu/inloop/viewmodel/sample/activity/SampleBindingActivity.java new file mode 100644 index 0000000..2967890 --- /dev/null +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/activity/SampleBindingActivity.java @@ -0,0 +1,28 @@ +package eu.inloop.viewmodel.sample.activity; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import butterknife.ButterKnife; +import eu.inloop.viewmodel.base.ViewModelBaseEmptyActivity; +import eu.inloop.viewmodel.sample.R; +import eu.inloop.viewmodel.sample.fragment.SampleBindingFragment; + +public class SampleBindingActivity extends ViewModelBaseEmptyActivity { + + public static Intent newIntent(Context context) { + return new Intent(context, SampleBindingActivity.class); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + ButterKnife.bind(this); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction().replace(R.id.root_content, new SampleBindingFragment(), "sample-binding-fragment").commit(); + } + } +} diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/activity/ViewPagerActivity.java b/sample/src/main/java/eu/inloop/viewmodel/sample/activity/ViewPagerActivity.java new file mode 100644 index 0000000..2e56405 --- /dev/null +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/activity/ViewPagerActivity.java @@ -0,0 +1,44 @@ +package eu.inloop.viewmodel.sample.activity; + +import android.os.Bundle; + + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.viewpager.widget.ViewPager; +import butterknife.BindView; +import butterknife.ButterKnife; +import eu.inloop.viewmodel.base.ViewModelBaseEmptyActivity; +import eu.inloop.viewmodel.sample.R; +import eu.inloop.viewmodel.sample.fragment.PagerFragment; +import eu.inloop.viewmodel.support.ViewModelStatePagerAdapter; + +public class ViewPagerActivity extends ViewModelBaseEmptyActivity { + + @BindView(R.id.pager) + ViewPager mViewPager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_pager); + ButterKnife.bind(this); + mViewPager.setAdapter(new TestPagerAdapter(getSupportFragmentManager())); + } + + private final static class TestPagerAdapter extends ViewModelStatePagerAdapter { + TestPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public Fragment getItem(int position) { + return PagerFragment.newInstance(position); + } + + @Override + public int getCount() { + return 10; + } + } +} diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/PagerFragment.java b/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/PagerFragment.java new file mode 100644 index 0000000..daf7d60 --- /dev/null +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/PagerFragment.java @@ -0,0 +1,50 @@ +package eu.inloop.viewmodel.sample.fragment; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.squareup.leakcanary.RefWatcher; + +import eu.inloop.viewmodel.base.ViewModelBaseFragment; +import eu.inloop.viewmodel.sample.R; +import eu.inloop.viewmodel.sample.SampleApplication; +import eu.inloop.viewmodel.sample.viewmodel.PageModel; +import eu.inloop.viewmodel.sample.viewmodel.view.IPageView; + +public class PagerFragment extends ViewModelBaseFragment implements IPageView { + + public static PagerFragment newInstance(int position) { + final Bundle bundle = new Bundle(); + bundle.putInt("position", position); + final PagerFragment fragment = new PagerFragment(); + fragment.setArguments(bundle); + return fragment; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_pager, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ((TextView)view.findViewById(R.id.text)).setText(Integer.toString(getArguments().getInt("position"))); + setModelView(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + + // watch for memory leaks + RefWatcher refWatcher = SampleApplication.getRefWatcher(requireActivity()); + refWatcher.watch(this); + } +} diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/SampleBindingFragment.java b/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/SampleBindingFragment.java new file mode 100644 index 0000000..934666f --- /dev/null +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/SampleBindingFragment.java @@ -0,0 +1,37 @@ +package eu.inloop.viewmodel.sample.fragment; + +import android.os.Bundle; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.view.View; + +import androidx.fragment.app.Fragment; +import eu.inloop.viewmodel.binding.ViewModelBaseBindingFragment; +import eu.inloop.viewmodel.binding.ViewModelBindingConfig; +import eu.inloop.viewmodel.sample.R; +import eu.inloop.viewmodel.sample.databinding.FragmentSampleBindingBinding; +import eu.inloop.viewmodel.sample.viewmodel.SampleBindingViewModel; +import eu.inloop.viewmodel.sample.viewmodel.view.ISampleBindingView; + +/** + * A simple {@link Fragment} subclass. + */ +public class SampleBindingFragment + extends ViewModelBaseBindingFragment + implements ISampleBindingView { + + public SampleBindingFragment() { + // Required empty public constructor + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setModelView(this); + } + + @Override + public ViewModelBindingConfig getViewModelBindingConfig() { + return new ViewModelBindingConfig(R.layout.fragment_sample_binding, requireActivity()); + } +} diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/SampleBundleFragment.java b/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/SampleBundleFragment.java index c3d6378..031a285 100644 --- a/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/SampleBundleFragment.java +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/SampleBundleFragment.java @@ -1,8 +1,7 @@ package eu.inloop.viewmodel.sample.fragment; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; +import androidx.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -33,12 +32,7 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - ButterKnife.inject(this, view); + ButterKnife.bind(this, view); setModelView(this); } - - @Override - public Class getViewModelClass() { - return SampleArgumentViewModel.class; - } } diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/UserListFragment.java b/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/UserListFragment.java index 41c777b..d415492 100644 --- a/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/UserListFragment.java +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/fragment/UserListFragment.java @@ -1,12 +1,15 @@ package eu.inloop.viewmodel.sample.fragment; +import android.content.Intent; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.Button; import android.widget.ListView; import android.widget.TextView; @@ -15,62 +18,72 @@ import java.util.ArrayList; import java.util.List; +import butterknife.BindView; import butterknife.ButterKnife; -import butterknife.InjectView; import eu.inloop.viewmodel.base.ViewModelBaseFragment; import eu.inloop.viewmodel.sample.R; import eu.inloop.viewmodel.sample.SampleApplication; +import eu.inloop.viewmodel.sample.activity.SampleBindingActivity; +import eu.inloop.viewmodel.sample.activity.ViewPagerActivity; import eu.inloop.viewmodel.sample.viewmodel.UserListViewModel; import eu.inloop.viewmodel.sample.viewmodel.view.IUserListView; public class UserListFragment extends ViewModelBaseFragment implements IUserListView { - @InjectView(android.R.id.progress) + @BindView(android.R.id.progress) View mProgressView; - @InjectView(R.id.progress_text) + @BindView(R.id.progress_text) TextView mProgressText; - @InjectView(android.R.id.list) + @BindView(android.R.id.list) ListView mListview; + @BindView(R.id.open_binding_fragment) + Button mOpenBindingFragment; private ArrayAdapter mAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mAdapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, new ArrayList()); + mAdapter = new ArrayAdapter<>(requireActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, new ArrayList()); } @Override - public Class getViewModelClass() { - return UserListViewModel.class; - } - - - @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_userlist, container, false); - ButterKnife.inject(this, view); + ButterKnife.bind(this, view); final View headerView = inflater.inflate(R.layout.view_header_info, null, false); headerView.findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - getFragmentManager().beginTransaction().replace(R.id.root_content, SampleBundleFragment.newInstance(1234), "empty-fragment").addToBackStack(null).commit(); + requireFragmentManager().beginTransaction().replace(R.id.root_content, SampleBundleFragment.newInstance(1234), "empty-fragment").addToBackStack(null).commit(); } }); headerView.findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - getActivity().finish(); - getActivity().startActivity(getActivity().getIntent()); + requireActivity().finish(); + requireActivity().startActivity(requireActivity().getIntent()); + } + }); + headerView.findViewById(R.id.button3).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startActivity(new Intent(requireContext(), ViewPagerActivity.class)); } }); mListview.addHeaderView(headerView, null, false); + mOpenBindingFragment.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(SampleBindingActivity.newIntent(requireActivity())); + } + }); return view; } @Override - public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mListview.setAdapter(mAdapter); mListview.setOnItemClickListener(new AdapterView.OnItemClickListener() { @@ -90,7 +103,7 @@ public void showUsers(List users) { mAdapter.setNotifyOnChange(true); mAdapter.notifyDataSetChanged(); } - + @Override public void showLoading(float progress) { mProgressView.setVisibility(View.VISIBLE); @@ -107,7 +120,7 @@ public void onDestroy() { super.onDestroy(); // watch for memory leaks - RefWatcher refWatcher = SampleApplication.getRefWatcher(getActivity()); + RefWatcher refWatcher = SampleApplication.getRefWatcher(requireActivity()); refWatcher.watch(this); } } diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/PageModel.java b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/PageModel.java new file mode 100644 index 0000000..a238f1e --- /dev/null +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/PageModel.java @@ -0,0 +1,15 @@ +package eu.inloop.viewmodel.sample.viewmodel; + +import android.os.Bundle; +import androidx.annotation.Nullable; + +import eu.inloop.viewmodel.AbstractViewModel; +import eu.inloop.viewmodel.sample.viewmodel.view.IPageView; + +public class PageModel extends AbstractViewModel { + + @Override + public void onCreate(@Nullable Bundle arguments, @Nullable Bundle savedInstanceState) { + super.onCreate(arguments, savedInstanceState); + } +} diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/SampleArgumentViewModel.java b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/SampleArgumentViewModel.java index a8275a5..c31e0d3 100644 --- a/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/SampleArgumentViewModel.java +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/SampleArgumentViewModel.java @@ -1,7 +1,7 @@ package eu.inloop.viewmodel.sample.viewmodel; import android.os.Bundle; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.Log; import eu.inloop.viewmodel.AbstractViewModel; diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/SampleBindingViewModel.java b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/SampleBindingViewModel.java new file mode 100644 index 0000000..21f832f --- /dev/null +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/SampleBindingViewModel.java @@ -0,0 +1,33 @@ +package eu.inloop.viewmodel.sample.viewmodel; + +import androidx.databinding.ObservableField; +import android.os.Bundle; +import androidx.annotation.Nullable; + +import eu.inloop.viewmodel.AbstractViewModel; +import eu.inloop.viewmodel.sample.viewmodel.view.ISampleBindingView; + +/** + * Created by stepansanda on 21/11/2016. + */ + +public class SampleBindingViewModel extends AbstractViewModel { + + public final ObservableField text = new ObservableField<>(); + private int mButtonClickedCounter = 0; + + @Override + public void onCreate(@Nullable Bundle arguments, @Nullable Bundle savedInstanceState) { + super.onCreate(arguments, savedInstanceState); + setButtonClickedText(); + } + + public void onButtonClick() { + mButtonClickedCounter++; + setButtonClickedText(); + } + + private void setButtonClickedText() { + text.set("Button Clicked: " + mButtonClickedCounter + " times"); + } +} diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/UserListViewModel.java b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/UserListViewModel.java index 43d3b53..58e5a0c 100644 --- a/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/UserListViewModel.java +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/UserListViewModel.java @@ -1,9 +1,10 @@ package eu.inloop.viewmodel.sample.viewmodel; +import android.annotation.SuppressLint; import android.os.AsyncTask; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.util.ArrayList; import java.util.List; @@ -32,8 +33,8 @@ public void onCreate(@Nullable Bundle arguments, @Nullable Bundle savedInstanceS } @Override - public void bindView(@NonNull IUserListView view) { - super.bindView(view); + public void onBindView(@NonNull IUserListView view) { + super.onBindView(view); //downloading list of users if (mLoadedUsers != null) { @@ -45,12 +46,11 @@ public void bindView(@NonNull IUserListView view) { } } + @SuppressLint("StaticFieldLeak") private void loadUsers() { mLoadingUsers = true; mCurrentLoadingProgress = 0; - if (getView() != null) { - getView().showLoading(mCurrentLoadingProgress); - } + getViewOptional().showLoading(mCurrentLoadingProgress); new AsyncTask>() { @Override @@ -74,9 +74,7 @@ protected List doInBackground(Void... voids) { protected void onProgressUpdate(Float... values) { super.onProgressUpdate(values); mCurrentLoadingProgress = values[0]; - if (getView() != null) { - getView().showLoading(mCurrentLoadingProgress); - } + getViewOptional().showLoading(mCurrentLoadingProgress); } @Override @@ -84,22 +82,19 @@ protected void onPostExecute(List s) { super.onPostExecute(s); mLoadedUsers = s; mLoadingUsers = false; - if (getView() != null) { - getView().showUsers(s); - getView().hideProgress(); - } + getViewOptional().showUsers(s); + getViewOptional().hideProgress(); } }.execute(); } + @SuppressLint("StaticFieldLeak") public void deleteUser(final int position) { if (position > mLoadedUsers.size() - 1) { return; } mLoadedUsers.set(position, "Deleting in 5 seconds..."); - if (getView() != null) { - getView().showUsers(mLoadedUsers); - } + getViewOptional().showUsers(mLoadedUsers); final String itemToDelete = mLoadedUsers.get(position); new AsyncTask() { @@ -118,25 +113,23 @@ protected Void doInBackground(Void... voids) { @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); - if (getView() != null) { - getView().showUsers(mLoadedUsers); - } + getViewOptional().showUsers(mLoadedUsers); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } @Override - public void saveState(Bundle bundle) { - super.saveState(bundle); + public void onSaveInstanceState(@NonNull final Bundle bundle) { + super.onSaveInstanceState(bundle); if (mLoadedUsers != null) { bundle.putStringArrayList("userlist", new ArrayList<>(mLoadedUsers)); } } @Override - public void onModelRemoved() { - super.onModelRemoved(); + public void onDestroy() { + super.onDestroy(); //use this to cancel any planned requests } } diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/view/IPageView.java b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/view/IPageView.java new file mode 100644 index 0000000..2e5267b --- /dev/null +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/view/IPageView.java @@ -0,0 +1,8 @@ +package eu.inloop.viewmodel.sample.viewmodel.view; + +import eu.inloop.viewmodel.IView; +import eu.inloop.viewmodel.binding.ViewModelBindingConfig; + +public interface IPageView extends IView { + ViewModelBindingConfig getViewModelBindingConfig(); +} diff --git a/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/view/ISampleBindingView.java b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/view/ISampleBindingView.java new file mode 100644 index 0000000..983c325 --- /dev/null +++ b/sample/src/main/java/eu/inloop/viewmodel/sample/viewmodel/view/ISampleBindingView.java @@ -0,0 +1,10 @@ +package eu.inloop.viewmodel.sample.viewmodel.view; + +import eu.inloop.viewmodel.IView; + +/** + * Created by stepansanda on 21/11/2016. + */ + +public interface ISampleBindingView extends IView { +} diff --git a/sample/src/main/res/layout/activity_pager.xml b/sample/src/main/res/layout/activity_pager.xml new file mode 100644 index 0000000..e6e6d08 --- /dev/null +++ b/sample/src/main/res/layout/activity_pager.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_sample_binding.xml b/sample/src/main/res/layout/activity_sample_binding.xml new file mode 100644 index 0000000..81d46df --- /dev/null +++ b/sample/src/main/res/layout/activity_sample_binding.xml @@ -0,0 +1,7 @@ + diff --git a/sample/src/main/res/layout/fragment_pager.xml b/sample/src/main/res/layout/fragment_pager.xml new file mode 100644 index 0000000..ec6ea51 --- /dev/null +++ b/sample/src/main/res/layout/fragment_pager.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/fragment_sample_binding.xml b/sample/src/main/res/layout/fragment_sample_binding.xml new file mode 100644 index 0000000..05b0828 --- /dev/null +++ b/sample/src/main/res/layout/fragment_sample_binding.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + +