I'm trying out the new Android Architecture components and have run into a road block when trying to use the MVVM model for a custom view.
Essentially I have created a custom view to encapsulate a common UI and it's respective logic to use throughout the app. I can set up the ViewModel in the custom view but then I'd have to either use observeForever() or manually set a LifecycleOwner in the custom view like below but neither seem correct.
Option 1) Using observeForever()
Activity
class MyActivity : AppCompatActivity() {
lateinit var myCustomView : CustomView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myCustomView = findViewById(R.id.custom_view)
myCustomView.onAttach()
}
override fun onStop() {
myCustomView.onDetach()
}
}
Custom View
class (context: Context, attrs: AttributeSet) : RelativeLayout(context,attrs){
private val viewModel = CustomViewModel()
fun onAttach() {
viewModel.state.observeForever{ myObserver }
}
fun onDetach() {
viewModel.state.removeObserver{ myObserver }
}
}
Option 2) Setting lifecycleOwner from Activity`
Activity
class MyActivity : AppCompatActivity() {
lateinit var myCustomView : CustomView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
myCustomView = findViewById(R.id.custom_view)
myCustomView.setLifeCycleOwner(this)
}
}
Custom View
class (context: Context, attrs: AttributeSet) : RelativeLayout(context,attrs){
private val viewModel = CustomViewModel()
fun setLifecycleOwner(lifecycleOwner: LifecycleOwner) {
viewModel.state.observe(lifecycleOwner)
}
}
Am I just misusing the patterns and components? I feel like there should be a cleaner way to compose complex views from multiple sub-views without tying them to the Activity/Fragment
1 Option -
With good intention, you still have to do some manual work - like, calling onAttach\ onDetach Main purpose of Architecture components is to prevent doing this.
2 Option -
In my opinion is better, but I would say it's a bit wrong to bind your logic around ViewModel and View. I believe you can do same logic inside Activity/Fragment without passing ViewModel and LifecycleOwner to CustomView. Single method updateData is enough for this purpose.
So, in this particular case, I would say it's overuse of Architecture Components.
it doesn't make sense to manage the lifecycle of the the view manually by passing some reference of the activity to the views and calling onAttach/onDetach, when we already have the context provided when that view is created.
I have a fragment in a NavigationView that has other fragments in a view pager, more like a nested fragment hierarchy scenario.
I have some custom views in these top-level fragments, when the custom view is directly in the top fragment, I can get an observer like this
viewModel.itemLiveData.observe((context as ContextWrapper).baseContext as LifecycleOwner,
binding.item.text = "some text from view model"
}
when I have the custom view as a direct child of an activity I set it up directly as
viewModel.itemLiveData.observe(context as LifecycleOwner,
binding.item.text = "some text from view model"
}
in these activities, if I have a fragment and it has some custom view and I use the 2nd approach, I get a ClassCastException(), and I have to reuse these custom views in different places, both activities, and fragments (that's the idea of having a custom view)
so i wrote an extension function to set the LifeCycleOwner
fun Context.getLifecycleOwner(): LifecycleOwner {
return try {
this as LifecycleOwner
} catch (exception: ClassCastException) {
(this as ContextWrapper).baseContext as LifecycleOwner
}
}
now i simply set it everywhere as
viewModel.itemLiveData.observe(context.getLifecycleOwner(),
binding.item.text = "some text from view model"
}
Related
I am getting a random crash "lateinit property binding has not been initialized". Most of the time it's working fine but a few time randomly we are getting this crash on crashlytics.
Please let me know what's wrong here
I have a BaseActivity with following code
abstract class BaseActivity<D : ViewDataBinding> : AppComptActivity() {
abstract val layoutId: Int
lateinit val binding: D
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState:Bundle)
binding = DataBindingUtil.setContentView(this, layoutId)
....
}
}
I have a HomeActivity which override BaseActivity with following code
class HomeActivity : BaseActivity<ActivityHomeBinding>() {
override val layoutId: Int get() = R.layout.activity_home
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState:Bundle)
....
}
}
I am using bottomNavigation menu and one of the fragment is HomeFragment
class HomeFragment : BaseFragment<FragmenntHomeBinding>() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState:Bundle)
(activity as HomeActivity).binding.appBarHome.visible(false)
//HERE I AM GETTING lateinit property binding has not been initialized crash
}
}
I don't want to use isInitialized property of lateinit as this will not solve my issue
As mentioned in the comment, I'd suggest instead of calling parent container (Activity) objects directly, register a listener to a navigation change like this in HomeActivity:
navController.addOnDestinationChangedListener { controller, destination, arguments ->
if(destination.id = R.id.homeFragment) {
// TODO hide/show your view here
}
}
In that case, you are sure that the view gets hidden/shown when it should be without relying on the HomeFragment being only in HomeActivity as this can change in the future and your app will start crashing
If you have an orientation change or other config change, or the OS process is killed while in the background and the user returns to the app, Android will recreate the Activity and the Fragments.
Unfortunately, it creates the Fragments first, before creating the Activity. So you cannot rely on the existence of the Activity until the Fragment has been attached to the Activity. You should move code that relies on the existence of the Activity to
onActivityCreated().
Note: I also agree with the comment about not doing it this way. Your Fragment should not make assumptions like this (that it is hosted by HomeActivity), but instead should make some callback to the hosting Activity and let the hosting Activity set the visibility of the app bar (or whatever else it wants to do).
I'm refactoring an activity that had grown too large. Ideally what I am trying to accomplish is to have my activity initialize all my view and set Listeners. Than off load the logic to a helper class. I pretty sure I would like to do this with an interface. But that's were I'm stuck.
For example, let have classes Main and MainHelper. Main has a CardView and a button. The button will show the cardview.
MainHelper is what has the interface and Main implements it?
How do I update views from MainHelper?
Is there a better approach to what I'm trying to accomplish?
class MainActivity : AppCompatActivity(), MainHelper.MainActivityHelper {
private lateinit var btn: Button = findViewById(R.id.btn)
private lateinit var menu: CardView = findViewById(R.id.menu)
override fun onCreate(savedInstanceState: Bundle?) {
btn.setOnClickListener { v: View? -> handleBtn()}
}
override fun handleBtn() {}
}
class MainHelper: AppCompatActivity() {
interface MainActivityHelper {
fun handleBtnM() {
menu.visibility = View.VISIBLE
}
}
Instead of using this Helper class, I would recommend that you use a more modern and better tested architecture pattern like Model-View-ModelView (MVVM). If so, you can leverage Android Jetpack's Architecture Component to help you better organize and separate concerns as explained here: https://developer.android.com/jetpack/guide.
Here's a more in-depth explanation of Android's ViewModel implementation: https://developer.android.com/topic/libraries/architecture/viewmodel.
I am new to the lifecycle observer (fragment). I am trying to link the views defined in XML with fragment. traditionally, we use to do it in onActivityCreated method using findViewById. How can we do it while using lifecycle observer?
Kindly do not suggest data binding. I am trying to avoid it in this scenario.
You can do it this way
class TestFragment : Fragment(), LifecycleObserver {
#OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun doSomethingOnActivityCreated(){
requireActivity().lifecycle.removeObserver(this)
//do stuff
}
override fun onAttach(context: Context) {
super.onAttach(context)
requireActivity().lifecycle.addObserver(this)
}
}
I'm trying to update values in a TextView from an external class but not work, I'm trying with many ways y many posts but unlucky me... BTW sent the TextView like a parameter in class and works but we know that not is the best way if I have many views.
So first with a basic code:
In Main Activity XML:
<TextView
android:id="#+id/tvHello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
In code:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ClaseDePrueba(this#MainActivity)
}
}
Finally in class with my last proof
class ClaseDePrueba(ctx : Context)
{
val view = LayoutInflater.from(ctx).inflate(R.layout.activity_main,null,false)
view.tvHello.text = "New Value"
}
Even I try to use the Kotlin android extensions like this web site but not work for me
https://antonioleiva.com/kotlin-android-extensions/
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.activity_main.view.*
A few hours ago I tried to implement an interface but I don't know how to reference the TextView and chance value, my code:
class MainActivity : AppCompatActivity() , ClaseDePrueba.MyInterfaceClass {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ClaseDePrueba(this#MainActivity)
}
override fun updateClass() {
TODO("Not yet implemented")
}
And my class
class ClaseDePrueba(ctx: Context)
{
interface MyInterfaceClass {
fun updateClass(
)
}
So the question is... What is the (best) way to fix and do it work correctly?
UPDATE and one solution
well I solved created a list of objects and pass as a parameter
I don't know if is the best way but at the moment works thanks all
class MainActivity : AppCompatActivity() , ClaseDePrueba.MyInterfaceClass {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val textViews = ArrayList<TextView>()
textViews.add(tvHello)
textViews.add(tvHello2)
ClaseDePrueba(this#MainActivity, textViews)
}
class ClaseDePrueba(ctx: Context, private val tviews: ArrayList<TextView>? = null)
{
init {
if (tviews != null) {
tviews[0].text = "Init1"
tviews[1].text = "Init2"
}
}
You can create an Interface and try to implement it in your activity.
interface MyInterface { fun update() }
Then try to access the function with the reference of class.
`Class MainActivity : AppCompatActivity() ,MyInterface{
override fun update(){
}
}'
Access this function with class reference and update the value.
If you have an Activity, and it inflates a layout, it creates the objects in the XML and assigns them IDs. The XML is like a recipe, the Activity is the cook, baking a delicious view layout.
So your Activity has some TextView objects. If you have another class, and that inflates the same XML file, it creates different objects. It's baking its own layout, and any changes you make to those View objects won't be seen in the Activity, because it has a completely different set of TextView instances (which happen to share ID values, because that's what the XML recipe says)
So if you want to mess with those TextViews in the Activity, you have three basic options:
have the Activity pass them to the other class, in a collection like a list. ("Here you are, here's the stuff you need to work with")
make them publicly accessible in the Activity, so you can pass the Activity instance to your other class, and it can poke them directly e.g. myActivity.coolTextView1.text = "wow!"
make some kind of interface on the Activity, e.g. fun updateText(id: Int, text: String) which the other class can call when it needs to update something. The Activity handles the details internally, like finding the relevant TextView object and updating it
either way, if you want to change those specific TextView instances displayed in your Activity, you need to access those instances somehow
Try doing,
class ClaseDePrueba(ctx : Context)
{
val txtView : TextView = (ctx as MainActivity).findViewById(R.id.tvHello) as TextView
txtView.setText("Hello")
}
(No need to inflate layout again)
The other way could be to implement an Interface
I have two fragments in my MainActivity, I want to make the first fragment inherit a method from the second fragment. Is this possible?
One approach might be to make both fragments delegate to the same object, which is more inline with the principle of preferring composition over inheritance.
Here is an example:
class FragmentA : Fragment(), SharedMethodsDelegate by SharedMethods {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedMethod()
}
}
class FragmentB : Fragment(), SharedMethodsDelegate by SharedMethods {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sharedMethod()
}
}
interface SharedMethodsDelegate{
fun sharedMethod()
}
object SharedMethods : SharedMethodsDelegate {
override fun sharedMethod() {
print("hey")
}
}
This approach would have the advantage of allowing you to use different base classes for your fragments. You could even use it to share methods between a fragment and an activity.
Another method is to extract that logic into another object, and then use that object in both fragments. Unless the method is directly related to view manipulation, this would be what I suggest. It also makes the method more testable.
Not without both Fragments sharing the same parent. you can create an instance of Fragment and call it BaseFragment, and then have both fragments that you're creating be children of BaseFragment. It won't be the same instance, but you'll have access to the same methods
Fragment
|
BaseFragment <- you create this and put the methods you want to share here
/\
/ \
FragmentA FragmentB