0. Problem
This question by gave me the idea to implement the Data Binding Library for the purpose of opening a Link in a Webview-Fragment on click of a Button in a Bottom Sheet Fragment.
I was able to implement the Data Binding as seen in the other Question (Link), but the WebView doesn't load the new URL when a Button is clicked in said Bottom Sheet Fragment. I get the feedback from the console that the Button was clicked and the LiveData was changed though.
So, I thought the WebView does reload automatically when the LiveData changes but that doesn't seem to be the case...so I do not see my Error and not sure if I implemented everything correctly.
Hopefully someone can help me.
1. Respective Classes
1.1. WebViewFragment
class WebviewFragment : Fragment() {
private lateinit var webView: WebView
companion object {
fun newInstance() = WebviewFragment()
}
private lateinit var viewModel: WebViewViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding: FragmentMainWebviewBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_main_webview, container, false)
viewModel = ViewModelProvider(this).get(WebViewViewModel::class.java)
binding.webViewModel = viewModel
binding.lifecycleOwner = this
return binding.root
}
#SuppressLint("SetJavaScriptEnabled", "JavascriptInterface")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
webView = webViewMain
webView.settings.javaScriptEnabled = true
}
}
1.2. WebViewModel
class WebViewViewModel : ViewModel() {
val webViewUrl = MutableLiveData<String>().apply{ value = "file:///android_asset/html_files/gallery_page.html" }
companion object WebViewUrlLoader {
#BindingAdapter("loadUrl")
#JvmStatic
fun WebView.setUrl(url: String) {
this.loadUrl(url)
}
}
}
1.3. WebView Layout (XML)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="webViewModel"
type="com.example.ui.main.webview.WebViewViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/main_screen_webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainFragmentGalleryView"
>
<WebView
android:id="#+id/webViewMain"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:loadUrl="#{webViewModel.webViewUrl}"
android:paddingBottom="52dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
1.4. BottomSheet Fragment
class BottomSheetFragment : BottomSheetDialogFragment() {
private var fragmentView: View? = null
private lateinit var viewModel: WebViewViewModel
companion object {
fun newInstance() = BottomSheetFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
fragmentView = inflater.inflate(R.layout.view_modal_bottom_sheet, container, false)
return fragmentView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(this).get(WebViewViewModel::class.java)
initView()
}
override fun getTheme(): Int {
return R.style.Theme_NoWiredStrapInNavigationBar
}
private val mBottomSheetBehaviorCallback: BottomSheetCallback = object : BottomSheetCallback() {
var isBottomSheetUp = false
override fun onSlide(bottomSheet: View, slideOffset: Float) {
//TODO("Not yet implemented")
}
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
isBottomSheetUp = false
dismiss()
} else isBottomSheetUp = true
}
}
override fun setupDialog(dialog: Dialog, style: Int) {
//super.setupDialog(dialog, style)
val contentView =
View.inflate(context,
R.layout.view_modal_bottom_sheet, null)
dialog.setContentView(contentView)
val layoutParams =
(contentView.parent as View).layoutParams as CoordinatorLayout.LayoutParams
val behavior = layoutParams.behavior
if (behavior != null && behavior is BottomSheetBehavior<*>) {
behavior.setBottomSheetCallback(mBottomSheetBehaviorCallback)
}
}
private fun initView() {
action_my_pictures.setOnClickListener {
viewModel.webViewUrl.value = "https://www.google.com/"
Log.d("BottomSheet", "Button 1 Clicked ${viewModel.webViewUrl.value}")
}
action_favorites.setOnClickListener {
viewModel.webViewUrl.value = "https://www.hotmail.de/"
Log.d("BottomSheet", "Button 2 Clicked ${viewModel.webViewUrl.value}")
}
action_ranking.setOnClickListener {
viewModel.webViewUrl.value = "https://amazon.com/"
Log.d("BottomSheet", "Button 3 Clicked ${viewModel.webViewUrl.value}")
}
action_hall_of_fame.setOnClickListener {
viewModel.webViewUrl.value = "https://m.daum.net/"
Log.d("BottomSheet", "Button 4 Clicked ${viewModel.webViewUrl.value}")
}
action_liked_pictures.setOnClickListener {
viewModel.webViewUrl.value = "https://m.nate.com/"
Log.d("BottomSheet", "Button 5 Clicked ${viewModel.webViewUrl.value}")
}
action_events.setOnClickListener {
viewModel.webViewUrl.value = "https://www.danawa.com/"
Log.d("BottomSheet", "Button 6 Clicked ${viewModel.webViewUrl.value}")
}
action_close_bottom_sheet.setOnClickListener {
dismiss()
}
}
}
1.5. BottomSheet Layout (XML)
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="webViewModel"
type="com.example.ui.main.webview.WebViewViewModel" />
</data>
<LinearLayout
android:id="#+id/bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/bottomNavigationViewBackground"
android:orientation="vertical"
app:behavior_hideable="true"
app:behavior_peekHeight="auto"
app:layout_behavior="#string/bottom_sheet_behavior"
app:behavior_fitToContents="true">
[7 Image/Icons as Buttons]
Example:
<LinearLayout
android:id="#+id/action_my_pictures"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
android:orientation="vertical"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="#+id/icon_my_pictures"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center_horizontal"
android:src="#drawable/ic_user_color" />
<TextView
android:id="#+id/icon_my_pictures_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:singleLine="true"
android:text="My Gallery"
android:paddingTop="6dp"
android:textAlignment="center"
android:textSize="12sp" />
</LinearLayout>
</LinearLayout>
</layout>
2. LOG output (for Button Clicks)
D/BottomSheet: Button 1 Clicked https://www.google.com/
D/BottomSheet: Button 2 Clicked https://www.hotmail.de/
D/BottomSheet: Button 3 Clicked https://amazon.com/
D/BottomSheet: Button 4 Clicked https://m.daum.net/
D/BottomSheet: Button 4 Clicked https://m.daum.net/
D/BottomSheet: Button 5 Clicked https://m.nate.com/
D/BottomSheet: Button 6 Clicked https://www.danawa.com/
A lot of thanks in advance.
I think problem is in scope of life that you provide for viewModel.
Currently you code looks like this:
viewModel = ViewModelProvider(this).get(WebViewViewModel::class.java)
This code is invoked inside two different fragments, and because ViewModelProvider is initialized with current instance of fragment (this). Your view model is only available in this scope (fragment scope). So at the end for each fragment you will get new ViewModel. To share ViewModel between fragments you should use different approach:
Instantiate ViewModelProvider with activity. (ViewModel will be share across all fragments inside activity)
Instantiate ViewModelProvider with parentFragment. (ViewModel will be shared across all child fragments)
sample:
viewModel = ViewModelProvider(requireActivity()).get(WebViewViewModel::class.java)
viewModel = ViewModelProvider(parentFragment).get(WebViewViewModel::class.java)
please also check:
How to scope ViewModels properly?
https://developer.android.com/reference/android/arch/lifecycle/ViewModelProviders
Related
I am trying to call a function in my fragment via expression binding from my XML file in "android:onclick...", but it will not work. The error is that the fragment is not attached to a context.
It is the
MaterialAlertDialogBuilder(requireContext())
which gives me headache.
How do I give the context to the fragment?
I have seen similar questions regarding that topic, but none that helped me.
Any help is much appreciated.
ItemDetailFragment.kt:
class ItemDetailFragment : Fragment() {
private lateinit var item: Item
private val viewModel: InventoryViewModel by activityViewModels {
InventoryViewModelFactory(
(activity?.application as InventoryApplication).database.itemDao()
)
}
private val navigationArgs: ItemDetailFragmentArgs by navArgs()
private var _binding: FragmentItemDetailBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentItemDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
binding.viewModel = viewModel
binding.fragment = ItemDetailFragment()
}
/**
* Displays an alert dialog to get the user's confirmation before deleting the item.
*/
fun showConfirmationDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(android.R.string.dialog_alert_title))
.setMessage(getString(R.string.delete_question))
.setCancelable(false)
.setNegativeButton(getString(R.string.no)) { _, _ -> }
.setPositiveButton(getString(R.string.yes)) { _, _ ->
deleteItem()
}
.show()
}
/**
* Called when fragment is destroyed.
*/
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
fragment_item_detail.kt:
<?xml version="1.0" encoding="utf-8"?><!--
<layout 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">
<data>
<variable
name="viewModel"
type="com.example.inventory.InventoryViewModel" />
<variable
name="fragment"
type="com.example.inventory.ItemDetailFragment" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="#dimen/margin"
tools:context=".ItemDetailFragment">
<Button
android:id="#+id/delete_item"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/margin"
android:onClick="#{()->fragment.showConfirmationDialog()}"
android:text="#string/delete"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/sell_item" />
</androidx.constraintlayout.widget.ConstraintLayout>
That is the error i am getting:
java.lang.IllegalStateException: Fragment ItemDetailFragment{e562873} (c6ab2144-3bdc-410b-91eb-e5668e8b617a) not attached to a context.
You should not pass your fragment instance as a data binding variable.
You could define a Boolean mutable live data variable in your InventoryViewModel and show the dialog when it changes:
private val _showConfirmation = MutableLiveData(false)
val showConfirmation
get() = _showConfirmation
fun onShowConfirmation() {
_showConfirmation.value = true
}
fun onConfirmationShown() {
_showConfirmation.value = false
}
Then, define an observer for this property in your ItemDetailFragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val id = navigationArgs.itemId
binding.viewModel = viewModel
binding.executePendingBindings()
viewModel.showConfirmation.observe(viewLifecycleOwner) {
if (it) {
showConfirmationDialog()
viewModel.onConfirmationShown()
}
}
}
Finally, remove the fragment variable from the XML and change your Button's onClick as:
<Button
...
android:onClick="#{() -> viewModel.onShowConfirmation()}"
/>
In my project that makes open new fragment with NavigationComponenet when click button. I want to test if fragment open when click button, But it don't work properly. Only it click button and does not open another fragment. So, I can't test if it works. Why it does not navigate?
#RunWith(AndroidJUnit4::class)
class WelcomeFragmentTestDoctor {
val phoneHelper = PhoneHelper
private lateinit var scenario: FragmentScenario<WelcomeFragment>
#Before
fun setup() {
scenario = launchFragmentInContainer(themeResId = R.style.AppTheme)
scenario.moveToState(Lifecycle.State.STARTED)
Intents.init()
}
#After
fun tearDown(){
Intents.release()
}
#Test
fun clickApplyAsADoctor(){
val navController = TestNavHostController(
ApplicationProvider.getApplicationContext())
scenario.onFragment { fragment ->
navController.setGraph(R.navigation.auth_navigation)
Navigation.setViewNavController(fragment.requireView(), navController)
}
onView(withId(R.id.buttonDoctor)).perform(click())
Assert.assertEquals(navController.currentDestination?.id, R.id.action_welcomeFragment_to_doctorRegistrationFragment)
}
}
fragment_doctor_registration.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="#dimen/_14sdp"
android:paddingBottom="#dimen/_14sdp"
app:layout_constraintTop_toTopOf="parent">
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
DoctorRegistrationFragment.kt
class DoctorRegistrationFragment : Fragment() {
private lateinit var mBinding: FragmentDoctorRegistrationBinding
private val mViewModel: DoctorRegistrationViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
mBinding = FragmentDoctorRegistrationBinding.inflate(inflater, container, false)
return mBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
DoctorRegistrationComponent.inject()
with(mBinding) {
backButton.setOnCrashOnClickListener {
findNavController().popBackStack()
}
btnSend.setOnCrashOnClickListener {
mViewModel.onEvent(
DoctorRegistrationInteractions.RegisterStart(fdr_name.text, fdr_surname.text,
fdr_title.text, fdr_diploma.text, fdr_branch.text,
inputLogin.lifEdittext.text.toString(), fdr_email.text, fdr_address.text,
fdr_company.text, fdr_tax.text
))
}
}
with(mViewModel) {
actions.map { it.getContentIfNotHandled() }.onEach(::handleActions).launchIn(viewLifecycleOwner.lifecycleScope)
}
}
private fun handleActions(action: DoctorRegistrationActions) {
when (action) {
is DoctorRegistrationActions.ErrorMessage -> PopupMessage.error(requireActivity(),message = action.message)
DoctorRegistrationActions.Init -> { }
is DoctorRegistrationActions.SuccessMessage -> {
PopupMessage.success(requireActivity(), message = action.message)
findNavController().popBackStack()
}
}
}
}
You need to check the fragment id not the Action id
Assert.assertEquals(navController.currentDestination?.id, R.id.doctorRegistrationFragment)
Assuming that doctorRegistrationFragment is the id of your fragment tag in your nav graph
<fragment
android:id="#+id/doctorRegistrationFragment"
/* rest of attrs */ >
I have a very weird problem. When I navigate from Fragment 1 to Fragment 2 using a btn.setOnClickListener and then navigating back from Fragment 2 to Fragment 1 using the back button, the btn.setOnClickListener in Fragment 1 does not work anymore and therefore is not able to navigate to Fragment 2 again.
Here is my code:
Button XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
// Custom Background for the button
<com.google.android.material.appbar.MaterialToolbar
android:clickable="false"
android:id="#+id/materialToolbar"
android:layout_width="match_parent"
android:layout_height="90dp"
android:background="#color/btnColorGray"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
</com.google.android.material.appbar.MaterialToolbar>
<com.google.android.material.button.MaterialButton
android:clickable="true"
android:focusable="true" />
</androidx.constraintlayout.widget.ConstraintLayout>
Main XML
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.view.fragments.home.calibrateAndRepair.CalibrateRepairMessageFragment">
... some other stuff
<!-- Included the button -->
<include
android:id="#+id/calibrate_repair_btn"
layout="#layout/calibrate_repair_btn"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
BaseFragment for databinding
abstract class BaseFragment<out T: ViewDataBinding>(val layout: Int) : Fragment() {
abstract val viewModel: ViewModel
private val _navController by lazy { findNavController() }
val navController: NavController
get() = _navController
fun navigateTo(fragment: Int, bundle: Bundle? = null) {
_navController.navigate(fragment, bundle)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return DataBindingUtil.inflate<T>(inflater, layout, container, false).apply {
lifecycleOwner = viewLifecycleOwner
setVariable(BR.viewModel, viewModel)
Timber.d("Created BaseFragment and binded View")
}.root
}
}
EmailFragment for initializing the button
abstract class EmailFragment<out T: ViewDataBinding>(
layout: Int,
private val progressBarDescription: ArrayList<String>,
private val stateNumber: StateProgressBar.StateNumber
) : BaseFragment<T>(layout) {
abstract val next: Int
abstract val bundleNext: Bundle?
// getting the button from the button.xml
private val btnNext: MaterialButton by lazy { btn_next_calibrate }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ... some other initializing which constantly work!
initButton()
}
// Initializing the button
private fun initButton() {
btnNext.setOnClickListener {
navigateTo(next, bundleNext)
Timber.d("Button clicked")
}
}
}
Fragment 1
#AndroidEntryPoint
class CalibrateRepairMessageFragment(
private val progressBarDescription: ArrayList<String>,
#StateNumberOne private val stateNumber: StateProgressBar.StateNumber,
) : EmailFragment<FragmentCalibrateRepairMessageBinding>(
R.layout.fragment_calibrate_repair_message,
progressBarDescription,
stateNumber
) {
// Overriding the values from EmailFragment
override val next: Int by lazy { R.id.action_calibrateRepairMessageFragment_to_calibrateRepairUserDataFragment }
override val bundleNext: Bundle by lazy { bundleOf("calibrate_repair_toolbar_text" to toolbarText) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ... some other stuff
}
}
Fragment 2
#AndroidEntryPoint
class CalibrateRepairUserDataFragment(
private val progressBarDescription: ArrayList<String>,
#StateNumberTwo private val stateNumber: StateProgressBar.StateNumber,
) : EmailFragment<FragmentCalibrateRepairUserDataBinding>(
R.layout.fragment_calibrate_repair_user_data,
progressBarDescription,
stateNumber
) {
override val next: Int by lazy { R.id.action_calibrateRepairUserDataFragment_to_calibrateRepairDataOverviewFragment }
override val bundleNext: Bundle by lazy { bundleOf("calibrate_repair_toolbar_text" to toolbarText) }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
}
I tried to delete everything that is not important for the question. You can ignore the constructor of BaseFragment, EmailFragment, CalibrateRepairMessageFragment and CalibrateRepairUserDataFragment. I am using navigation component and dagger-hilt.
I appreciate every help, thank you.
P.S: I've noticed that using button:onClick in the .xml file solves this problem but in this situation I can't use the xml version.
The problem with this should be your lazy initialization of btnNext.
The Fragment1 state is saved when navigating to Fragment2. When navigating back the XML View will be reloaded but the lazy value of btnNext won't change as it is already initialized and is pointing to the old reference of the button view. Thus your OnClickListener will always be set to the old reference.
Instead of assigning your button lazily you should assign it in EmailFragment's onCreateView()
PS: Also from btn_next_calibrate I suppose you are using kotlin synthetic view binding. If so you would not have to use a class variable.
I've created one dialog fragemnt with view model (mvvm). Dialog consist of one button (custom view). when using view model with data binding, button click is not working when livedata change.I'm using boolean value to check if button is clicked or not. What is causing issue? Also suggest any other approach if needed.
profile_dialog_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<data>
<variable
name="viewmodel"
type="com.test.ui.ProfileDialogViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.ProfileDialog">
<com.google.android.material.button.MaterialButton
android:id="#+id/login"
style="#style/TextAppearance.MaterialComponents.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Login"
android:onClick="#{() -> viewmodel.onLoginButtonClick()}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ProfileDialog.kt
class ProfileDialog : DialogFragment() {
companion object {
fun newInstance() = ProfileDialog()
}
private val viewModel: ProfileDialogViewModel by viewModel()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val binding = ProfileDialogFragmentBinding.inflate(inflater, container, false)
.apply {
this.lifecycleOwner = this#ProfileDialog
this.viewmodel = viewmodel
}
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.startLogin.observe(viewLifecycleOwner, Observer {
Log.d("insta", "This is working")
if (it == null) return#Observer
if(it) {
Log.d("insta", "This is not working")
val loginIntent = Intent(this.context, LoginActivity::class.java)
this.context?.startActivity(loginIntent)
}
})
}
}
ProfileDialogViewModel.kt
class ProfileDialogViewModel : ViewModel() {
private val _startLogin = MutableLiveData<Boolean>(false)
val startLogin: LiveData<Boolean>
get() = _startLogin
fun onLoginButtonClick() {
Log.d("insta", "This ain't working")
_startLogin.postValue(true)
}
}
Your viewmodel is defined in
private val viewModel: ProfileDialogViewModel by viewModel()
So, pay attention to viewModel. The problem located in
this.viewmodel = viewmodel
where this points to ProfileDialogFragmentBinding. Here you assinging ProfileDialogFragmentBinding.viewmodel = ProfileDialogFragmentBinding.viewmodel - that's why it's not working.
To solve problem, properly assign it like that:
this.viewmodel = viewModel
I have a pretty straightforward case. I want to implement shared element transition between an item in recyclerView and fragment. I'm using android navigation component in my app.
There is an article about shared transition on developer.android and topic on stackoverflow but this solution works only for view that located in fragment layout that starts transition and doesn't work for items from RecyclerView. Also there is a lib on github but i don't want to rely on 3rd party libs and do it by myself.
Is there some solution for this? Maybe it should work and this is just a bug? But I haven't found any information about it.
code sample:
transition start
class TransitionStartFragment: Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_transition_start, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val testData = listOf("one", "two", "three")
val adapter = TestAdapter(testData, View.OnClickListener { transitionWithTextViewInRecyclerViewItem(it) })
val recyclerView = view.findViewById<RecyclerView>(R.id.test_list)
recyclerView.adapter = adapter
val button = view.findViewById<Button>(R.id.open_transition_end_fragment)
button.setOnClickListener { transitionWithTextViewInFragment() }
}
private fun transitionWithTextViewInFragment(){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(transition_start_text to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
private fun transitionWithTextViewInRecyclerViewItem(view: View){
val destination = TransitionStartFragmentDirections.openTransitionEndFragment()
val extras = FragmentNavigatorExtras(view to "transitionTextEnd")
findNavController().navigate(destination, extras)
}
}
layout
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/transition_start_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextStart"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="#+id/open_transition_end_fragment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="#id/transition_start_text"
android:text="open transition end fragment" />
<androidx.recyclerview.widget.RecyclerView
android:id="#+id/test_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="#id/open_transition_end_fragment"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
adapter for recyclerView
class TestAdapter(
private val items: List<String>,
private val onItemClickListener: View.OnClickListener
) : RecyclerView.Adapter<TestAdapter.ViewHodler>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHodler {
return ViewHodler(LayoutInflater.from(parent.context).inflate(R.layout.item_test, parent, false))
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHodler, position: Int) {
val item = items[position]
holder.transitionText.text = item
holder.itemView.setOnClickListener { onItemClickListener.onClick(holder.transitionText) }
}
class ViewHodler(itemView: View) : RecyclerView.ViewHolder(itemView) {
val transitionText = itemView.findViewById<TextView>(R.id.item_test_text)
}
}
in onItemClick I pass the textView form item in recyclerView for transition
transition end
class TransitionEndFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setUpTransition()
return inflater.inflate(R.layout.fragment_transition_end, container, false)
}
private fun setUpTransition(){
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
layout
<androidx.constraintlayout.widget.ConstraintLayout
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:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="#+id/transition_end_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="transition"
android:transitionName="transitionTextEnd"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
fun transitionWithTextViewInFragment() - has transition.
fun transitionWithTextViewInRecyclerViewItem(view: View) - no transition.
To solve the return transition problem you need to add this lines on the Source Fragment (the fragment with the recycler view) where you initialize your recycler view
// your recyclerView
recyclerView.apply {
...
adapter = myAdapter
postponeEnterTransition()
viewTreeObserver
.addOnPreDrawListener {
startPostponedEnterTransition()
true
}
}
Here is my example with RecyclerView that have fragment shared transition.
In my adapter i am setting different transition name for each item based on position(In my example it is ImageView).
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.itemView.txtView.text=item
ViewCompat.setTransitionName(holder.itemView.imgViewIcon, "Test_$position")
holder.setClickListener(object : ViewHolder.ClickListener {
override fun onClick(v: View, position: Int) {
when (v.id) {
R.id.linearLayout -> listener.onClick(item, holder.itemView.imgViewIcon, position)
}
}
})
}
And when clicking on item, my interface that implemented in source fragment:
override fun onClick(text: String, img: ImageView, position: Int) {
val action = MainFragmentDirections.actionMainFragmentToSecondFragment(text, position)
val extras = FragmentNavigator.Extras.Builder()
.addSharedElement(img, ViewCompat.getTransitionName(img)!!)
.build()
NavHostFragment.findNavController(this#MainFragment).navigate(action, extras)
}
And in my destination fragment:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
info("onCreate")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
info("onCreateView")
return inflater.inflate(R.layout.fragment_second, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
info("onViewCreated")
val name=SecondFragmentArgs.fromBundle(arguments).name
val position=SecondFragmentArgs.fromBundle(arguments).position
txtViewName.text=name
ViewCompat.setTransitionName(imgViewSecond, "Test_$position")
}
Faced the same issue as many on SO with the return transition but for me the root cause of the problem was that Navigation currently only uses replace for fragment transactions and it caused my recycler in the start fragment to reload every time you hit back which was a problem by itself.
So by solving the second (root) problem the return transition started to work without delayed animations. For those of you who are looking to keep the initial state when hitting back here is what I did :
just adding a simple check in onCreateView as so
private lateinit var binding: FragmentSearchResultsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return if (::binding.isInitialized) {
binding.root
} else {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_search_results, container, false)
with(binding) {
//doing some stuff here
root
}
}
So triple win here: recycler is not redrawn, no refetching from server and also return transitions are working as expected.
I have managed return transitions to work.
Actually this is not a bug in Android and not a problem with setReorderingAllowed = true. What happens here is the original fragment (to which we return) trying to start transition before its views/data are settled up.
To fix this we have to use postponeEnterTransition() and startPostponedEnterTransition().
For example:
Original fragment:
class FragmentOne : Fragment(R.layout.f1) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postponeEnterTransition()
val items = listOf("one", "two", "three", "four", "five")
.zip(listOf(Color.RED, Color.GRAY, Color.GREEN, Color.BLUE, Color.YELLOW))
.map { Item(it.first, it.second) }
val rv = view.findViewById<RecyclerView>(R.id.rvItems)
rv.adapter = ItemsAdapter(items) { item, view -> navigateOn(item, view) }
view.doOnPreDraw { startPostponedEnterTransition() }
}
private fun navigateOn(item: Item, view: View) {
val extras = FragmentNavigatorExtras(view to "yura")
findNavController().navigate(FragmentOneDirections.toTwo(item), extras)
}
}
Next fragment:
class FragmentTwo : Fragment(R.layout.f2) {
val item: Item by lazy { arguments?.getSerializable("item") as Item }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedElementEnterTransition =
TransitionInflater.from(context).inflateTransition(android.R.transition.move)
val tv = view.findViewById<TextView>(R.id.tvItemId)
with(tv) {
text = item.id
transitionName = "yura"
setBackgroundColor(item.color)
}
}
}
For more details and deeper explanation see:
https://issuetracker.google.com/issues/118475573
and
https://chris.banes.dev/2018/02/18/fragmented-transitions/
Android material design library contains MaterialContainerTransform class which allows to easily implement container transitions including transitions on recycler-view items. See container transform section for more details.
Here's an example of such a transition:
// FooListFragment.kt
class FooListFragment : Fragment() {
...
private val itemListener = object : FooListener {
override fun onClick(item: Foo, itemView: View) {
...
val transitionName = getString(R.string.foo_details_transition_name)
val extras = FragmentNavigatorExtras(itemView to transitionName)
navController.navigate(directions, extras)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Postpone enter transitions to allow shared element transitions to run.
// https://github.com/googlesamples/android-architecture-components/issues/495
postponeEnterTransition()
view.doOnPreDraw { startPostponedEnterTransition() }
...
}
// FooDetailsFragment.kt
class FooDetailsFragment : Fragment() {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedElementEnterTransition = MaterialContainerTransform().apply {
duration = 1000
}
}
}
And don't forget to add unique transition names to the views:
<!-- foo_list_item.xml -->
<LinearLayout ...
android:transitionName="#{#string/foo_item_transition_name(foo.id)}">...</LinearLayout>
<!-- fragment_foo_details.xml -->
<LinearLayout ...
android:transitionName="#string/foo_details_transition_name">...</LinearLayout>
<!-- strings.xml -->
<resources>
...
<string name="foo_item_transition_name" translatable="false">foo_item_transition_%1$s</string>
<string name="foo_details_transition_name" translatable="false">foo_details_transition</string>
</resources>
The full sample is available on GitHub.
You can also take a look at Reply - an official android material sample app where a similar transition is implemented, see HomeFragment.kt & EmailFragment.kt. There's a codelab describing the process of implementing transitions in the app, and a video tutorial.