blog

DeNAのエンジニアが考えていることや、担当しているサービスについて情報発信しています

2024.09.27 イベントレポート

Introduction and Solution Explanation for the Jetpack Compose Crossword Quiz

by tsutomu hayakawa

#droidkaigi #mobile #android #jetpackcompose #crossword #english

This is an English translation for the article: Jetpack Compose Crossword Quizの問題紹介&解答解説

Hello, I’m Tsutomu Hayakawa from the Engineering Department!

DeNA participated as a Gold Sponsor at DroidKaigi 2024, held offline from Wednesday, September 11th to Friday, September 13th, 2024.
This year we set up a Sponsor Booth where two activities were held: “Introduction to SWET Team’s Initiatives” and “Jetpack Compose Crossword Quiz”!

Booth

The DeNA Booth

A large number of attendees came to visit our booth, and just like last year, the activities were a huge success: a total of 307 answer sheets were collected just for the “Jetpack Compose Crossword Quiz”!
In this blog, we will introduce the questions and answers that were presented this year for this quiz.

※ The import statements are omitted in the problem descriptions.
※ The answers are provided below the questions and previews. If you would like to challenge yourself, please stop scrolling and try to solve them first!

Question 1

Below are the problem, preview, and answer for Question 1. This problem was created by @shirataki707 .
We collected 53 answers during the event, with a correct answer rate of 83%.

@Composable
fun Question1() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        BusinessCard()
    }
}

@Composable
fun BusinessCard() {
    Row(
        modifier = Modifier
            .size(width = 320.dp, height = 180.dp)
            .□□□□□□(width = 1.dp, color = Color.Black)  // A-2
    ) {
        Box(
            modifier = Modifier
                .background(color = colorResource(id = R.color.orange))
                .size(width = 160.dp, height = 180.dp),
            contentAlignment = Alignment.Center
        ) {
            Image(
                painter = painterResource(id = R.drawable.dena_logo),
                contentDescription = "DeNA Logo",
                modifier = Modifier.size(256.dp)
            )
        }

        Box(
            modifier = Modifier
                .background(Color.White)
                .size(width = 160.dp, height = 180.dp)
        ) {
            Column(
                modifier = Modifier
                    .fillMaxSize(),
                □□□□□□□□□□Alignment = Alignment.Start   // A-1
            ) {
                Text(
                    text = "ドロイド太郎",
                    modifier = Modifier.padding(start = 16.dp, □□□ = 16.dp) // A-4
                )
                Text(text = "Taro Droid", modifier = Modifier.padding(start = 20.dp))
                □□□□□□(modifier = Modifier.weight(1f))  // A-3
                Text(
                    text = "Android事業本部\nKotlin部",
                    style = □□□□□□□□□(fontSize = 12.sp),    // A-5
                    modifier = Modifier.padding(start = 16.dp, bottom = 16.dp),
                )
            }
        }
    }
}

Question1

Question 1 Preview

Crossword and Answers

Question1-Crossword

Question 1 Crossword

Explanation

Since the difficulty level is Easy, basic parts of Jetpack Compose were used to create this problem, adding a DeNA feel to it by reprecating an actual DeNA business card.

Key Points

The problem is about the positioning of Composables, for example margins with Spacer and padding’s directions (start, top, end, bottom).

Reflections

The difficulty level seemed just right. Since it’s a BusinessCard, maybe the Card Composable should have been used.

Question 2

Below are the problem, preview, and answer for Question 2. This problem was created by @shirataki707 .
We collected 38 answers during the event, with a correct answer rate of 13%.

@Composable
fun Question2Page() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        val imageBrush =
            □□□□□□□□□□□(ImageShader(ImageBitmap.imageResource(id = R.drawable.golden_gate_bridge)))   // A-1

        Box(
            modifier = Modifier
                .clip(□□□□□□□□□□□□□Shape(0.dp))     // A-6
                .size(300.dp)
                .□□□□□□□□□□□□□ {                    // A-2
                    clip = true
                    shape = CircleShape
                    □□□□□□□□□□□□ = 30.dp.toPx()     // A-3
                }
                .background(Color(0xFFF06292))
        ) {
            Text(
                text = "Hello\nDroidKaigi 2024",
                style = TextStyle(
                    □□□□□ = imageBrush,             // A-4
                    fontWeight = FontWeight.ExtraBold,
                    fontSize = 36.sp,
                ),
                modifier = Modifier
                    .align(Alignment.Center)
                    .padding(16.dp)
            )
        }

        Box {
            BalloonCord(offset = Offset(-65f, 0f), degrees = -20f)
            BalloonCord(offset = Offset(-20f, 0f), degrees = -10f)
            BalloonCord(offset = Offset(65f, 0f), degrees = 20f)
            BalloonCord(offset = Offset(20f, 0f), degrees = 10f)
        }

        Box(
            modifier = Modifier
                .width(100.dp)
                .height(70.dp)
                .background(Color(0xFF6A4E42))
        )
    }
}

@Composable
fun BalloonCord(offset: Offset, degrees: Float) {
    Box(
        modifier = Modifier
            .offset(x = offset.x.dp)
            .size(width = 6.dp, height = 120.dp)
            .□□□□□□(degrees = degrees)                  // A-5
            .background(color = Color(0xFFD2B48C))
    )
}

Question2

Question 2 Preview

Crossword and Answers

Question2-Crossword

Question 2 Crossword

Key Points

There are two key points:

  • Shape Clipping

    • To clip shapes in Compose, you can use graphicsLayer or its wrapper Modifier.clip. In this problem, both are used.
    • For A-6, although it clips to a rectangle, RoundedCornerShape was intentionally used instead of RectangleShape (This might have made the problem somewhat more complex).
  • Balloon Shape of the Balloon Section

    • The official Android graphics guide was used as a reference to create it.
    • By leveraging graphicsLayer’s translationY, the circle can be moved downwards without changing the boundaries of the Box Composable. This way, by first clipping with a rectangle, the portion that extends out of the rectangle is removed.

Reflections

Cramming various aspects of the image like Brush and clip into the problem might have made the difficulty higher than expected. Additionally, the layout of the balloon section required close inspection of the image and the code, which we realize in hindsight was inappropriate for a normal-level problem.

Question 3

Below are the problem, preview, and answer for Question 3. This problem was created by @emusute1212 .
We collected 31 answers during the event, with a correct answer rate of 35%.

@Composable
fun Question3Page() {
    val progressWidth = 300.dp
    val progressHeight = 20.dp
    val thumbsWidth = 4.dp
    val percentage = 0.6f

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.White),
        contentAlignment = Alignment.Center,
    ) {
        Box(
            modifier = Modifier
                .width(progressWidth)
                .height(progressHeight)
                .background(color = Color.LightGray)
        ) {
            Box(
                modifier = Modifier
                    .□□□□□□□□□□□□(□□□□□□□□ = percentage) // A-6, A-3
                    .fillMaxHeight()
                    .background(color = Color.Cyan)
            )
            Thumbs(
                modifier = Modifier
                    .offset(x = (□□□□□□□□□□□□□ * □□□□□□□□□□) - (□□□□□□□□□□□ / 2)) // A-2, A-4, A-5
                    .□□□□□□□□□□□□□□□(□□□□□□□□□ = true), // A-7, A-1
                width = thumbsWidth,
                height = progressHeight + 4.dp,
            )
        }
    }
}

@Composable
fun Thumbs(
    modifier: Modifier = Modifier,
    width: Dp,
    height: Dp,
) {
    Box(
        modifier = modifier
            .width(width)
            .height(height)
            .blur(2.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded)
            .background(color = Color.Blue)
    )
}

Question3

Question 3 Preview

Crossword and Answers

Question3-Crossword

Question 3 Crossword

Key Points

Recently there was an opportunity for the problem creator to implement a rich progress bar, allowing him to try some Modifiers that he had never used before. This good learning experience was turned into this problem. Specifying a percentage to the fraction argument of fillMaxWidth will make the progress bar length stretches accordingly. Additionally, the blue bar at the end of the progress bar extends beyond the size of the parent element. To express this, the unbounded argument of wrapContentSize is set to true, allowing the blue bar to be drawn beyond the parent element.

Common Mistakes

It seemed that many people considered fillMaxWidth as weight. Also, it seemed that many people were thinking of using requiredHeight instead of wrapContentSize to achieve off-screen drawing.

Reflections

The gradient effect on the blue bar at the end made it a little hard to understand that it extends beyond the screen. There might have been room for improvement with this problem, such as making the overflow more prominent.

Question 4

Below are the problem, preview, and answer for Question 4. This problem was created by @emusute1212 .
We collected 37 answers during the event, with a correct answer rate of 57%.

@Composable
fun Question4() {
    val numbers = listOf(
        listOf(7, 8, 9),
        listOf(4, 5, 6),
        listOf(1, 2, 3),
        listOf(0),
    )

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(color = Color.White),
        contentAlignment = Alignment.Center,
    ) {
        Column(
            modifier = Modifier
                .width(200.dp),
        ) {
            □□□□□□□.forEach { // A-5
                Row(
                    □□□□□□□□□□Arrangement = Arrangement.□□□□□□□□(20.dp), // A-4, A-2
                ) {
                    it.forEach { number ->
                        Text(
                            text = number.toString(),
                            style = □□□□□□□□□( // A-3
                                fontSize = 80.sp,
                                □□□□□□□□□ = TextAlign.Center, // A-1
                            ),
                            modifier = Modifier.weight(1f),
                        )
                    }
                }
            }
        }
    }
}

Question4

Question 4 Preview

Crossword and Answers

Question4-Crossword

Question 4 Crossword

Key Points

This is a sample that arranges numbers in a grid pattern. The key to this problem is how to use two-dimensional arrays and Arrangement. It demonstrates how to arrange items in a grid pattern, by iterating over numbers and its elements with forEach. The spaces between elements are achieved by using Arrangement’s spacedBy.

Common Mistakes

We noticed that most mistakes were made because people were not very familiar with using Arrangement’s spacedBy.

Reflections

A point for reflection is that the problem was made a bit too easy. It might have been better to include at least one more twist or challenging element. Additionally, to make the spacing between characters more visible, it would have been better to add a border to the Text elements.

Question 5

Below are the problem, preview, and answer for Question 5. This problem was created by @shirataki707 .
We collected 35 answers during the event, with a correct answer rate of 88%.

@Composable
fun Question5() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.□□□□□□ // A-4
    ) {
        DroidKunFace()
        DroidKunRightEye()
        DroidKunLeftEye()
        DroidKunRightEar()

        // DroidKunLeftEar
        Box(
            modifier = □□□□□□□□ // A-5
                .□□□□□□(x = (-75).dp, y = (-150).dp)    // A-1
                .size(width = 12.dp, □□□□□□ = 60.dp)    // A-3
                .□□□□□□(degrees = -30f)                 // A-6
                .□□□□□□□□□□(color = Color(0xFFA4CA39))  // A-2
        )
    }
}


Question5

Question 5 Preview

Crossword and Answers

Question5-Crossword

Question 5 Crossword

Explanation

Since the difficulty level is Easy, only the basic parts of Compose were used, and an Android feel was brought by drawing Droid-kun. To reduce the amount of code shown in the problem and to prevent the crossword answers from appearing in multiple places, Composable functions such as DroidKunFace() were defined separately.

Key Points

Instead of using Canvas, which would increase the difficulty level, the character was drawn by using offset and rotate.

Reflections

The part DroidKunLeftEar is named to indicate the ear on the left side of the screen. However, considering it from Droid-kun’s perspective, this would be its right ear. This might have caused some confusion.

Question 6

Below are the problem, preview, and answer for Question 6. This problem was created by @chnotchy .
We collected 46 answers during the event, with a correct answer rate of 56%.

@Composable
fun Question6() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp)
    ) {
        Assets() // display `back.png` and `grad.png`
        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier
                .□□□□□□□□□□□(1f)  // A-3
                .fillMaxSize()
                .clip(□□□□□□□□□□□)  // A-1
        ) {
            □□□□□(  // A-4
                painter = painterResource(id = R.drawable.□□□□),  // A-5
                contentDescription = "icon",
                modifier = Modifier.fillMaxSize()
            )
            □□□□□(  // A-4
                painter = painterResource(id = R.drawable.□□□□),  // A-2
                contentDescription = "icon",
                modifier = Modifier.fillMaxSize()
            )
        }
    }
}

Question6

Question 6 Preview

Crossword and Answers

Question6-Crossword

Question 6 Crossword

Key Points

The key points are as follows:

  • Box display order
  • Circular clip
  • Fixed aspect ratio

First, the basics: in a Box, the Composable defined last will be displayed in front of the one defined before. In this case, back.png needs to be in the background, while grad.png needs to be displayed in the front. Therefore, the first image (A-5) is back, and the next image (A-2) is grad. A-1 refers to the CircleShape used to clip an image into a circular shape. Finally, the aspect ratio of the image is fixed with the rather rarely used aspectRatio (A-3). The hint here is the argument 1f. If this Modifier is not specified, the image will stretch to fill the entire space, so when displayed on a vertically long screen, the image will become vertically long. For further details, please refer to the following document. https://developer.android.com/develop/ui/compose/graphics/images/customize?hl=en#custom-aspect-ratio

Question 7

Below are the problem, preview, and answer for Question 7. This problem was created by @akkiee76 .
We collected 36 answers during the event, with a correct answer rate of 41%.

@Composable
fun Question09() {
    var isPresented by remember { mutableStateOf(true) }
    val bottomSheetState = rememberModalBottomSheetState()

    Column(
        modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.SpaceBetween
    ) {
        Box(
            modifier = Modifier
                .aspectRatio(1f)
                .fillMaxWidth()
        ) {
            MapView()
        }
        Spacer(modifier = Modifier.weight(1f))
    }

    □□□□□□□□□□□□□□□□( // A-2
        on□□□□□□□Request = { isPresented = false }, // A-6
        modifier = Modifier.fillMaxSize(),
        sheetState = bottomSheetState,
        shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
        □□□□□Color = Color.Transparent // A-4
    ) {
        Column(modifier = Modifier.□□□□□□□(horizontal = 8.dp)) { // A-5
            Text(
                text = "Golden Gate Bridge", fontSize = 24.sp
            )
            Text(
                text = "San Francisco, California",
                fontSize = 16.sp,
                color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
            )
            Divider(modifier = Modifier.padding(vertical = 8.dp))
            Text(
                text = "A suspension bridge spanning the Golden Gate Strait connecting San Francisco Bay on the west coast of the United States with the Pacific Ocean."
            )
            Spacer(modifier = Modifier.height(8.dp))
            □□□( // A-8
                modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center
            ) {
                □□□□□( // A-10
                    painter = painterResource(id = R.□□□□□□□□.golden_gate_bridge), // A-7
                    contentDescription = "Golden Gate Bridge",
                    modifier = Modifier.clip(RoundedCornerShape(8.dp)),
                    contentScale = ContentScale.Fit
                )
            }
            Spacer(modifier = Modifier.height(8.dp))
            Text(
                text = "photo by @mvdheuvel on Unsplash", fontSize = 16.sp
            )
        }
    }
}

@Composable
fun MapView() {
    val goldenGateBridge = LatLng(37.8199, -122.4783)
    val cameraPositionState = rememberCameraPositionState {
        position = CameraPosition.fromLatLngZoom(goldenGateBridge, 12.5f)
    }
    □□□□□□□□□( // A-3
        modifier = Modifier.fillMaxSize(), cameraPositionState = cameraPositionState
    ) {
        □□□□□□( // A-1
            state = MarkerState(□□□□□□□□ = goldenGateBridge), // A-9
            title = "GoldenGateBridge",
            snippet = "Marker in GoldenGateBridge"
        )
    }
}

Question7

Question 7 Preview

Crossword and Answers

Question7-Crossword

Question 7 Crossword

Explanation

This problem showcases a typical map application using Google Maps. The answers focus on basic Modifiers. The Google Maps keywords are designed to be easy to figure out even if you have never used Google Maps before, so feel free to try to guess them from the surrounding code!

Question 8

Below are the problem, preview, and answer for Question 8. This problem was created by @akkiee76 .
We collected 31 answers during the event, with a correct answer rate of 58%.

@Composable
fun Question8() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .□□□□□□□□□□(Color(0xFF3DDC84)), // A-3
        □□□□□□□□□□□□□□□□ = Alignment.Center // A-4
    ) {
        Canvas(modifier = Modifier.size(1080f.dp)) {
            drawPath(
                path = shadowPath, brush = Brush.linear□□□□□□□□( // A-1
                    colors = listOf(Color(0x44000000), Color(0x00000000)),
                    start = Offset(429.492f, 495.9793f),
                    end = Offset(858.4757f, 924.963f)
                )
            )
            drawPath(
                path = droidPath, color = Color.White, style = □□□□ // A-5
            )
        }

        Text(
            text = "DroidKaigi 2024",
            modifier = Modifier.□□□□□□(y = 48.dp), // A-2
            color = Color.White,
            style = □□□□□□□□□( // A-6
                fontSize = 36.sp, fontWeight = FontWeight.Bold
            )
        )
    }
}

Question8

Question 8 Preview

Crossword and Answers

Question8-Crossword

Question 8 Crossword

Explanation

This problem is based on a design using Canvas. It features questions on basic Modifiers. A-1 and A-5 are questions about the Modifier for drawPath, so please try them out while looking closely at the preview!

Backup Question

Below are the problem, preview, and answer for a question that we had prepared as a backup. This problem was created by @rokuro.ichihara.
Although there was no opportunity for everyone to attempt this question on-site, feel free to give it a shot now!

@Composable
fun QuestionSpare() {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .□□□□□□□□□□(Color(0xFF3DDC84)), // A-3
        □□□□□□□□□□□□□□□□ = Alignment.Center // A-4
    ) {
        Canvas(modifier = Modifier.size(1080f.dp)) {
            drawPath(
                path = shadowPath, brush = Brush.linear□□□□□□□□( // A-1
                    colors = listOf(Color(0x44000000), Color(0x00000000)),
                    start = Offset(429.492f, 495.9793f),
                    end = Offset(858.4757f, 924.963f)
                )
            )
            drawPath(
                path = droidPath, color = Color.White, style = □□□□ // A-5
            )
        }

        Text(
            text = "DroidKaigi 2024",
            modifier = Modifier.□□□□□□(y = 48.dp), // A-2
            color = Color.White,
            style = □□□□□□□□□( // A-6
                fontSize = 36.sp, fontWeight = FontWeight.Bold
            )
        )
    }
}

Question9

Question9 Preview

Crossword and Answers

Question9-Crossword

Question9-Crossword

Explanation

Since the difficulty level is Easy, this question can be solved with basic knowledge of Compose, by focusing on Compose’s fundamental layouts and Modifier class functions. The UI is composed of a banner using DroidKaigi’s theme color and text quoted from Google’s official site.


This concludes the explanation of the “Jetpack Compose Crossword Quiz”. We are very happy that so many people tried the problems at our booth this DroidKaigi 2024! Some participants came back to try the problems as they changed, resulting in over 300 responses in total. We hope to continue holding booth activities that many people can try and experience in the future. Comments and feedback are welcome and appreciated. Thank you very much for reading to the end!

最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。

recruit

DeNAでは、失敗を恐れず常に挑戦し続けるエンジニアを募集しています。