diff --git a/app/build.gradle b/app/build.gradle index c44ae9d..a07ee15 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,6 @@ plugins { id 'com.android.application' id 'kotlin-android' - id 'kotlin-android-extensions' } android { @@ -30,6 +29,10 @@ android { sourceSets { main.java.srcDirs += 'src/main/kotlin' } + + buildFeatures { + viewBinding true + } } dependencies { @@ -40,7 +43,9 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.2.1' - implementation 'androidx.constraintlayout:constraintlayout:2.0.2' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.5' testImplementation 'junit:junit:4.13.1' androidTestImplementation 'androidx.test.ext:junit:1.1.2' diff --git a/app/src/main/kotlin/com/vojtkovszky/singleactivitynavigationexample/MainActivity.kt b/app/src/main/kotlin/com/vojtkovszky/singleactivitynavigationexample/MainActivity.kt index 9d80af0..032aecf 100644 --- a/app/src/main/kotlin/com/vojtkovszky/singleactivitynavigationexample/MainActivity.kt +++ b/app/src/main/kotlin/com/vojtkovszky/singleactivitynavigationexample/MainActivity.kt @@ -4,8 +4,7 @@ import android.os.Bundle import android.view.View import com.vojtkovszky.singleactivitynavigation.BaseSingleActivity import com.vojtkovszky.singleactivitynavigation.BaseSingleFragment -import com.vojtkovszky.singleactivitynavigation.FragmentType -import kotlinx.android.synthetic.main.activity_main.* +import com.vojtkovszky.singleactivitynavigationexample.databinding.ActivityMainBinding class MainActivity : BaseSingleActivity() { @@ -23,6 +22,8 @@ class MainActivity : BaseSingleActivity() { private var selectedTabIndex = ROOT_FRAGMENT_POS_HOME + private lateinit var binding: ActivityMainBinding + // define main fragments based on position override fun getNewRootFragmentInstance(positionIndex: Int): BaseSingleFragment? { return when (positionIndex) { @@ -35,10 +36,12 @@ class MainActivity : BaseSingleActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) // we'll be switching main fragments with out bottom navigation - navigationView.setOnNavigationItemSelectedListener { + binding.navigationView.setOnNavigationItemSelectedListener { selectedTabIndex = when (it.itemId) { R.id.navigation_home -> ROOT_FRAGMENT_POS_HOME R.id.navigation_dashboard -> ROOT_FRAGMENT_POS_DASHBOARD @@ -89,6 +92,6 @@ class MainActivity : BaseSingleActivity() { private fun handleNavigationViewVisibility(backStackCount: Int) { // only make bottom bar visible if we're on root screen - navigationView.visibility = if (backStackCount == 0) View.VISIBLE else View.GONE + binding.navigationView.visibility = if (backStackCount == 0) View.VISIBLE else View.GONE } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/vojtkovszky/singleactivitynavigationexample/MainFragment.kt b/app/src/main/kotlin/com/vojtkovszky/singleactivitynavigationexample/MainFragment.kt index 4e2a01f..28a9add 100644 --- a/app/src/main/kotlin/com/vojtkovszky/singleactivitynavigationexample/MainFragment.kt +++ b/app/src/main/kotlin/com/vojtkovszky/singleactivitynavigationexample/MainFragment.kt @@ -6,12 +6,16 @@ import android.view.View import android.view.ViewGroup import com.vojtkovszky.singleactivitynavigation.BaseSingleFragment import com.vojtkovszky.singleactivitynavigation.FragmentType -import kotlinx.android.synthetic.main.fragment_main.* +import com.vojtkovszky.singleactivitynavigationexample.databinding.FragmentMainBinding class MainFragment : BaseSingleFragment() { + private var _binding: FragmentMainBinding? = null + private val binding get() = _binding!! + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_main, container, false) + _binding = FragmentMainBinding.inflate(inflater, container, false) + return binding.root } override fun onResume() { @@ -28,8 +32,8 @@ class MainFragment : BaseSingleFragment() { }) // use it to set status text - textStatus.text = getString(R.string.this_is_type_fragment, title) - textStackSize.text = getString(R.string.stack_size_is, + binding.textStatus.text = getString(R.string.this_is_type_fragment, title) + binding.textStackSize.text = getString(R.string.stack_size_is, activity?.supportFragmentManager?.backStackEntryCount ?: -1) // and change title, but not needed in dialog if (!fragmentType.isDialogOrBottomSheet()) { @@ -37,25 +41,30 @@ class MainFragment : BaseSingleFragment() { } // click listeners on buttons - buttonOpenRegular.let { + binding.buttonOpenRegular.let { it.text = getString(R.string.open_type_fragment, getString(R.string.type_regular)) it.setOnClickListener { navigateTo(MainFragment()) } } - buttonOpenModal.let { + binding.buttonOpenModal.let { it.text = getString(R.string.open_type_fragment, getString(R.string.type_modal)) it.setOnClickListener { navigateTo(MainFragment(), openAsModal = true) } } - buttonOpenBottomSheet.let { + binding.buttonOpenBottomSheet.let { it.text = getString(R.string.open_type_fragment, getString(R.string.type_bottom_sheet)) it.setOnClickListener { openBottomSheet(MainFragment()) } } - buttonOpenDialog.let { + binding.buttonOpenDialog.let { it.text = getString(R.string.open_type_fragment, getString(R.string.type_dialog)) it.setOnClickListener { openDialog(MainFragment())} } - buttonBackToRoot.let { + binding.buttonBackToRoot.let { it.text = getString(R.string.back_to_root) it.setOnClickListener { navigateBackToRoot() } } } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 3b5b2bb..140e670 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,11 @@ buildscript { - ext.kotlin_version = "1.4.10" + ext.kotlin_version = "1.4.20" repositories { google() jcenter() } dependencies { - classpath "com.android.tools.build:gradle:4.1.0" + classpath 'com.android.tools.build:gradle:4.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/singleactivitynavigation/build.gradle b/singleactivitynavigation/build.gradle index a9a2612..3c894d7 100644 --- a/singleactivitynavigation/build.gradle +++ b/singleactivitynavigation/build.gradle @@ -1,7 +1,6 @@ plugins { id 'com.android.library' id 'kotlin-android' - id 'kotlin-android-extensions' id 'maven-publish' } @@ -21,8 +20,7 @@ android { defaultConfig { minSdkVersion 17 targetSdkVersion 30 - versionCode 1 - versionName "2.1.0" + versionName "2.2.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleActivity.kt b/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleActivity.kt index cd53b4b..d0a35ec 100644 --- a/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleActivity.kt +++ b/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleActivity.kt @@ -168,8 +168,7 @@ abstract class BaseSingleActivity: AppCompatActivity() { fun openBottomSheet(fragment: BaseSingleFragment) { closeCurrentlyOpenBottomSheet() with(BaseSingleBottomSheetFragment()) { - this.fragment = fragment - this.fragment.addFragmentTypeToBundle(FragmentType.BOTTOM_SHEET) + this.fragment = fragment.also { it.addFragmentTypeToBundle(FragmentType.BOTTOM_SHEET) } this.show(supportFragmentManager, fragment::class.simpleName) } } @@ -190,8 +189,7 @@ abstract class BaseSingleActivity: AppCompatActivity() { closeCurrentlyOpenDialog() with(BaseSingleDialogFragment.newInstance(anchorView, useFullWidth)) { setStyle(dialogStyle, dialogTheme) - this.fragment = fragment - this.fragment.addFragmentTypeToBundle(FragmentType.DIALOG) + this.fragment = fragment.also { it.addFragmentTypeToBundle(FragmentType.DIALOG) } this.show(supportFragmentManager, fragment::class.simpleName) } } diff --git a/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleBottomSheetFragment.kt b/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleBottomSheetFragment.kt index b3135ee..8981ca9 100644 --- a/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleBottomSheetFragment.kt +++ b/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleBottomSheetFragment.kt @@ -17,47 +17,42 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment */ internal class BaseSingleBottomSheetFragment: BottomSheetDialogFragment() { - // fragment is to be set before dialog is created. - // It will be attached to dialog's base view when the dialog is created. - // Don't rely on this instance as it will be lost when/if underlying activity is recreated. - // To retrieve fragment once attached, use getInnerFragment() instead - internal lateinit var fragment: BaseSingleFragment + // Fragment is to be set before dialog is created and will be attached to dialog's + // base view when the dialog is created. The reference will be removed immediately after. + internal var fragment: BaseSingleFragment? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return FrameLayout(requireActivity()).apply { id = R.id.bottomSheetFragment - // fragment transaction is not needed if restoring from instance state as fragment + // Fragment transaction is not needed if restoring from instance state as fragment // will be recreated in container. - if (savedInstanceState == null && ::fragment.isInitialized) { + if (savedInstanceState == null && fragment != null) { childFragmentManager .beginTransaction() - .replace(R.id.bottomSheetFragment, fragment, fragment::class.simpleName) + .replace(R.id.bottomSheetFragment, fragment!!, fragment!!::class.simpleName) + .runOnCommit { + // At this point fragment reference is not needed anymore and it's + // important we remove it to avoid memory leaks. + fragment = null + } .commitAllowingStateLoss() } } } - override fun onDestroyView() { - super.onDestroyView() - - // remove fragment from view when dialog's view is getting destroyed - if (::fragment.isInitialized) { - childFragmentManager - .beginTransaction() - .remove(fragment) - .commitAllowingStateLoss() - } - } - override fun show(manager: FragmentManager, tag: String?) { - require(::fragment.isInitialized) { + require(fragment != null) { "Fragment needs to be set before calling show" } - super.show(manager, tag) + + // only show if we can guarantee we won't get into IllegalStateException due to state loss commit + if (!manager.isDestroyed && !manager.isStateSaved) { + super.show(manager, tag) + } } /** - * Returns current child fragment attached to this fragment + * Returns current child [BaseSingleFragment] attached to this fragment */ internal fun getInnerFragment(): BaseSingleFragment? { return childFragmentManager.fragments.filterIsInstance().lastOrNull() diff --git a/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleDialogFragment.kt b/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleDialogFragment.kt index c107c81..b2d99a1 100644 --- a/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleDialogFragment.kt +++ b/singleactivitynavigation/src/main/kotlin/com/vojtkovszky/singleactivitynavigation/BaseSingleDialogFragment.kt @@ -35,11 +35,9 @@ internal class BaseSingleDialogFragment: AppCompatDialogFragment() { } } - // fragment is to be set before dialog is created. - // It will be attached to dialog's base view when the dialog is created. - // Don't rely on this instance as it will be lost when/if underlying activity is recreated. - // To retrieve fragment once attached, use getInnerFragment() instead - internal lateinit var fragment: BaseSingleFragment + // Fragment is to be set before dialog is created and will be attached to dialog's + // base view when the dialog is created. The reference will be removed immediately after. + internal var fragment: BaseSingleFragment? = null private var anchorHeight: Int = 0 private var anchorX: Float = 0f @@ -56,15 +54,20 @@ internal class BaseSingleDialogFragment: AppCompatDialogFragment() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { return FrameLayout(requireActivity()).apply { id = R.id.dialogFragment // fragment transaction is not needed if restoring from instance state as fragment // will be recreated in container. - if (savedInstanceState == null && ::fragment.isInitialized) { + if (savedInstanceState == null && fragment != null) { childFragmentManager .beginTransaction() - .replace(R.id.dialogFragment, fragment, fragment::class.simpleName) + .replace(R.id.dialogFragment, fragment!!, fragment!!::class.simpleName) + .runOnCommit { + // At this point fragment reference is not needed anymore and it's + // important we remove it to avoid memory leaks. + fragment = null + } .commitAllowingStateLoss() } } @@ -97,27 +100,19 @@ internal class BaseSingleDialogFragment: AppCompatDialogFragment() { } } - override fun onDestroyView() { - super.onDestroyView() - - // remove fragment from view when dialog's view is getting destroyed - if (::fragment.isInitialized) { - childFragmentManager - .beginTransaction() - .remove(fragment) - .commitAllowingStateLoss() - } - } - override fun show(manager: FragmentManager, tag: String?) { - require(::fragment.isInitialized) { + require(fragment != null) { "Fragment or view needs to be set before calling show" } - super.show(manager, tag) + + // only show if we can guarantee we won't get into IllegalStateException due to state loss commit + if (!manager.isDestroyed && !manager.isStateSaved) { + super.show(manager, tag) + } } /** - * Returns current child fragment attached to this fragment + * Returns current child [BaseSingleFragment] attached to this fragment */ internal fun getInnerFragment(): BaseSingleFragment? { return childFragmentManager.fragments.filterIsInstance().lastOrNull()