Skip to content

DroidKaigi/conference-app-2024

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

image

DroidKaigi 2024 official app

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.

Features

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!

image

Try the app

You can try the app on your device by clicking the button below. Try it on your device via DeployGate

Contributing

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をご覧ください。初めての方でも分かりやすいステップバイステップのガイドを用意しています。

Requirements

Stable Android Studio Koala or higher. You can download it from this page.

Design

You can check out the design on Figma.

DroidKaigi 2024 App UI

Designer: nobonobopurin

Development

Overview of the architecture

In addition to general Android practices, we are exploring and implementing various concepts. Details for each are discussed further in this README.

image

Understanding the App's Data Flow

To contribute to the app effectively, understanding its data flow is crucial for comprehending the app's code structure. Let's examine this further.

1. Displaying Sessions on the Timetable Screen

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
image
@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))
        },

2. User Interaction with the Bookmark Button

Here, the interaction of bookmarking a session is detailed, showcasing how events trigger updates within the presenter.

image
      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)
            }
            ...
        }
    }
    ...
}

3. Saving the bookmarked session

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)
    }

4. Recomposing the Repository's Timetable Upon Bookmarking

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.

5. Passing the Updated Timetable to the Presenter

Describes the flow of updated session data back to the screen presenter, highlighting how the UI state is refreshed.

image
                 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
    )
}

6. Displaying the Updated Timetable on the Screen

This final step illustrates how the updated timetable is displayed on the screen, completing the cycle of user interaction and data update. image

          TimetableScreenUiState
timetableScreenPresenter ----> TimetableScreen
@Composable
fun TimetableScreen(
    ...,
    uiState: TimetableScreenUiState = timetableScreenPresenter(
        events = eventFlow,
    ),
) {
    ...
    TimetableScreen(
        uiState = uiState,

How to Check Composable Preview

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.

image

To capture a screenshot of the Composable Preview, run the Roborazzi Gradle task in the RoborazziPreview.

image

After running the task, you should see the screenshot in the RoborazziPreview.

image

Understanding the App's Testing

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.

Key Components

Robolectric: A framework 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's Experimental Challenges

Rewriting Coroutine Flow to Composable Function

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.

Behavior driven development and screenshot testing

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.

Flexible Integration of Compose Multiplatform in iOS Apps

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.