Add API to identify Fragment from view
Currently there is no way to identify if a view is associated with a
Fragment, or what Fragment the view may be associated with.
This changed adds a static API to FragmentManager to allow a Fragment to
be found from a view. The fragment is stored in the view as a tag with
a static id after the fragment view is created, but before the view is
added to the fragment container. The fragment can then be retrieved from
any view that is a child of the fragment's view.
Test: Added Tests in FragmentViewTest, ./gradlew checkApi
BUG: 136494650
Change-Id: Idd79a1bb21c1c62473d1498267ab8ddf5e27cda9
diff --git a/fragment/fragment-ktx/api/1.2.0-alpha02.txt b/fragment/fragment-ktx/api/1.2.0-alpha02.txt
index dd42d83..ab3b3b7 100644
--- a/fragment/fragment-ktx/api/1.2.0-alpha02.txt
+++ b/fragment/fragment-ktx/api/1.2.0-alpha02.txt
@@ -22,5 +22,10 @@
method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer = { this }, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer = null);
}
+ public final class ViewKt {
+ ctor public ViewKt();
+ method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
+ }
+
}
diff --git a/fragment/fragment-ktx/api/current.txt b/fragment/fragment-ktx/api/current.txt
index dd42d83..ab3b3b7 100644
--- a/fragment/fragment-ktx/api/current.txt
+++ b/fragment/fragment-ktx/api/current.txt
@@ -22,5 +22,10 @@
method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer = { this }, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer = null);
}
+ public final class ViewKt {
+ ctor public ViewKt();
+ method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
+ }
+
}
diff --git a/fragment/fragment-ktx/api/restricted_1.2.0-alpha02.txt b/fragment/fragment-ktx/api/restricted_1.2.0-alpha02.txt
index dd42d83..ab3b3b7 100644
--- a/fragment/fragment-ktx/api/restricted_1.2.0-alpha02.txt
+++ b/fragment/fragment-ktx/api/restricted_1.2.0-alpha02.txt
@@ -22,5 +22,10 @@
method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer = { this }, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer = null);
}
+ public final class ViewKt {
+ ctor public ViewKt();
+ method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
+ }
+
}
diff --git a/fragment/fragment-ktx/api/restricted_current.txt b/fragment/fragment-ktx/api/restricted_current.txt
index dd42d83..ab3b3b7 100644
--- a/fragment/fragment-ktx/api/restricted_current.txt
+++ b/fragment/fragment-ktx/api/restricted_current.txt
@@ -22,5 +22,10 @@
method @MainThread public static inline <reified VM extends androidx.lifecycle.ViewModel> kotlin.Lazy<VM> viewModels(androidx.fragment.app.Fragment, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelStoreOwner> ownerProducer = { this }, kotlin.jvm.functions.Function0<? extends androidx.lifecycle.ViewModelProvider.Factory>? factoryProducer = null);
}
+ public final class ViewKt {
+ ctor public ViewKt();
+ method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
+ }
+
}
diff --git a/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/ViewTest.kt b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/ViewTest.kt
new file mode 100644
index 0000000..d1eb53a
--- /dev/null
+++ b/fragment/fragment-ktx/src/androidTest/java/androidx/fragment/app/ViewTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * 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 androidx.fragment.app
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.annotation.UiThreadTest
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.test.rule.ActivityTestRule
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Assert.fail
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ViewTest {
+ @get:Rule val activityRule = ActivityTestRule<TestActivity>(TestActivity::class.java)
+ private val fragmentManager get() = activityRule.activity.supportFragmentManager
+
+ @UiThreadTest
+ @Test
+ fun findFragment() {
+ val fragment = ViewFragment()
+ fragmentManager.commitNow {
+ add(android.R.id.content, fragment)
+ }
+
+ val foundFragment = fragment.requireView().findFragment<ViewFragment>()
+ assertWithMessage("View should have Fragment set")
+ .that(foundFragment)
+ .isSameInstanceAs(fragment)
+ }
+
+ @Test
+ fun findFragmentNull() {
+ val view = View(ApplicationProvider.getApplicationContext() as Context)
+ try {
+ view.findFragment<Fragment>()
+ fail("findFragment should throw IllegalStateException if a Fragment was not set")
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("View $view does not have a Fragment set")
+ }
+ }
+}
+
+class ViewFragment : Fragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return View(context)
+ }
+}
diff --git a/fragment/fragment-ktx/src/main/java/androidx/fragment/app/View.kt b/fragment/fragment-ktx/src/main/java/androidx/fragment/app/View.kt
new file mode 100644
index 0000000..63ae98f
--- /dev/null
+++ b/fragment/fragment-ktx/src/main/java/androidx/fragment/app/View.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * 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 androidx.fragment.app
+
+import android.view.View
+
+/**
+ * Find a [Fragment] associated with a [View].
+ *
+ * This method will locate the [Fragment] associated with this view. This is automatically
+ * populated for the View returned by [Fragment.onCreateView] and its children.
+ *
+ * Calling this on a View that does not have a Fragment set will result in an
+ * [IllegalStateException]
+ */
+fun <F : Fragment> View.findFragment(): F = FragmentManager.findFragment(this)
diff --git a/fragment/fragment/api/1.2.0-alpha02.txt b/fragment/fragment/api/1.2.0-alpha02.txt
index 1bebde6..346d9b7 100644
--- a/fragment/fragment/api/1.2.0-alpha02.txt
+++ b/fragment/fragment/api/1.2.0-alpha02.txt
@@ -269,6 +269,7 @@
method public void dump(String, java.io.FileDescriptor?, java.io.PrintWriter, String![]?);
method public static void enableDebugLogging(boolean);
method public boolean executePendingTransactions();
+ method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
method public androidx.fragment.app.Fragment? findFragmentById(@IdRes int);
method public androidx.fragment.app.Fragment? findFragmentByTag(String?);
method public androidx.fragment.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index 1bebde6..346d9b7 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -269,6 +269,7 @@
method public void dump(String, java.io.FileDescriptor?, java.io.PrintWriter, String![]?);
method public static void enableDebugLogging(boolean);
method public boolean executePendingTransactions();
+ method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
method public androidx.fragment.app.Fragment? findFragmentById(@IdRes int);
method public androidx.fragment.app.Fragment? findFragmentByTag(String?);
method public androidx.fragment.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
diff --git a/fragment/fragment/api/restricted_1.2.0-alpha02.txt b/fragment/fragment/api/restricted_1.2.0-alpha02.txt
index a0aa372..7b475f2 100644
--- a/fragment/fragment/api/restricted_1.2.0-alpha02.txt
+++ b/fragment/fragment/api/restricted_1.2.0-alpha02.txt
@@ -274,6 +274,7 @@
method public void dump(String, java.io.FileDescriptor?, java.io.PrintWriter, String![]?);
method public static void enableDebugLogging(boolean);
method public boolean executePendingTransactions();
+ method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
method public androidx.fragment.app.Fragment? findFragmentById(@IdRes int);
method public androidx.fragment.app.Fragment? findFragmentByTag(String?);
method public androidx.fragment.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index a0aa372..7b475f2 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -274,6 +274,7 @@
method public void dump(String, java.io.FileDescriptor?, java.io.PrintWriter, String![]?);
method public static void enableDebugLogging(boolean);
method public boolean executePendingTransactions();
+ method public static <F extends androidx.fragment.app.Fragment> F findFragment(android.view.View);
method public androidx.fragment.app.Fragment? findFragmentById(@IdRes int);
method public androidx.fragment.app.Fragment? findFragmentByTag(String?);
method public androidx.fragment.app.FragmentManager.BackStackEntry getBackStackEntryAt(int);
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
index 70e0dfb..e65aa30 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewTest.kt
@@ -310,6 +310,46 @@
.isEqualTo(Lifecycle.State.INITIALIZED)
}
+ @Test
+ fun findFragmentNoTagSet() {
+ val view = View(activityRule.activity)
+ try {
+ FragmentManager.findFragment<Fragment>(view)
+ fail("findFragment should throw IllegalStateException if a Fragment was not set")
+ } catch (e: IllegalStateException) {
+ assertThat(e)
+ .hasMessageThat().contains("View $view does not have a Fragment set")
+ }
+ }
+
+ @Test
+ fun findFragmentAfterAdd() {
+ activityRule.setContentView(R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+
+ val fragment = StrictViewFragment()
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment).commit()
+ activityRule.executePendingTransactions()
+
+ assertThat(FragmentManager.findFragment<StrictViewFragment>(fragment.requireView()))
+ .isSameInstanceAs(fragment)
+ }
+
+ @Test
+ fun findFragmentFindByIdChildView() {
+ activityRule.setContentView(R.layout.simple_container)
+ val fm = activityRule.activity.supportFragmentManager
+
+ val fragment = StrictViewFragment(R.layout.fragment_a)
+ fm.beginTransaction().add(R.id.fragmentContainer, fragment).commit()
+ activityRule.executePendingTransactions()
+
+ val view = fragment.requireView().findViewById<View>(R.id.textA)
+ assertThat(view).isNotNull()
+ assertThat(FragmentManager.findFragment<StrictViewFragment>(view))
+ .isSameInstanceAs(fragment)
+ }
+
// Hide a fragment and its View should be GONE. Then pop it and the View should be VISIBLE
@Test
fun hideFragment() {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index e76789e..c1354c8 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -36,6 +36,7 @@
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
@@ -753,6 +754,55 @@
}
/**
+ * Find a {@link Fragment} associated with the given {@link View}.
+ *
+ * This method will locate the {@link Fragment} associated with this view. This is automatically
+ * populated for the View returned by {@link Fragment#onCreateView} and its children.
+ *
+ * @param view the view to search from
+ * @return the locally scoped {@link Fragment} to the given view
+ * @throws IllegalStateException if the given view does not correspond with a
+ * {@link Fragment}.
+ */
+ @NonNull
+ @SuppressWarnings("unchecked") // We should throw a ClassCast exception if the type is wrong
+ public static <F extends Fragment> F findFragment(@NonNull View view) {
+ Fragment fragment = findViewFragment(view);
+ if (fragment == null) {
+ throw new IllegalStateException("View " + view + " does not have a Fragment set");
+ }
+ return (F) fragment;
+ }
+
+ /**
+ * Recurse up the view hierarchy, looking for the Fragment
+ * @param view the view to search from
+ * @return the locally scoped {@link Fragment} to the given view, if found
+ */
+ @Nullable
+ static Fragment findViewFragment(@NonNull View view) {
+ while (view != null) {
+ Object tag = view.getTag(R.id.fragment_container_view_tag);
+ if (tag instanceof Fragment) {
+ return (Fragment) tag;
+ }
+ ViewParent parent = view.getParent();
+ view = parent instanceof View ? (View) parent : null;
+ }
+ return null;
+ }
+
+ /**
+ * Used to store the Fragment inside of its view's tag. This is done after the fragment's view
+ * is created, but before the view is added to the container.
+ *
+ * @param fragment The fragment to be set as a tag on its view
+ */
+ void setViewTag(@NonNull Fragment fragment) {
+ fragment.mView.setTag(R.id.fragment_container_view_tag, fragment);
+ }
+
+ /**
* Get a list of all fragments that are currently added to the FragmentManager.
* This may include those that are hidden as well as those that are shown.
* This will not include any fragments only in the back stack, or fragments that
@@ -1259,6 +1309,7 @@
if (f.mView != null) {
f.mInnerView = f.mView;
f.mView.setSaveFromParentEnabled(false);
+ setViewTag(f);
if (container != null) {
container.addView(f.mView);
}
diff --git a/fragment/fragment/src/main/res/values/ids.xml b/fragment/fragment/src/main/res/values/ids.xml
new file mode 100644
index 0000000..db4ce6e
--- /dev/null
+++ b/fragment/fragment/src/main/res/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 The Android Open Source Project
+
+ 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.
+ -->
+
+<resources>
+ <item type="id" name="fragment_container_view_tag" />
+</resources>
\ No newline at end of file