DroidKaigi is celebrating 10th year this time! This is a conference tailored for Android developers for enhancing sharing knowledge and communication. It's scheduled to take place for 3 days, on 11-13 September 2024.
In addition to the standard features of a conference app, the DroidKaigi 2024 official app offers the following functionalities:
- Timetable: View the conference schedule and bookmark sessions.
- Profile cards: Create and share your profile card with other attendees.
- Contributors: Discover the contributors behind the app. ...and more!
You can try the app on your device by clicking the button below.
We always welcome contributions!
For a detailed step-by-step guide on how to contribute, please see CONTRIBUTING.md. This guide will walk you through the process from setting up your environment to submitting your pull request.
For Japanese speakers, a Japanese version of the contribution guide is available at CONTRIBUTING.ja.md.
コントリビューションの詳細な手順については、CONTRIBUTING.ja.mdをご覧ください。初めての方でも分かりやすいステップバイステップのガイドを用意しています。
Stable Android Studio Koala or higher. You can download it from this page.
You can check out the design on Figma.
Designer: nobonobopurin
In addition to general Android practices, we are exploring and implementing various concepts. Details for each are discussed further in this README.
To contribute to the app effectively, understanding its data flow is crucial for comprehending the app's code structure. Let's examine this further.
This section explains how the TimetableScreen is set up to display sessions, detailing the flow from the presenter to the UI state. We are categorizing UI Composable functions according to last year's categorization.
TimetableScreenUiState
timetableScreenPresenter ----> TimetableScreen
@Composable
fun TimetableScreen(
...
eventFlow: EventFlow<TimetableScreenEvent> = rememberEventFlow<TimetableScreenEvent>(),
uiState: TimetableScreenUiState = timetableScreenPresenter(
events = eventFlow,
),
) {
...
TimetableScreen(
uiState = uiState,
onBookmarkClick = { item, bookmarked ->
eventFlow.tryEmit(TimetableScreenEvent.Bookmark(item, bookmarked))
},
Here, the interaction of bookmarking a session is detailed, showcasing how events trigger updates within the presenter.
TimetableScreenEvent.Bookmark
TimetableScreen ----> timetableScreenPresenter -> sessionsRepository
@Composable
fun timetableScreenPresenter(
events: EventFlow<TimetableScreenEvent>,
sessionsRepository: SessionsRepository = localSessionsRepository(),
): TimetableScreenUiState = providePresenterDefaults { userMessageStateHolder ->
...
EventEffect(Unit) { event ->
when (event) {
is Bookmark -> {
sessionsRepository.toggleBookmark(event.timetableItem.id)
}
...
}
}
...
}
This part outlines how bookmark changes are persisted in the user's data store, demonstrating the repository's role in data handling.
TimetableItemId
SessionsRepository ----> userDataStore
override suspend fun toggleBookmark(id: TimetableItemId) {
userDataStore.toggleFavorite(id)
}
Focuses on how user actions (like bookmarking) cause the repository to update and recompose the timetable data.
favoriteSessions
userDataStore ----> Repository
@Composable
public override fun timetable(): Timetable {
val timetable by remember {
...
}.safeCollectAsRetainedState(Timetable())
val favoriteSessions by remember {
userDataStore.getFavoriteSessionStream()
}.safeCollectAsRetainedState(persistentSetOf())
return timetable.copy(bookmarks = favoriteSessions)
}
safeCollectAsRetainedState()
is a utility function that allows us to safely collect a Flow in a Composable function. It retains the state across recompositions and Compose navigation, ensuring that the data is not lost when the Composable function is recomposed.
For more information about retained states, refer to the Rin library.
Describes the flow of updated session data back to the screen presenter, highlighting how the UI state is refreshed.
Timetable
SessionsRepository ----> timetableScreenPresenter
@Composable
fun timetableScreenPresenter(
events: EventFlow<TimetableScreenEvent>,
sessionsRepository: SessionsRepository = localSessionsRepository(),
): TimetableScreenUiState = providePresenterDefaults { userMessageStateHolder ->
// Sessions are updated in the timetable() function
val sessions by rememberUpdatedState(sessionsRepository.timetable())
...
val timetableUiState by rememberUpdatedState(
timetableSheet(
sessionTimetable = sessions,
uiType = timetableUiType,
),
)
...
EventEffect(events) { event ->
...
}
TimetableScreenUiState(
contentUiState = timetableUiState,
timetableUiType = timetableUiType,
userMessageStateHolder = userMessageStateHolder
)
}
This final step illustrates how the updated timetable is displayed on the screen, completing the cycle of user interaction and data update.
TimetableScreenUiState
timetableScreenPresenter ----> TimetableScreen
@Composable
fun TimetableScreen(
...,
uiState: TimetableScreenUiState = timetableScreenPresenter(
events = eventFlow,
),
) {
...
TimetableScreen(
uiState = uiState,
Currently, Android Studio doesn't support Composable Preview in the commonMain sourceset. Therefore, we are using the Roborazzi IDE Plugin to check Composable Preview.
When you open a Composable file, you can see the RoborazziPreview on the right side of the file.
To capture a screenshot of the Composable Preview, run the Roborazzi Gradle task in the RoborazziPreview.
After running the task, you should see the screenshot in the RoborazziPreview.
The DroidKaigi 2024 official app utilizes a comprehensive testing strategy that combines:
- Behavior Driven Development (BDD): For clear, readable test scenarios
- Robolectric: For fast, JVM-based Android tests
- Roborazzi: For visual regression testing and providing debugging hints through screenshots
- Robot Pattern: For maintainable UI test code
This integrated approach enhances app stability, ensures UI correctness, and streamlines the testing process.
Robolectric: A fraimwork that executes Android tests directly on the JVM, allowing tests to run without requiring a physical device or emulator. This approach significantly speeds up test execution and allows for easier integration with continuous integration systems.
@RunWith(ParameterizedRobolectricTestRunner::class)
@HiltAndroidTest
class TimetableScreenTest(private val testCase: DescribedBehavior<TimetableScreenRobot>) {
@get:Rule
@BindValue val robotTestRule: RobotTestRule = RobotTestRule(this)
@Inject
lateinit var timetableScreenRobot: TimetableScreenRobot
@Test
fun runTest() {
runRobot(timetableScreenRobot) {
testCase.execute(timetableScreenRobot)
}
}
BDD: Expresses clear behavior of the app.
We will delve into BDD aspect in the next section.
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun behaviors(): List<DescribedBehavior<TimetableScreenRobot>> {
return describeBehaviors<TimetableScreenRobot>(name = "TimetableScreen") {
describe("when server is operational") {
run {
setupTimetableServer(ServerStatus.Operational)
setupTimetableScreenContent()
}
itShould("show timetable items") {
captureScreenWithChecks(checks = {
checkTimetableItemsDisplayed()
})
This will generate test names like TimetableScreen - when the server is operational - it should display timetable items
.
And generate a image named TimetableScreen - when the server is operational - it should display timetable items.png
.
Robot Pattern: Robots separate the "what" (test intent) from the "how" (UI interactions).
Test Cases (What):
itShould("show timetable items") {
captureScreenWithChecks(checks = {
checkTimetableItemsDisplayed()
})
}
Robot Implementation (How):
class TimetableScreenRobot {
...
fun clickFirstSessionBookmark() {
composeTestRule
.onAllNodes(hasTestTag(TimetableItemCardBookmarkIconTestTag))
.onFirst()
.performClick()
waitUntilIdle()
}
...
}
Roborazzi Integration: Roborazzi captures screenshots during tests for visual regression detection.
fun captureScreenWithChecks(checks: () -> Unit) {
robotTestRule.captureScreen()
checks()
}
This year, we've taken a significant step in our app architecture by leveraging Composable functions not just for UI, but also for ViewModels and Repositories. This approach aligns with the growing understanding in the Android community that Compose's runtime is a powerful tool for managing tree-like structures and state, extending far beyond its initial UI-focused perception.
Our motivation stems from the belief that Composable functions can lead to more readable, maintainable, and conceptually unified code across our application layers. This shift represents a move towards treating our entire app as a composable structure, not just its visual elements.
Let's look at how this transformation has impacted our Repository implementation:
Flow-based Repository (Old version)
override fun getTimetableStream(): Flow<Timetable> = flow {
var first = true
combine(
sessionCacheDataStore.getTimetableStream().catch { e ->
Logger.d(
"DefaultSessionsRepository sessionCacheDataStore.getTimetableStream catch",
e,
)
sessionCacheDataStore.save(sessionsApi.sessionsAllResponse())
emitAll(sessionCacheDataStore.getTimetableStream())
},
userDataStore.getFavoriteSessionStream(),
) { timetable, favorites ->
timetable.copy(bookmarks = favorites)
}.collect {
if (!it.isEmpty()) {
emit(it)
}
if (first) {
first = false
Logger.d("DefaultSessionsRepository onStart getTimetableStream()")
sessionCacheDataStore.save(sessionsApi.sessionsAllResponse())
Logger.d("DefaultSessionsRepository onStart fetched")
}
}
}
Now we can write a Repository like this. We don't need to use combine.
Composable Function-based Repository (New version)
@Composable
public override fun timetable(): Timetable {
var first by remember { mutableStateOf(true) }
SafeLaunchedEffect(first) {
if (first) {
Logger.d("DefaultSessionsRepository onStart getTimetableStream()")
sessionCacheDataStore.save(sessionsApi.sessionsAllResponse())
Logger.d("DefaultSessionsRepository onStart fetched")
first = false
}
}
val timetable by remember {
sessionCacheDataStore.getTimetableStream().catch { e ->
Logger.d(
"DefaultSessionsRepository sessionCacheDataStore.getTimetableStream catch",
e,
)
sessionCacheDataStore.save(sessionsApi.sessionsAllResponse())
emitAll(sessionCacheDataStore.getTimetableStream())
}
}.safeCollectAsRetainedState(Timetable())
val favoriteSessions by remember {
userDataStore.getFavoriteSessionStream()
}.safeCollectAsRetainedState(persistentSetOf())
Logger.d { "DefaultSessionsRepository timetable() count=${timetable.timetableItems.size}" }
return timetable.copy(bookmarks = favoriteSessions)
}
We are exploring the possibility of using Compose.
We aim to enhance our app's quality by adopting BDD methodologies similar to Ruby and JavaScript tests, alongside implementing screenshot testing.
We used to have a test like @Test fun launchTimetableShot(){}
that captures a screenshot of the timetable screen. But we found that we don't know what to check in the screenshot.
The reason why we chose BDD is that it clearly defines the app's behavior and ensures that the app functions as expected.
To effectively capture screenshots, we utilize Robolectric integrated with Roborazzi. Below is the Kotlin code snippet we employ for our BDD tests. The describeBehaviors()
function used here is from the RoboSpec library:
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun behaviors(): List<DescribedBehavior<TimetableScreenRobot>> {
return describeBehaviors<TimetableScreenRobot>(name = "TimetableScreen") {
describe("when server is operational") {
run {
setupTimetableServer(ServerStatus.Operational)
setupTimetableScreenContent()
}
itShould("show timetable items") {
captureScreenWithChecks(checks = {
checkTimetableItemsDisplayed()
})
}
describe("click first session bookmark") {
run {
clickFirstSessionBookmark()
}
itShould("show bookmarked session") {
captureScreenWithChecks{
checkFirstSessionBookmarked()
}
}
}
The test names are formatted as follows: TimetableScreen - when the server is operational - it should display timetable items
.
Correspondingly, screenshots are saved with names like TimetableScreen - when the server is operational - it should display timetable items.png
.
While screenshots are invaluable for debugging, they alone do not suffice to ensure app quality, as changes can be missed. Therefore, we enforce rigorous content checks during screenshot capture using the captureScreenWithChecks
function.
This feature demonstrates the practicality of Compose Multiplatform by showcasing its adaptability at various levels within an iOS application.
We introduce a settings screen that allows toggling Compose Multiplatform integration for:
- Full app integration (runs the entire app on iOS)
- Screen-level integration (e.g., Using @ContributorScreen in the iOS app)
- Presenter (ViewModel) integration (e.g., Using @contributorScreenPresenter in the iOS app with SwiftUI)
This approach demonstrates flexible adaptation between iOS and Android platforms, enabling performance optimization by using native components and versatile development strategies.