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