Streamlined Adding and Replacing Fragments in Kotlin - android

I am working on a small App with just a few screens that I want to implement as a single Activity which calls several fragments. I have some experience in Java for Android but using Kotlin is new territory.
I have understood how to basically add fragments in Kotlin, but I'd like to do it in a reusable way.
I came across the solution mentioned here and it works, but there is something I don't quite understand. My following MainActivity works so far, but I have to create the var welcomeFragment in the onCreate method like I did it here:
class MainActivity : AppCompatActivity(), NavigationView.OnNavigationItemSelectedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val toggle = ActionBarDrawerToggle(
this, drawer_layout, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
drawer_layout.addDrawerListener(toggle)
toggle.syncState()
nav_view.setNavigationItemSelectedListener(this)
var welcomeFragment = Fragment.instantiate(this#MainActivity,
WelcomeFragment::class.java!!.getName())
addFragment( welcomeFragment, welcome)
}
inline fun android.support.v4.app.FragmentManager.inTransaction(func: android.support.v4.app.FragmentTransaction.() -> android.support.v4.app.FragmentTransaction) {
beginTransaction().func().commit()
}
fun AppCompatActivity.addFragment(fragment: Fragment, frameId: Int){
supportFragmentManager.inTransaction { add(frameId, fragment) }
}
fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int) {
supportFragmentManager.inTransaction{replace(frameId, fragment)}
}
}
I don't seem to get why I can't declare the property and later assign it. It's possible to declare it with lateinit but when I then try to assign it like this:
var welcomeFragment = Fragment.instantiate(this#MainActivity,
WelcomeFragment::class.java!!.getName())
I get "Type mismatch - Required: Fragment, found: WelcomeFragment!"
Also, if I try to do this:
addFragment(WelcomeFragment, welcome)
Android Studio complains about WelcomeFragment ยด(which is right now just empty except for a TextView saying "Welcome") not having a companion object. I have tried to make one but I don't really understand what it has to do. Reading the documentation didnt help me either with this one.
So my question is: How do I do the instantiating right, and or do I have to do it at all when most of my fragments will be one instance only. Can I avoid this by properly adding a companion object to my fragments? How would that look?
Any help on this, hints at misconceptions I have here, or general stupidity pointed out would be greatly appreciated!

Please be careful with the imports, sometimes we have a android.support.v4.app.Fragment
and then we can't use android.app.Fragment.instantiate function in it.

Related

UninitializedPropertyAccessException: lateinit property binding has not been initialized

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).

Android findViewById() returns null when trying to make a utility/helper class

I am currently developing an app and I am trying to make a Utils file with some general functions that I can use throughout my application to avoid having to copy/paste the same function many times. I have a Utils class file and I am passing the current Activity into that class. For some reason, when I call findViewById() in init, it returns null and the app crashes. I have tried logging the activity that is being passed into the Utils class, and to my eyes it appears like the right activity, so I am not sure why it is failing. I will show the relevant code below:
Utils.kt (There is more, but it isn't relevant to the question)
class Utils(activity: Activity) {
private var currentActivity: Activity
private var fragmentManager: FragmentManager
private var topAppBar: MaterialToolbar
val activitiesList = listOf(
"recipes", "budget", "inventory", "customers",
"reports"
)
init {
currentActivity = activity
println(currentActivity)
fragmentManager = (activity as AppCompatActivity).supportFragmentManager
topAppBar = currentActivity.findViewById(R.id.top_app_bar)
}
}
MainActivity.kt (Again there is more, but not relevant)
class MainActivity : AppCompatActivity() {
private lateinit var drawerLayout: DrawerLayout
private lateinit var topAppBar: MaterialToolbar
private lateinit var utils: Utils
private val fragmentManager = supportFragmentManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
fragmentManager.beginTransaction().replace(R.id.frame_layout, HomeFragment()).commit()
utils = Utils(this#MainActivity)
topAppBar = findViewById(R.id.top_app_bar)
drawerLayout = findViewById(R.id.drawer_layout)
val navigationView: NavigationView = findViewById(R.id.navigation_view)
The error comes from the Utils.kt file in the last line of actual code where I try to assign topAppBar to the view of id top_app_bar, but it returns null. My println (yes I know I should be using Log.i but println is quicker to type) in the Utils class returns com.example.bakingapp.MainActivity#501533d, which is what I would expect if I am understanding what is happening correctly. Any insight as to why I can't find the views would be appreciated.
I figured out the problem. It is a scope issue. I am trying to find the id of a view outside of the current Activity/Fragment I am looking at. I realized then a lot of my code could/should be moved into the Fragment I was trying to target and this has fixed my issues.

Seperating an Activity's Views and it's logic

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.

android navigation pass arguments to fragment constructor

I have created Navigation Drawer Activity:
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
val drawerLayout: DrawerLayout = findViewById(R.id.drawer_layout)
val navView: NavigationView = findViewById(R.id.nav_view)
val navController = findNavController(R.id.nav_host_fragment)
appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController)
}
}
And I have mobile_navigation.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation 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/mobile_navigation"
app:startDestination="#id/databaseFragment">
<fragment
android:id="#+id/databaseFragment"
android:name="com.acmpo6ou.myaccounts.ui.DatabaseFragment"
android:label="fragment_database_list"
tools:layout="#layout/fragment_database_list" >
<action
android:id="#+id/actionCreateDatabase"
app:destination="#id/createDatabaseFragment" />
</fragment>
<fragment
android:id="#+id/createDatabaseFragment"
android:name="com.acmpo6ou.myaccounts.ui.CreateDatabaseFragment"
android:label="create_edit_database_fragment"
tools:layout="#layout/create_edit_database_fragment" />
</navigation>
The start destination is DatabaseFragment. However there is a problem, here is my DatabaseFragment:
class DatabaseFragment(
override val adapter: DatabasesAdapterInter,
val presenter: DatabasesPresenterInter
) : Fragment(), DatabaseFragmentInter {
...
companion object {
#JvmStatic
fun newInstance(
adapter: DatabasesAdapterInter,
presenter: DatabasesPresenterInter
) = DatabaseFragment(adapter, presenter)
}
}
As you can see my DatabaseFragment should receive two arguments to its constructor: adapter and presenter. This is because of dependency injection, in my tests I can instantiate DatabaseFragment passing through mocked adapter and presenter. Like this:
...
val adapter = mock<DatabasesAdapterInter>()
val presenter = mock<DatabasesPresenterInter>()
val fragment = DatabaseFragment(adapter, presenter)
...
It works with tests, but it doesn't work with android navigation. It seems that Android Navigation Components create DatabaseFragment instead of me, but they don't pass any arguments to fragment's constructor and it fails with error that is too long to post it here.
Is there a way to tell Navigation Components so that they pass appropriate arguments to my fragments when instantiating them?
Thanks!
Short answer is no, you can not pass arguments to Fragment.
All subclasses of Fragment must include a public no-argument constructor. The framework will often re-instantiate a fragment class when needed, in particular during state restore, and needs to be able to find this constructor to instantiate it. If the no-argument constructor is not available, a runtime exception will occur in some cases during state restore.
I just want to add to i30mb1 answer:
Is there really a necessity for you to pass those two arguments in the constructor?
As far as I know and as far as I have experimented with MVP, each view should have a presenter. So for example when I create a new fragment, I create a new presenter for it. Then the parent activity should have another presenter. If you need that presenter so that the fragment can make changes in the Activities view, you could implement interfaces, but that's another topic.
If you ever need to pass simple arguments using navigation like POJOS or even simplier objects like Strings etc.. you can use SafeArgs https://developer.android.com/guide/navigation/navigation-pass-data
I fixed everything pretty easily using default arguments, like this:
class DatabaseFragment(
override val adapter: DatabasesAdapterInter = DatabasesAdapter(),
val presenter: DatabasesPresenterInter = DatabasesPresenter()
) : Fragment(), DatabaseFragmentInter {
...
companion object {
#JvmStatic
fun newInstance() = DatabaseFragment()
}
}

kotlin - what is the better way than passing activity to non_activity classes

I've written some classes for making and using some tools in my app , for example I've written a class for making drawer navigation and I use it in some of my activities . for preventing over using the same code, I've used a custom class and I just call it in my activity and pass the activity to it
this is my code :
class Drawer(_activity: Activity) :NavigationView.OnNavigationItemSelectedListener {
private val activity:Activity
private val drawerLayout: DrawerLayout
private val nav_view:NavigationView
init{
activity=_activity
drawerLayout=activity.findViewById(R.id.drawer_layout)
nav_view=activity.findViewById(R.id.drawer_nav_view)
}
fun makeDrawer() {
drawerLayout.openDrawer(Gravity.RIGHT)
nav_view.setNavigationItemSelectedListener(this)
}
override fun onNavigationItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.drawer_home -> {
Log.v(Constants.TAG,"home clicked")
}
}
closeDrawer()
return true
}
fun closeDrawer()=drawerLayout.closeDrawer(Gravity.RIGHT)
is it a good way to pass activity to non-activity classes to prevent over using some codes ? is there any alternative for that ?
Passing reference of activity leads to possible crashes.
There is a better way. You can create DrawerActivity that extends AppCompatActivity and put most of this logic into it.
Then when you have activity that uses drawer, you just extend DrawerActivity instead of AppCompatActivity.
Make few abstract functions like
abstract fun getDrawerLayout(): DrawerLayout
abstract fun getNavView(): NavView
That you will implement this methods in activity that extends your DrawerActivity.
This way you will have available all things that you need in you DrawerActivity to connect logic for drawer.
Your function
fun makeDrawer() {
drawerLayout.openDrawer(Gravity.RIGHT)
nav_view.setNavigationItemSelectedListener(this)
}
would now be
fun makeDrawer() {
getDrawer().openDrawer(Gravity.RIGHT)
getNavView().setNavigationItemSelectedListener(this)
}
Other parts of class you can also replace.

Categories

Resources