Android espresso does not navigate to other Fragment? - android

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 */ >

Related

Fragment not attached to a context error when calling function from XML with binding expression

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()}"
/>

How to avoid refreshing recyclerview refreshing on viewpager swipe?

In my android application the app structure according to feature requirement is as follows:
MainActivity has a Frame layout and a MainFragment.
When Activity onCreate method is called main fragment is setup on frame layout.
In MainFragment there is a Viewpager and a Tablayout.
Viewpager loads 5 Fragments lets say Fragment1, Fragment2, Fragment3, Fragment4, Fragment5
Each Fragment has a recyclerview.
All Fragments share same Recyclerview.Adapter the data is set accordingly.
Issue:
When app is loaded and MainFragment is setup all fragments are loaded at once as i have used viewPager.offscreenPageLimit = 4 but on swipe the recyclerview's onBindViewHolder method is called again and again which causes recyclerview data to blink and it all ends up in lagging on viewpager swipe.
Below is my code part
MainFragment.kt
#Keep
class MainFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val rootView = inflater.inflate(R.layout.mainfragment, container, false)
mainActivity = activity as MainActivity
return rootView
}
companion object {
#JvmStatic
fun newInstance() =
BottomNavFragment().apply {
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setUpTabLayout()
}
private fun setUpTabLayout() {
try {
val adapter = FileViewPagerAdapterTEST(childFragmentManager)
adapter.addFragments()
viewPager.adapter = adapter
viewPager.offscreenPageLimit = 4
viewPager.currentItem = 1
// viewPager.post { viewPager.currentItem = 1 }
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
}
override fun onPageSelected(position: Int) {
mainActivity.clearSearch()
}
override fun onPageScrollStateChanged(state: Int) {
}
})
tabLayout.setupWithViewPager(viewPager)
tabLayout.getTabAt(0)?.icon = mainActivity.getDrawable(R.drawable.icimg)
tabLayout.getTabAt(1)?.icon = mainActivity.getDrawable(R.drawable.icimgg)
tabLayout.getTabAt(2)?.icon = mainActivity.getDrawable(R.drawable.icimggg)
tabLayout.getTabAt(3)?.icon = mainActivity.getDrawable(R.drawable.icimggggg)
tabLayout.getTabAt(4)?.icon = mainActivity.getDrawable(R.drawable.icimgggggg)
tabLayout.getTabAt(0)?.text = getString(R.string.text1)
tabLayout.getTabAt(1)?.text = getString(R.string.text2)
tabLayout.getTabAt(2)?.text = getString(R.string.text3)
tabLayout.getTabAt(3)?.text = getString(R.string.text4)
tabLayout.getTabAt(4)?.text = getString(R.string.text5)
} catch (ex: Exception) {
ex.printStackTrace()
}
}
}
mainfragment.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
>
<View
android:id="#+id/view"
android:layout_width="match_parent"
android:layout_height="#dimen/_50sdp"
android:background="#color/colorPrimary" />
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="#dimen/_60sdp"
android:layout_marginStart="#dimen/_10sdp"
android:layout_marginTop="#dimen/_20sdp"
android:layout_marginEnd="#dimen/_10sdp"
android:background="#drawable/bg_round_view_white"
app:tabSelectedTextColor="#color/colorPrimary"
app:tabTextAppearance="#style/ThemeTextAppearanceTab">
</com.google.android.material.tabs.TabLayout>
<androidx.viewpager.widget.ViewPager
android:id="#+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="#+id/tabLayout" />
</RelativeLayout>
ViewPager Adapter
class FileViewPagerAdapterTEST(fm: FragmentManager) : FragmentPagerAdapter(
fm,
BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
) {
val fragments = arrayListOf<Fragment>()
fun addFragments() {
fragments.add(Fragment1())
fragments.add(Fragment2())
fragments.add(Fragment3())
fragments.add(Fragment4())
fragments.add(Fragment5())
}
override fun getItem(pos: Int): Fragment {
return fragments[pos]
}
override fun getCount(): Int {
return 5
}
}
Fragment1.kt (Funtionality in all child fragments is implemented same way)
#Keep
class Fragment1 : Fragment() {
private val fragmentViewModel: FileFragmentsViewModel by viewModel()
private val activityViewModel: MainActivityViewModel by activityViewModels()
private val sharedPreferencesManager = get<SharedPreferencesManager>()
private val repository = get<FilesRepository>()
private var listAdapter: ListRowItemAdapter? = null
private var tabType = TabType.ALL
private var listOfTab2 = mutableListOf<MyModelClass>()
private var listOfTab3 = mutableListOf<MyModelClass>()
private var lisOfTab1 = mutableListOf<MyModelClass>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val rootView = inflater.inflate(R.layout.fragment_first, container, false)
try {
fragmentViewModel.loadAllFiles()
listAdapter = ListRowItemAdapter(
sharedPreferencesManager,
fragmentViewModel,
activityViewModel,
repository,
inflater.context,
)
refreshFilesOnAdapter()
} catch (ex: Exception) {
ex.printStackTrace()
}
return rootView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Handler().postDelayed({
layoutProgress.visibility = View.GONE
val ll = LinearLayoutManager(context)
ll.isSmoothScrollbarEnabled = false;
setUpRecyclerView(ll, null)
handleViewModelObservers()
}, 2000)
}
private fun setUpRecyclerView(
layoutManager: RecyclerView.LayoutManager,
filesToSet: MutableList<MyModelClass>?
) {
// rvAllFiles.itemAnimator=null
// (rvAllFiles.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
// rvAllFiles.itemAnimator?.changeDuration=0
(rvAllFiles.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
rvAllFiles.isNestedScrollingEnabled = false
rvAllFiles.layoutManager = layoutManager
rvAllFiles.adapter = listAdapter
if (filesToSet != null) {
when (tabType) {
TabType.TYPE1 -> {
listAdapter!!.setDataList(fragmentName, filesToSet, TabType.TYPE1)
}
TabType.TYPE2 -> {
listAdapter!!.setDataList(fragmentName, filesToSet, TabType.TYPE2)
}
TabType.TYPE3 -> {
listAdapter!!.setDataList(fragmentName, filesToSet, TabType.TYPE3)
}
}
}
}
private fun handleViewModelObservers() {
observeBottomTabChange()
}
private fun observeBottomTabChange() {
//Bottom navigation menu change (i.e Documents,Recent,Bookmarks)
activityViewModel.selectedFragmentMain.observe(viewLifecycleOwner,
{ changedTabType ->
listAdapter!!.setCurrentBottomTabType(fragmentName, changedTabType)
tabType = changedTabType
})
}
private fun refreshFilesOnAdapter() {
fragmentViewModel.getAllItems().observe(viewLifecycleOwner,
{ filesList ->
lisOfAllItems = filesList
listAdapter!!.setAllItemsList(filesList)
})
fragmentViewModel.getLiveDataFromDb()?.observe(viewLifecycleOwner, { filesList ->
fragmentViewModel.getAllItems().postValue(
fragmentViewModel.checkFilesWithDB(
filesList,
fragmentViewModel.getAllItems().value
)
)
fragmentViewModel.getTab2FileFromDB(FileType.ALL)
?.observe(viewLifecycleOwner,
{ tab2list ->
listOfTab2 = tab2list
listAdapter!!.setttab2list(
listOfTab2
.toMutableList().asReversed()
)
})
fragmentViewModel.getTab3FileFromDB
(FileType.ALL)
?.observe(viewLifecycleOwner,
{ tab3list ->
listOfTab3 = tablist3
listAdapter!!.setttab3list
(listOfTab3
})
})
}
}
fragment_first.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="#string/appbar_scrolling_view_behavior"
android:paddingBottom="#dimen/_10sdp"
android:clipToPadding="false"
android:id="#+id/rvAllFiles" />
<include
layout="#layout/layout_fragment_list_status"
android:id="#+id/layoutNoResults"
android:visibility="gone" />
<include
layout="#layout/layout_fragment_list_progress"
android:id="#+id/layoutProgress"
/>
</RelativeLayout>
How this lag issue can be solved so that when MainActivity is loaded and MainFragment is set on it all fragments are loaded at once and Viewpager retains them so that on swipe only loaded fragment data should be displayed instead of refreshing Recyclerviews of fragments.
Can somebody please help me out. Any help will be appreciated.

Loading URL in WebView from Button Click in BottomSheetDialogFragment via Data-Binding

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

Data binding returning null

I might be doing this all wrong, but I have the same exact implementation in another fragment/viewmodel with no problems. Maybe because it's a dialog? Every time I log message or message.messagebody it returns null. Can anyone maybe point out why? Currently learning mvvm.
xml: (the important bit since it's long)
<?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="user"
type="com.catbellystudio.knodee.models.Users" />
<variable
name="vm"
type="com.catbellystudio.knodee.ui.profile.ProfileViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="400dp"
android:layout_margin="10dp"
android:background="#drawable/custom_background_popup"
android:elevation="10dp"
android:orientation="vertical">
<ScrollView
android:id="#+id/popupTextLayout"
android:layout_width="match_parent"
android:layout_height="277dp"
android:layout_marginTop="8dp">
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/colorPrimary"
android:hint="#string/your_message"
android:inputType="textMultiLine"
android:padding="10dp"
android:text="#{vm.message.messageBody}" />
</ScrollView>
</LinearLayout>
</layout>
viewmodel:
class ProfileViewModel(
private val userRepository: UserRepository,
private val messageRepository: MessageRepository
) : ViewModel() {
var message: Message = Message()
var sender: Users? = null
var receiver: Users? = null
var string:String?=null
fun getLoggedInUser() = runBlocking { userRepository.getUser() }
fun onBackPressed(view: View) {
Navigation.findNavController(view).navigateUp()
}
fun postMessage(view:View) {
Coroutines.main {
Log.e("messagevm", message.toString())
}
}
}
fragment:
class MessageFragment : Fragment(), KodeinAware {
private lateinit var viewModel: ProfileViewModel
private lateinit var profileBinding: FragmentProfileBinding
private lateinit var popupBinding: FragmentPopupBinding
override val kodein by kodein()
private val factory: ProfileViewModelFactory by instance()
private lateinit var dialog: Dialog
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProviders.of(this, factory).get(ProfileViewModel::class.java)
profileBinding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_profile,
container,
false
)
popupBinding = FragmentPopupBinding.inflate(LayoutInflater.from(context))
dialog = context?.let { Dialog(it) }!!
dialog.setContentView(popupBinding.root)
dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
profileBinding.viewModel = viewModel
popupBinding.vm = viewModel
getSender()
return profileBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
settingsButtonProfile.visibility = View.GONE
messageButtonProfile.setOnClickListener {
showPopUp()
}
val receiver by lazy {
arguments?.let { fromBundle(it).user }
}
viewModel.receiver = receiver
}
private fun showPopUp() {
dialog.show()
val switch = dialog.visibilitySwitchPopup
val visibilityTextView = dialog.visibilityTextViewPopup
dialog.closeButtonPopUp?.setOnClickListener {
dialog.dismiss()
}
switch?.setOnClickListener {
val isIconEnabled = switch.isIconEnabled
if (isIconEnabled) {
visibilityTextView?.text = getString(R.string.anonymous_prompt)
} else {
visibilityTextView?.text = getString(R.string.anonymous_warning)
}
switch.switchState()
}
}
private fun getSender() {
viewModel.getLoggedInUser()?.let { viewModel.sender = it }
}
}
Any help would be appreciated!
move this "popupBinding.vm = viewModel" line to onViewCreated() method and also include this line "popupBinding.lifeCycleOwner=this" in same method
Solved by changing
android:text="#{vm.message.messageBody}
to
android:text="#={vm.message.messageBody}

Android Navigation Component - Navigate up opens the same fragment

I'm having a problem where when executing
findNavController(R.id.main_nav_host).navigateUp()
or
findNavController(R.id.main_nav_host).popBackStack()
Instead of going back to the last fragment in the backstack, it reopens/navigates to the same/current fragment.
Can somebody point me in the right direction why this is happening?
Navigation graph:
<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/main_navigation_root"
app:startDestination="#+id/dest_main">
<fragment
android:id="#+id/dest_main"
android:name="com.example.popularmovies.ui.main.views.MainMoviesFragment"
android:label="#string/home"
tools:layout="#layout/fragment_main_movies">
<action
android:id="#+id/action_dest_main_to_dest_movie_details"
app:destination="#+id/dest_movie_details"
app:enterAnim="#anim/slide_in_right"
app:exitAnim="#anim/slide_out_left"
app:popEnterAnim="#anim/slide_in_left"
app:popExitAnim="#anim/slide_out_right" />
</fragment>
<fragment
android:id="#+id/dest_movie_details"
android:name="com.example.popularmovies.ui.details.movie.view.MovieDetailsFragment"
android:label="#string/movie_details"
tools:layout="#layout/fragment_movie_details"/>
</navigation>
MainActivity layout:
<FrameLayout
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"
app:layout_behavior="#string/appbar_scrolling_view_behavior">
<fragment
android:id="#+id/main_nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/main_navigation"/>
</FrameLayout>
MainActivity:
class MainActivity : AppCompatActivity(), HasSupportFragmentInjector {
#Inject
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
initNavUi()
}
override fun onBackPressed() {
findNavController(R.id.main_nav_host).popBackStack()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
}
}
override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.main_nav_host).navigateUp()
}
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
return dispatchingAndroidInjector
}
private fun initNavUi() {
val navController = Navigation.findNavController(this, R.id.main_nav_host)
appBarConfiguration = AppBarConfiguration(
setOf(R.id.dest_main)
)
NavigationUI.setupActionBarWithNavController(this, navController, appBarConfiguration)
}
}
Destination home fragment:
class MainMoviesFragment : Fragment(), Injectable, MovieViewHolder.MovieClickListener {
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private lateinit var fragmentViewModel: MainMoviesFragmentViewModel
private lateinit var moviesRv: RecyclerView
private lateinit var moviesAdapter: MainMoviesAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_main_movies, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews(view)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
fragmentViewModel = ViewModelProviders.of(this,viewModelFactory).get(MainMoviesFragmentViewModel::class.java)
fragmentViewModel.start()
observe()
}
override fun onMovieClicked(position: Int) {
fragmentViewModel.onMovieClicked(position)
}
private fun initViews(view: View) {
moviesRv = view.findViewById<RecyclerView>(R.id.fragment_main_movies_rv).apply{
layoutManager = LinearLayoutManager(context)
setHasFixedSize(true)
moviesAdapter = MainMoviesAdapter(this#MainMoviesFragment)
adapter = moviesAdapter
}
}
private fun observe() {
fragmentViewModel.moviesLiveData.observe(this, Observer { moviesAdapter.submitList(it) })
fragmentViewModel.onMovieClickedLiveEvent.observe(this, Observer { handleMovieClickedEvent(it) })
}
private fun handleMovieClickedEvent(movieModel: MovieModel?){
val action = MainMoviesFragmentDirections.actionDestMainToDestMovieDetails()
findNavController().navigate(action)
}
}
Destination target fragment:
class MovieDetailsFragment : Fragment() {
private lateinit var viewModel: MovieDetailsFragmentViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_movie_details, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProviders.of(this).get(MovieDetailsFragmentViewModel::class.java)
}
}
The project code on GitHub can be found here
Your onMovieClickedLiveEvent, used in your MainMoviesFragmentViewModel, is firing every time you go back to your MainMoviesFragment since MutableLiveData saves the current value. This means that popBackStack() works just fine, but then you instantly get navigated back to the detail page (note: you'll still want to remove your code in onBackPressed() since right now you can't exit the app by hitting the back button).
It seems like, particularly with the name of the variable, that you should be using the SingleLiveEvent class, instead of MutableLiveData directly, as per this blog post.
Of course, there's no particular reason to use a LiveData or go through the ViewModel at all in this case. Your MovieViewHolder could pass the MovieModel directly to onMovieClicked, which could call handleMovieClickedEvent directly. That would avoid the use of LiveData (which is designed to store state, not events) and better model what you actually want to achieve: an event listener.

Categories

Resources