diff --git a/build.gradle b/build.gradle index f1c7d4f9..dbf86bfe 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ buildscript { allprojects { repositories { jcenter() + maven { url "https://maven.google.com" } } tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') @@ -24,11 +25,12 @@ ext { VERSION_NAME = "0.8.1-SNAPSHOT" VERSION_CODE = 6 MIN_SDK_VERSION = 14 - TARGET_SDK_VERSION = 25 - COMPILE_SDK_VERSION = 25 + TARGET_SDK_VERSION = 26 + COMPILE_SDK_VERSION = 26 BUILD_TOOLS_VERSION = '25.0.2' - supportLibraryVersion = '25.2.0' + //TODO wait for stable 26.0.0 release + supportLibraryVersion = '26.0.0-beta2' bintrayVersion = '0.3.4' junitVersion = '4.12' diff --git a/sample/src/main/java/net/grandcentrix/thirtyinch/sample/HelloWorldActivity.java b/sample/src/main/java/net/grandcentrix/thirtyinch/sample/HelloWorldActivity.java index be7d3a99..3af126d9 100644 --- a/sample/src/main/java/net/grandcentrix/thirtyinch/sample/HelloWorldActivity.java +++ b/sample/src/main/java/net/grandcentrix/thirtyinch/sample/HelloWorldActivity.java @@ -18,34 +18,31 @@ import com.jakewharton.rxbinding.view.RxView; -import net.grandcentrix.thirtyinch.TiActivity; +import net.grandcentrix.thirtyinch.TiPresenterBinder; +import net.grandcentrix.thirtyinch.TiPresenterBinders; import net.grandcentrix.thirtyinch.logginginterceptor.LoggingInterceptor; import net.grandcentrix.thirtyinch.sample.fragmentlifecycle.FragmentLifecycleActivity; import net.grandcentrix.thirtyinch.sample.fragmentlifecycle.viewpager.LifecycleViewPagerActivity; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.NonNull; +import android.support.v7.app.AppCompatActivity; import android.view.Menu; import android.view.MenuItem; -import android.view.View; import android.widget.Button; import android.widget.TextView; import rx.Observable; -public class HelloWorldActivity extends TiActivity - implements HelloWorldView { +public class HelloWorldActivity extends AppCompatActivity implements HelloWorldView { private Button mButton; private TextView mOutput; - private TextView mUptime; + private HelloWorldPresenter mPresenter; - public HelloWorldActivity() { - addBindViewInterceptor(new LoggingInterceptor()); - } + private TextView mUptime; @Override public Observable onButtonClicked() { @@ -71,12 +68,6 @@ public boolean onOptionsItemSelected(final MenuItem item) { return false; } - @NonNull - @Override - public HelloWorldPresenter providePresenter() { - return new HelloWorldPresenter(); - } - @Override public void showPresenterUpTime(final Long uptime) { mUptime.setText(String.format("Presenter alive for %ss", uptime)); @@ -90,11 +81,16 @@ public void showText(final String text) { @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); + final TiPresenterBinder binder = TiPresenterBinders + .attachPresenter(this, savedInstanceState, () -> new HelloWorldPresenter()); + binder.addBindViewInterceptor(new LoggingInterceptor()); + mPresenter = binder.getPresenter(); + setContentView(R.layout.activity_hello_world); - mButton = (Button) findViewById(R.id.button); - mOutput = (TextView) findViewById(R.id.output); - mUptime = (TextView) findViewById(R.id.uptime); + mButton = findViewById(R.id.button); + mOutput = findViewById(R.id.output); + mUptime = findViewById(R.id.uptime); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() @@ -102,12 +98,7 @@ protected void onCreate(final Bundle savedInstanceState) { .commit(); } - findViewById(R.id.recreate).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(final View v) { - recreate(); - } - }); + findViewById(R.id.recreate).setOnClickListener(v -> recreate()); } } diff --git a/sample/src/main/java/net/grandcentrix/thirtyinch/sample/SampleFragment.java b/sample/src/main/java/net/grandcentrix/thirtyinch/sample/SampleFragment.java index 87c0654c..69ac992b 100644 --- a/sample/src/main/java/net/grandcentrix/thirtyinch/sample/SampleFragment.java +++ b/sample/src/main/java/net/grandcentrix/thirtyinch/sample/SampleFragment.java @@ -15,20 +15,31 @@ package net.grandcentrix.thirtyinch.sample; -import net.grandcentrix.thirtyinch.TiFragment; +import net.grandcentrix.thirtyinch.TiPresenterBinders; 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 android.widget.TextView; -public class SampleFragment extends TiFragment implements SampleView { +public class SampleFragment extends Fragment implements SampleView { + + + private SamplePresenter mPresenter; private TextView mSampleText; + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mPresenter = TiPresenterBinders.attachPresenter(this, savedInstanceState, + () -> new SamplePresenter()).getPresenter(); + } + @Nullable @Override public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container, @@ -38,16 +49,10 @@ public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGrou final ViewGroup view = (ViewGroup) inflater .inflate(R.layout.fragment_sample, container, false); - mSampleText = (TextView) view.findViewById(R.id.sample_text); + mSampleText = view.findViewById(R.id.sample_text); return view; } - @NonNull - @Override - public SamplePresenter providePresenter() { - return new SamplePresenter(); - } - @Override public void showText(final String s) { mSampleText.setText(s); diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenterBinder.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenterBinder.java new file mode 100644 index 00000000..e0c6710a --- /dev/null +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenterBinder.java @@ -0,0 +1,26 @@ +/* + * 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; + + +import net.grandcentrix.thirtyinch.internal.InterceptableViewBinder; +import net.grandcentrix.thirtyinch.internal.PresenterAccessor; + +//TODO I'm not happy with the binder name +public interface TiPresenterBinder

, V extends TiView> + extends PresenterAccessor, InterceptableViewBinder { + +} diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenterBinders.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenterBinders.java new file mode 100644 index 00000000..3362913d --- /dev/null +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/TiPresenterBinders.java @@ -0,0 +1,102 @@ +/* + * 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; + + +import net.grandcentrix.thirtyinch.internal.ActivityPresenterBinder; +import net.grandcentrix.thirtyinch.internal.FragmentPresenterBinder; +import net.grandcentrix.thirtyinch.internal.PresenterAccessor; +import net.grandcentrix.thirtyinch.internal.TiPresenterProvider; +import net.grandcentrix.thirtyinch.internal.TiViewProvider; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; + +public class TiPresenterBinders { + + /** + * must be called in {@link Activity#onCreate(Bundle)} + */ + public static

, V extends TiView> TiPresenterBinder attachPresenter( + @NonNull final Activity activity, + @Nullable final Bundle savedInstanceState, + @NonNull final TiPresenterProvider

presenterProvider) { + + return attachPresenter(activity, savedInstanceState, presenterProvider, null); + } + + /** + * must be called in {@link Activity#onCreate(Bundle)} + */ + public static

, V extends TiView> TiPresenterBinder attachPresenter( + @NonNull final Activity activity, + @Nullable final Bundle savedInstanceState, + @NonNull final TiPresenterProvider

presenterProvider, + @Nullable final TiViewProvider viewProvider) { + + if (activity instanceof TiActivity) { + throw new IllegalStateException( + "Can't attach a TiPresenter to a TiActivity which already has a TiPresenter"); + } + + final ActivityPresenterBinder binder = new ActivityPresenterBinder<>(activity, + savedInstanceState, presenterProvider, viewProvider); + + Application app = activity.getApplication(); + app.registerActivityLifecycleCallbacks(binder); + + return binder; + } + + /** + * must be called in {@link Fragment#onCreate(Bundle)} + */ + public static

, V extends TiView> PresenterAccessor attachPresenter( + @NonNull final Fragment fragment, + @Nullable final Bundle savedInstanceState, + @NonNull final TiPresenterProvider

presenterProvider, + @Nullable final TiViewProvider viewProvider) { + + if (fragment instanceof TiFragment) { + throw new IllegalStateException( + "Can't attach a TiPresenter to a TiFragment which already has a TiPresenter"); + } + + final FragmentPresenterBinder binder = new FragmentPresenterBinder<>(fragment, + savedInstanceState, presenterProvider, viewProvider); + + FragmentManager fragmentManager = fragment.getActivity().getSupportFragmentManager(); + fragmentManager.registerFragmentLifecycleCallbacks(binder, false); + + return binder; + } + + /** + * must be called in {@link Fragment#onCreate(Bundle)} + */ + public static

, V extends TiView> PresenterAccessor attachPresenter( + @NonNull final Fragment fragment, + @Nullable final Bundle savedInstanceState, + @NonNull final TiPresenterProvider

presenterProvider) { + + return attachPresenter(fragment, savedInstanceState, presenterProvider, null); + } +} diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/ActivityPresenterBinder.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/ActivityPresenterBinder.java new file mode 100644 index 00000000..7e54e2ca --- /dev/null +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/ActivityPresenterBinder.java @@ -0,0 +1,186 @@ +/* + * 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.internal; + + +import net.grandcentrix.thirtyinch.BindViewInterceptor; +import net.grandcentrix.thirtyinch.Removable; +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.TiPresenterBinder; +import net.grandcentrix.thirtyinch.TiView; +import net.grandcentrix.thirtyinch.util.AnnotationUtil; + +import android.app.Activity; +import android.app.Application; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.List; +import java.util.concurrent.Executor; + +public class ActivityPresenterBinder

, V extends TiView> + implements DelegatedTiActivity, TiViewProvider, TiLoggingTagProvider, + Application.ActivityLifecycleCallbacks, TiPresenterBinder { + + private final String TAG; + + private final Activity mActivity; + + private final TiActivityDelegate mDelegate; + + private final UiThreadExecutor mUiThreadExecutor = new UiThreadExecutor(); + + public ActivityPresenterBinder(@NonNull final Activity activity, + final Bundle savedInstanceState, + final TiPresenterProvider

presenterProvider, + final TiViewProvider viewProvider) { + mActivity = activity; + + TAG = mActivity.getClass().getSimpleName() + + "@" + Integer.toHexString(mActivity.hashCode()); + + mDelegate = new TiActivityDelegate<>(this, viewProvider != null ? viewProvider : this, + presenterProvider, this, PresenterSavior.getInstance()); + + // There is no onPreActivityCreate(Bundle) callback to execute code as code in + // super.onCreate(Bundle) would be executed. Therefore this class must be initialized in + // Activity#onCreate(Bundle) where this method will be called directly. + // The presenter is available immediately with #getPresenter() + mDelegate.onCreate_afterSuper(savedInstanceState); + } + + @NonNull + @Override + public Removable addBindViewInterceptor(@NonNull final BindViewInterceptor interceptor) { + return mDelegate.addBindViewInterceptor(interceptor); + } + + @Override + public Object getHostingContainer() { + return mActivity; + } + + @Nullable + @Override + public V getInterceptedViewOf(@NonNull final BindViewInterceptor interceptor) { + return mDelegate.getInterceptedViewOf(interceptor); + } + + @NonNull + @Override + public List getInterceptors( + @NonNull final Filter predicate) { + return mDelegate.getInterceptors(predicate); + } + + @Override + public String getLoggingTag() { + return TAG; + } + + @Override + public P getPresenter() { + return mDelegate.getPresenter(); + } + + @Override + public Executor getUiThreadExecutor() { + return mUiThreadExecutor; + } + + @Override + public void invalidateView() { + mDelegate.invalidateView(); + } + + @Override + public boolean isActivityFinishing() { + return mActivity.isFinishing(); + } + + @Override + public void onActivityCreated(final Activity activity, + final Bundle savedInstanceState) { + // already called in constructor + } + + @Override + public void onActivityDestroyed(final Activity activity) { + if (activity == mActivity) { + mDelegate.onDestroy_afterSuper(); + + // always expect call on attachPresenter in Activity constructor + mActivity.getApplication().unregisterActivityLifecycleCallbacks(this); + } + } + + @Override + public void onActivityPaused(final Activity activity) { + // noop + } + + @Override + public void onActivityResumed(final Activity activity) { + // noop + } + + @Override + public void onActivitySaveInstanceState(final Activity activity, + final Bundle outState) { + if (activity == mActivity) { + mDelegate.onSaveInstanceState_afterSuper(outState); + } + } + + @Override + public void onActivityStarted(final Activity activity) { + if (activity == mActivity) { + mDelegate.onStart_afterSuper(); + } + } + + @Override + public void onActivityStopped(final Activity activity) { + if (activity == mActivity) { + mDelegate.onStop_beforeSuper(); + mDelegate.onStop_afterSuper(); + } + } + + @SuppressWarnings("unchecked") + @NonNull + @Override + public V provideView() { + final Class foundViewInterface = AnnotationUtil + .getInterfaceOfClassExtendingGivenInterface(mActivity.getClass(), TiView.class); + + if (foundViewInterface == null) { + throw new IllegalArgumentException( + "This Activity 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 activity itself is the view and implements the TiView interface + return (V) mActivity; + } + } + } +} diff --git a/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/FragmentPresenterBinder.java b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/FragmentPresenterBinder.java new file mode 100644 index 00000000..bbbcf926 --- /dev/null +++ b/thirtyinch/src/main/java/net/grandcentrix/thirtyinch/internal/FragmentPresenterBinder.java @@ -0,0 +1,209 @@ +/* + * 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.internal; + + +import net.grandcentrix.thirtyinch.BindViewInterceptor; +import net.grandcentrix.thirtyinch.Removable; +import net.grandcentrix.thirtyinch.TiPresenter; +import net.grandcentrix.thirtyinch.TiPresenterBinder; +import net.grandcentrix.thirtyinch.TiView; +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.BackstackReader; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentActivity; +import android.support.v4.app.FragmentManager; +import android.view.View; + +import java.util.List; +import java.util.concurrent.Executor; + +public class FragmentPresenterBinder

, V extends TiView> + extends FragmentManager.FragmentLifecycleCallbacks + implements DelegatedTiFragment, TiViewProvider, TiLoggingTagProvider, + TiPresenterBinder { + + private final String TAG; + + private final FragmentActivity mActivity; + + private final TiFragmentDelegate mDelegate; + + private final Fragment mFragment; + + private final UiThreadExecutor mUiThreadExecutor = new UiThreadExecutor(); + + public FragmentPresenterBinder(final Fragment fragment, + final Bundle savedInstanceState, + final TiPresenterProvider

presenterProvider, + final TiViewProvider viewProvider) { + mFragment = fragment; + mActivity = fragment.getActivity(); + + TAG = mActivity.getClass().getSimpleName() + + "@" + Integer.toHexString(mActivity.hashCode()); + + mDelegate = new TiFragmentDelegate<>(this, viewProvider != null ? viewProvider : this, + presenterProvider, this, PresenterSavior.getInstance()); + + // There is no onFragmentPreCreated(Bundle) callback to execute code as code in + // super.onCreate(Bundle) would be executed. Therefore this class must be initialized in + // Fragment#onCreate(Bundle) where this method will be called directly. + // The presenter is available immediately with #getPresenter() + mDelegate.onCreate_afterSuper(savedInstanceState); + } + + @NonNull + @Override + public Removable addBindViewInterceptor(@NonNull final BindViewInterceptor interceptor) { + return mDelegate.addBindViewInterceptor(interceptor); + } + + @Override + public Object getHostingContainer() { + return mFragment.getActivity(); + } + + @Nullable + @Override + public V getInterceptedViewOf(@NonNull final BindViewInterceptor interceptor) { + return mDelegate.getInterceptedViewOf(interceptor); + } + + @NonNull + @Override + public List getInterceptors( + @NonNull final Filter predicate) { + return mDelegate.getInterceptors(predicate); + } + + @Override + public String getLoggingTag() { + return TAG; + } + + @Override + public P getPresenter() { + return mDelegate.getPresenter(); + } + + @Override + public Executor getUiThreadExecutor() { + return mUiThreadExecutor; + } + + @Override + public void invalidateView() { + mDelegate.invalidateView(); + } + + @Override + public boolean isFragmentAdded() { + return mFragment.isAdded(); + } + + @Override + public boolean isFragmentDetached() { + return mFragment.isDetached(); + } + + @Override + public boolean isFragmentInBackstack() { + return BackstackReader.isInBackStack(mFragment); + } + + @Override + public boolean isFragmentRemoving() { + return mFragment.isRemoving(); + } + + @Override + public void onFragmentDestroyed(final FragmentManager fm, final Fragment f) { + super.onFragmentDestroyed(fm, f); + if (f == mFragment) { + mDelegate.onDestroy_afterSuper(); + } + } + + @Override + public void onFragmentSaveInstanceState(final FragmentManager fm, final Fragment f, + final Bundle outState) { + super.onFragmentSaveInstanceState(fm, f, outState); + if (f == mFragment) { + mDelegate.onSaveInstanceState_afterSuper(outState); + } + } + + @Override + public void onFragmentStarted(final FragmentManager fm, final Fragment f) { + super.onFragmentStarted(fm, f); + if (f == mFragment) { + mDelegate.onStart_afterSuper(); + } + } + + @Override + public void onFragmentStopped(final FragmentManager fm, final Fragment f) { + super.onFragmentStopped(fm, f); + if (f == mFragment) { + mDelegate.onStop_beforeSuper(); + } + } + + @Override + public void onFragmentViewCreated(final FragmentManager fm, final Fragment f, final View v, + final Bundle savedInstanceState) { + super.onFragmentViewCreated(fm, f, v, savedInstanceState); + if (f == mFragment) { + mDelegate.onCreateView_beforeSuper(null, null, savedInstanceState); + } + } + + @Override + public void onFragmentViewDestroyed(final FragmentManager fm, final Fragment f) { + super.onFragmentViewDestroyed(fm, f); + if (f == mFragment) { + mDelegate.onDestroyView_beforeSuper(); + } + } + + @SuppressWarnings("unchecked") + @NonNull + @Override + public V provideView() { + final Class foundViewInterface = AnnotationUtil + .getInterfaceOfClassExtendingGivenInterface(mFragment.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 activity itself is the view and implements the TiView interface + return (V) mFragment; + } + } + } +}