Skip to content

Commit

Permalink
Added example to show back press
Browse files Browse the repository at this point in the history
  • Loading branch information
vinaygaba committed Jun 24, 2020
1 parent 2d1ccc0 commit 3a75a37
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ sense.
|[How do I use LiveData/ViewModels with Jetpack Compose?](https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example/blob/master/app/src/main/java/com/example/jetpackcompose/state/livedata/LiveDataActivity.kt) | <img src ="screenshots/live_data.gif" width=214 height=400> |
|[How do I use Coroutine Flow to update my components/views with Jetpack Compose?](https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example/blob/master/app/src/main/java/com/example/jetpackcompose/state/coroutine/CoroutineFlowActivity.kt)|<img src ="screenshots/coroutine_flow.gif" width=214 height=400> |
|[How do you launch a Coroutine inside a Composable? <br/> How do you do side-effects in Jetpack Compose?](https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example/blob/master/app/src/main/java/com/example/jetpackcompose/state/livedata/LiveDataActivity.kt#L170) |<img src ="screenshots/live_data.gif" width=214 height=400> |
|[How do you handle back press in Jetpack Compose?](https://github.com/vinaygaba/Learn-Jetpack-Compose-By-Example/blob/master/app/src/main/java/com/example/jetpackcompose/state/backpress/BackPressActivity.kt) | <img src ="screenshots/back_press.gif" width=214 height=400>|


### Material Design
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@
android:exported="true"
android:label="@string/title_measuring_scale_example"
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".state.backpress.BackPressActivity"
android:exported="true"
android:label="@string/title_back_press_example"
android:theme="@style/AppTheme.NoActionBar" />
</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import com.example.jetpackcompose.material.FixedActionButtonActivity
import com.example.jetpackcompose.material.FlowRowActivity
import com.example.jetpackcompose.material.MaterialActivity
import com.example.jetpackcompose.state.ProcessDeathActivity
import com.example.jetpackcompose.state.backpress.BackPressActivity
import com.example.jetpackcompose.state.coroutine.CoroutineFlowActivity
import com.example.jetpackcompose.state.livedata.LiveDataActivity
import com.example.jetpackcompose.theme.DarkModeActivity
Expand Down Expand Up @@ -169,4 +170,8 @@ class MainActivity : AppCompatActivity() {
fun startMeasuringScaleExample(view: View) {
startActivity(Intent(this, MeasuringScaleActivity::class.java))
}

fun startBackPressExample(view: View) {
startActivity(Intent(this, BackPressActivity::class.java))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.example.jetpackcompose.state.backpress

import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.compose.Composable
import androidx.compose.Providers
import androidx.compose.onCommit
import androidx.compose.remember
import androidx.compose.staticAmbientOf
import androidx.ui.core.LifecycleOwnerAmbient

/**
* Related discussion -
* https://kotlinlang.slack.com/archives/CJLTWPH7S/p1591558155394500?thread_ts=1591558024.394400&cid=CJLTWPH7S
*/

// We create a static Ambient of the type OnBackPressedDispatcherOwner. This was present even in
// Classic Android and we will make use of this dispatcher to power our back press handling.

// What are Ambients?
// In Compose, we typicall pass data through the composition tree explicitly through means of
// parameters to composable functions. This is inline with the principles of unidirection
// data flow that Compose heavily recommends using. There are situations where this won't
// always be possible. For these cases, [Ambient]s can be used as an implicit way to have
// data flow through a composition.

// Another way to think about Providers is that I can get access to a value in the middle of
// a composition, without having to pass the value in. Some other examples of Providers and
// Ambients are ContextAmbient(to get access to the context), CoroutineContextAmbient, etc.
private val AmbientBackPressedDispatcher =
staticAmbientOf<OnBackPressedDispatcherOwner?> { null }

// Simple implementation of OnBackPressedCallback interface. Holds a reference to a lambda that's
// used to describe the onBackPressed action and calls it at the right instance (when
// handleOnBackPressed is called)
private class ComposableBackHandler(enabled: Boolean) : OnBackPressedCallback(enabled) {
lateinit var onBackPressed: () -> Unit

override fun handleOnBackPressed() {
onBackPressed()
}
}

// We represent a Composable function by annotating it with the @Composable annotation. Composable
// functions can only be called from within the scope of other composable functions. We should
// think of composable functions to be similar to lego blocks - each composable function is in turn
// built up of smaller composable functions.
@Composable
internal fun handler(
enabled: Boolean = true,
onBackPressed: () -> Unit
) {
val dispatcher = (AmbientBackPressedDispatcher.current ?: return).onBackPressedDispatcher
// remember{} is a helper composable that calculates the value passed to it only during the
// first composition. It then returns the same value for every subsequent composition. In the
// example below, it initializes the value of ComposableBackHandler() and does it only during
// the first composition. It's important to understand that the subsequent screens where this
// value is passed to are still allowed to modify the value (depending on whether it has
// mutable properties).
val handler = remember { ComposableBackHandler(enabled) }
// The onCommmit block is a lifecycle effect composable that is called every time this
// composition is committed. In simpler words, the first onCommit block below will run each
// time the value of dispatcher changes (in addition to the first time the handler composable
// is called)
onCommit(dispatcher) {
dispatcher.addCallback(handler)
// The onDispose block inside the onCommit effect is called to do any clean up(if
// necessary) for the side effect that executed inside onCommit.
onDispose { handler.remove() }
}
onCommit(enabled) {
handler.isEnabled = enabled
}
onCommit(onBackPressed) {
handler.onBackPressed = onBackPressed
}
}

// We represent a Composable function by annotating it with the @Composable annotation. Composable
// functions can only be called from within the scope of other composable functions. We should
// think of composable functions to be similar to lego blocks - each composable function is in turn
// built up of smaller composable functions.
@Composable
internal fun BackButtonHandler(onBackPressed: () -> Unit) {
// Providers allows you to map values to Ambients. You can think of binding values to a key
// represented by Ambients.

// What are Ambients?
// In Compose, we typicall pass data through the composition tree explicitly through means of
// parameters to composable functions. This is inline with the principles of unidirection
// data flow that Compose heavily recommends using. There are situations where this won't
// always be possible. For these cases, [Ambient]s can be used as an implicit way to have
// data flow through a composition.

// Another way to think about Providers is that I can get access to a value in the middle of
// a composition, without having to pass the value in. Some other examples of Providers and
// Ambients are ContextAmbient(to get access to the context), CoroutineContextAmbient, etc.
Providers(
AmbientBackPressedDispatcher provides LifecycleOwnerAmbient.current as ComponentActivity
) {
handler {
onBackPressed()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package com.example.jetpackcompose.state.backpress

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.Composable
import androidx.compose.getValue
import androidx.compose.mutableStateOf
import androidx.compose.remember
import androidx.compose.setValue
import androidx.ui.core.LifecycleOwnerAmbient
import androidx.ui.core.Modifier
import androidx.ui.core.setContent
import androidx.ui.foundation.Box
import androidx.ui.foundation.ContentGravity
import androidx.ui.graphics.Color
import androidx.ui.layout.fillMaxSize
import androidx.ui.layout.padding
import androidx.ui.material.Button
import androidx.ui.unit.dp
import com.example.jetpackcompose.image.TitleComponent

class BackPressActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// This is an extension function of Activity that sets the @Composable function that's
// passed to it as the root view of the activity. This is meant to replace the .xml file
// that we would typically set using the setContent(R.id.xml_file) method. The setContent
// block defines the activity's layout.
setContent {
// Reacting to state changes is core to how Jetpack Compose works. This state variable
// "appState" is used to maintain the current active screen. The value is updated
// based on the actions of the user across the different screens. Every time the value
// of this variable changes, the relevant sub-composables that depends on it is
// automatically updated/recomposed.

// remember{} is a helper composable that calculates the value passed to it only
// during the first composition. It then returns the same value for every subsequent
// composition. In the example below, it initializes the value of AppState() and does
// it only during the first composition. It's important to understand that the
// subsequent screens where this value is passed to are still allowed to modify the
// value (depending on whether it has mutable properties).
val appState = remember { AppState() }
BackPressApp(appState)
}
}
}

// We represent a Composable function by annotating it with the @Composable annotation. Composable
// functions can only be called from within the scope of other composable functions. We should
// think of composable functions to be similar to lego blocks - each composable function is in turn
// built up of smaller composable functions.
@Composable
fun BackPressApp(appState: AppState) {
// Choose which screen to show based on the value in the currentScreen variable inside AppState
when (appState.currentScreen) {
CurrentScreen.SCREEN1 -> Screen1(appState)
CurrentScreen.SCREEN2 -> Screen2(appState)
CurrentScreen.SCREEN3 -> Screen3(appState)
}
}

// We represent a Composable function by annotating it with the @Composable annotation. Composable
// functions can only be called from within the scope of other composable functions. We should
// think of composable functions to be similar to lego blocks - each composable function is in turn
// built up of smaller composable functions.
@Composable
fun Screen1(appState: AppState) {
val activity = (LifecycleOwnerAmbient.current as ComponentActivity)
// Box is a predefined convenience composable that allows you to apply common draw & layout
// logic. We give it a ContentGravity of Center to ensure the children of this composable
// are placed in its center. In addition we also pass a few modifiers to it.

// You can think of Modifiers as implementations of the decorators pattern that are used to
// modify the composable that its applied to. In this example, as the Box composable to
// occupy the entire available height & width using Modifier.fillMaxSize().
Box(
modifier = Modifier.fillMaxSize() + Modifier.padding(16.dp),
gravity = ContentGravity.Center
) {
// TitleComponent is a composable we created in one of the files that merely renders
// text on the screen.
TitleComponent(title = "This is Screen 1")
// Button is a pre-defined Material Design implementation of a contained button -
// https://material.io/design/components/buttons.html#contained-button.
Button(
backgroundColor = Color.Gray,
onClick = {
appState.currentScreen = CurrentScreen.SCREEN2
}) {
TitleComponent(title = "Go To Screen 2")
}
TitleComponent(title = "Press back to exit this activity")
}

BackButtonHandler {
activity.finish()
}
}

// We represent a Composable function by annotating it with the @Composable annotation. Composable
// functions can only be called from within the scope of other composable functions. We should
// think of composable functions to be similar to lego blocks - each composable function is in turn
// built up of smaller composable functions.
@Composable
fun Screen2(appState: AppState) {
// Box is a predefined convenience composable that allows you to apply common draw & layout
// logic. We give it a ContentGravity of Center to ensure the children of this composable
// are placed in its center. In addition we also pass a few modifiers to it.

// You can think of Modifiers as implementations of the decorators pattern that are used to
// modify the composable that its applied to. In this example, as the Box composable to
// occupy the entire available height & width using Modifier.fillMaxSize().
Box(
modifier = Modifier.fillMaxSize() + Modifier.padding(16.dp),
gravity = ContentGravity.Center
) {
// TitleComponent is a composable we created in one of the files that merely renders
// text on the screen.
TitleComponent(title = "This is Screen 2")
// Button is a pre-defined Material Design implementation of a contained button -
// https://material.io/design/components/buttons.html#contained-button.
Button(
backgroundColor = Color.Gray,
onClick = {
appState.currentScreen = CurrentScreen.SCREEN3
}) {
TitleComponent(title = "Go To Screen 3")
}
TitleComponent(title = "Press back to go to Screen 1")
}
BackButtonHandler {
appState.currentScreen = CurrentScreen.SCREEN1
}
}

// We represent a Composable function by annotating it with the @Composable annotation. Composable
// functions can only be called from within the scope of other composable functions. We should
// think of composable functions to be similar to lego blocks - each composable function is in turn
// built up of smaller composable functions.
@Composable
fun Screen3(appState: AppState) {
// Box is a predefined convenience composable that allows you to apply common draw & layout
// logic. We give it a ContentGravity of Center to ensure the children of this composable
// are placed in its center. In addition we also pass a few modifiers to it.

// You can think of Modifiers as implementations of the decorators pattern that are used to
// modify the composable that its applied to. In this example, as the Box composable to
// occupy the entire available height & width using Modifier.fillMaxSize().
Box(
modifier = Modifier.fillMaxSize() + Modifier.padding(16.dp),
gravity = ContentGravity.Center
) {
// TitleComponent is a composable we created in one of the files that merely renders
// text on the screen.
TitleComponent(title = "This is Screen 3")
// TitleComponent is a composable we created in one of the files that merely renders
// text on the screen.
TitleComponent(title = "You can only go back from here. Press back to go to Screen 2.")
}
BackButtonHandler {
appState.currentScreen = CurrentScreen.SCREEN2
}
}

// Simple class that we created to hold the value for the current active screen.
class AppState {
var currentScreen by mutableStateOf(CurrentScreen.SCREEN1)
}

enum class CurrentScreen {
SCREEN1,
SCREEN2,
SCREEN3
}
19 changes: 19 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -600,5 +600,24 @@
android:fontFamily="monospace"
/>
</androidx.cardview.widget.CardView>

<androidx.cardview.widget.CardView
android:id="@+id/load_back_press_example"
android:layout_width="match_parent"
android:layout_height="@dimen/cardview_height"
app:cardBackgroundColor="@color/colorPrimary"
android:padding="@dimen/default_padding"
android:onClick="startBackPressExample"
android:layout_marginTop="@dimen/default_padding"
app:cardCornerRadius="@dimen/card_corner_radius">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/tv_back_press_example"
android:textColor="@color/white"
android:layout_gravity="center"
android:fontFamily="monospace"
/>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<string name="title_coroutine_flow_example">Coroutine Flow Example</string>
<string name="title_compose_classic_example">Compose In Classic Android Example</string>
<string name="title_measuring_scale_example">Measuring Scale Example</string>
<string name="title_back_press_example">Back Press Example</string>

<string name="tv_simple_text_example">Display Text</string>
<string name="tv_custom_text_example">Display Styled Text</string>
Expand Down Expand Up @@ -62,4 +63,5 @@
<string name="tv_coroutine_flow_example">Coroutine Flow Component</string>
<string name="tv_compose_classic_example">Compose In Classic Android Component</string>
<string name="tv_measuring_scale_example">Measuring Scale Component</string>
<string name="tv_back_press_example">Back Press Component</string>
</resources>
Binary file added screenshots/back_press.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3a75a37

Please sign in to comment.