Android XML binding of MutableLiveData not working - android

I have an activity with a CoordinatorLayout which contains a CollapsingToolbarLayout. I have bind the title of the activity to the title property of CollapsingToolbarLayout, like shown below(posting only relevant parts of code since the layout is too big):
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable name="viewModel"
type="com.android.myapp.ui.activity.home.HomeActivityViewModel"/>
</data>
<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">
<androidx.drawerlayout.widget.DrawerLayout
android:id="#+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="#+id/main_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="#+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_behavior="com.android.myapp.ui.behavior.scroll.appbar.DisableableAppBarLayoutBehavior"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="#+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:title="#{viewModel.pageTitle}"
app:collapsedTitleTextAppearance="#style/CollapsedAppBarTitleStyle"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleGravity="bottom|center_horizontal"
app:expandedTitleMarginBottom="85dp"
app:expandedTitleTextAppearance="#style/ExpandedAppBarTitleStyle"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<!--Remaining layout-->
</layout>
As you can see the pageTitle property is bind to the xml CollapsingToolbarLayout title property.
Now in the HomeActivityViewModel:
class HomeActivityViewModel(application: Application) : AndroidViewModel(application) {
var isToolbarExpandable = MutableLiveData<Boolean>() // For handling the expansion of toolbar on navigation. This works perfectly
var pageTitle = MutableLiveData<String>()
init {
isToolbarExpandable.value = true
pageTitle.value = "Home"
}
}
And in HomeActivity:
class HomeActivity : AppCompatActivity() {
private lateinit var homeViewModel: HomeActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivitySafaricomHomeBinding>(this, R.layout.activity_safaricom_home)
binding.lifecycleOwner = this
setSupportActionBar(toolbar)
homeViewModel = getHomeViewModel()
binding.viewModel = homeViewModel
}
private fun getHomeViewModel(): HomeActivityViewModel = ViewModelProviders.of(this).get(HomeActivityViewModel::class.java)
}
The page title is set correctly in this case. But the trouble starts when I try setting the title anytime after the Activity and the first child fragment is created(I am using NavHostFragment, so the first fragment is created and attached as soon as the activity is created). The only place I can change the the title is the onCreate on activity and onCreate life cycle methods of start fragment. I am using the viewmodel as a shared viewmodel to change title from all child fragments. I am getting the ViewModel in Fragments using:
ViewModelProviders.of(activity!!).get(HomeActivityViewModel::class.java)
After that, whenever I try to change the title, it simple won't change. Whether it be on a button click, after a delay, on the next fragment, nothing works. The only working place is onCreate of Activity till onCreateView of first fragment. I added an observer for pageTitle in HomeActivity, but after the first change, even the observer is not being triggered. On debugging, I could see that it is the same instance of viewmodel that is retrieved, and the pageTitle is being changed actually, but it simply stops observing after creation. Any idea what is the issue here? Please help.

LiveData doesn't work with DataBinding since it doesn't have a lifecycle.
After
binding.viewModel = homeViewModel
Try Adding
binding.lifecycleOwner = this
So, all together
binding.viewModel = homeViewModel
binding.lifecycleOwner = this

Related

How to refresh fragment from activity in KOTLIN

In my kotlin project, I want to refresh fragments from an Actvity. When I finish Activity, it should refresh the fragment. I was thinking this would be done in onResume. So how to do refresh current fragment when I resume an activity? My fragment page contains listviews, hence it should be updated, when i finish activity.
try this
Use Swipe to refresh layout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="#+id/listView"
/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
MainActivity
class MainActivity : AppCompatActivity() {
lateinit var swipeRefreshLayout: SwipeRefreshLayout
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
swipeRefreshLayout = findViewById(R.id.swipeRefreshLayout)
swipeRefreshLayout.setOnRefreshListener {
//TODO operation
}
}
}

View does not have a navController set at [duplicate]

I am using Navigation Component for navigating in my app. It works fine inside fragments but it fails to find the nav host in the activity that holds the actual navigation host.
I am trying to open a new fragment when the user clicks on FAB, which I included in Main activity's XML. When I call findNavController() it fails to find the controller. The nav host controller is in the XML layout. I can't understand why it fails to find it.
MainActivity
class MainActivity : AppCompatActivity(), OnActivityComponentRequest {
override fun getTabLayout(): TabLayout {
return this.tabLayout
}
override fun getFap(): FloatingActionButton {
return this.floatingActionButton
}
private lateinit var tabLayout: TabLayout
private lateinit var floatingActionButton: FloatingActionButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
this.tabLayout = tabs
this.floatingActionButton = fab
fab.setOnClickListener {
it.findNavController().navigate(R.id.addNewWorkoutFragment)
}
}
}
Activity main XML
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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"
tools:context=".domain.MainActivity"
android:animateLayoutChanges="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:theme="#style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="#style/AppTheme.PopupOverlay"/>
<com.google.android.material.tabs.TabLayout
android:id="#+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.tabs.TabItem
android:text="Test 1"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
<com.google.android.material.tabs.TabItem
android:text="Test 2"
android:layout_height="match_parent"
android:layout_width="match_parent"/>
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="#+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="#navigation/main_navigation" />
<com.google.android.material.bottomappbar.BottomAppBar
android:id="#+id/bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_anchorGravity="right|top"
app:layout_anchor="#+id/bar"
android:src="#drawable/ic_add_black_24dp"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
I was getting the same exeception until I managed to get the navController this way:
val navHostFragment = supportFragmentManager.findFragmentById(R.id.my_fragment_container_view_id) as NavHostFragment
val navController = navHostFragment.navController
Obtained from this answer:
Before finding the nav controller, you need to set the view, if you are using binding:
binding = BaseLayoutBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
If not:
setContentView(R.layout.activity_main)
Use the corresponding binding for fragments.
Try setting up onClickListener of Fab button in onStart of the Activity as in onCreate Activity is just inflating the View and haven't set the NavHostController. So if you setup onClickListener in onStart of activity is will work as expected.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
this.tabLayout = tabs
this.floatingActionButton = fab
}
override fun onStart() {
super.onStart()
floatingActionButton.setOnClickListener {
it.findNavController().navigate(R.id.addNewWorkoutFragment)
}
}
In Java you can find NavController inside activity this way:
Navigation.findNavController(this,R.id.nav_host).navigate(R.id.YourFragment);
The problem might be that FAB was added to the activity or a fragment different from the one used by NavHost fragment. In this case, when you call it.findNavController() it cannot find the navigation controller.
You can either check that your FAB belongs to the fragment pulled by NavHost or call an Activity's findNavController(<id>) and pass it id of the fragment you're looking up
override fun onCreate(savedInstanceState: Bundle?) {
...
fab.setOnClickListener {
findNavController(R.id.nav_host_fragment)
.navigate(R.id.addNewWorkoutFragment)
}
}
Retrieve it later, in onPostCreate. like this:
#Override
public void onPostCreate(Bundle savedInstanceState ) {
super.onPostCreate(savedInstanceState);
navController = Navigation.findNavController(this, R.id.fragment_container_view);
}
Turns out that activity that holds navigation controller... doesn't have navigation component.
The solution is to manually set the NavController to each view contained in the activity.
val navController = findNavController(R.id.nav_host_fragment)
Navigation.setViewNavController(fab, navController)
Now this would work:
fab.setOnClickListener {
it.findNavController().navigate(R.id.addNewWorkoutFragment)
}
I still don't understand why this works the way it works so any explanation would be more than welcome :)
As of now, Android API simply doesn't make much sense.
Source: Navigate to fragment on FAB click (Navigation Architecture Components)
Kotlin Only
Android Navigation KTX provides an extension function for Activity
All I had to do was
findNavController(R.id.myNavHostFragment).navigate(R.id.to_someFragment)
I do have the following dependencies in my app/gradle.build.kts file (
implementation ("androidx.navigation:navigation-fragment-ktx:2.3.5")
implementation ("androidx.navigation:navigation-runtime-ktx:2.3.5")
implementation ("androidx.navigation:navigation-ui-ktx:2.3.5")
Using Directions
Alternatively you could also use generated Directions class. Since this is a global action the Directions class is found in the NavigationDirections.to_someFragment()
//very useful if you're passing args then you could do something like
//NavigationDirections.to_someFragment(myArg)
NavigationDirections.to_someFragment()findNavController(R.id.myNavHostFragment).navigate(NavigationDirections.to_someFragment())
Side-note the specific extension function is being contributed from the runtime-ktx aar
you can still get access in your oncreate by doing
//the id would be the id of the Fragment Element In Your Activity XML File
val navGraph = Navigation.findNavController(this, R.id.activity_main)
binding.mainFab.setOnClickListener {
navGraph.navigate(R.id.destinationFragment)
}
I found this answer helpful. In your activity paste this code
Navigation.findNavController(this, R.id.nav_host_fragment).navigate(R.id.yourDestination)
This is Extension version for Kotlin:
fun AppCompatActivity.findNavController(
#IdRes navHostViewId: Int
): NavController {
val navHostFragment =
supportFragmentManager.findFragmentById(navHostViewId) as NavHostFragment
return navHostFragment.navController
}
The navHostViewId is id of your androidx.fragment.app.FragmentContainerView.
The best way to access navController in OnCreate of Activity is to access it after inflating the layout
that can achieved by kotlin extention function doOnPreDraw()
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val navController: NavController
get() = Navigation.findNavController(this, R.id.nav_host_fragment)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.root.doOnPreDraw {
// here you can safely use navController
binding.toolbar.setupWithNavController(navController)
}
}
}
You can use binding.root.findNavController().navigate(id)

Android - Activity and Fragment initialization

I have an application composed of one activity and several fragments, as recommanded by Google. Other details here. I would like to keep a menu still and to switch my fragments in the container in the center.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:someProperties="propertiesValues">
<androidx.appcompat.widget.Toolbar
android:id="#+id/toolbar"
android:someProperties="propertiesValues" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="#+id/fragment_container_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:navGraph="#navigation/navigation_map"
/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="#+id/fab"
android:someProperties="propertiesValues" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("Activity creation")
val binding = ActivityMainBinding.inflate(layoutInflater)
println("Activity creation part 2")
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
User.initSharedPref(this)
}
Fragment
private lateinit var mylist: MutableList<String>>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("Fragment creation")
mylist = User.loadScenarioList()
}
User
object User
{
private lateinit var sharedPref : SharedPreferences
fun initSharedPref(context: Context){
sharedPref = context.getSharedPreferences("JeuDePisteKtPreferenceFileKey",Context.MODE_PRIVATE)
}
fun loadList(): MutableList<String>> {
val json = sharedPref.getString(KEY_LIST, "") ?: ""
if (json == "") return mutableListOf()
return Json.decodeFromString(json)
}
}
Problem encountered
When i start the activity, it initialize a variable sharedPref as shown in code.
But when in fragment onCreate i use this variable (mylist = User.loadScenarioList()), the binding line in activity fail with Binary XML file line #31: Error inflating class androidx.fragment.app.FragmentContainerView as shown in logcat below
Logcat & error
Here is the full logcat, we can see the the second sout is missing, but with no error thrown at this point.
The problem here came from the order of creation call
We can see it in the corrected code logcat
The activity onCreate is called first, but the first inflate call the fragment OnCreate, before resuming the activity OnCreate.
So every variable used in the fragment onCreate should be initialized before inflate call

how to make a deep fragment to take the whole screen with a jetpack navigation and BottomNavigationView on screen?

I study the google example at https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample .And it works like this :
But I need the about fragment to take the whole screen.What is best practice?
I have try this :
activity?.supportFragmentManager?.beginTransaction()?.replace(R.id.root_activity,Detail())?.addToBackStack("About")?.commit()
and get this:
the activity_main.xml:
<LinearLayout 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:id="#+id/root_activity"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.android.navigationadvancedsample.MainActivity">
<androidx.fragment.app.FragmentContainerView
android:id="#+id/nav_host_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:menu="#menu/bottom_nav"/>
I can set activity?.findViewById<BottomNavigationView>(R.id.bottom_nav)?.visibility=View.GONE
or activity?.findViewById<BottomNavigationView>(R.id.bottom_nav)?.visibility=View.INVISIBLE
But when it comes back,it looks like this,I think this is not good way:
thanks!!
You have to toggle visibility of BottomNavigationView as shown below:
NavController.OnDestinationChangedListener destinationChangedListener = new NavController.OnDestinationChangedListener() {
#Override
public void onDestinationChanged(#NonNull NavController controller, #NonNull NavDestination destination, #Nullable Bundle arguments) {
if(destination.getId() == R.id.navigation_notifications){
navView.setVisibility(View.GONE);
}else{
navView.setVisibility(View.VISIBLE);
}
}
};
navController.addOnDestinationChangedListener(destinationChangedListener);
in case anybody has ths same question,I post my answer here.
to work around ,I put BottomNavigationView into a MainFragment instead of MainActivity . And in the MainActivity dynamically instantiate the MainFragment,so I can replace the MainFragment as a whole in my deep child fragment .
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val container = FrameLayout(this)
container.id = R.id.activity_container
setContentView(container)
supportFragmentManager.beginTransaction().replace(R.id.activity_container,MainFragment()).commit()
}
}
in the nested fragment :
activity?.supportFragmentManager?.beginTransaction()?.replace(R.id.activity_container,About())?.addToBackStack("Abouts")?.commit()
final effect(have strip away the toolbar):

Abstract Nav Drawer Android

I'm trying to abstract my Navigation Drawer to streamline my Android app but I'm running into some issues. The Nav Drawer simply doesn't respond to touches, I can't figure out why.
MainActivity.kt
class MainActivity : NavActivity() {
/** ON CREATE **/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
main_seekbar_price!!.setOnSeekBarChangeListener(this)
mDrawerLayout = findViewById(R.id.drawer_layout) // Variable in superclass
...
NavDrawer.kt
abstract class NavActivity : BaseActivity() {
/** Variables **/
lateinit var mDrawerLayout: DrawerLayout
/** ON CREATE **/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_nav)
// Set Nav Drawer listener
nav_view.setNavigationItemSelectedListener { menuItem ->
Log.d("NAV", "nav selected listener header")
menuItem.isChecked = true
mDrawerLayout.closeDrawers()
navMenuSwitch(menuItem)
true
}
// Set Nav Footer listener
nav_footer.setNavigationItemSelectedListener { menuItem ->
Log.d("NAV", "nav selected listener footer")
menuItem.isChecked = false
mDrawerLayout.closeDrawers()
navMenuSwitch(menuItem)
true
}
}
...
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout 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/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/colorBackground"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<FrameLayout
android:id="#+id/content_frame"
android:layout_width="match_parent"
android:layout_height="match_parent" >
...
</FrameLayout>
<include layout="#layout/activity_nav" />
</android.support.v4.widget.DrawerLayout>
activity_nav.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.NavigationView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:menu="#menu/drawer_header"
app:headerLayout="#layout/nav_header">
<android.support.design.widget.NavigationView
android:id="#+id/nav_footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:menu="#menu/drawer_footer">
</android.support.design.widget.NavigationView>
</android.support.design.widget.NavigationView>
I thought it has something to do with assigning mDrawerLayout in the child class but that didn't fix anything. It never calls the listeners though, no matter how much you click it it won't even print Logs in that listener.
Any help is welcome as I can't figure this out for the life of me, thank you in advance!
Four things happen, in this order, when your MainActivity's onCreate() is called:
MainActivity's content view is set by inflating activity_nav.xml.
The nav_view and nav_footer Kotlin variables are assigned by searching the content view for them. (Kotlin does this for you, so it's easy to forget that it's happening.)
Listeners are attached to the nav_view and nav_footer views.
MainActivity's content view is set again, this time by inflating activity_main.xml. (The include tag inside activity_main.xml tells the framework that it should inflate another copy of activity_nav.xml as part of the process.) The old content view is overwritten.
So by the time MainActivity.onCreate() finishes, the views with listeners attached are gone. One way to fix the problem would be to delete the line setContentView(R.layout.activity_nav) from NavActivity.onCreate() and to move the rest into an initNavDrawerListeners() method that's called from MainActivity.onCreate().

Categories

Resources