diff --git a/app/src/main/java/com/example/androidconcepts/MainActivity.kt b/app/src/main/java/com/example/androidconcepts/MainActivity.kt index 542ecfb..0c79921 100644 --- a/app/src/main/java/com/example/androidconcepts/MainActivity.kt +++ b/app/src/main/java/com/example/androidconcepts/MainActivity.kt @@ -2,13 +2,61 @@ package com.example.androidconcepts import android.os.Bundle import androidx.appcompat.app.AppCompatActivity +import com.example.androidconcepts.fragments.problems.problem1.FragProbOneFragment +import com.example.androidconcepts.fragments.problems.problem2.FragProbTwoFragment class MainActivity : AppCompatActivity() { + enum class CURRENT_BUILD { + FRAG_PROBLEM_1, FRAG_PROBLEM_2 + } + + // change this to modify app's state. + private val currentBuild = CURRENT_BUILD.FRAG_PROBLEM_2 + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) // AndroidThreadSpammer().execute() + + if (currentBuild == CURRENT_BUILD.FRAG_PROBLEM_1) { + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .replace(R.id.root_fragment_container, FragProbOneFragment()) + .commit() + } + } else if (currentBuild == CURRENT_BUILD.FRAG_PROBLEM_2) { + if (savedInstanceState == null) { + supportFragmentManager.beginTransaction() + .replace(R.id.root_fragment_container, FragProbTwoFragment()) + .commit() + } + } + } + + override fun onBackPressed() { + +// ------------------------------ +// ---------- for FRAG PROBLEM 1 + +// if (currentBuild == CURRENT_BUILD.FRAG_PROBLEM_1) { + +// SOLUTION : manually remove the unintended fragments attached. + +// val currentFragment = supportFragmentManager.findFragmentById(R.id.root_fragment_container) +// ?: return + +// Log.d("frag problem", "current frag : " + (currentFragment as FragProbOneFragment).number) + +// this does the main fix, i.e remove currently unintended shown fragment from the fm. + +// supportFragmentManager.beginTransaction().remove(currentFragment).commit() +// } + +// ---------- for FRAG PROBLEM 1 +// ------------------------------ + + super.onBackPressed() // internally does popBackStackImmediate or finish activity. } } \ No newline at end of file diff --git a/app/src/main/java/com/example/androidconcepts/fragments/problems/problem1/FragProbOneFragment.kt b/app/src/main/java/com/example/androidconcepts/fragments/problems/problem1/FragProbOneFragment.kt new file mode 100644 index 0000000..d2c4be7 --- /dev/null +++ b/app/src/main/java/com/example/androidconcepts/fragments/problems/problem1/FragProbOneFragment.kt @@ -0,0 +1,83 @@ +package com.example.androidconcepts.fragments.problems.problem1 + +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.FrameLayout.LayoutParams +import android.widget.Toast +import androidx.fragment.app.Fragment +import com.example.androidconcepts.R +import com.google.android.material.button.MaterialButton + +class FragProbOneFragment : Fragment() { + + private lateinit var button: MaterialButton + + var number = 0 + + private fun getInstance(number: Int): FragProbOneFragment { + return FragProbOneFragment().apply { + val bundle = Bundle() + bundle.putInt("number", number) + this.arguments = bundle + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + number = arguments?.getInt("number", 0) ?: 0 + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val rootView = FrameLayout(requireContext()) + + rootView.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + rootView.setBackgroundColor(Color.WHITE) // hides the other fragments, due to this I never found this issue. + rootView.isClickable = true // prevents clicking through the current fragment. + + button = MaterialButton(requireContext()) + button.text = "Fragment Problem : $number" + val lp = LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + ) + lp.gravity = Gravity.TOP + lp.topMargin = number * 50 + button.layoutParams = lp + + rootView.addView(button) + + return rootView + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + Log.d("frag problem", "frags size : " + requireActivity().supportFragmentManager.fragments.size + + " backstack count : " + requireActivity().supportFragmentManager.backStackEntryCount) + + button.setOnClickListener { + val ft = requireActivity().supportFragmentManager.beginTransaction() + ft.replace(R.id.root_fragment_container, getInstance(number + 1)) + + if (number % 2 == 1) // only add if current fragment number is odd. eg: frag 3 to 4 is added but 4 to 5 isn't. + ft.addToBackStack(null) + + ft.commit() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/androidconcepts/fragments/problems/problem2/FragProbTwoFragment.kt b/app/src/main/java/com/example/androidconcepts/fragments/problems/problem2/FragProbTwoFragment.kt new file mode 100644 index 0000000..288f31b --- /dev/null +++ b/app/src/main/java/com/example/androidconcepts/fragments/problems/problem2/FragProbTwoFragment.kt @@ -0,0 +1,113 @@ +package com.example.androidconcepts.fragments.problems.problem2 + +import android.graphics.Color +import android.os.Bundle +import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.fragment.app.Fragment +import com.example.androidconcepts.R +import com.google.android.material.button.MaterialButton + +class FragProbTwoFragment : Fragment() { + + private lateinit var button: MaterialButton + + var number = 0 + + private fun getInstance(number: Int): FragProbTwoFragment { + return FragProbTwoFragment().apply { + val bundle = Bundle() + bundle.putInt("number", number) + this.arguments = bundle + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + number = arguments?.getInt("number", 0) ?: 0 + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val rootView = FrameLayout(requireContext()) + + rootView.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + rootView.setBackgroundColor(Color.WHITE) // hides the other fragments, due to this I never found this issue. + rootView.isClickable = true // prevents clicking through the current fragment. + + button = MaterialButton(requireContext()) + button.text = "Fragment Problem : $number" + val lp = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ) + lp.gravity = Gravity.TOP + lp.topMargin = number * 50 + button.layoutParams = lp + + rootView.addView(button) + + return rootView + } + + override fun onSaveInstanceState(outState: Bundle) { + Log.d("frag problem", "$number onSaveInstanceState") + super.onSaveInstanceState(outState) + } + + override fun onStart() { + // state is set to false here. eg : minimize app, state = true, reopen, state = false. + Log.d("frag problem", "$number onStart, isStateSaved : $isStateSaved") + super.onStart() + } + + override fun onResume() { + Log.d("frag problem", "$number onResume, isStateSaved : $isStateSaved") + super.onResume() + } + + override fun onPause() { + + // as far as experimented, isStateSaved is false in onPause. + Log.d("frag problem", "$number onPause, isStateSaved : $isStateSaved") + + super.onPause() + + // very common case in which we do a fragment transaction on a network call result. + // user initiates the network call and minimises the app and before opening again the result is fetched + // and hence a frag transaction is committed. + // then perhaps, the app would crash throwing "java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState". + + button.postDelayed({ + val fm = requireActivity().supportFragmentManager + fm.beginTransaction() + .replace(R.id.root_fragment_container, this.getInstance(number + 1)) + .addToBackStack(null) + .commit() + }, 1500L) + } + + override fun onStop() { + // weirdly, isStateSaved is true here even when onSaveInstanceState is called yet. + // also if I commit frag transactions in onStop then isStateSaved is false. + Log.d("frag problem", "$number onStop, isStateSaved : $isStateSaved") + super.onStop() + } + + override fun onDestroyView() { + Log.d("frag problem", "$number onDestroyView, isStateSaved : $isStateSaved") + super.onDestroyView() + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 712b091..e905b89 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,43 +7,9 @@ android:background="@color/white" tools:context=".MainActivity"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/src/test/java/com/example/androidconcepts/common/Benchmark.kt b/app/src/test/java/com/example/androidconcepts/common/Benchmark.kt index 6a47d08..caf148c 100644 --- a/app/src/test/java/com/example/androidconcepts/common/Benchmark.kt +++ b/app/src/test/java/com/example/androidconcepts/common/Benchmark.kt @@ -5,6 +5,6 @@ fun measureTime(action: () -> Unit): Long { action.invoke() val endTime = System.currentTimeMillis() val measuredTime = endTime - startTime - println("measure time = " + measuredTime + "ms") + println("measured time = " + measuredTime + "ms") return measuredTime } \ No newline at end of file diff --git a/app/src/test/java/com/example/androidconcepts/coroutines/performance/CoroutinePerformanceTest.kt b/app/src/test/java/com/example/androidconcepts/coroutines/performance/CoroutinePerformanceTest.kt index 06b26bd..2024d28 100644 --- a/app/src/test/java/com/example/androidconcepts/coroutines/performance/CoroutinePerformanceTest.kt +++ b/app/src/test/java/com/example/androidconcepts/coroutines/performance/CoroutinePerformanceTest.kt @@ -1,9 +1,13 @@ package com.example.androidconcepts.coroutines.performance +import com.example.androidconcepts.common.BgThreadPoster +import com.example.androidconcepts.common.measureTime import kotlinx.coroutines.* import org.junit.Test import java.util.concurrent.CountDownLatch import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit class CoroutinePerformanceTest { @@ -98,4 +102,65 @@ class CoroutinePerformanceTest { joinAll(backgroundJob, userJob) } + + @Test + fun performanceTestOfNThreads() { + val processImageUseCase = ProcessImageUseCase() + val executor = Executors.newFixedThreadPool(8) + + val futures = mutableListOf>() + + measureTime { + repeat(1000) { + val result = executor.submit { + processImageUseCase.processBlocking(10) + } + + futures += result + } + + futures.forEach { + it.get() + } + } + + // RESULTS : + // 1005 Threads, 1000 repeat, 10ms task time -> 1291 ms + // 500 Threads, 1000 repeat, 10ms task time -> 1036 ms + // 100 Threads, 1000 repeat, 10ms task time -> 690 ms + // 50 Threads, 1000 repeat, 10ms task time -> 649 ms + // 8 (no of cores) Threads, 1000 repeat, 10ms task time -> 1262 seconds + // 5 Threads, 1000 repeat, 10ms task time -> 2 seconds + } + + @Test + fun performanceTestOfNThreadedDispatcher() { + val processImageUseCase = ProcessImageUseCase() + val executor = Executors.newFixedThreadPool(5).asCoroutineDispatcher() + val scope = CoroutineScope(executor) + + val jobs = mutableListOf() + + measureTime { + runBlocking { + repeat(1000) { + val result = scope.launch { + processImageUseCase.processBlocking(10) + } + + jobs += result + } + + jobs.joinAll() + } + } + + // RESULTS : + // 1005 Threads, 1000 repeat, 10ms task time -> 1344 ms + // 500 Threads, 1000 repeat, 10ms task time -> 1056 ms + // 100 Threads, 1000 repeat, 10ms task time -> 785 ms + // 50 Threads, 1000 repeat, 10ms task time -> 743 ms + // 8 (no of cores) Threads, 1000 repeat, 10ms task time -> 1326 seconds + // 5 Threads, 1000 repeat, 10ms task time -> 2 seconds + } } \ No newline at end of file diff --git a/app/src/test/java/com/example/androidconcepts/coroutines/performance/ProcessImageUseCase.kt b/app/src/test/java/com/example/androidconcepts/coroutines/performance/ProcessImageUseCase.kt index 0956024..d1df4f1 100644 --- a/app/src/test/java/com/example/androidconcepts/coroutines/performance/ProcessImageUseCase.kt +++ b/app/src/test/java/com/example/androidconcepts/coroutines/performance/ProcessImageUseCase.kt @@ -16,4 +16,17 @@ class ProcessImageUseCase { println("wow, your device has really fast cores") } } + + fun processBlocking(time: Int) { + // simulate algorithm invocation for time ms in a "blocking manner". + // (fake load just to avoid elimination optimization by compilers) + val finishTimeMillis = System.currentTimeMillis() + time + var counter = 0 + while (System.currentTimeMillis() < finishTimeMillis) { + counter++ + } + if (counter > 250000000) { + println("wow, your device has really fast cores") + } + } }