iwmh’s blog

ただの書き散らし/メモ

Jetpack Composeのnavigation routeにURLを含めるときは、URLEncodeする必要がある

  • 失敗するコード ・NavigationGraph
@Composable
fun MainNavGraph(
        navController: NavHostController,
        modifier: Modifier = Modifier
){
    NavHost(
            navController = navController,
            startDestination = Screen.Home.route,
            modifier = modifier
    ){
        // このrouteにnavigateしたい
        composable(
            route = "${Screen.EpisodeDetail.route}/" +
                    "{" + Constants.nav_episodeName + "}/" +
                    "{" + Constants.nav_imageUrl + "}/" +
                    "{" + Constants.nav_description  + "}/" +
                    "{" + Constants.nav_duration + "}/" +
                    "{" + Constants.nav_releaseDate + "}",
            arguments = listOf(
                navArgument(Constants.nav_episodeName){
                    type = NavType.StringType
                },
                navArgument(Constants.nav_imageUrl){
                    type = NavType.StringType
                },
                navArgument(Constants.nav_description){
                    type = NavType.StringType
                },
                navArgument(Constants.nav_duration){
                    type = NavType.IntType
                },
                navArgument(Constants.nav_releaseDate){
                    type = NavType.StringType
                },
            )
        ){ navBackStackEntry ->
            val episodeName = navBackStackEntry.arguments?.getString(Constants.nav_episodeName)
            val imageUrl = navBackStackEntry.arguments?.getString(Constants.nav_imageUrl)
            val description = navBackStackEntry.arguments?.getString(Constants.nav_description)
            val duration = navBackStackEntry.arguments?.getInt(Constants.nav_duration)
            val releaseDate = navBackStackEntry.arguments?.getString(Constants.nav_releaseDate)
            EpisodeDetailScreen(
                episodeName = episodeName!!,
                imageUrl = imageUrl!!,
                description = description!!,
                duration = duration!!,
                releaseDate = releaseDate!!
            )
        }
    }

}

・遷移

@Composable
fun EpisodesScreen(
    navController: NavController,
    showId: String?
) {
    val viewModel: EpisodesScreenViewModel = hiltViewModel()
    // Hold the Show ID for this screen.
    // TODO: Needs better code here.
    viewModel.showId = showId

    val lazyPagingItems = viewModel.pagingFlow.collectAsLazyPagingItems()

    val isRefreshing by viewModel.isRefreshing.collectAsState()

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = {
            lazyPagingItems.refresh()
        }
    ) {
        Column {
            Text(text = "Episodes")
            Text(text = lazyPagingItems.itemCount.toString())
            LazyColumn {
                items(lazyPagingItems) { episodeItem ->
                    EpisodeCardSquare(
                        episodeName = episodeItem!!.name,
                        imageUrl = episodeItem!!.images[2].url,
                        description = episodeItem!!.description,
                        duration = episodeItem!!.duration_ms,
                        releaseDate = episodeItem!!.release_date,
                        // Navigate to episode-detail screen.
                        onClick = {
                            navController.navigate(
                                "${Screen.EpisodeDetail.route}/" +
                                        "${episodeItem!!.name}/" +
                                        "${episodeItem!!.images[2].url}/" +
                                        "${episodeItem!!.description}/" +
                                        "${episodeItem!!.duration_ms}/" +
                                        "${episodeItem!!.release_date}"
                            )
                        }
                    )
                }
            }
        }
    }
}
  • 修正後コード ・NavigationGraphは変更なし ・遷移元
@Composable
fun EpisodesScreen(
    navController: NavController,
    showId: String?
) {
    val viewModel: EpisodesScreenViewModel = hiltViewModel()
    // Hold the Show ID for this screen.
    // TODO: Needs better code here.
    viewModel.showId = showId

    val lazyPagingItems = viewModel.pagingFlow.collectAsLazyPagingItems()

    val isRefreshing by viewModel.isRefreshing.collectAsState()

    SwipeRefresh(
        state = rememberSwipeRefreshState(isRefreshing),
        onRefresh = {
            lazyPagingItems.refresh()
        }
    ) {
        Column {
            Text(text = "Episodes")
            Text(text = lazyPagingItems.itemCount.toString())
            LazyColumn {
                items(lazyPagingItems) { episodeItem ->
                    EpisodeCardSquare(
                        episodeName = episodeItem!!.name,
                        imageUrl = episodeItem!!.images[2].url,
                        description = episodeItem!!.description,
                        duration = episodeItem!!.duration_ms,
                        releaseDate = episodeItem!!.release_date,
                        // Navigate to episode-detail screen.
                        onClick = {
                        // URLが含まれる情報をURLEncodeしてから、navController.navitateに渡す。 
                            val encodedImageUrl
                                    = URLEncoder.encode(episodeItem!!.images[2].url, StandardCharsets.UTF_8.toString())
                            val encodedDescription
                                    = URLEncoder.encode(episodeItem!!.description, StandardCharsets.UTF_8.toString())
                            navController.navigate(
                                "${Screen.EpisodeDetail.route}/" +
                                        "${episodeItem!!.name}/" +
                                        "${encodedImageUrl}/" +
                                        "${encodedDescription}/" +
                                        "${episodeItem!!.duration_ms}/" +
                                        "${episodeItem!!.release_date}"
                            )
                        }
                    )
                }
            }
        }
    }
}

OkHttpClientのresponse.body?.string()が、try{}で囲まれると失敗する

  • 失敗するコード
class WebApiClientImpl @Inject constructor(
    private val injectableConstants: InjectableConstants,
    private val authStateManager: AuthStateManager,
    private val okHttpClient: OkHttpClient,
    private val gson: Gson
) : WebApiClient {

    // Get User's Saved Shows
    override suspend fun getUsersSavedShows(): PagingObject<ItemShow> {

        val url = injectableConstants.baseUrl + "/me/shows";

        // Make a request to API endpoint.
        val request = Request.Builder()
            .url(url)
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer ${authStateManager.authState.accessToken}")
            .build()

        return withContext(Dispatchers.IO) {

            try {
                val response = okHttpClient.newCall(request).execute()
                if (!response.isSuccessful) {
                    throw Exception(response.toString())
                }

                val tokenType = object : TypeToken<PagingObject<ItemShow>>() {}.type
                var respString = response.body?.string()

                gson.fromJson(
                    respString,
                    tokenType
                )
            } catch (e: Exception){
                throw e
            }
        }
    }

}
  • テストコード
@SmallTest
class InstrumentedUnitTest{

    companion object {
        lateinit var localKeyValueStorageImpl: LocalKeyValueStorageImpl
        lateinit var remoteDataSourceImpl: RemoteDataSourceImpl
        lateinit var mainRepository: MainRepository

        lateinit var mockWebServer: MockWebServer

        @BeforeClass
        @JvmStatic
        fun init() {
            // Context of the app under test.
            val context = InstrumentationRegistry.getInstrumentation().targetContext

            val mainKey = MasterKey.Builder(context)
                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                .build()

            // Backup the initial AuthState value.
            val prefs = EncryptedSharedPreferences.create(
                context,
                Constants.shared_prefs_file,
                mainKey,
                EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
                EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
            )

            // Init for storage, remotedatasource and repository.
            val okHttpClient = OkHttpClient()
            localKeyValueStorageImpl = LocalKeyValueStorageImpl(prefs)
            val authStateManager = AuthStateManager(localKeyValueStorageImpl)
            authStateManager.authService = AuthorizationService(context)
            val gson = Gson()
            val injectableConstants = InjectableConstants(
                baseUrl = "http://localhost:8080"
            )
            val webApiClientImpl = WebApiClientImpl(injectableConstants ,authStateManager, okHttpClient, gson)

            remoteDataSourceImpl = RemoteDataSourceImpl(webApiClientImpl, LocalKeyValueStorageImpl(prefs))

            mainRepository = MainRepositoryImpl(remoteDataSourceImpl)

            // mock web server

            mockWebServer = MockWebServer()
            mockWebServer.start(8080)
        }

        @AfterClass
        @JvmStatic
        fun end(){
            mockWebServer.shutdown()
        }

    }

    @Test
    fun get_following_shows() = runBlocking{

        val successResponseData = """
            {
              // JSONデータ。中身省略
              "total": 15
            }
        """

        val successResponse = MockResponse().apply {
            setResponseCode(200)
            setHeader("Content-Type", "application/json")
            setBody(successResponseData)
        }

        mockWebServer.enqueue(successResponse)

        val result = mainRepository.getUsersSavedShows()

        assertEquals(15, result.items.count())
    }
}
  • エラーメッセージ

cannot be cast to java.lang.Void

  • 修正後コード(抜粋)
    // Get User's Saved Shows
    override suspend fun getUsersSavedShows(): PagingObject<ItemShow> {

        val url = injectableConstants.baseUrl + "/me/shows";

        // Make a request to API endpoint.
        val request = Request.Builder()
            .url(url)
            .header("Content-Type", "application/json")
            .header("Authorization", "Bearer ${authStateManager.authState.accessToken}")
            .build()

        return withContext(Dispatchers.IO) {

            val response = okHttpClient.newCall(request).execute()
            if (!response.isSuccessful) {
                throw Exception(response.toString())
            }

            val tokenType = object : TypeToken<PagingObject<ItemShow>>() {}.type
            var respString = response.body?.string()

            gson.fromJson(
                respString,
                tokenType
            )
        }
    }