Events are any input tapped on the screen, any text changes, and basically any user interaction or system notification that makes any change to an application. Handling events is a fundamental aspect of building interactive and responsive Android apps.
We can separate events by User events and System events.
User events are generated by actions like tapping the screen or pressing a button.
System events are changes in network responses, battery level, screen orientation or configuration changes.
When we want to get value or any response from event that is triggered, we use event listener to listen to this event.
Event Types
Click events are used on Compose elements, such as buttons, icons, or other interactive components. You can use the onClick parameter that is already predefined for button to specify what should happen when a user clicks on a Compose element.
Below is a method code example which creates a button with an onClick method so after button is clicked event will be triggered and any method that is inside { /* Handle click event here */ } will be run.
Button(onClick = { /* Handle click event here */ }) {
Text("Click me")
}And another example with clickable text with a Modifier.clickable.
Text("Text title", Modifier.clickable {/* Handle click event here */})It is also possible to useModifier.combinedClickable for double or long clicks, but if you intend to use it only for clicks then it's better to use Modifier.clickable.
Text("Text title: $title", Modifier.combinedClickable(
onClick = {/* Handle click event here */},
onDoubleClick = {/* Handle click event here */},
onLongClick = {/* Handle click event here */}
))Gesture events are part of the context of user interfaces, which refer to interactions that the user does on screen. These interactions involve various physical actions and movements, like tapping, swiping, pinching, and rotating, and they are used to control and interact with the user interface. Gesture events play a crucial role in providing a more intuitive and engaging user experience on touch-based devices.
Jetpack Compose offers a variety of modifiers tailored to seamlessly handle diverse gesture interactions:
For taps and presses, use the clickable, combinedClickable, selectable, toggleable, and triStateToggleable modifiers.
For scrolling, use horizontalScroll or verticalScroll.
For dragging, use draggable or swipeable modifier.
For multi-touch gestures like panning, rotating, and zooming, use transformable modifier.
When a user places one or more fingers on the screen, this triggers the callback onTouchEvent() on the view that receives the touch events, such as position, pressure, size, and addition of another finger. Whenever a sequence of touch events is recognized as a gesture, the onTouchEvent() function is triggered multiple times.
The gestural interaction begins with the user's first touch on the screen and continues as the system tracks the position of the finger or fingers. It ends when the last finger is lifted from the screen, marking the final event. Throughout this interaction, the MotionEvent passed to the onTouchEvent() method provides detailed information about each touch event. The application can use the data from MotionEvent to determine if a specific gesture has been made.
Draggable: If you want a composable to be draggable, you can use the draggable modifier along with the onDrag callback.
var offset by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.offset { IntOffset(offset.roundToInt(), 0) }
.draggable(
orientation = Orientation.Horizontal,
state = rememberDraggableState { delta ->
offset += delta
}
),
)Scrollable: To create a scrollable area, you can use the verticalScroll or horizontalScroll modifier and provide the content inside.
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
// Scrollable content
}Swipeable: For handling swipe gestures, Swipeable is deprecated so you can use the AnchoredDraggable.
fun ExampleScreen(viewModel: ExampleViewModel) {
val draggedDownAnchorTop = with(LocalDensity.current) { 200.dp.toPx() }
val anchors = DraggableAnchors {
AnchoredDraggableCardState.DRAGGED_DOWN at draggedDownAnchorTop
AnchoredDraggableCardState.DRAGGED_UP at 0f
}Text input events: For example, to capture user input, use the TextField composable and listen for changes using the onValueChange callback.
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { value ->
text = value
// Handle text change event
}
)Focus Handling: To handle focus changes (e.g., when the text field gains or loses focus), use the onFocusChange callback.
var inputValue by remember { mutableStateOf("") } // Initialize inputValue with remember
var isFocused by remember { mutableStateOf(false)}
TextField(
value = inputValue,
onValueChange = { newValue ->
inputValue = newValue
// Handle text change event here if needed
},
modifier = Modifier.onFocusChanged { focusState ->
isFocused = focusState.isFocused
// Handle focus change event here if needed
})System events
Handling system events is crucial for creating a responsive application. These events can include changes in connectivity, battery levels, configuration changes (like device orientation), or app lifecycle events. Proper handling ensures that your app behaves correctly under various system conditions and can adapt to or recover from changes.
Lifecycle Events: Use
Lifecycleobservers to react to changes in the app's lifecycle state. This is essential for starting or stopping operations based on the app's visibility or lifecycle state to conserve resources and prevent leaks.Configuration Changes: Handling configuration changes, such as orientation changes, requires careful management to maintain state and user progress. Use Jetpack Compose's
rememberSaveableor ViewModel to persist state across configuration changes.Connectivity Changes: Monitoring network connectivity changes allows your app to respond to the availability of network access. Utilize
ConnectivityManagerwith aNetworkCallbackto observe changes in network state and adjust your app's behavior accordingly, such as pausing downloads or notifying the user.Battery and Power Management: Responding to battery level changes or power-saving modes can help your app adjust its behavior to conserve battery. For example, you might reduce background activity or postpone non-essential operations when the battery is low.
Network responses
In Jetpack Compose, handling network events primarily involves managing asynchronous operations, such as making network requests, fetching data from APIs, and updating the UI based on the received data.
sealed class NetworkResponse<out T> {
data class Success<out T>(val data: T) : NetworkResponse<T>()
data class Error(val exception: Exception) : NetworkResponse<Nothing>()
object Loading : NetworkResponse<Nothing>() // Added Loading state
}
// Simulate a network request
suspend fun performNetworkRequest(): String {
delay(2000) // Simulate network delay
return "Network request success" // Return success result
}
}In the above example, the NetworkRequestScreen composable function uses a mutableStateOf to hold the current state of the network response. Inside the LaunchedEffect, the network request is performed, and the appropriate response state is updated accordingly.
The UI is then updated based on the current state of the response using a when statement. If the response is NetworkResponse.Success, the success state is handled, and the data is displayed. If the response is NetworkResponse.Error, the error state is handled, and the error message is displayed. If the response is NetworkResponse.Loading, the loading state is handled, and a progress indicator is shown.
In extensive projects, it's highly recommended to follow a robust architectural pattern like Clean Architecture or Model-View-ViewModel (MVVM). Business logic should reside in the domain layer, ensuring a clear separation of concerns.
For medium-sized projects, organizing business logic in a dedicated data layer is a good practice. This data layer can manage data retrieval, transformation, and storage, while the domain layer can handle the core business rules. The ViewModel layer can act as a bridge between the UI and data/domain layers.
Even in small projects, handling business logic in the data layer is a good idea.
Handling events in Compose
Managing events in Compose is typically used with the ViewModel, which oversees the business logic associated with user actions. For instance, when a user clicks a button to refresh data, the ViewModel often manages this by providing functions that the UI can invoke. Notably, every time a state is modified, a recomposition occurs. In Compose, a composable must be explicitly informed of the new state to refresh appropriately.
@Composable
fun ExampleScreen(viewModel: ExampleViewModel) {
// This is a state defined in your ViewModel
val exampleState by viewModel.exampleState.collectAsState()
Column {
Button(onClick = { viewModel.refreshData() }) {
Text("Refresh Data")
}
if (exampleState != null) {
Text("Data: ${exampleState.data}")
} else {
Text("No data available")
}
}
}Conclusion
Jetpack Compose with ViewModel provides a powerful and efficient way to handle user events and manage state in Android applications. The declarative UI paradigm in Compose ensures that your UI is always up-to-date with the latest state. By observing state changes in the ViewModel, Compose can automatically re-render the affected composables when state changes occur. This leads to more readable and maintainable code, as the UI layer is kept separate from the business logic. It’s a significant shift from the traditional imperative programming model, offering a streamlined, unidirectional data flow and simplified UI development.