Skip to content

Commit 69af95f

Browse files
committedAug 19, 2020
[Jetchat] Addresses eng reviews and README
Change-Id: I17dab1614b7aa7972d5006e8761f2b1488116286
1 parent ddb084f commit 69af95f

File tree

7 files changed

+101
-48
lines changed

7 files changed

+101
-48
lines changed
 

‎Jetchat/README.md

+91-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,96 @@
1-
##Jetchat
1+
<img src="screenshots/jetchatlogo.png"/>
22

3-
TODO
3+
# Jetchat sample
44

5-
### Known issues
5+
Jetchat is a sample chat app built with [Jetpack Compose][compose]. This sample showcases:
6+
7+
* UI state management
8+
* Integration with Architecture Components: Navigation, Fragments, LiveData, ViewModel
9+
* Back button handling
10+
* Text Input and focus management
11+
* Multiple types of animations and transitions
12+
* Saved state across configuration changes
13+
* Basic Material Design theming
14+
* UI tests
15+
16+
<img src="screenshots/jetchat.gif"/>
17+
18+
### Status: 🚧 In progress
19+
20+
Jetchat is still in under development, and some features are not yet implemented.
21+
22+
## Features
23+
24+
### UI State management
25+
The [ConversationContent](app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt) composable is the entry point to this screen and takes a [ConversationUiState](app/src/main/java/com/example/compose/jetchat/conversation/ConversationUiState.kt) that defines the data to be displayed. This doesn't mean all the state is served from a single point: composables can have their own state too. For an example, see `scrollState` in [ConversationContent](app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt) or `currentInputSelector` in [UserInput](app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt)
26+
27+
### Architecture Components
28+
The [ProfileFragment](app/src/main/java/com/example/compose/jetchat/profile/ProfileFragment.kt) shows how to pass data between fragments with the [Navigation component](https://developer.android.com/guide/navigation) and observe state from a
29+
[ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel), served via [LiveData](https://developer.android.com/topic/libraries/architecture/livedata).
30+
31+
### Back button handling
32+
When the Emoji selector is shown, pressing back in the app closes it, intercepting any navigation events. This feature shows a way to integrate Compose and APIs from the Android Framework like [OnBackPressedDispatcherOwner](https://developer.android.com/reference/androidx/activity/OnBackPressedDispatcher) via [Ambients](https://developer.android.com/reference/kotlin/androidx/compose/Ambient). The implementation can be found in [ConversationUiState](app/src/main/java/com/example/compose/jetchat/conversation/BackHandler.kt).
33+
34+
### Text Input and focus management
35+
When the Emoji panel is shown the keyboard must be hidden and vice versa. This is achieved with a combination of the [FocusRequester](https://developer.android.com/reference/kotlin/androidx/compose/ui/focus/FocusRequester) and [FocusObserver](https://developer.android.com/reference/kotlin/androidx/compose/ui/FocusObserverModifier) APIs.
36+
37+
### Multiple types of animations and transitions
38+
This sample uses animations ranging from simple `AnimatedVisibility` in [FunctionalityNotAvailablePanel](app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt) to choreographed transitions found in the [FloatingActionButton](https://material.io/develop/android/components/floating-action-button) of the Profile screen and implemented in [AnimatingFabContent](app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt)
39+
40+
### Saved state across configuration changes
41+
Some composable state survives activity or process recreation, like `currentInputSelector` in [UserInput](app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt).
42+
43+
### Basic Material Design theming
44+
Jetchat follows the Material Design principles and uses the `MaterialTheme` ambient, with custom light and dark themes. In some cases colors it might be necessary to create additional colors, that can be specified as an overlay or combination of two, or as a specific elevation in dark mode. Jetchat uses some convenient extensions on the Material palette and can be used as follows:
45+
46+
[UserInput](app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt)
47+
```kotlin
48+
@Composable
49+
fun getSelectorExpandedColor(): Color {
50+
return if (MaterialTheme.colors.isLight) {
51+
MaterialTheme.colors.compositedOnSurface(0.04f)
52+
} else {
53+
MaterialTheme.colors.elevatedSurface(8.dp)
54+
}
55+
}
56+
```
57+
58+
### UI tests
59+
In [androidTest](app/src/androidTest/java/com/example/compose/jetchat) you'll find a suite of UI tests that showcase interesting patterns in Compose:
60+
61+
#### [ConversationTest](app/src/androidTest/java/com/example/compose/jetchat/ConversationTest.kt)
62+
UI tests for the Conversation screen. Includes a test that checks the behavior of the app when dark mode changes.
63+
64+
#### [NavigationTest](app/src/androidTest/java/com/example/compose/jetchat/NavigationTest.kt)
65+
Shows how to write tests that assert directly on the [Navigation Controller](https://developer.android.com/reference/androidx/navigation/NavController).
66+
67+
#### [UserInputTest](app/src/androidTest/java/com/example/compose/jetchat/UserInputTest.kt)
68+
Checks that the user input composable, including extended controls, behave as expected showing and hiding the keyboard.
69+
70+
71+
## Known issues
672
1. If the emoji selector is shown, clicking on the TextField can sometimes show both input methods.
773
Tracked in https://issuetracker.google.com/164859446
874

9-
2. There are only two profiles, clicking on anybody except "me" will show the same data.
75+
2. There are only two profiles, clicking on anybody except "me" will show the same data.
76+
77+
78+
## License
79+
```
80+
Copyright 2020 The Android Open Source Project
81+
82+
Licensed under the Apache License, Version 2.0 (the "License");
83+
you may not use this file except in compliance with the License.
84+
You may obtain a copy of the License at
85+
86+
https://www.apache.org/licenses/LICENSE-2.0
87+
88+
Unless required by applicable law or agreed to in writing, software
89+
distributed under the License is distributed on an "AS IS" BASIS,
90+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
91+
See the License for the specific language governing permissions and
92+
limitations under the License.
93+
```
94+
95+
[compose]: https://developer.android.com/jetpack/compose
96+
[coil-accompanist]: https://github.com/chrisbanes/accompanist

‎Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/ConversationFragment.kt

+3-10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import android.view.ViewGroup
2323
import android.widget.FrameLayout
2424
import androidx.compose.runtime.Providers
2525
import androidx.compose.runtime.Recomposer
26+
import androidx.compose.ui.platform.ComposeView
2627
import androidx.compose.ui.platform.setContent
2728
import androidx.core.os.bundleOf
2829
import androidx.fragment.app.Fragment
@@ -39,16 +40,8 @@ class ConversationFragment : Fragment() {
3940
container: ViewGroup?,
4041
savedInstanceState: Bundle?
4142
): View? {
42-
return FrameLayout(requireContext()).apply {
43-
44-
// In order for savedState to work, the same ID needs to be used for all instances.
45-
id = R.id.conversation_fragment
46-
47-
layoutParams = ViewGroup.LayoutParams(
48-
ViewGroup.LayoutParams.MATCH_PARENT,
49-
ViewGroup.LayoutParams.MATCH_PARENT
50-
)
51-
setContent(Recomposer.current()) {
43+
return ComposeView(context = requireContext()).apply {
44+
setContent {
5245
Providers(BackPressedDispatcherAmbient provides requireActivity()) {
5346
JetchatTheme {
5447
ConversationContent(

‎Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/UserInput.kt

-22
Original file line numberDiff line numberDiff line change
@@ -249,28 +249,6 @@ fun getSelectorExpandedColor(): Color {
249249
}
250250
}
251251

252-
@Composable
253-
fun MapSelector(modifier: Modifier = Modifier) {
254-
Surface(modifier = modifier.fillMaxWidth().preferredHeight(128.dp), color = Color.LightGray) {
255-
Stack {
256-
// TODO
257-
Text(modifier = Modifier.gravity(Alignment.Center), text = "I'm here!")
258-
}
259-
}
260-
}
261-
262-
@Composable
263-
fun StickerSelector(modifier: Modifier = Modifier) {
264-
// TODO
265-
Text("Stickers…", modifier = modifier)
266-
}
267-
268-
@Composable
269-
fun StartCall(modifier: Modifier = Modifier) {
270-
// TODO
271-
Text("Calling…", modifier = modifier)
272-
}
273-
274252
@Composable
275253
private fun UserInputSelector(
276254
onSelectorChange: (InputSelector) -> Unit,

‎Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileFragment.kt

+3-10
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import android.view.ViewGroup
2424
import android.widget.FrameLayout
2525
import androidx.compose.runtime.Recomposer
2626
import androidx.compose.runtime.livedata.observeAsState
27+
import androidx.compose.ui.platform.ComposeView
2728
import androidx.compose.ui.platform.setContent
2829
import androidx.fragment.app.Fragment
2930
import androidx.fragment.app.viewModels
@@ -48,16 +49,8 @@ class ProfileFragment : Fragment() {
4849
savedInstanceState: Bundle?
4950
): View? {
5051

51-
return FrameLayout(requireContext()).apply {
52-
53-
// In order for savedState to work, the same ID needs to be used for all instances.
54-
id = R.id.profile_fragment
55-
56-
layoutParams = ViewGroup.LayoutParams(
57-
ViewGroup.LayoutParams.MATCH_PARENT,
58-
ViewGroup.LayoutParams.MATCH_PARENT
59-
)
60-
setContent(Recomposer.current()) {
52+
return ComposeView(context = requireContext()).apply {
53+
setContent {
6154
viewModel.userData.observeAsState().value.let { userData: ProfileScreenState? ->
6255
JetchatTheme {
6356
if (userData == null) {

‎Jetchat/app/src/main/java/com/example/compose/jetchat/profile/ProfileViewModel.kt

+4-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.example.compose.jetchat.profile
1818

1919
import androidx.annotation.DrawableRes
2020
import androidx.compose.runtime.Immutable
21+
import androidx.lifecycle.LiveData
2122
import androidx.lifecycle.MutableLiveData
2223
import androidx.lifecycle.ViewModel
2324
import com.example.compose.jetchat.data.colleagueProfile
@@ -31,10 +32,11 @@ class ProfileViewModel : ViewModel() {
3132
if (newUserId != userId) {
3233
userId = newUserId ?: meProfile.userId
3334
}
34-
userData.value = if (userId == meProfile.userId) meProfile else colleagueProfile
35+
_userData.value = if (userId == meProfile.userId) meProfile else colleagueProfile
3536
}
3637

37-
val userData = MutableLiveData<ProfileScreenState>()
38+
private val _userData = MutableLiveData<ProfileScreenState>()
39+
val userData: LiveData<ProfileScreenState> = _userData
3840
}
3941

4042
@Immutable

‎Jetchat/screenshots/jetchat.gif

3.44 MB
Loading

‎Jetchat/screenshots/jetchatlogo.png

6.78 KB
Loading

0 commit comments

Comments
 (0)
Please sign in to comment.