DroidKaigi 2023 will be held from September 14 to September 16, 2023. We are developing its application. Let's develop the app together and make it exciting.
This is a video of an app in development, and it will be updated as needed.
258569310-b30d8912-387c-48cc-8eb3-a4ea4b8ccb21.webm
The app is currently in preparation for release on Google Play and the App Store. In the meantime, you can try the app on DeployGate. Stay tuned for updates!
We always welcome any and all contributions! See CONTRIBUTING.md for more information.
For Japanese speakers, please see CONTRIBUTING.ja.md.
Stable Android Studio Giraffe or higher. You can download it from this page.
You can check out the design on Figma.
https://www.figma.com/file/MbElhCEnjqnuodmvwabh9K/DroidKaigi-2023-App-UI
In addition to general Android practices, we are exploring and implementing various concepts. Details for each are discussed further in this README.
We are adopting the module separation approach used in Now in Android, such as splitting into 'feature' and 'core' modules. We've added experimental support for Compose Multiplatform on certain screens, making the features accessible from the iOS app module as well."
Composable functions are categorized into three types: Screen, Section, and Component. This categorization does not have a definitive rule, but it serves as a guide for better structure and improved readability.
sessions
├── TimetableScreen.kt
│ ├── TimetableScreenUiState
│ └── TimetableScreen
├── TimetableScreenViewModel.kt
├── component
│ └── TimetableListItem.kt
└── section
├── TimetableContent.kt
│ ├── TimetableContentUiState
│ └── TimetableContent
└── TimetableList.kt
├── TimetableListUiState
└── TimetableList
The basic dependency rule is as follows:
Screen -> Section -> Component
For example, TimetableScreen
depends on TimetableContent
and TimetableListItem
.
Also, a Section can depend on other Sections, and components can depend on other components.
Screen
refers to an entire screen within your application.
Both Screen and Section are managed with UiState to handle their individual states, which are created by the ViewModel.
Typically, each ViewModel is directly linked with a single Screen.
data class TimetableScreenUiState(
val contentUiState: TimetableContentUiState,
val isFavoriteFilterChecked: Boolean,
)
@Composable
private fun TimetableScreen(
uiState: TimetableScreenUiState,
...
) {
...
Section
refers to groups of components within screens, like containers including lists, which can dynamically adjust in size or complexity as the needs of the application change. An example could be a TimetableList.
Both Screen and Section are managed with UiState to handle their individual states, which are created by the ViewModel.
data class TimetableListUiState(
val timetableItemMap: PersistentMap<String, List<TimetableItem>>,
val timetable: Timetable,
)
@Composable
fun TimetableList(
uiState: TimetableListUiState,
onBookmarkClick: (TimetableItem) -> Unit,
onTimetableItemClick: (TimetableItem) -> Unit,
modifier: Modifier = Modifier,
) {
...
'Component' refers to the finer units of UI, designed to serve specific roles within the application. While they may not be as dynamic as Sections, they can still vary in their content or appearance to fit the specific needs of the app. Examples include TimetableListItem and TimeText.
Through clear delineation of roles and responsibilities of different composables, this classification assists in enhancing code organization and maintainability.
A Component should not have its own UiState as it could make things overly complicated.
Our application leverages Kotlin Multiplatform to create a flexible and type-safe system for handling multiple languages. This system exhibits the following key characteristics:
-
Language separation: Each language is managed separately within its distinct mapping structure, providing a clean and well-structured layout.
-
Type-safe handling of strings: We leverage Kotlin's sealed classes and enums to represent strings, which are validated at compile-time.
-
Type-safe arguments: The system allows adding arguments to strings in a type-safe manner, supporting dynamic data inclusion within strings like
data class Time(val hours: Int, val minutes: Int)
-
Module-specific management: The system allows managing translations on a per-module basis, enhancing modularity and ease of maintenance.
-
Gradual translation support: Translations can be added gradually, which is beneficial for evolving projects where translations are continuously updated.
-
Assurance of translation completion: Kotlin's
when
helps detect missing translations, ensuring completeness of all language representations.
sealed class SessionsStrings : Strings<SessionsStrings>(Bindings) {
object Timetable : SessionsStrings()
object Hoge : SessionsStrings()
data class Time(val hours: Int, val minutes: Int) : SessionsStrings()
private object Bindings : StringsBindings<SessionsStrings>(
Lang.Japanese to { item, _ ->
when (item) {
Timetable -> "タイムテーブル"
Hoge -> "ホゲ"
is Time -> "${item.hour}時${item.minutes}分"
}
},
Lang.English to { item, bindings ->
when (item) {
Timetable -> "Timetable"
// You can use defaultBinding to use default language's string
Hoge -> bindings.defaultBinding(item, bindings)
is Time -> "${item.hour}:${item.minutes}"
}
},
default = Lang.Japanese
)
}
In the above example, SessionsStrings
is a sealed class that represents different strings. Each string is defined as an object within the sealed class, and the translations are provided in StringsBindings
.
To fetch a string:
println(SessionsStrings.Timetable.asString())
The buildUiState() {} function promotes the Single Source of Truth (SSoT) principle in our application by combining multiple StateFlow objects into a single UI state. This ensures that data is managed and accessed from a single, consistent, and reliable source.
By working with StateFlow objects, the function can also compute initial values, further enhancing the SSOT principle.
Here's an example of using the buildUiState() function:
private val timetableContentUiState: StateFlow<TimetableContentUiState> = buildUiState(
sessionsStateFlow,
filtersStateFlow,
) { sessionTimetable, filters ->
if (sessionTimetable.timetableItems.isEmpty()) {
return@buildUiState TimetableContentUiState.Empty
}
TimetableContentUiState.ListTimetable(
TimetableListUiState(
timetable = sessionTimetable.filtered(filters),
),
)
}
The buildUiState() function combines the data from sessionsStateFlow and filtersStateFlow into a single timetableContentUiState instance. This simplifies state management and ensures that the UI always displays consistent and up-to-date information.
This project runs on GitHub Actions. This year's workflows contain new challenges!
This project is an OSS so we cannot assign write-able tokens to workflow-runs that need the codes of the forked repos. To solve this problem, this project shares artifacts with multiple workflows via artifacts API and use them in safe workflows that have more-powerful permission but consist of safe actions.
This achieves to post comments on forked PRs safely. For example, you can see the results of the visual testing reports even on your PRs! (See Architecture > Testing for the visual testing).
Testing an app involves balancing fidelity, how closely the test resembles actual use, and reliability, the consistency of test results. This year, our goal is to improve both using several methods.
Overview Diagram
Detailed Diagram
Robolectric Native Graphics (RNG) allows us to take app screenshots without needing an emulator or a device. This approach is faster and more reliable than taking device screenshots. While device screenshots may replicate real-world usage slightly more accurately, we believe the benefits of RNG's speed and reliability outweigh this. We use Roborazzi to compare the current app's screenshots to the old ones, allowing us to spot and fix any visual changes.
Screenshot tests are extremely effective as they allow us to spot visual changes without writing many assertions. However, there is a risk of mistakenly using incorrect baseline images.
So, for important features, we should add assertion tests to these parts. The tests will typically look like this:
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@HiltAndroidTest
@Config(
qualifiers = RobolectricDeviceQualifiers.NexusOne
)
class TimetableScreenTest {
@get:Rule
@BindValue val robotTestRule: RobotTestRule = RobotTestRule<MainActivity>(this)
@Inject lateinit var timetableScreenRobot: TimetableScreenRobot
// A screenshot test
@Test
@Category(ScreenshotTests::class)
fun checkLaunchShot() {
timetableScreenRobot {
setupTimetableScreenContent()
checkScreenCapture()
}
}
// An assertion test for an important feature
@Test
fun checkLaunch() {
timetableScreenRobot {
setupTimetableScreenContent()
checkTimetableItemsDisplayed()
}
}
...
}
We use the companion branch approach to store screenshots of feature branches. This method involves saving screenshots to a companion branch whenever a pull request is made, ensuring that we keep only relevant images and reduce the repository size.
- Why not GitHub Actions Artifacts, Git LFS, or Feature Branch Commits?
While GitHub Actions Artifacts and Git LFS could be used for storing screenshots, they don't allow for direct image viewing in pull requests. Committing screenshots directly to the feature branch, on the other hand, can lead to an unnecessary increase in the repository size.
The Testing Robot Pattern simplifies writing UI tests. It splits the test code into two main parts: the 'how to test' portion, handled by the robot class, and the 'what to test' portion, managed by the test class. This separation provides benefits when writing screenshot tests, making the test code more maintainable and easier to understand.
File: TimetableScreenTest.kt
@Test
@Category(ScreenshotTests::class)
fun checkScrollShot() {
timetableScreenRobot {
// Define what functionalities of the screen to test
setupTimetableScreenContent() // Setup the screen with the content
scrollTimetable() // Perform a scrolling action
checkTimetableListCapture() // Validate the visual state by capturing a screenshot
}
}
File: TimetableScreenRobot.kt
// Sets up the content for the Timetable screen
fun setupTimetableScreenContent() {
composeTestRule.setContent {
KaigiTheme {
TimetableScreen(
onSearchClick = { },
onTimetableItemClick = { },
onBookmarkIconClick = { },
)
}
}
waitUntilIdle()
}
// Performs a scrolling action on the Timetable screen
fun scrollTimetable() {
composeTestRule
.onNode(hasTestTag(TimetableScreenTestTag))
.performTouchInput {
swipeUp(
startY = visibleSize.height * 3F / 4,
endY = visibleSize.height / 2F,
)
}
}
// Validates the Timetable screen by capturing a screenshot
fun checkTimetableListCapture() {
composeTestRule
.onNode(hasTestTag(TimetableScreenTestTag))
.captureRoboImage()
}
And now, you can check the scrolled screenshot!
This screenshot testing has been useful in that we can find bugs. For example, we found a bug where the tab was hidden when scrolling.
To ensure stable and comprehensive testing of our app, we opt to fake our API rather than use actual API.
We have also designed our API to manage its own state and to allow us to change its status as needed. For instance, although we're not using it here, we could place an AccessCounter
field inside the Status
class to keep track of how many times the API has been hit. By managing our fake API in this way with Kotlin, we can adapt to changes in the response without having to rewrite the entire application.
interface SessionsApi {
suspend fun timetable(): Timetable
}
class FakeSessionsApi : SessionsApi {
sealed class Status : SessionsApi {
object Operational : Status() {
override suspend fun timetable(): Timetable {
return Timetable.fake()
}
}
object Error : Status() {
override suspend fun timetable(): Timetable {
throw IOException("Fake IO Exception")
}
}
}
private var status: Status = Status.Operational
fun setup(status: Status) {
this.status = status
}
override suspend fun timetable(): Timetable {
return status.timetable()
}
}
We use the FakeSessionsApi
throughout our tests. It's provided by the FakeSessionsApiModule
, which replaces the original SessionsApiModule
during testing.
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [SessionsApiModule::class]
)
class FakeSessionsApiModule {
@Provides
fun provideSessionsApi(): SessionsApi {
return FakeSessionsApi()
}
}
- You need to install the following tools.
- JDK 17
- You can install via SDKMAN,
sdk install $(cat .sdkmanrc | sed -e 's/=/ /')
- Xcode,
.xcode-version
version- You can install via Xcodes
- Ruby,
.ruby-version
version- bundler (you can install by
gem install bundler
orsudo gem install bundler
)
- bundler (you can install by
-
Setup
bundle install
cd app-ios && bundle exec fastlane shared
-
open
DroidKaigi2023.xcodeproj
by Xcode
- You can filter XCFramework arch by
arch
option atlocal.properties
- e.g. if you need only
x86_64
binary, you can setarch=x86_64
- e.g. if you need only
- You can build and debug on Android Studio with KMM plugin
- After install KMM plugin, you can see
app-ios
module on Android Studio's run configurations. - Set configs like below
- Contributors of DroidKaigi 2023 official app
- UI Lead: upon0426
- Build/CI Lead: tomoya0x00
- Designer: nobonobopurin
- Material3 Lead: Nabe
- iOS Lead: ry-itto
- Server / API Lead: ryunen344
- DroidKaigi Co-Organizer / Architecture Lead: takahirom