From a5f5be25f376af5c05bb850e335c3a831f847e6e Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Thu, 15 Dec 2016 19:37:56 +0100 Subject: [PATCH 1/6] Add FragmentPlugin for Ti --- plugin/build.gradle | 1 + .../thirtyinch/plugin/TiFragmentPlugin.java | 252 ++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java diff --git a/plugin/build.gradle b/plugin/build.gradle index 87392745..d8613d55 100644 --- a/plugin/build.gradle +++ b/plugin/build.gradle @@ -38,6 +38,7 @@ android { dependencies { compile project(':thirtyinch') provided "com.pascalwelsch.compositeandroid:activity:$supportLibraryVersion" + provided "com.pascalwelsch.compositeandroid:fragment:$supportLibraryVersion" } publish { diff --git a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java new file mode 100644 index 00000000..39f70bda --- /dev/null +++ b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2016 grandcentrix GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.grandcentrix.thirtyinch.plugin; + + +import com.pascalwelsch.compositeandroid.fragment.FragmentPlugin; + +import net.grandcentrix.thirtyinch.BindViewInterceptor; +import net.grandcentrix.thirtyinch.Removable; +import net.grandcentrix.thirtyinch.TiFragment; +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.TiView; +import net.grandcentrix.thirtyinch.internal.DelegatedTiFragment; +import net.grandcentrix.thirtyinch.internal.InterceptableViewBinder; +import net.grandcentrix.thirtyinch.internal.TiFragmentDelegate; +import net.grandcentrix.thirtyinch.internal.TiLoggingTagProvider; +import net.grandcentrix.thirtyinch.internal.TiPresenterProvider; +import net.grandcentrix.thirtyinch.internal.TiViewProvider; +import net.grandcentrix.thirtyinch.util.AndroidDeveloperOptions; +import net.grandcentrix.thirtyinch.util.AnnotationUtil; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import java.util.List; + +public class TiFragmentPlugin

, V extends TiView> extends FragmentPlugin + implements TiViewProvider, DelegatedTiFragment, TiLoggingTagProvider, + InterceptableViewBinder { + + private String TAG = this.getClass().getSimpleName() + + "@" + Integer.toHexString(this.hashCode()); + + private TiFragmentDelegate mDelegate; + + /** + * Binds a {@link TiPresenter} returned by the {@link TiPresenterProvider} to the {@link + * Fragment} and all future {@link Fragment} instances created due to configuration changes. + * The provider will be only called once during {@link TiFragmentPlugin#onCreate(Bundle)}. This + * lets you inject objects which require a {@link android.content.Context} and can't be + * instantiated earlier. Using the interface also prevents instantiating the (possibly) heavy + * {@link TiPresenter} which will never be used when a presenter is already created for this + * {@link Fragment}. + * + * @param presenterProvider callback returning the presenter. + */ + public TiFragmentPlugin(@NonNull final TiPresenterProvider

presenterProvider) { + mDelegate = new TiFragmentDelegate<>(this, this, presenterProvider, this); + } + + @NonNull + @Override + public Removable addBindViewInterceptor(@NonNull final BindViewInterceptor interceptor) { + return mDelegate.addBindViewInterceptor(interceptor); + } + + /** + * @return the cached result of {@link BindViewInterceptor#intercept(TiView)} + */ + @Nullable + @Override + public V getInterceptedViewOf(@NonNull final BindViewInterceptor interceptor) { + return mDelegate.getInterceptedViewOf(interceptor); + } + + /** + * @param predicate filter the results + * @return all interceptors matching the filter + */ + @NonNull + @Override + public List getInterceptors( + @NonNull final Filter predicate) { + return mDelegate.getInterceptors(predicate); + } + + + @Override + public String getLoggingTag() { + return TAG; + } + + public P getPresenter() { + return mDelegate.getPresenter(); + } + + /** + * Invalidates the cache of the latest bound view. Forces the next binding of the view to run + * through all the interceptors (again). + */ + @Override + public void invalidateView() { + mDelegate.invalidateView(); + } + + + @Override + public boolean isActivityChangingConfigurations() { + return getFragment().getActivity().isChangingConfigurations(); + } + + @Override + public boolean isActivityFinishing() { + return getFragment().getActivity().isFinishing(); + } + + @Override + public boolean isDontKeepActivitiesEnabled() { + return AndroidDeveloperOptions.isDontKeepActivitiesEnabled(getFragment().getActivity()); + } + + @Override + public boolean isFragmentAdded() { + return getFragment().isAdded(); + } + + @Override + public boolean isFragmentDetached() { + return getFragment().isDetached(); + } + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mDelegate.onCreate(savedInstanceState); + } + + @Nullable + @Override + public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { + mDelegate.onCreateView(inflater, container, savedInstanceState); + return super.onCreateView(inflater, container, savedInstanceState); + } + + @Override + public void onDestroy() { + super.onDestroy(); + mDelegate.onDestroy(); + } + + @Override + public void onDestroyView() { + mDelegate.onDestroyView(); + super.onDestroyView(); + } + + @Override + public void onSaveInstanceState(final Bundle outState) { + super.onSaveInstanceState(outState); + mDelegate.onSaveInstanceState(outState); + } + + @Override + public void onStart() { + super.onStart(); + mDelegate.onStart(); + } + + @Override + public void onStop() { + mDelegate.onStop(); + super.onStop(); + } + + @Override + public boolean postToMessageQueue(final Runnable runnable) { + return getFragment().getActivity().getWindow().getDecorView().post(runnable); + } + + /** + * the default implementation assumes that the fragment is the view and implements the {@link + * TiView} interface. Override this method for a different behaviour. + * + * @return the object implementing the TiView interface + */ + @NonNull + public V provideView() { + + final Class foundViewInterface = AnnotationUtil + .getInterfaceOfClassExtendingGivenInterface(getFragment().getClass(), TiView.class); + + if (foundViewInterface == null) { + throw new IllegalArgumentException( + "This Fragment doesn't implement a TiView interface. " + + "This is the default behaviour. Override provideView() to explicitly change this."); + } else { + if (foundViewInterface.getSimpleName().equals("TiView")) { + throw new IllegalArgumentException( + "extending TiView doesn't make sense, it's an empty interface." + + " This is the default behaviour. Override provideView() to explicitly change this."); + } else { + // assume that the fragment itself is the view and implements the TiView interface + //noinspection unchecked + return (V) getFragment(); + } + } + } + + @Override + public void setFragmentRetainInstance(final boolean retain) { + setRetainInstance(retain); + } + + @Override + public String toString() { + String presenter = mDelegate.getPresenter() == null ? "null" : + mDelegate.getPresenter().getClass().getSimpleName() + + "@" + Integer.toHexString(mDelegate.getPresenter().hashCode()); + + return getClass().getSimpleName() + + ":" + TiFragment.class.getSimpleName() + + "@" + Integer.toHexString(hashCode()) + + "{presenter=" + presenter + "}"; + } + + @Override + protected void onAddedToDelegate() { + super.onAddedToDelegate(); + TAG = getClass().getSimpleName() + + ":" + TiFragment.class.getSimpleName() + + "@" + Integer.toHexString(this.hashCode()) + + ":" + getOriginal().getClass().getSimpleName() + + "@" + Integer.toHexString(getOriginal().hashCode()); + } + + @Override + protected void onRemovedFromDelegated() { + super.onRemovedFromDelegated(); + TAG = getClass().getSimpleName() + + ":" + TiFragment.class.getSimpleName() + + "@" + Integer.toHexString(this.hashCode()); + } +} From e033cdabab52667525fcabccc986a7ca5fddce9a Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Fri, 16 Dec 2016 11:34:44 +0100 Subject: [PATCH 2/6] Add before/after postfixes --- .../thirtyinch/plugin/TiFragmentPlugin.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java index 39f70bda..08c5b0e6 100644 --- a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java +++ b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java @@ -140,44 +140,44 @@ public boolean isFragmentDetached() { @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mDelegate.onCreate(savedInstanceState); + mDelegate.onCreate_afterSuper(savedInstanceState); } @Nullable @Override public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - mDelegate.onCreateView(inflater, container, savedInstanceState); + mDelegate.onCreateView_beforeSuper(inflater, container, savedInstanceState); return super.onCreateView(inflater, container, savedInstanceState); } @Override public void onDestroy() { super.onDestroy(); - mDelegate.onDestroy(); + mDelegate.onDestroy_afterSuper(); } @Override public void onDestroyView() { - mDelegate.onDestroyView(); + mDelegate.onDestroyView_beforeSuper(); super.onDestroyView(); } @Override public void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); - mDelegate.onSaveInstanceState(outState); + mDelegate.onSaveInstanceState_afterSuper(outState); } @Override public void onStart() { super.onStart(); - mDelegate.onStart(); + mDelegate.onStart_afterSuper(); } @Override public void onStop() { - mDelegate.onStop(); + mDelegate.onStop_beforeSuper(); super.onStop(); } From 3d1b2e0a91fbab344a67a2c2834e22061c913b2a Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Fri, 16 Dec 2016 11:39:07 +0100 Subject: [PATCH 3/6] Add plugin headers --- .../grandcentrix/thirtyinch/plugin/TiActivityPlugin.java | 6 ++++++ .../grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiActivityPlugin.java b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiActivityPlugin.java index 70f3872a..99dd590d 100644 --- a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiActivityPlugin.java +++ b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiActivityPlugin.java @@ -40,6 +40,12 @@ import java.util.List; +/** + * Binds a {@link TiPresenter} to an {@link Activity} + * + * @param

{@link TiPresenter} with will be attached + * @param View, expected by the {@link TiPresenter} + */ public class TiActivityPlugin

, V extends TiView> extends ActivityPlugin implements TiViewProvider, DelegatedTiActivity

, TiLoggingTagProvider, InterceptableViewBinder { diff --git a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java index 08c5b0e6..a4a7c1ab 100644 --- a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java +++ b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java @@ -42,6 +42,13 @@ import java.util.List; +/** + * Adds a {@link TiPresenter} to a Fragment. Can be used for both, {@link Fragment} and + * {@link android.support.v4.app.DialogFragment} + * + * @param

{@link TiPresenter} with will be attached + * @param View, expected by the {@link TiPresenter} + */ public class TiFragmentPlugin

, V extends TiView> extends FragmentPlugin implements TiViewProvider, DelegatedTiFragment, TiLoggingTagProvider, InterceptableViewBinder { From 2b327aa515ce6318a3ec9cbbc14ab068f068b6f8 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Fri, 16 Dec 2016 14:49:40 +0100 Subject: [PATCH 4/6] Rename overridden methods --- .../thirtyinch/plugin/TiFragmentPlugin.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java index a4a7c1ab..566a4fc6 100644 --- a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java +++ b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java @@ -118,17 +118,6 @@ public void invalidateView() { mDelegate.invalidateView(); } - - @Override - public boolean isActivityChangingConfigurations() { - return getFragment().getActivity().isChangingConfigurations(); - } - - @Override - public boolean isActivityFinishing() { - return getFragment().getActivity().isFinishing(); - } - @Override public boolean isDontKeepActivitiesEnabled() { return AndroidDeveloperOptions.isDontKeepActivitiesEnabled(getFragment().getActivity()); @@ -144,6 +133,16 @@ public boolean isFragmentDetached() { return getFragment().isDetached(); } + @Override + public boolean isHostingActivityChangingConfigurations() { + return getFragment().getActivity().isChangingConfigurations(); + } + + @Override + public boolean isHostingActivityFinishing() { + return getFragment().getActivity().isFinishing(); + } + @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); From a3b81f41f9fe8971ea1f1777817ed6b6e1980354 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Mon, 2 Jan 2017 11:53:24 +0100 Subject: [PATCH 5/6] Fix missing delegate call --- .../net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java index 566a4fc6..603e3479 100644 --- a/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java +++ b/plugin/src/main/java/net/grandcentrix/thirtyinch/plugin/TiFragmentPlugin.java @@ -223,7 +223,7 @@ public V provideView() { @Override public void setFragmentRetainInstance(final boolean retain) { - setRetainInstance(retain); + getFragment().setRetainInstance(retain); } @Override From 20e3b220a98b1a61cd86e377540bc59bb28ec302 Mon Sep 17 00:00:00 2001 From: Pascal Welsch Date: Mon, 2 Jan 2017 11:55:29 +0100 Subject: [PATCH 6/6] Add fragment to plugin test --- plugin-test/build.gradle | 1 + .../thirtyinch/plugin_test/TiPluginTest.java | 8 ++- .../thirtyinch/plugin_test/TestActivity.java | 9 +++ .../thirtyinch/plugin_test/TestFragment.java | 60 +++++++++++++++++++ .../src/main/res/layout/activity_test.xml | 10 +++- .../src/main/res/layout/fragment_test.xml | 13 ++++ 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 plugin-test/src/main/java/net/grandcentrix/thirtyinch/plugin_test/TestFragment.java create mode 100644 plugin-test/src/main/res/layout/fragment_test.xml diff --git a/plugin-test/build.gradle b/plugin-test/build.gradle index 4e4a53b6..d51513e9 100644 --- a/plugin-test/build.gradle +++ b/plugin-test/build.gradle @@ -29,6 +29,7 @@ dependencies { compile "com.android.support:appcompat-v7:$supportLibraryVersion" compile "com.pascalwelsch.compositeandroid:activity:$supportLibraryVersion" + compile "com.pascalwelsch.compositeandroid:fragment:$supportLibraryVersion" androidTestCompile "com.android.support:appcompat-v7:$supportLibraryVersion" androidTestCompile 'com.android.support.test:runner:0.5'; diff --git a/plugin-test/src/androidTest/java/net/grandcentrix/thirtyinch/plugin_test/TiPluginTest.java b/plugin-test/src/androidTest/java/net/grandcentrix/thirtyinch/plugin_test/TiPluginTest.java index c146a8c1..2192bda5 100644 --- a/plugin-test/src/androidTest/java/net/grandcentrix/thirtyinch/plugin_test/TiPluginTest.java +++ b/plugin-test/src/androidTest/java/net/grandcentrix/thirtyinch/plugin_test/TiPluginTest.java @@ -18,7 +18,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import android.app.Activity; import android.app.Instrumentation; import android.content.Intent; import android.support.test.InstrumentationRegistry; @@ -48,6 +47,9 @@ public void startTestActivity() throws Exception { Espresso.onView(withId(R.id.helloworld_text)) .check(matches(allOf(isDisplayed(), withText("Hello World 1")))); + Espresso.onView(withId(R.id.fragment_helloworld_text)) + .check(matches(allOf(isDisplayed(), withText("Hello World 1")))); + activity.finish(); } @@ -64,6 +66,8 @@ public void testFullLifecycleIncludingConfigurationChange() throws Throwable { // make sure the attached presenter filled the UI Espresso.onView(withId(R.id.helloworld_text)) .check(matches(allOf(isDisplayed(), withText("Hello World 1")))); + Espresso.onView(withId(R.id.fragment_helloworld_text)) + .check(matches(allOf(isDisplayed(), withText("Hello World 1")))); // restart the activity rotateOrientation(activity); @@ -72,6 +76,8 @@ public void testFullLifecycleIncludingConfigurationChange() throws Throwable { // correctly Espresso.onView(withId(R.id.helloworld_text)) .check(matches(allOf(isDisplayed(), withText("Hello World 2")))); + Espresso.onView(withId(R.id.fragment_helloworld_text)) + .check(matches(allOf(isDisplayed(), withText("Hello World 2")))); activity.finish(); } diff --git a/plugin-test/src/main/java/net/grandcentrix/thirtyinch/plugin_test/TestActivity.java b/plugin-test/src/main/java/net/grandcentrix/thirtyinch/plugin_test/TestActivity.java index 8de4e10a..d3a74144 100644 --- a/plugin-test/src/main/java/net/grandcentrix/thirtyinch/plugin_test/TestActivity.java +++ b/plugin-test/src/main/java/net/grandcentrix/thirtyinch/plugin_test/TestActivity.java @@ -47,6 +47,15 @@ public void onCreate(@Nullable final Bundle savedInstanceState) { setContentView(R.layout.activity_test); mText = (TextView) findViewById(R.id.helloworld_text); + + if (savedInstanceState == null) { + + final TestFragment testFragment = new TestFragment(); + + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragment_container, testFragment) + .commit(); + } } @Override diff --git a/plugin-test/src/main/java/net/grandcentrix/thirtyinch/plugin_test/TestFragment.java b/plugin-test/src/main/java/net/grandcentrix/thirtyinch/plugin_test/TestFragment.java new file mode 100644 index 00000000..bf2f37d8 --- /dev/null +++ b/plugin-test/src/main/java/net/grandcentrix/thirtyinch/plugin_test/TestFragment.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 grandcentrix GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.grandcentrix.thirtyinch.plugin_test; + + +import com.pascalwelsch.compositeandroid.fragment.CompositeFragment; + +import net.grandcentrix.thirtyinch.internal.TiPresenterProvider; +import net.grandcentrix.thirtyinch.plugin.TiFragmentPlugin; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class TestFragment extends CompositeFragment implements TestView { + + private TextView mTextView; + + public TestFragment() { + addPlugin(new TiFragmentPlugin<>(new TiPresenterProvider() { + @NonNull + @Override + public TestPresenter providePresenter() { + return new TestPresenter(); + } + })); + } + + @Nullable + @Override + public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, + @Nullable final Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + final View rootView = inflater.inflate(R.layout.fragment_test, container, false); + mTextView = (TextView) rootView.findViewById(R.id.fragment_helloworld_text); + return rootView; + } + + @Override + public void showText(final String s) { + mTextView.setText(s); + } +} diff --git a/plugin-test/src/main/res/layout/activity_test.xml b/plugin-test/src/main/res/layout/activity_test.xml index e7264878..d6cbe830 100644 --- a/plugin-test/src/main/res/layout/activity_test.xml +++ b/plugin-test/src/main/res/layout/activity_test.xml @@ -6,8 +6,14 @@ + android:layout_height="wrap_content" + android:gravity="center" /> + + \ No newline at end of file diff --git a/plugin-test/src/main/res/layout/fragment_test.xml b/plugin-test/src/main/res/layout/fragment_test.xml new file mode 100644 index 00000000..dee0133d --- /dev/null +++ b/plugin-test/src/main/res/layout/fragment_test.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file