I want to open an existing fragment from compose or if i can add inside the same compose in full screen, that will also work. Also how to navigate from one existing fragment to another existing fragment from compose.
Our recommendation for using Fragments in Compose is documented here.
Specifically, you should use the AndroidViewBinding composable to inflate an XML containing a FragmentContainerView hosting the fragment you want to use in Compose. AndroidViewBinding has fragment-specific handling which is why you want to use this over AndroidView. Note that you need to have view binding enabled for this.
Example XML file:
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment_container_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:name="com.example.MyFragment" />
And then in your Composable function:
#Composable
fun FragmentInComposeExample() {
AndroidViewBinding(MyFragmentLayoutBinding::inflate) {
val myFragment = fragmentContainerView.getFragment<MyFragment>()
// ...
}
}
I solved this as following:
#Composable
fun ReusableFragmentComponent(
someArgumentForFragment: FragmentArgument,
fragmentManager: FragmentManager,
modifier: Modifier = Modifier,
tag: String = "ReusableFragmentTag"
) {
AndroidView(
modifier = modifier,
factory = { context ->
FrameLayout(context).apply {
id = ViewCompat.generateViewId()
}
},
update = {
val fragmentAlreadyAdded = fragmentManager.findFragmentByTag(tag) != null
if (!fragmentAlreadyAdded) {
fragmentManager.commit {
add(it.id, ReusableFragment.newInstance(someArgumentForFragment), tag)
}
}
}
)
}
In my case I called this from a fragment (hosted by navigation component), in an effort to make our reusable fragments compose compatible. I did that like so:
class ReusableFragments : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
Column {
Text("ReusableFragment 1")
ReusableFragmentComponent(someArgument1, childFragmentManager, tag = "ReusableFragmentTag1")
Text("ReusableFragment 2")
ReusableFragmentComponent(someArgument2, childFragmentManager, tag = "ReusableFragmentTag2")
}
}
}
}
}
By making the tag customizable it's possible to add multiple different instances of the same fragment to the same fragment manager.
In order to work with your existing xml views and fragments, in a Jetpack Compose app, you can refer to Interoperability APIs.
Specifically, I understand that you want to manage Android Views in Compose.
I want to open an existing fragment from compose
#Composable
fun CustomView() {
AndroidView(
factory = { context ->
MyView(context).apply {
// myView listeners
}
},
update = { view ->
// Recomposition logics
}
)
}
..how to navigate from one existing fragment to another existing fragment from compose.
In the above example you now have a composable function, so just use Jetpack Compose navigation library.
This helped me:
#Composable
fun MyFragmentView(
fragmentManager: FragmentManager,
modifier: Modifier = Modifier
) {
AndroidView(
modifier = modifier.fillMaxSize(),
factory = { context ->
val containerId = R.id.container // some unique id
val fragmentContainerView = FragmentContainerView(context).apply {
id = containerId
}
val fragment = MyFragment()
fragmentManager.beginTransaction()
.replace(containerId, fragment, fragment.javaClass.simpleName)
.commitAllowingStateLoss()
fragmentContainerView
}
)
}
ids.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="container" type="id"/>
</resources>
Related
I am integrating Compose into my app and I am confused about the use of multiple remember calls when managing state using state holders. Consider the following example:
State holder:
class MyScreenState(
val listState: LazyListState,
private val fragment: Fragment
) {
fun doSomethingWithFragment() {
...
...
}
}
Remember function:
#Composable
fun rememberMyScreenState(
listState: LazyListState = rememberLazyListState(),
fragment: Fragment
) = remember(
listState,
fragment
) {
MyScreenState(
listState = listState,
fragment = fragment
)
}
Fragment:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
val state = rememberMyScreenState(this#MyScreenFragment)
...
}
}
}
As it is stated in documentation, each state holder should also provide its remember function which calls remember with the passed values and creates the holder.
But why are the default values of this function usually another remember functions? What additional value does it bring to the table? Why can't I simply default to to LazyListState():
#Composable
fun rememberMyScreenState(
listState: LazyListState = LazyListState(),
fragment: Fragment
)
And what should I do if I have a value that doesn't have its own remember "wrapper", like fragment from the example above. Can I safely pass it like this or do I also need to wrap it for some reason?
remember will save everything inside its block, but this does not apply to function parameters.
So this line listState: LazyListState = LazyListState() will create a new state every time it is recomposed - and the old state will be forgotten.
So all parameters should be remembered by themselves (if necessary).
I'm attempting to add Jetpack Compose composables to my xml file in a fragment.
When I try to run it on a device I'm getting an error:
Cannot add views to ComposeView; only Compose content is supported
Fragment:
class ComposeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(
R.layout.activity_main, container, false
)
view.findViewById<ComposeView>(R.id.compose_view).setContent {
Column(
modifier = Modifier
.border(border = BorderStroke(1.dp, Color.Black))
.padding(16.dp)
) {
Text("THIS IS A COMPOSABLE INSIDE THE FRAGMENT XML")
Spacer(modifier = Modifier.padding(10.dp))
CircularProgressIndicator()
Spacer(modifier = Modifier.padding(10.dp))
Text("NEAT")
Spacer(modifier = Modifier.padding(10.dp))
}
}
return view
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/main_container"
/>
<androidx.compose.ui.platform.ComposeView
android:id="#+id/compose_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</RelativeLayout>
MainActivity.kt:
open class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
supportFragmentManager.beginTransaction()
.replace(R.id.compose_view, ComposeFragment())
.commit()
}
}
I'm not really sure what this error means or how to address it. I can't find this error anywhere by googling.
In your MainActivity you are trying to put a fragment inside a ComposeView (which takes only Composable).
You should specify the right container for your Fragment:
supportFragmentManager.beginTransaction()
.replace(R.id.main_container, ComposeFragment())
.commit()
replace(R.id.compose_view, ComposeFragment()) is adding a Fragment to your ComposeView, not to your FragmentContainerView - that's what is adding a View to your ComposeView.
If you want to add a Fragment to your FragmentContainerView, you'll need to use the ID of the FragmentContainerView:
open class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Always surround any FragmentTransactions in a
// savedInstanceState == null since fragment are
// automatically restored and you don't want to replace
// those restored fragments
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.main_container, ComposeFragment())
.commit()
}
}
}
It isn't clear why your Activity is inflating activity_main and your Fragment is also inflating activity_main - those should be different layouts and you should choose if you want your activity's layout to only host a fragment (thereby removing the ComposeView from its layout) and have the fragment contain your ComposeView or if you want to skip the fragment entirely and directly have your ComposeView in your Activity. Doing both (in particular, in your layout where they would overlap one another due to your use of RelativeLayout) doesn't make much sense.
I'm working on an Android app using Kotlin. The app uses fragment-based navigation but I am using some Jetpack Compose to build some elements of it instead of using RecyclerViews and such.
Right now I have a card composable that builds itself off an object and another one that creates a list of those with a LazyColumn. The card has it's own separate file but the list composable is part of the code of the fragment that uses it. This is because when one of the cards is clicked, it calls a function to load a fragment that lists the details of the object the card represents (Events in this case).
This is the code in my list fragment:
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_liste_evenement,container,false).apply {
val listeEvens : ArrayList<Événement> = ArrayList<Événement>()
listeEvens.add(évén)
listeEvens.add(évén2)
listeEvens.add(évén3)
val composeView = findViewById<ComposeView>(R.id.listeBlocsEven)
composeView.setContent {
ListeCarteÉvénements(événements = listeEvens)
}
}
}
#Composable
fun ListeCarteÉvénements(événements: List<Événement>) {
LazyColumn {
items(événements) { e ->
CarteÉvénement(événement = e,clickEvent = { loadFragment(details_evenement(e)) })
}
}
}
This is the card composable's declaration:
#Composable
fun CarteÉvénement(événement: Événement,clickEvent: () -> Unit) {
Column(modifier = Modifier
.clip(RectangleShape)
.padding(all = 8.dp)
.fillMaxWidth()
.height(300.dp)
.background(MaterialTheme.colors.primaryVariant)
.clickable(onClick = clickEvent))
private fun loadFragment(fragment: Fragment) {
val transaction = requireActivity().supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragmentContainerView, fragment)
transaction.addToBackStack(null)
transaction.commit()
}
As you can see, doing it this way allows me to get direct access to the event cards so that I can give my details fragment the clicked event as an attribute.
This all works but my question is: If I wanted to put the list composable in the same file as the card(outside of the fragment), how would I pass it the loadFragment function that receives a fragment that also has it's own parameter(in this case the event from the clicked card)?
You can pass a lambda callback to ListeCarteÉvénements which receives the event as an argument.
override fun onCreateView (...) : View {
...
composeView.setContent {
ListeCarteÉvénements(
événements = listeEvens,
onItemClick = { e -> loadFragment(details_evenement(e)) }
)
}
...
}
#Composable
fun ListeCarteÉvénements(événements: List<Événement>, onItemClick: (Événement) -> Unit {
LazyColumn {
items(événements) { e ->
CarteÉvénement(événement = e,clickEvent = { onItemClick(e) })
}
}
}
I followed the compose documentation to create a bottom navigation bar by creating such a sealed class
sealed class Screen(val route: String, val label: String, val icon: ImageVector) {
object ScreenA: Screen("screenA", "ScreenA", Icons.Default.AccountBox)
object ScreenB: Screen("screenB", "ScreenB", Icons.Default.ThumbUp
}
with something such as the following screen, simply containing a Text item.
#Composable
fun ScreenA() {
Text(text = "This is ScreenA",
style = TextStyle(color = Color.Black, fontSize = 36.sp),
textAlign = TextAlign.Center)
}
and have also implemented the Scaffold, BottomNavigation, and NavHost exactly like in the docs and everything is working just fine.
Let´s say I now want to have one of the screens with a whole bunch of data displayed in a list with all sorts of business logic methods which would need a Fragment, ViewModel, Repository, and so on.
What would be the correct approach be? Can I still create the fragments and somehow pass them to the sealed class or should we forget about fragments and make everything composable?
Since you are following the documentation you need to forget about Fragments. Documentations suggests that view models per screen will be instantiated when the composable function is called/navigated to. The viewmodel will live while the composeable is not disposed. This is also motivated by single activity approach.
#Inject lateinit var factory : ViewModelProvider.Factory
#Composable
fun Profile(profileViewModel = viewModel(factory),
userId: String,
navigateToFriendProfile: (friendUserId: String) -> Unit) {
}
This can be seen in compose samples here.
If you want to mix both, you can omit using navigation-compose, and roll with navigation component and use Fragment and only use composable for fragment UI.
#AndroidEntryPoint
class ProfileFragment: BaseFragment() {
private val profileViewModel by viewModels<ProfileViewModel>(viewModelFactory)
private val profileFragmentArgs by navArg<ProfileFragmentArgs>()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return ComposeView(requireContext()).apply {
setContent {
Profile(profileViewModel = profileViewModel, id = profileFragmentArgs.id, navigateToFriendProfile = { findNavController().navigate(...) })
}
}
}
Is it possible to use the new Navigation Architecture Component with DialogFragment? Do I have to create a custom Navigator?
I would love to use them with the new features in my navigation graph.
May 2019 Update:
DialogFragment are now fully supported starting from Navigation 2.1.0, you can read more here and here
Old Answer for Navigation <= 2.1.0-alpha02:
I proceeded in this way:
1) Update Navigation library at least to version 2.1.0-alpha01 and copy both files of this modified gist in your project.
2) Then in your navigation host fragment, change the name parameter to your custom NavHostFragment
<fragment
android:id="#+id/nav_host_fragment"
android:name="com.example.app.navigation.MyNavHostFragment"
app:defaultNavHost="true"
app:navGraph="#navigation/nav_graph"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/toolbar" />
3) Create your DialogFragment subclasses and add them to your nav_graph.xml with:
<dialog
android:id="#+id/my_dialog"
android:name="com.example.ui.MyDialogFragment"
tools:layout="#layout/my_dialog" />
4) Now launch them from fragments or activity with
findNavController().navigate(R.id.my_dialog)
or similar methods.
No, as of the 1.0.0-alpha01 build, there is no support for dialogs as part of your Navigation Graph. You should just continue to use show() to show a DialogFragment.
Yes. The framework is made in such a way that you can create a class extending the Navigator abstract class for the views that does not come out-of-the box and add it to your NavController with the method getNavigatorProvider().addNavigator(Navigator navigator)
If you are using the NavHostFragment, you will also need to extend it to add the custom Navigator or just create your own MyFragment implementing NavHost interface. It's so flexible that you can create your own xml parameters with custom attrs defined in values, like you do creating custom views. Something like this (not tested):
#Navigator.Name("dialog-fragment")
class DialogFragmentNavigator(
val context: Context,
private val fragmentManager: FragmentManager
) : Navigator<DialogFragmentNavigator.Destination>() {
override fun navigate(destination: Destination, args: Bundle?,
navOptions: NavOptions?, navigatorExtras: Extras?
): NavDestination {
val fragment = Class.forName(destination.name).newInstance() as DialogFragment
fragment.show(fragmentManager, destination.id.toString())
return destination
}
override fun createDestination(): Destination = Destination(this)
override fun popBackStack() = fragmentManager.popBackStackImmediate()
class Destination(navigator: DialogFragmentNavigator) : NavDestination(navigator) {
// The value of <dialog-fragment app:name="com.example.MyFragmentDialog"/>
lateinit var name: String
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
val a = context.resources.obtainAttributes(
attrs, R.styleable.FragmentNavigator
)
name = a.getString(R.styleable.FragmentNavigator_android_name)
?: throw RuntimeException("Error while inflating XML. " +
"`name` attribute is required")
a.recycle()
}
}
}
Usage
my_navigation.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/navigation"
app:startDestination="#id/navigation_home">
<fragment
android:id="#+id/navigation_assistant"
android:name="com.example.ui.HomeFragment"
tools:layout="#layout/home">
<action
android:id="#+id/action_nav_to_dialog"
app:destination="#id/navigation_dialog" />
</fragment>
<dialog-fragment
android:id="#+id/navigation_dialog"
android:name="com.example.ui.MyDialogFragment"
tools:layout="#layout/my_dialog" />
</navigation>
The fragment that will navigate.
class HomeFragment : Fragment(), NavHost {
private val navControllerInternal: NavController by lazy(LazyThreadSafetyMode.NONE){
NavController(context!!)
}
override fun getNavController(): NavController = navControllerInternal
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Built-in navigator for `fragment` XML tag
navControllerInternal.navigatorProvider.addNavigator(
FragmentNavigator(context!!, childFragmentManager, this.id)
)
// Your custom navigator for `dialog-fragment` XML tag
navControllerInternal.navigatorProvider.addNavigator(
DialogFragmentNavigator(context!!, childFragmentManager)
)
navControllerInternal.setGraph(R.navigation.my_navigation)
}
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
val view = inflater.inflate(R.layout.home)
view.id = this.id
view.button.setOnClickListener{
getNavController().navigate(R.id.action_nav_to_dialog)
}
return view
}
}
Yes it is possible, You can access view of parent fragment from dialog fragment by calling getParentFragment().getView(). And use the view for navigation.
Here is the example
Navigation.findNavController(getParentFragment().getView()).navigate(R.id.nextfragment);
I created custom navigator for DialogFragment.
Sample is here.
(It's just sample, so it might be any problem.)
#Navigator.Name("dialog_fragment")
class DialogNavigator(
private val fragmentManager: FragmentManager
) : Navigator<DialogNavigator.Destination>() {
companion object {
private const val TAG = "dialog"
}
override fun navigate(destination: Destination, args: Bundle?,
navOptions: NavOptions?, navigatorExtras: Extras?) {
val fragment = destination.createFragment(args)
fragment.setTargetFragment(fragmentManager.primaryNavigationFragment,
SimpleDialogArgs.fromBundle(args).requestCode)
fragment.show(fragmentManager, TAG)
dispatchOnNavigatorNavigated(destination.id, BACK_STACK_UNCHANGED)
}
override fun createDestination(): Destination {
return Destination(this)
}
override fun popBackStack(): Boolean {
return true
}
class Destination(
navigator: Navigator<out NavDestination>
) : NavDestination(navigator) {
private var fragmentClass: Class<out DialogFragment>? = null
override fun onInflate(context: Context, attrs: AttributeSet) {
super.onInflate(context, attrs)
val a = context.resources.obtainAttributes(attrs,
R.styleable.FragmentNavigator)
a.getString(R.styleable.FragmentNavigator_android_name)
?.let { className ->
fragmentClass = parseClassFromName(context, className,
DialogFragment::class.java)
}
a.recycle()
}
fun createFragment(args: Bundle?): DialogFragment {
val fragment = fragmentClass?.newInstance()
?: throw IllegalStateException("fragment class not set")
args?.let {
fragment.arguments = it
}
return fragment
}
}
}
Version 2.1.0-alpha03 was Released so we can finally use DialogFragments. Unfortunately for me, I have some issues with the backstack when using cancelable dialogs. Probably I have a faulty implementation of my dialogs..
[LATER-EDIT] My implementation was good, the problem is related to Wrong dialog counting for DialogFragmentNavigator as is described in the issue tracker
As a workaround you can have a look on my recommendation
Updated for:
implementation "androidx.navigation:navigation-ui-ktx:2.2.0-rc04"
And use in my_nav_graph.xml
<dialog
android:id="#+id/my_dialog"
android:name="com.example.ui.MyDialogFragment"
tools:layout="#layout/my_dialog" />
Yes. It's possible in the latest update of Navigation Component. You can check this link to have a clear concept. raywenderlich.com
One option would be to just use a regular fragment and make it look similar to a dialog. I found it was not worth the hassle so I used the standard way using show(). If you insist See here for a way of doing it.
Yes, It is possible now. In it's initial release it wasn't possible but now
from "androidx.navigation:navigation-fragment:2.1.0-alpha03" this navigation version you can use dialog fragment in navigation component.
Check this out:- Naviagtion dialog fragment support