I have the following code snippets
Please help me figure out the difference between the same
Snippet 1
navbar.setOnItemSelectedListener { id ->
var fragment: Fragment? = null
when (id) {
R.id.home -> fragment = HomeFragment()
R.id.graphical_stats -> fragment = GraphicalStats()
R.id.sources -> fragment = SourcesFragment()
}
if (fragment != null) {
supportFragmentManager.beginTransaction().replace(R.id.container_frame, fragment)
.commit()
} else {
Log.e(TAG, "Error Creating Fragment")
}
}
Snippet 2
navbar.setOnItemSelectedListener { object : ChipNavigationBar.OnItemSelectedListener{
override fun onItemSelected(id: Int) {
var fragment: Fragment? = null
when (id) {
R.id.home -> fragment = HomeFragment()
R.id.graphical_stats -> fragment = GraphicalStats()
R.id.sources -> fragment = SourcesFragment()
}
if (fragment != null) {
supportFragmentManager.beginTransaction().replace(R.id.container_frame, fragment)
.commit()
} else {
Log.e(TAG, "Error Creating Fragment")
}
}
I am using ChipNavigationBar and have three fragments namely Home Graphical Stats and Sources which will be created or swapped accordingly
Functionality wise, both the snippets are the same.
Snippet 1 is using a lambda expression for setOnItemSelectedListener whereas the Snippet 2 uses a singleton object implementing the interface ChipNavigationBar.OnItemSelectedListener.
To simply your code, you should use a lambda expression.
Also, you can use lambda expression only if your interface has only 1 function that's need to overridden.
Related
I have some custom DialogFragments in my Android application. I would prevent the user from opening multiple dialogs, and I would also avoid disabling the button which shows the dialog once it's pressed or using a variable or it.
So I was trying to make an extension for the fragment which checks for shown fragments via their tags like this:
fun Fragment.isFragmentVisible(tag: String): Boolean {
val fragment = childFragmentManager.findFragmentByTag(tag)
if (fragment != null && fragment.isVisible) {
return true
}
return false
}
And before showing the dialog to check it like this:
binding.destination.editText?.setOnClickListener {
if (!isFragmentVisible(ShopsDialog.TAG)) {
ShopsDialog.newInstance(this, "Destinazione") { bundle ->
val shopId = bundle.getString("shopId")
viewModel.setDestination(shopId)
}
}
}
While the newInstance is:
fun newInstance(fragment: Fragment, title: String, bundle: (Bundle) -> Unit) = ShopsDialog().apply {
arguments = Bundle().apply {
putString(ARG_TITLE, title)
}
fragment.setFragmentResultListener(KEY_CALLBACK_BUNDLE) { requestKey: String, bundle: Bundle ->
if (requestKey == KEY_CALLBACK_BUNDLE) {
bundle(bundle)
}
}
show(fragment.childFragmentManager, TAG)
}
But this seems not to work, I've tried even to use requireActivity().supportFragmentManager instead childFragmentManager but still nothing...
Before showing bottomsheetdialogfragment , i think you can check it like this
if(childFragmentManager.findFragmentByTag(ShopsDialog::class.java.simpleName) == null){
// show your bottom sheet dialog fragment
}
If you are going to show bottomSheetDialogFragment in Fragment then use childFragmentManager and if it is activity then use supportFragmentManager
I have a setup where an Activity holds two fragments (A, B), and B has a ViewPager with 3 Fragments (B1, B2, B3)
In the activity (ViewModel) I observe a model (Model) from Room, and publish the results to a local shared flow.
val mSharedFlow = MutableSharedFlow<Model>` that I publish the model updates to:
...
viewModelScope.launch { repo.observeModel().collect(sharedFlow) }
Fragments A and B (ViewModels) have access to the sharedFlow through (Parent) fun getModelFlow(): Flow<Model>
There are no problems running the collects for each Fragment:
viewModelScope.launch {
parent.getModelFlow().collect { model -> doStuff(model) }
}
But, the problem is in the nested fragments (B1 etc.)
In the fragment (ViewModel) for B1 I have another parent.getModelFlow() that in turn calls Fragment B (ViewModel) parent.getParentFlow()
I have no problem acquiring the flow (i.e the SharedFlow (as Flow from the activity ViewModel)); But the collect in B1 does nothing.
Why am I not able to collect from the shared flow in the nested B1? (When A and B works fine)
Am I already not taking some flow rules into consideration? Additional launch{}'es, other withContexts(Some?), flowOn, launchIn etc.?
(The providing of the flow is not the problem. Even if I create intermediary flows, or place the sharedFlow in a kotlin Singleton object I still have the same problem)
=== EDIT ===
I was asked to add more information, unfortunately (for all) I can't paste the actual code because it would just appear verbose and foreign (see my comment below). But here's some psuedo-code that should be equivalent.
One clarification, that you can see below, Activity, FragmentA, FragmentB, FragmentB1 (etc.) are all running at the same time- but only one of A and B is visible at one time.
class TheActivity {
fun onCreate() {
setupFragments()
}
/** Two fragments active at the same time,
but only one FrameLayout is visible at one time */
private fun setupFragments() {
val a = FragmentA.newInstance()
val b = FragmentB.newInstance()
supportFragmentManager.commit {
add(R.id.fragment_holder_a, a)
add(R.id.fragment_holder_b, b)
}
}
}
class ActivityViewModel {
val activityModelFlow = MutableSharedFlow<Model>()
fun start() {
viewModelScope.launch {
getRoomFlow(id).collect(activityModelFlow)
}
}
}
class FragmentA { // Not very interesting
val viewModel: ViewModelA
}
class ViewModelA {
fun start() {
viewModelScope.launch {
parentViewModel.activityModelFlow.collect { model ->
log("A model: $model")
}
}
}
}
class FragmentB {
val viewModel: ViewModelB
val viewPagerAdapter = object : PagesAdapter.Pages {
override val count: Int = 1
override fun title(position: Int): String = "B1"
override fun render(position: Int): Fragment = FragmentB1.newInstance()
}
}
class ViewModelB {
val bModelFlow: Flow<Model> get() = parentViewModel.activityModelFlow
fun start() {
viewModelScope.launch {
parentViewModel.activityModelFlow.collect { model ->
log("B model: $model")
}
}
}
}
class Fragment B1 {
val viewModel: ViewModelB1
}
class ViewModelB1 {
fun start() {
viewModelScope.launch {
// in essence:
// - ViewModelB.bModelFlow ->
// - ActivityViewModel.modelFlow
parentViewModel.bModelFlow.collect { model ->
log("B1 model: $model")
}
}
}
}
So, all of the connections of acquiring parentViewModels, DI, fragment creation etc. is all working fine. But B1 model: $model is never called! Why?
This had very little (read no) connection to fragments, lifecycle, flows and coroutines blocking - which I thought was behind this.
val activityModelFlow = MutableSharedFlow<Model>()
// is the same as
val activityModelFlow = MutableSharedFlow<Model>(
replay = 0,
extraBufferCapacity = 0,
onBufferOverflow = BufferOverflow.SUSPEND
)
This means that new subscribers will get access to 0(!) of the values that are stored in the replay cache. So when FragmentB1 gets around to subscribing, the model has already been emitted.
Solution (without any optimisation or further consideration)
private val bulletFlow = MutableSharedFlow<Bullet>(replay = 1)
(or use a StateFlow, but I don't want to bother with initial state/value)
I want to resume android fragments that are in the backstack. When switching between different tabs on bottomnavigation,i don't want the views of fragments to be recreated.
I read this, this,this and some other questions on stackoverflow related to this. Common suggestions are to use show and hide method of fragment transactions but it is not working. Here is my kotlin code:
bottomnavigation.setOnNavigationItemSelectedListener {
item ->
when(item.itemId){
R.id.first_fragment_item -> {
var fragment:Fragment = FirstFragment.newInstance()
replaceFragment(fragment)
return#setOnNavigationItemSelectedListener true
}
R.id.second_fragment_item -> {
var fragment:Fragment = SecondFragment.newInstance()
replaceFragment(fragment)
return#setOnNavigationItemSelectedListener true
}
}
return#setOnNavigationItemSelectedListener false
}
}
fun replaceFragment(fragment:Fragment) {
var fragmentName:String = fragment::class.simpleName!!
if(supportFragmentManager.findFragmentByTag(fragmentName)==null) {
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.fragment_container, fragment, fragmentName)
fragmentTransaction.addToBackStack(fragmentName)
fragmentTransaction.commit()
}
else{
if(fragmentName == "FirstFragment") {
supportFragmentManager.beginTransaction().
hide(supportFragmentManager.findFragmentByTag("SecondFragment"))
.show(supportFragmentManager.findFragmentByTag("FirstFragment"))
.commit()
}
else{
supportFragmentManager.beginTransaction()
.hide(supportFragmentManager.findFragmentByTag("FirstFragment"))
.show(supportFragmentManager.findFragmentByTag("SecondFragment"))
.commit()
}
}
}
The last fragment of backstack is always shown. When i want to hide it and show
the other fragment, that fragment screen becomes white with nothing on it.
My bottomnavigation has four items but for testing purpose i am only using two. Two of fragments are FirstFragment and SecondFragment. I am using v4.app.fragments.
With the help of Kishan Viramgama comment's, i solved my problem. I used "add" method instead of "replace". This is my code for resuming fragments: `
fun replaceFragment(fragment: Fragment) {
var nameOfFragment: String? = fragment::class.simpleName
var fragmentTransaction = supportFragmentManager.beginTransaction()
var fm: FragmentManager = supportFragmentManager
if (supportFragmentManager.findFragmentByTag(nameOfFragment) == null) {
fragmentTransaction.add(R.id.root_fragments, fragment, nameOfFragment) //if fragment is not added to the backstack,add it/ don't use replace because it creates new fragment emptying fragment container
fragmentTransaction.addToBackStack(nameOfFragment)
fragmentTransaction.commit()
} else {
var fragmentToShow:Fragment = supportFragmentManager.findFragmentByTag(nameOfFragment) //if fragment is already in backstack,show it and hide all other fragments.
for (i in 0 until fm.backStackEntryCount) {
var fragmentToHide: Fragment = fm.findFragmentByTag(fm.getBackStackEntryAt(i).name)
if (fragmentToHide::class.simpleName != fragment::class.simpleName)
fragmentTransaction.hide(fragmentToHide)
}
fragmentTransaction.show(fragmentToShow)
fragmentTransaction.commit()
}
}
`
The problem
I'm using the BottomNavigationView from the Android Design Support Library on one of my Activities, alongside with Fragments for each navigation item.
Each time I select an item on the bar, I do a fragment transaction, like the snippet below (some parts of the code was removed for brevity):
private var fragmentToSet: Fragment? = null
private val onNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
fragmentToSet = when (item.itemId) {
// Choose fragment based on selection
// ...
}
// ...
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragmentToSet)
.commit()
}
The problem is... The bottom bar animation gets super laggy, and only finishes after the fragment is fully loaded and displayed on the screen.
This issue is not exactly new since it can also happen while using the Navigation Menu, but at least it's possible to solve it by using the DrawerLayout.DrawerListener and do the actual Fragment transaction only after the drawer is closed.
What I've tried so far
I tried to "cache" the fragments, holding their reference to avoid recreating the objects every time (e.g. MyFragment.newInstance()), but that didn't work.
I also tried to use handlers, which kinda solved the problem, but it might lead me to an exception in some cases. Something like the snippet below:
handler.postDelayed({changeFragment(fragmentToSet!!)}, 200)
Is there a way to solve this issue without using handlers (or other async calls), on a similar fashion to this solution while using the Navigation Menu?
I handled this situation by hiding and showing fragments using fragment manager. I wrote a sample code to deal with it as below.
class MainActivity : BaseActivity() {
private val homeFragment = HomeFragment.newInstance()
private val categoryFragment = CategoryFragment.newInstance()
private val searchFragment = SearchFragment.newInstance()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener)
navigation.menu.findItem(R.id.navigation_home).isChecked = true
supportFragmentManager.beginTransaction()
.add(R.id.containerFrameLayout, homeFragment)
.add(R.id.containerFrameLayout, categoryFragment)
.add(R.id.containerFrameLayout, searchFragment)
.commit()
setTabStateFragment(TabState.HOME).commit()
}
override fun onBackPressed() {
if (supportFragmentManager.backStackEntryCount > 0 || !homeFragment.isHidden) {
super.onBackPressed()
} else {
setTabStateFragment(TabState.HOME).commit()
navigation.menu.findItem(R.id.navigation_home).isChecked = true
}
}
private fun setTabStateFragment(state: TabState): FragmentTransaction {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
val transaction = supportFragmentManager.beginTransaction()
transaction.setCustomAnimations(R.anim.fragment_enter, R.anim.fragment_exit)
when (state) {
TabState.HOME -> {
transaction.show(homeFragment)
transaction.hide(categoryFragment)
transaction.hide(searchFragment)
}
TabState.CATEGORY -> {
transaction.hide(homeFragment)
transaction.show(categoryFragment)
transaction.hide(searchFragment)
}
TabState.SEARCH -> {
transaction.hide(homeFragment)
transaction.hide(categoryFragment)
transaction.show(searchFragment)
}
}
return transaction
}
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.navigation_home -> {
setTabStateFragment(TabState.HOME).commit()
return#OnNavigationItemSelectedListener true
}
R.id.navigation_category -> {
setTabStateFragment(TabState.CATEGORY).commit()
return#OnNavigationItemSelectedListener true
}
R.id.navigation_search -> {
setTabStateFragment(TabState.SEARCH).commit()
return#OnNavigationItemSelectedListener true
}
}
false
}
internal enum class TabState {
HOME,
CATEGORY,
SEARCH,
}
}
I have started to study Kotlin and do not know all the functionality of the language.
The function is used to show the fragment in the FrameLayout. The logic is so that the first time it should always add() the fragment and next times it will replace(). Also in some cases I need to use addToBackStack() and also in the same situations to disable the left-side menu.
fun showFragment(fragment : Fragment,
isReplace: Boolean = true,
backStackTag: String? = null,
isEnabled: Boolean = true)
{
/* Defining fragment transaction */
val fragmentTransaction = supportFragmentManager
.beginTransaction()
/* Select if to replace or add a fragment */
if(isReplace)
fragmentTransaction.replace(R.id.frameLayoutContent, fragment, backStackTag)
else
fragmentTransaction.add(R.id.frameLayoutContent, fragment)
/* Select if to add to back stack */
if(backStackTag != null)
fragmentTransaction.addToBackStack(fragment.javaClass.name)
fragmentTransaction.commit()
enableDrawer(isEnabled)
}
Question: Are there some possible improvements of the function code related to the specifications of Kotlin language to make code cleaner as for now the function looks as a mass.
I have posted a blog about the below answer.
I will write an Extension function to the FragmentManager which accepts a Lambda with Receiver as an argument.
inline fun FragmentManager.inTransaction(func: FragmentTransaction.() -> Unit) {
val fragmentTransaction = beginTransaction()
fragmentTransaction.func()
fragmentTransaction.commit()
}
To add a fragment, we can call like this from the Activity now:
supportFragmentManager.inTransaction {
add(R.id.frameLayoutContent, fragment)
}
The advantage of this is we don't have to call beginTransaction() and commit() every time we add or replace a Fragment now. How many hours have you wasted debugging only to find out that you have missed calling commit() in Java?
Next, I will write Extension functions to AppCompatActivity:
fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int, backStackTag: String? = null) {
supportFragmentManager.inTransaction {
add(frameId, fragment)
backStackTag?.let { addToBackStack(fragment.javaClass.name) }
}
}
fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int, backStackTag: String? = null) {
supportFragmentManager.inTransaction {
replace(frameId, fragment)
backStackTag?.let { addToBackStack(fragment.javaClass.name) }
}
}
So that now we can add/ replace Fragment from any Activity in a single line, without any additional qualifiers:
addFragment(yourFragment, R.id.frameLayoutContent, "tag")
replaceFragment(yourFragment, R.id.frameLayoutContent, "tag")
I like to use with and let when I have some code related to using a lot some variable and null checks
So I'd do something like this:
fun showFragment(fragment : Fragment,
isReplace: Boolean = true,
backStackTag: String? = null,
isEnabled: Boolean = true)
{
/* Defining fragment transaction */
with(supportFragmentManager.beginTransaction()) {
/* Select if to replace or add a fragment */
if(isReplace)
replace(R.id.frameLayoutContent, fragment, backStackTag)
else
add(R.id.frameLayoutContent, fragment)
/* Select if to add to back stack */
backStackTag?.let { addToBackStack(it) }
commit()
}
enableDrawer(isEnabled)
}
Everything is fine. Probably here
if(backStackTag != null)
fragmentTransaction.addToBackStack(fragment.javaClass.name)
you want to add fragment using backStackTag like this
if(backStackTag != null)
fragmentTransaction.addToBackStack(backStackTag)
You can create an extension function like this:
Import android.support.v4.app.FragmentManager and android.support.v4.app.FragmentTransaction if you wish to make your app compatible with devices running system versions as low as Android 1.6 using supportFragmentManager(which uses the support library) instead of fragmentManager.
Define the following function on the top level, i.e. directly under the package:
inline fun FragmentManager.inTransaction(func: FragmentTransaction.() Unit) {
val fragmentTransaction = beginTransaction()
fragmentTransaction.func()
fragmentTransaction.commit()
}
Call the function from any activity:
supportFragmentManager.inTransaction {
add(R.id.frameLayoutContent, fragment)
}
You can use below code for replace:
fun Fragment.moveAnotherFragment(fragment: Fragment,addToBackStack: Boolean = true, func : Bundle?. () -> Unit = {}) {
fragment.enterTransition =
activity?.supportFragmentManager?.beginTransaction()?.apply {
replace(
R.id.frame, fragment.apply {
enterTransition = Slide(Gravity.END)
exitTransition = Slide(Gravity.START)
arguments.apply(func)
}
)
if (addToBackStack) addToBackStack(null)
commit()
}
}