Replace BottomSheetDialogFragment with Jetpack Compose - android

I am working on a modulare project where the navigation is handled using NavController and deeplinks.
I have several BottomSheetDialogFragments that I'd like to replace with Jetpack Compose.
The current implementation of BottomSheetDialogFragment looks like this
class MyBottomSheetDialogFragment : BottomSheetDialogFragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel: MyViewModel by viewModels { viewModelFactory }
override fun onAttach(context: Context) {
super.onAttach(context)
AndroidSupportInjection.inject(this)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return BottomSheetFragmentBinding.inflate(
inflater,
container,
false
).root
}
}
It's included in a nav_graph.xml
<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/xx"
app:startDestination="#+id/xxx">
<dialog
android:id="#+id/myBottomSheetDialogFragment"
android:name="path.to.MyBottomSheetDialogFragment">
<deepLink app:uri="deeplink://myBottomSheetDialogFragment" />
</dialog>
</navigation>
It's shown using deeplink
findNavController().navigate(
NavDeepLinkRequest.Builder
.fromUri(
Uri
.parse("deeplink://myBottomSheetDialogFragment")
.buildUpon()
.build()
)
.build()
)
And it looks like this
I'd like to use JetpackCompose to generate this dialog but I'm confused how to do it.
First I don't know if I should use BottomSheetDialogFragment() and simply use SetContent{} in onCreateView or I should use BottomSheetScaffold.
The important thing is that it should be shown via deeplink because the modules don't have dependency on each other, so it can't simply create an instance of the dialog and use .show()

Related

How to prevent ViewModel from being cleared when navigating away in BottomNavigationView using Navigation Component

I'm having an issue saving the state of my fragment's UI and I can't seem to find any solutions regarding this problem when using a BottomNavigationView. Here's my current setup:
I have a NavHostFragment and a BottomNavigationView setup in activity_main.xml:
<LinearLayout>
...
<androidx.fragment.app.FragmentContainerView
android:id="#+id/bottom_nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="#navigation/bottom_tab_nav" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="72dp"
app:elevation="6dp"
app:menu="#menu/bottom_tab_menu"
app:itemIconTint="#color/tab_btn_color"
app:itemTextColor="#color/tab_btn_color"
app:itemTextAppearanceActive="#style/TabText"
app:itemTextAppearanceInactive="#style/TabText"
app:itemRippleColor="#color/background"/>
...
</LinearLayout>
I setup the navController in MainActivity.kt as follows:
class MainActivity : AppCompatActivity() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.bottom_nav_host_fragment) as NavHostFragment
val bottomNavController = navHostFragment.navController
binding.bottomNavView.setupWithNavController(bottomNavController)
...
}
In my navigation graph, I have two separate Fragment destinations:
HomeFragment which is the one I want to save.
ProfileFragment which is currently empty.
For the time being, HomeFragment contains all the UI logic that I want to migrate to HomeViewModel and save all my state in there. Here's my HomeViewModel.kt class:
class HomeViewModel: ViewModel() {
init {
Log.d(TAG, "HomeViewModel is Initialized.")
}
override fun onCleared() {
super.onCleared()
Log.d(TAG, "HomeViewModel is removed.")
}
}
And in HomeFragment.kt I initialize my ViewModel as follows:
class HomeFragment : Fragment() {
...
private lateinit var viewModel: HomeViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
...
viewModel = ViewModelProvider(this)[HomeViewModel::class.java]
...
return binding.root
}
}
My Problem:
Whenever I navigate to ProfileFragment I get nothing in the console, and then when I navigate back to HomeFragment again I get this output:
D/HomeViewModel: HomeViewModel is removed.
D/HomeViewModel: HomeViewModel is Initialized.
Which means my HomeViewModel is getting re-created.
Note that I'm not manually calling any navigate methods to navigate between the two fragments, navigation is done automatically by setting the menu.xml item ids to the same ids as the HomeFragment and ProfileFragment destinations.
Question:
How can I persist the state of HomeViewModel when navigating away from HomeFragment so that I could later on migrate all of my UI logic to it?
Also, any insights on how to manage the state of my HomeFragment using ViewModel would be apperciated.
When you move away from a Fragment, Navigation component destroy its View later to be created from scratch whenever you navigate into it. In your HomeFragment's onCreateView, you're creating a new instance of the HomeViewModel every time this happens. Instead, you can use a Fragment scoped ViewModel by using by viewModels() extension provided by fragment-ktx (see here)
import androidx.fragment.app.viewModels
class HomeFragment : Fragment() {
...
private val viewModel by viewModels<HomeViewModel>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
...
// Do not instantiate a ViewModel here
...
return binding.root
}
}

Image not being downloaded by Glide inside a Fragment

I am trying to download an image into an ImageView inside a Fragment, but it seems like the network request does not even get made by Glide. (I use Proxyman to watch network traffic from my physical Android device). I am not sure what else to try.
Here is the fragment code:
class ExampleFragment : Fragment(R.layout.fragment_example) {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var viewModel: ExampleViewModel
private lateinit var binding: FragmentExampleBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentExampleBinding.inflate(layoutInflater)
(activity as ExampleActivity).appComponent.inject(this)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ExampleViewModel::class.java)
// I have also tried `activity as FragmentActivity` instead of this
Glide.with(this).load("https://www.popwebdesign.net/popart_blog/wp-content/uploads/2018/01/tiny-png-panda.jpg")
.override(200)
.into(binding.imageViewFaceTaggingStart)
}
}
Extra details:
I have read http://bumptech.github.io/glide/doc/debugging.html#missing-images-and-local-logs which mentions a few things I could try. I can confirm I call into(), am not using Custom Targets, and have explicitly set the size and background colour of the image view to ensure it is not "zero width".
I am using FragmentContainerView inside the Activity.
Here is a simplification of the XML file:
<androidx.constraintlayout.widget.ConstraintLayout ... >
<androidx.appcompat.widget.Toolbar ...></androidx.appcompat.widget.Toolbar>
<ScrollView ...>
<LinearLayout ...>
<ImageView
android:id="#+id/image_view_face_tagging_start"
android:layout_width="200dp"
android:layout_height="200dp"
android:visibility="visible"
android:background="#android:color/black"
android:scaleType="centerCrop"/>
....more views (TextView, Space, Checkbox)
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
The view that is inflated and the view reference i.e. binding that you are using are not same,
class ExampleFragment : Fragment() {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
lateinit var viewModel: ExampleViewModel
private lateinit var binding: FragmentExampleBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentExampleBinding.inflate(layoutInflater)
return this.binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as ExampleActivity).appComponent.inject(this)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ExampleViewModel::class.java)
// I have also tried `activity as FragmentActivity` instead of this
Glide.with(this).load("https://www.popwebdesign.net/popart_blog/wp-content/uploads/2018/01/tiny-png-panda.jpg")
.override(200)
.into(binding.imageViewFaceTaggingStart)
}
}
Now the view of the fragment is the binding, Try with this.
issue in here class ExampleFragment : Fragment(R.layout.fragment_example)
Now the fragment will inflate this R.layout.fragment_example not the binding, so the binding refers to another view,

Kodein Framework - property delegate must have a provideDelegate(...) method

I'm trying to build an app with the following architecture: LoginActivity -> MainActivity -> everything else handled in fragments hosted by MainActivity. I'm also using the Kodein Framework for the first time and get the following error in my starting fragment:
Property delegate must have a 'provideDelegate(HomeFragment,
KProperty*>' method. None of the following functions is suitable.
provideDelegate(Context [highlighted in red], KProperty<>?) defined
in org.kodein.di.android.KodeinPropertyDelegateProvider Type
'KodeinPropertyDelegateProvider' has no method
'getValue(HomeFragment, KProperty<>)' and thus it cannot serve as a
delegate
This is my code so far:
class HomeFragment : Fragment(), KodeinAware {
override val kodein by kodein()
private val factory : MainViewModelFactory by instance()
private lateinit var viewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding : FragmentHomeBinding = FragmentHomeBinding.inflate(inflater, container, false)
viewModel = ViewModelProviders.of(this, factory).get(MainViewModel::class.java)
binding.viewModel = viewModel
return binding.root
}
}
How can I fix this?
Thanks :)
Nevermind, adding a type declaration after kodein did the trick... :)
In your imports change
import org.kodein.di.android.kodein
to
import org.kodein.di.android.x.kodein
You can do it like this:
override val kodein:Kodein by kodein()

Android Jetpack Navigation Component issue with edit text masks and error messages

I'm trying to use android-input-mask by RedMadRobot in my Android Kotlin project. But currently, I'm dealing with very strange behavior. The library only works when I disable the Android Navigation Component.
My activity_main.xml layout has the following fragment:
<fragment
android:id="#+id/nav_host"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/navigation"
app:defaultNavHost="true"/>
Then, in the start destination defined in the navigation component I have:
<EditText
android:id="#+id/test"
android:inputType="number"
android:digits="1234567890+-() "
{ omitted for sake of simplicity } />
Finally, in the the SignUpFragment.kt file I have these lines of code:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val listener = MaskedTextChangedListener.installOn(
test,
"+7 ([000]) [000]-[00]-[00]",
object : MaskedTextChangedListener.ValueListener {
override fun onTextChanged(maskFilled: Boolean, extractedValue: String, formattedValue: String) {
Log.d("TAG", extractedValue)
Log.d("TAG", maskFilled.toString())
}
}
)
test.hint = listener.placeholder()
}
But it does not works, as you can see in the following image:
However, when I hard code the signup fragment in the activity_main.xml file all works fine:
<fragment
android:id="#+id/fragment"
android:name="my.app.SignUpFragment"
{ omitted for sake of simplicity } />
My question is: is there any plausible explanation for this "bug"? Am I making some confusion? How can I solve it?
Thanks for the help.
EDIT:
Same behavior for error messages. If I put this line of code:
test.error = "Error message"
using Android Navigation Component no error message is shown. However, if I hard code the fragment in the main activity layout, the error message is displayed.
Ok, after a lot of time spent in searching for answers, I found that my issue is related to the Android Data Binding Library. More specifically, I need to set listeners and the error message in the binding object created in the onCreateView of my SignUpFragment, as follows:
private lateinit var binding: FragmentSignUpBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_sign_up, container, false)
// saving the instance of FragmentSignUpBinding
binding = DataBindingUtil.setContentView(activity!!, R.layout.fragment_sign_up)
binding.signupViewModel = signUpViewModel
binding.lifecycleOwner = this
setObservers()
return view
}
then, in the onViewCreated:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
MaskedTextChangedListener.installOn(
binding.editCpf, // insted of simply edit_cpf
"[000].[000].[000]-[00]",
object : MaskedTextChangedListener.ValueListener {
override fun onTextChanged(maskFilled: Boolean, extractedValue: String, formattedValue: String) {
Log.d("TAG", extractedValue)
Log.d("TAG", maskFilled.toString())
}
}
)
}
and then it works fine.

Navigation Architecture Component - Dialog Fragments

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

Categories

Resources