BaseFragment architecture for android project - android

I am starting on a new project and I will be working in a team of 10 developers. I am setting up the base structure for our Android app. As I am working with a team and I want everyone to follow the same structure i.e creating ViewModel for each fragment and use data binding. How can I make it strict so that developers get an error if they don't create ViewModel for their Fragment?
So I have created the below BaseFragment:
abstract class BaseFragment<out VM : BaseViewModel, DB : ViewDataBinding> : Fragment() {
open lateinit var binding: DB
private fun init(inflater: LayoutInflater, container: ViewGroup?) {
binding = DataBindingUtil.inflate(inflater, getLayoutRes(), container, false)
}
#LayoutRes
abstract fun getLayoutRes(): Int
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View {
init(inflater, container)
super.onCreateView(inflater, container, savedInstanceState)
return binding.root
}
open fun refresh() {}
}
How can I improve it more?

One of the possible ways to improve your base fragment is to use reified like this:
protected inline fun <reified T : ViewModel> getViewModel(): T =
ViewModelProviders.of(this)[T::class.java]
and call is:
private val loginViewModel: LoginViewModel = getViewModel()
Useful links on this approach:http://www.albertgao.xyz/2018/05/22/3-ways-to-handle-view-model-creation-in-android-with-dagger-and-kotlin/
How does the reified keyword in Kotlin work?
https://proandroiddev.com/how-reified-type-makes-kotlin-so-much-better-7ae539ed0304

Related

Kotlin inflate generic viewbinding class in parent

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
}

How to implement databinding with Reflection in BaseFragment()

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

changing between fragments the viewmodel is not working

I'm using viewmodel with injection. In my scnario, I have two fragments. A one is A, and the other is B.
When using A Viewmodel, it's working. But after changing A -> B -> A, The A Viewmodel is not working.
I don't understand why the viewmodel is not working. the only way I solved is generating a new fragment A. But I want to use DaggerFragment. please let me know why this happens.
viewmodel injection code
class AFragment : DaggerFragment(){
#Inject
lateinit var viewModelFactory : ViewModelProvider.Factory
private val viewModel by viewModels<HomeViewModel>{viewModelFactory}
override fun onCreateView (inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentHomeFirstwalkingBinding.inflate(layoutInflater, container,false).apply {
viewmodel = viewModel
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initLayout()
}
private fun initLayout(){
binding.title.setOnClickListener {
viewModel.getTrainingDataList("sad")
}
}
}
binding.title.setOnClickListener is not working after changing fragments.

Kodein Framework - property delegate must have a provideDelegate(...) method

I'm trying to build an app with the following architecture: LoginActivity -> MainActivity -> everything else handled in fragments hosted by MainActivity. I'm also using the Kodein Framework for the first time and get the following error in my starting fragment:
Property delegate must have a 'provideDelegate(HomeFragment,
KProperty*>' method. None of the following functions is suitable.
provideDelegate(Context [highlighted in red], KProperty<>?) defined
in org.kodein.di.android.KodeinPropertyDelegateProvider Type
'KodeinPropertyDelegateProvider' has no method
'getValue(HomeFragment, KProperty<>)' and thus it cannot serve as a
delegate
This is my code so far:
class HomeFragment : Fragment(), KodeinAware {
override val kodein by kodein()
private val factory : MainViewModelFactory by instance()
private lateinit var viewModel: MainViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
val binding : FragmentHomeBinding = FragmentHomeBinding.inflate(inflater, container, false)
viewModel = ViewModelProviders.of(this, factory).get(MainViewModel::class.java)
binding.viewModel = viewModel
return binding.root
}
}
How can I fix this?
Thanks :)
Nevermind, adding a type declaration after kodein did the trick... :)
In your imports change
import org.kodein.di.android.kodein
to
import org.kodein.di.android.x.kodein
You can do it like this:
override val kodein:Kodein by kodein()

How to generify data binding layout inflation?

I wanna make a BaseFragment. For this, I have to use ViewDataBinding and ViewModel. using generic, I can use variable but not static field. For example I have to Inflate writing this code "FragmentSecondBinding.inflate(layoutInflater, container, false) ". So I tried this code "T.inflate(layoutInflater, container, false)" but got some error. Also ViewModel is like this.
How can I make this code to BaseCode?
abstract class BaseFragment<T: ViewDataBinding, M : ViewModel> : DaggerFragment(){
abstract val layoutId : T
private lateinit var binding : T
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<M> { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = T.inflate(inflater, container, false).apply {
viewmodel = viewModel
}
return binding.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
binding.lifecycleOwner = this.viewLifecycleOwner
}
There is a way to abstract out the particular ViewDataBinding, however, it would require to provide a layout resource reference for each concrete fragment implementation:
protected abstract val layoutResource: Int
#Inject
lateinit var viewModelFactory: ViewModelProvider.Factory
private val viewModel by viewModels<M> { viewModelFactory }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, layoutResource, container, false).apply {
viewmodel = viewModel
}
return binding.root
}

Categories

Resources