Consider the following simple setup. 1 Fragment with 1 ViewModel:
Fragment
class TestFragment : Fragment() {
private val viewModel by lazy {
ViewModelProviders.of(this).get(TestViewModel::class.java)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_test, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.testLiveData.observe(viewLifecycleOwner, androidx.lifecycle.Observer {
Log.d("###", "whattt")
})
}
}
ViewModel
class TestViewModel : ViewModel() {
private val myVariable = Log.d("###", "Test")
val testLiveData = MutableLiveData(false)
}
Why do I get a log output from both Fragment and ViewModel up to three times??
D/###: Test
D/###: whattt
D/###: Test
D/###: Test
D/###: whattt
D/###: whattt
After the comment from #tynn I realized that the problem might come from some actions happening before the fragment actually is involved. Long story short: I have a multi-module project: 1 app module, 1 data module and 1 network module.
Both the network module and the app module were planting a Timber DebugTree...So everything was logged twice facepalm
I searched a little bit if there is a good way to keep the modules independent in this regard. The only thing I could found was this SO answer:
https://stackoverflow.com/a/53872754/990129
object TimberLogImplementation {
fun initLogging() {
if(Timber.treeCount() != 0) return
if (BuildConfig.DEBUG) Timber.plant(DebugTree())
else Timber.plant(ReleaseTree())
}
}
Related
GameActivity
____FragmentQuest
____FragmentFight
class MapLvl.kt
this is a text RPG Fragment Quest displays a journey through different maps (changing the content in one fragment according to a template) text, pictures, navigation buttons. if there is a mob on the map, then the "Fight" button appears and a fragment of the turn-based fight FightFragment opens (hit the head \ legs\ body protect the head \ legs\ body). after the battle, return to QuestFragment
class MapLvl fills with the content of FightFragment
I need to change QuestFragment from classLvl to FightFragment. how to do it?
it doesn't work:
class MapLvl.kt:
class MapLevels(){
fun changeLvl (bind: FragmentQuestBinding,hero: Hero, activity: GameActivity,db: Maindb) {
when (hero.mapLvl) {
1 -> MapLevels().mapLevel1(bind, activity, hero, db)
2 -> MapLevels().mapLevel2(bind, activity, hero,db)
else -> {}
}
}
fun mapLevel2 (bind: FragmentQuestBinding,activity: GameActivity,hero:Hero,db: Maindb) {
bind.btnAtack.visibility= View.VISIBLE
//the problem is here:
bind.btnAtack.setOnClickListener {
(activity as GameActivity).supportFragmentManager
.beginTransaction()
.replace(R.id.placeHolder,FightFragment.newInstance())
.commit()
}
}
}
error: FragmentManager has not been attached to a host
QuestFragment:
class QuestFragment : Fragment() {
lateinit var bind:FragmentQuestBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
bind = FragmentQuestBinding.inflate(inflater)
return bind.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val hero = Hero()
val db = Maindb.heroSetDb(requireActivity())
hero.extractHeroData(db,hero)
scopeMain.launch {
delay(50)
MapLevels().changeLvl(bind,hero,GameActivity(),db)
}
if you make a call directly from a Fragment, then it works: (but it is necessary not from the fragment but from the class)
QuestFragment:
class QuestFragment : Fragment() {
lateinit var bind:FragmentQuestBinding
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
bind = FragmentQuestBinding.inflate(inflater)
return bind.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val hero = Hero()
val db = Maindb.heroSetDb(requireActivity())
hero.extractHeroData(db,hero)
scopeMain.launch {
delay(50)
MapLevels().changeLvl(bind,hero,GameActivity(),db)
}
bind.btnAtack.setOnClickListener {
(activity as GameActivity).supportFragmentManager
.beginTransaction()
.replace(R.id.placeHolder,FightFragment.newInstance())
.commit()
}
it Work:
class MapLevels(val bind: FragmentQuestBinding,
val hero: Hero,
val db: Maindb,
val activity: FragmentActivity ){
fun changeLvl() {
when (hero.mapLvl) {
1 -> MapLevels(bind,hero,db,activity).mapLevel1()
2 -> MapLevels(bind,hero,db,activity).mapLevel2()
else -> {}
}
}
fun mapLevel2() {
bind.imgLocation.setImageResource(R.drawable.map_loc02)
bind.txtLocationDiscription.text ="text"
bind.btnAtack.visibility= View.VISIBLE
bind.btnAtack.setOnClickListener {
(activity as FragmentActivity).supportFragmentManager
.beginTransaction()
.replace(R.id.placeHolder,FightFragment.newInstance())
.commit()
MapLevels().changeLvl(bind,hero,GameActivity(),db)
It looks like you are creating a new GameActivity instance to pass to your changeLvl() method. YOU SHOULD NEVER DO THIS. The new activity is NOT the same one that currently is displayed on the screen. Instead, you should use the requireAcitivty() to get the fragment's parent activity:
MapLevels().changeLvl(bind,hero,requireActivity(),db)
I can't be sure this will fix your current problem because I'm not even sure what the problem is exactly. But it is one issue that you need to change.
I have tried this problem all days long and still cannot be resolved.
I am writing an app which has three fragments, connected by Navigation Component (with only one activity).
Fragment A
Fragment B
Fragment C
And I observed one LiveData variable from ViewModel in these three fragments.
But only Fragment B is responding. The other two are not.
I have read almost all similar problems from this stack over flow. But still can't see the answer.
Please suggest me.
I have tried removing the observer from FragmentB (which is ok with observing Live Data). But the other two is not still observing)
Here is my code:
Fragment B: (Observe Working)
class ServerFragment : Fragment() {
private lateinit var binding: ServerLayoutBinding
private val vm: DataModel by viewModels()
private lateinit var devices_observer: Observer<MutableList<ClientSocket>>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = ServerLayoutBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var client_list_adapter = ClientListAdapter(layoutInflater)
binding.clientListRcv.apply {
layoutManager = LinearLayoutManager(this.context)
addItemDecoration(
DividerItemDecoration(
this.context,
DividerItemDecoration.VERTICAL
)
)
adapter = client_list_adapter
}
client_list_adapter.submitList(vm.clientDevices.value?.toList())
vm.clientDevices.observe(viewLifecycleOwner, Observer {
client_list_adapter.submitList(vm.clientDevices.value?.toList())} )
}
}
Fragment C: (Observe Not Working)
class FileListFragment: Fragment() {
private val vm: DataModel by viewModels()
private lateinit var binding: FileListBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding=FileListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
//Here I am populating my recycler view with ViewModel data
//And ViewModel live dat is obeserving here and
vm.clientDevices.observe(viewLifecycleOwner, Observer{
///Here UI updating work will be done. But did'not responded. Here is the problem point I think.
Toast.makeText(context, "OKKK", Toast.LENGTH_SHORT).show()
})
}
}
Aim is to declare a base class
abstract class BaseDialog<T : ViewBinding> : AppCompatDialogFragment() {
lateinit var binding: T
}
and all child classes should extend this parent class
class ChildClass: BaseDialog<ChildClassViewBinding>() {
}
Then I want to inflate the binding in parent class and save it to binding property
This seems out of my scope of knowledge of kotlin
Is this really possible to do?
If I were to do this, I'd do it like this:
class BaseDialogFragment<T: ViewBinding>(private val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> T)
: AppCompatDialogFragment() {
var _binding: T? = null
val binding: T get() = _binding ?: error("Must only access binding while fragment is attached.")
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = bindingInflater(inflater, container, false)
return binding.root
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
with usage like:
class ChildClass: BaseDialog(ChildClassViewBinding::inflate) {
}
However, I would not do this in the first place (since there's a nice alternative). It can become messy pretty quickly to rely on inheritance for these kinds of things. What happens if you want to add some other features for a dependency injection framework, or some other common things you like to use? What if there's some features you like to use in some of your fragments but not all of them? And are you also creating base classes like this for Activity and non-Dialog Fragments?
These problems are why there's a programming axiom: "composition over inheritance".
Sometimes there's no choice but to use inheritance to avoid code duplication. But in the case of Fragments and Bindings, I don't think so. You can pass your layout reference to the super constructor, and use ViewBinding.bind() instead of inflate(). Since bindings rarely need to be accessed outside the onViewCreated function, you usually don't need a property for it.
class ChildClass: AppCompatDialogFragment(R.layout.child_class_view) {
override fun onViewCreated(view: View, bundle: Bundle?) {
super.onViewCreated(view, bundle)
val binding = ChildClassViewBinding.bind(view)
//...
}
}
If you do need a property for it, #EpicPandaForce has a library that makes it a one-liner and handles the leak-avoidance on destroy for you inside a property delegate.
Library here
Usage:
class ChildClass: AppCompatDialogFragment(R.layout.child_class_view) {
private val binding by viewBinding(ChildClassViewBinding::bind)
}
Create Base Fragment
abstract class BaseFragment<VB : ViewBinding> : Fragment() {
private var _bi: VB? = null
protected val bi: VB get() = _bi!!
abstract val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> VB
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_bi = bindingInflater(inflater, container, false)
return _bi!!.root
}
override fun onDestroyView() {
super.onDestroyView()
_bi = null
}
}
In your Child Fragment
class HomeFragment : BaseFragment<HomeFragmentBinding>() {
override val bindingInflater: (LayoutInflater, ViewGroup?, Boolean) -> HomeFragmentBinding
get() = HomeFragmentBinding::inflate
}
I'm trying to implement a BaseFragment in which I will pass the layout resource on it and it should outputs the binding to work in the fragment itself instead of need to do it everytime the fragment is extended.
For example I have this BaseFragment
open class BaseFragment(#LayoutRes contentLayoutId : Int = 0) : Fragment(contentLayoutId) {
private lateinit var onInteractionListener: OnFragmentInteractionListener
val toolbar : Toolbar?
get() {
return if(activity is BaseActivity)
(activity as BaseActivity).toolbar
else
null
}
override fun onAttach(context: Context) {
super.onAttach(context)
setOnInteractionListener(context)
}
...
In which I use like this
class A(): BaseFragment(R.layout.myFragment) { ... }
Now, if I use this I will need to do the definition of the binding class again in my onCreateView
class A(): BaseFragment(R.layout.myFragment) {
private lateinit var binding: MyFragmentBinding
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.myFragment, container, false)
return binding.root
}
override fun onDestroy(){
binding = null
}
}
What I want to implement is that since I'm passwing the layout to my BaseFragment, I want my BaseFragment to handle the creation of the binding and just return me the binding in the fragment which I use to extend BaseFragment
What I want to have is something like this
class A(): BaseFragment(R.layout.myFragment) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.myTextView = ""
}
}
So my question is, how I can implement inside BaseFragment the onDestroy() and the onCreateView to always create a binding for me from the layout I'm passing in ?
I heard that I should use reflection but I'm not that sure on how to accomplish it
I didn't hear about the possibility to get the databinding just from a layout, but even if it's possible, I don't think that is the recommended way, because of two reasons:
Reflection is slow
It makes things more complicated than they are.
Instead of making magic with Reflection, you could do something like this:
abstract class BaseFragment<out VB: ViewDataBinding>(
private val layout: Int,
// Other Dependencies if wanted
) : Fragment() {
abstract val viewModel: ViewModel
// other variables that all fragments need
// This does not cause any memory leak, because you are not storing the binding property.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = DataBindingUtil.inflate<VB>(inflater, layout, container, false).apply {
lifecycleOwner = viewLifecycleOwner
setVariable(BR.viewModel, viewModel)
}.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// Do some basic work here that all fragments need
// like a progressbar that all fragments share, or a button, toolbar etc.
}
And then, when you still need the bindingProperty, I would suggest the following library (it handles all the onDestoryView stuff etc):
implementation 'com.kirich1409.viewbindingpropertydelegate:viewbindingpropertydelegate:1.2.2'
You can then use this like:
class YourFragment(yourLayout: Int) : BaseFragment<YourBindingClass>() {
private val yourBinding: YourBindingClass by viewBinding()
override val viewModel: YourViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// do binding stuff
}
}
Let me know if this worked for you.
Cheers
Im using ViewModel with states and LiveData with one observer .
I have an activity and a fragment . everything works fine until Im rotating the screen
and then, the states I want to observe , clash.
What can I do in order to prevent it and make it work as expected?
I know I can add more observers but I don't want to solve it in this way , it may lead to problems with the other code.
MainActivity code :
private var appsDetailsHmap = HashMap< String , AppsDetails>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
progressBar = CircleProgressBarDialog(this)
viewModel.getState().observe(this, Observer {state->
when(state){
is SettingsViewModelStates.GetAppsDetails->initUI(state.list)
is SettingsViewModelStates.ShowDialog->progressBar.showOrHide(state.visibility)
is SettingsViewModelStates.GetCachedData->setCachedSettings(state.appDetailsHmap,state.selectedApp,state.speechResultAppName)
}
})
if (savedInstanceState==null){
viewModel.initSettingsActivityUI(appsDetailsHmap)
}
else{
viewModel.initCachedSettingsActivityUI()
}
Fragment code
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val view= inflater.inflate(R.layout.fragment_added_apps, container, false)
viewModel.getCachedApplist()
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.getState().observe(viewLifecycleOwner, Observer {state->
when(state){
is SettingsViewModelStates.PassAppsToFragment->{
initRecyclerView(state.list)
}
}
})
}
ViewModel
private var state=MutableLiveData<SettingsViewModelStates>()
private var appsDetailsHmap=HashMap<String,AppsDetails>()
private var addedApps= HashMap<String,Drawable?>()
fun getState()=state as LiveData<SettingsViewModelStates>
fun getCachedApplist(){
if (addedApps.isEmpty()){
getAppsList()
println("empty")
}
else
state.setValue(SettingsViewModelStates.PassAppsToFragment(addedApps))
}
fun initCachedSettingsActivityUI(){
state.setValue(SettingsViewModelStates.GetCachedData(appsDetailsHmap,selectedApp,speechResultAppName))
}
fun initSettingsActivityUI(list:HashMap<String, AppsDetails>) {
appsDetailsHmap=list
state.setValue( SettingsViewModelStates.GetAppsDetails(list))
}
states:
sealed class SettingsViewModelStates {
data class GetAppsDetails(val list:HashMap<String, AppsDetails>):SettingsViewModelStates()
data class ShowDialog(val visibility: Int) : SettingsViewModelStates()
data class PassAppsToFragment(val list:HashMap<String,Drawable?>) : SettingsViewModelStates()
data class GetCachedData(val appDetailsHmap:HashMap<String,AppsDetails>,
val selectedApp: AppsDetails,
val speechResultAppName:String ) : SettingsViewModelStates()
}