Kotlin Activities and Fragments — Android UI Management Guide
In this tutorial, you will learn about Kotlin Activities and Fragments. We cover key concepts, practical examples, and best practices to help you master this topic.
Android activities represent screens with user interfaces, while fragments encapsulate reusable UI portions within activities, both managed through lifecycle callbacks and navigation patterns in Kotlin.
What You'll Learn
- Understand the Activity and Fragment lifecycles
- Create and register activities in the Android manifest
- Add fragments to activities programmatically and declaratively
- Handle configuration changes (rotation, locale changes)
- Communicate between fragments and activities
- Use the Navigation Component for screen transitions
- Save and restore state across configuration changes
Why It Matters
Activities and fragments are the building blocks of Android UIs. The lifecycle model determines how your app behaves when users rotate the device, receive a phone call, or switch apps. Mismanaging the lifecycle causes crashes, data loss, and memory leaks. Kotlin's concise syntax and Android KTX extensions simplify lifecycle handling compared to Java.
Real-World Use
DodaTech's Android utility app uses a single-activity architecture with multiple fragments managed by the Navigation Component. This pattern simplifies lifecycle management and enables seamless transitions. The configuration utility fragment handles device settings while the main activity manages navigation and permissions.
Learning Path
flowchart LR A[Android Setup] --> B[Activities & Fragments\nYou are here] B --> C[Compose Basics] style B fill:#f90,color:#fff
Activity Lifecycle
Activities go through a well-defined lifecycle: onCreate, onStart, onResume, onPause, onStop, onDestroy.
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
companion object {
private const val TAG = "MainActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate called")
if (savedInstanceState != null) {
// Restoring previous state
val count = savedInstanceState.getInt("counter", 0)
Log.d(TAG, "Restored count: $count")
}
setContentView(R.layout.activity_main)
}
override fun onStart() {
super.onStart()
Log.d(TAG, "onStart called")
}
override fun onResume() {
super.onResume()
Log.d(TAG, "onResume called - app is visible and interactive")
}
override fun onPause() {
super.onPause()
Log.d(TAG, "onPause called - app is partially visible")
}
override fun onStop() {
super.onStop()
Log.d(TAG, "onStop called - app is not visible")
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy called - activity is being destroyed")
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("counter", 42)
Log.d(TAG, "Saving instance state")
}
}
Output: Logcat shows the lifecycle sequence. When rotating, the activity is destroyed and recreated. The counter is restored from the saved instance state.
Fragment Lifecycle
Fragments have their own lifecycle that is tied to the host activity: onAttach, onCreate, onCreateView, onViewCreated, onStart, onResume, onPause, onStop, onDestroyView, onDestroy, onDetach.
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
class DetailFragment : Fragment() {
private var counter = 0
private lateinit var textView: TextView
override fun onAttach(context: android.content.Context) {
super.onAttach(context)
// context is the host activity
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
counter = savedInstanceState?.getInt("counter") ?: 0
retainInstance = true // Deprecated; use ViewModel instead
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_detail, container, false)
textView = view.findViewById(R.id.text_view)
return view
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textView.text = "Count: $counter"
view.findViewById<android.widget.Button>(R.id.button).setOnClickListener {
counter++
textView.text = "Count: $counter"
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("counter", counter)
}
}
Output: The fragment manages its own UI and state. The counter survives rotation through onSaveInstanceState.
Adding Fragments
Fragments can be added programmatically or declaratively in XML.
Declarative (in XML layout)
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="com.example.DetailFragment" />
Programmatic
supportFragmentManager.commit {
replace(R.id.fragment_container, DetailFragment())
addToBackStack(null)
}
Kotlin extension functions from fragment-ktx make transactions concise.
Communication Between Fragments and Activities
Fragments communicate with activities and each other through interfaces, shared ViewModel, or the parent activity.
Interface Pattern
interface OnItemSelectedListener {
fun onItemSelected(itemId: Long, itemName: String)
}
class ListFragment : Fragment() {
private var listener: OnItemSelectedListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
listener = context as? OnItemSelectedListener
}
private fun selectItem(id: Long, name: String) {
listener?.onItemSelected(id, name)
}
override fun onDetach() {
super.onDetach()
listener = null
}
}
class MainActivity : AppCompatActivity(), OnItemSelectedListener {
override fun onItemSelected(itemId: Long, itemName: String) {
supportFragmentManager.commit {
replace(R.id.fragment_container, DetailFragment().apply {
arguments = Bundle().apply {
putLong("item_id", itemId)
putString("item_name", itemName)
}
})
addToBackStack("detail")
}
}
}
Output: The activity receives callbacks from the fragment and navigates accordingly.
Shared ViewModel Pattern (Modern Approach)
class SharedViewModel : ViewModel() {
private val _selectedItem = MutableLiveData<Pair<Long, String>>()
val selectedItem: LiveData<Pair<Long, String>> = _selectedItem
fun selectItem(id: Long, name: String) {
_selectedItem.value = id to name
}
}
class ListFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
private fun onItemClick(id: Long, name: String) {
viewModel.selectItem(id, name)
}
}
class DetailFragment : Fragment() {
private val viewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.selectedItem.observe(viewLifecycleOwner) { (id, name) ->
textView.text = "Item: $name (ID: $id)"
}
}
}
Output: Both fragments observe the same ViewModel scoped to the activity. Changes in ListFragment automatically update DetailFragment.
Navigation Component
The Navigation Component simplifies screen transitions with a type-safe approach.
// In build.gradle.kts
dependencies {
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
}
// MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navController = findNavController(R.id.nav_host_fragment)
}
}
// In a fragment
findNavController().navigate(R.id.action_list_to_detail, Bundle().apply {
putString("itemId", "42")
})
Output: The Navigation Component manages the back stack, transitions, and argument passing automatically.
Saving State
Use savedStateHandle (with Navigation) or onSaveInstanceState to preserve state across configuration changes.
class MyFragment : Fragment() {
private val savedStateHandle: SavedStateHandle by navArgs()
// Modern approach using ViewModel with SavedStateHandle
class MyViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val currentValue = savedStateHandle.getLiveData<Int>("key", 0)
fun updateValue(value: Int) {
savedStateHandle["key"] = value
}
}
}
Output: SavedStateHandle persists values across Process death and configuration changes automatically.
Common Mistakes
Not handling configuration changes: When the device rotates, the activity is recreated. Without onSaveInstanceState, user data is lost. Use ViewModel for complex data that should survive configuration changes.
Memory leaks from inner classes: Anonymous inner classes and non-static inner classes hold references to the activity. Use weak references or lifecycle-aware components.
Fragment transactions outside lifecycle: Committing a fragment Transaction after onSaveInstanceState causes an IllegalStateException. Use commitNow() or commitAllowingStateLoss() when appropriate.
Not checking isAdded before fragment operations: Calling methods on a detached fragment crashes. Always check isAdded before fragment operations.
Using Fragment without ViewModel for state: Fragments should not hold significant state directly. Use ViewModels scoped to the activity or navigation graph.
Overusing addToBackStack: Adding every transaction to the back stack creates a deep navigation history. Only add transactions that represent meaningful navigation steps.
Practice Questions
- What is the Activity lifecycle sequence during a screen rotation?
Answer: onPause -> onStop -> onSaveInstanceState -> onDestroy -> onCreate -> onStart -> onResume. The activity is fully destroyed and recreated.
- How do you communicate between two fragments in the same activity?
Answer: Use a shared ViewModel scoped to the activity, or define an interface in the fragment that the activity implements.
- What is the purpose of onSaveInstanceState?
Answer: It saves transient UI state before the activity is destroyed. The saved Bundle is passed back to onCreate when the activity is recreated.
- How does the Navigation Component simplify fragment management?
Answer: It provides type-safe argument passing, automated back stack management, deep linking support, and visual navigation graphs.
- Challenge: Create a two-pane layout that shows a list in one fragment and details in another. On phones, show one fragment at a time with navigation. On tablets, show both side by side.
Answer: Use a layout-sw600dp resource folder for the tablet layout. Use Navigation Component for phone navigation. Use a shared ViewModel for data communication. The activity checks the layout to determine single-pane or dual-pane mode.
Mini Project
Build a multi-screen notes app with activities and fragments. Requirements:
- NotesListFragment showing a RecyclerView of notes
- NoteDetailFragment showing note content and edit capabilities
- Navigation Component for screen transitions
- Shared ViewModel for communicating between fragments
- Handle configuration changes with onSaveInstanceState
- Support both phone and tablet layouts
- Use DialogFragment for delete confirmation
This project consolidates all activity and fragment patterns in a practical app.
FAQ
What's Next
After mastering activities and fragments, learn Jetpack Compose basics for modern declarative UI. You can also explore ViewModel for managing UI state.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro