Skip to content

Commit 569a662

Browse files
Merge pull request #30 from google-developer-training/m3-updates
M3 migration with custom theme
2 parents fbd50c4 + b10870b commit 569a662

47 files changed

Lines changed: 2519 additions & 241 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/build.gradle

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,17 @@ android {
4848
}
4949
kotlinOptions {
5050
jvmTarget = '1.8'
51+
allWarningsAsErrors = false
52+
freeCompilerArgs += [
53+
'-opt-in=androidx.compose.material3.ExperimentalMaterial3Api',
54+
'-opt-in=androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi',
55+
]
5156
}
5257
buildFeatures {
5358
compose true
5459
}
5560
composeOptions {
56-
kotlinCompilerExtensionVersion '1.4.3'
61+
kotlinCompilerExtensionVersion '1.4.0'
5762
}
5863
packagingOptions {
5964
resources {
@@ -63,14 +68,24 @@ android {
6368
}
6469

6570
dependencies {
66-
implementation platform('androidx.compose:compose-bom:2023.01.00')
67-
implementation 'androidx.activity:activity-compose:1.6.1'
71+
72+
implementation platform('androidx.compose:compose-bom:2022.10.00')
73+
implementation 'androidx.activity:activity-compose:1.7.1'
6874
implementation 'androidx.compose.material3:material3'
75+
implementation "androidx.compose.material3:material3-window-size-class"
76+
implementation "androidx.compose.material:material-icons-extended"
6977
implementation 'androidx.compose.ui:ui'
7078
implementation 'androidx.compose.ui:ui-graphics'
7179
implementation 'androidx.compose.ui:ui-tooling-preview'
72-
implementation 'androidx.core:core-ktx:1.9.0'
73-
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.0'
80+
implementation 'androidx.core:core-ktx:1.10.0'
81+
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
82+
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version"
83+
84+
androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
85+
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
86+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
87+
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
88+
testImplementation 'junit:junit:4.13.2'
7489

7590
debugImplementation 'androidx.compose.ui:ui-test-manifest'
7691
debugImplementation 'androidx.compose.ui:ui-tooling'
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (C) 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.reply.test
17+
18+
import androidx.activity.ComponentActivity
19+
import androidx.annotation.StringRes
20+
import androidx.compose.ui.test.SemanticsNodeInteraction
21+
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
22+
import androidx.compose.ui.test.onNodeWithContentDescription
23+
import androidx.compose.ui.test.onNodeWithTag
24+
import androidx.compose.ui.test.onNodeWithText
25+
import androidx.test.ext.junit.rules.ActivityScenarioRule
26+
27+
/**
28+
* Finds a semantics node with the given string resource id.
29+
*
30+
* The [onNodeWithText] finder provided by compose ui test API, doesn't support usage of
31+
* string resource id to find the semantics node. This extension function accesses string resource
32+
* using underlying activity property and passes it to [onNodeWithText] function as argument and
33+
* returns the [SemanticsNodeInteraction] object.
34+
*/
35+
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>.onNodeWithStringId(
36+
@StringRes id: Int
37+
): SemanticsNodeInteraction = onNodeWithText(activity.getString(id))
38+
39+
/**
40+
* Finds a semantics node from the content description with the given string resource id.
41+
*
42+
* The [onNodeWithContentDescription] finder provided by compose ui test API, doesn't support usage
43+
* of string resource id to find the semantics node from the node's content description.
44+
* This extension function accesses string resource using underlying activity property
45+
* and passes it to [onNodeWithContentDescription] function as argument and
46+
* returns the [SemanticsNodeInteraction] object.
47+
*/
48+
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>
49+
.onNodeWithContentDescriptionForStringId(
50+
@StringRes id: Int
51+
): SemanticsNodeInteraction = onNodeWithContentDescription(activity.getString(id))
52+
53+
/**
54+
* Finds a semantics node from the content description with the given string resource id.
55+
*
56+
* The [onNodeWithTag] finder provided by compose ui test API, doesn't support usage of
57+
* string resource id to find the semantics node from the node's test tag.
58+
* This extension function accesses string resource using underlying activity property
59+
* and passes it to [onNodeWithTag] function as argument and
60+
* returns the [SemanticsNodeInteraction] object.
61+
*/
62+
fun <A : ComponentActivity> AndroidComposeTestRule<ActivityScenarioRule<A>, A>
63+
.onNodeWithTagForStringId(
64+
@StringRes id: Int
65+
): SemanticsNodeInteraction = onNodeWithTag(activity.getString(id))
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright (C) 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.reply.test
17+
18+
import androidx.activity.ComponentActivity
19+
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
20+
import androidx.compose.ui.test.assertAny
21+
import androidx.compose.ui.test.assertIsDisplayed
22+
import androidx.compose.ui.test.hasAnyDescendant
23+
import androidx.compose.ui.test.hasText
24+
import androidx.compose.ui.test.junit4.StateRestorationTester
25+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
26+
import androidx.compose.ui.test.onChildren
27+
import androidx.compose.ui.test.onNodeWithText
28+
import androidx.compose.ui.test.performClick
29+
import com.example.reply.R
30+
import com.example.reply.data.local.LocalEmailsDataProvider
31+
import com.example.reply.ui.ReplyApp
32+
import org.junit.Rule
33+
import org.junit.Test
34+
35+
class ReplyAppStateRestorationTest {
36+
37+
/**
38+
* Note: To access to an empty activity, the code uses ComponentActivity instead of
39+
* MainActivity.
40+
*/
41+
@get:Rule
42+
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
43+
44+
@Test
45+
@TestCompactWidth
46+
fun compactDevice_selectedEmailEmailRetained_afterConfigChange() {
47+
// Setup compact window
48+
val stateRestorationTester = StateRestorationTester(composeTestRule)
49+
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Compact) }
50+
51+
// Given third email is displayed
52+
composeTestRule.onNodeWithText(
53+
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
54+
).assertIsDisplayed()
55+
56+
// Open detailed page
57+
composeTestRule.onNodeWithText(
58+
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
59+
).performClick()
60+
61+
// Verify that it shows the detailed screen for the correct email
62+
composeTestRule.onNodeWithContentDescriptionForStringId(
63+
R.string.navigation_back
64+
).assertExists()
65+
composeTestRule.onNodeWithText(
66+
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
67+
).assertExists()
68+
69+
// Simulate a config change
70+
stateRestorationTester.emulateSavedInstanceStateRestore()
71+
72+
// Verify that it still shows the detailed screen for the same email
73+
composeTestRule.onNodeWithContentDescriptionForStringId(
74+
R.string.navigation_back
75+
).assertExists()
76+
composeTestRule.onNodeWithText(
77+
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
78+
).assertExists()
79+
}
80+
81+
@Test
82+
@TestExpandedWidth
83+
fun expandedDevice_selectedEmailEmailRetained_afterConfigChange() {
84+
// Setup expanded window
85+
val stateRestorationTester = StateRestorationTester(composeTestRule)
86+
stateRestorationTester.setContent { ReplyApp(windowSize = WindowWidthSizeClass.Expanded) }
87+
88+
// Given third email is displayed
89+
composeTestRule.onNodeWithText(
90+
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)
91+
).assertIsDisplayed()
92+
93+
// Select third email
94+
composeTestRule.onNodeWithText(
95+
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].subject)
96+
).performClick()
97+
98+
// Verify that third email is displayed on the details screen
99+
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
100+
.assertAny(hasAnyDescendant(hasText(
101+
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
102+
)
103+
104+
// Simulate a config change
105+
stateRestorationTester.emulateSavedInstanceStateRestore()
106+
107+
// Verify that third email is still displayed on the details screen
108+
composeTestRule.onNodeWithTagForStringId(R.string.details_screen).onChildren()
109+
.assertAny(hasAnyDescendant(hasText(
110+
composeTestRule.activity.getString(LocalEmailsDataProvider.allEmails[2].body)))
111+
)
112+
}
113+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright (C) 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.reply.test
17+
18+
import androidx.activity.ComponentActivity
19+
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
20+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
21+
import com.example.reply.R
22+
import com.example.reply.ui.ReplyApp
23+
import org.junit.Rule
24+
import org.junit.Test
25+
26+
class ReplyAppTest {
27+
28+
/**
29+
* Note: To access to an empty activity, the code uses ComponentActivity instead of
30+
* MainActivity.
31+
*/
32+
@get:Rule
33+
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
34+
35+
@Test
36+
@TestCompactWidth
37+
fun compactDevice_verifyUsingBottomNavigation() {
38+
// Set up compact window
39+
composeTestRule.setContent {
40+
ReplyApp(
41+
windowSize = WindowWidthSizeClass.Compact
42+
)
43+
}
44+
// Bottom navigation is displayed
45+
composeTestRule.onNodeWithTagForStringId(
46+
R.string.navigation_bottom
47+
).assertExists()
48+
}
49+
50+
@Test
51+
@TestMediumWidth
52+
fun mediumDevice_verifyUsingNavigationRail() {
53+
// Set up medium window
54+
composeTestRule.setContent {
55+
ReplyApp(
56+
windowSize = WindowWidthSizeClass.Medium
57+
)
58+
}
59+
// Navigation rail is displayed
60+
composeTestRule.onNodeWithTagForStringId(
61+
R.string.navigation_rail
62+
).assertExists()
63+
}
64+
65+
@Test
66+
@TestExpandedWidth
67+
fun expandedDevice_verifyUsingNavigationDrawer() {
68+
// Set up expanded window
69+
composeTestRule.setContent {
70+
ReplyApp(
71+
windowSize = WindowWidthSizeClass.Expanded
72+
)
73+
}
74+
// Navigation drawer is displayed
75+
composeTestRule.onNodeWithTagForStringId(
76+
R.string.navigation_drawer
77+
).assertExists()
78+
}
79+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright (C) 2023 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.example.reply.test
17+
18+
annotation class TestCompactWidth
19+
annotation class TestMediumWidth
20+
annotation class TestExpandedWidth

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
<application
2121
android:allowBackup="true"
22+
android:dataExtractionRules="@xml/data_extraction_rules"
23+
android:fullBackupContent="@xml/backup_rules"
2224
android:icon="@mipmap/ic_launcher"
2325
android:label="@string/app_name"
2426
android:roundIcon="@mipmap/ic_launcher_round"
@@ -32,6 +34,7 @@
3234
android:theme="@style/Theme.Reply">
3335
<intent-filter>
3436
<action android:name="android.intent.action.MAIN" />
37+
3538
<category android:name="android.intent.category.LAUNCHER" />
3639
</intent-filter>
3740
</activity>

0 commit comments

Comments
 (0)