ReactNavigationで他のナビゲーターのスクリーンに移動したり型チェックを行いたい
企画
次のような画面構成でスクリーンを構築したい。
Bottom Tabs Navigator
にスクリーンの代わりにStack Navigator
を2つ配置して、それぞれのStack Navigator
に2つずつスクリーンを配置する感じ。
そして、それぞれのスクリーンから各ナビゲーションのスクリーンに遷移したい。
これを実現するためにReact Navigationの機能を使っていくが、ルート名とパラメータの型チェックを行いたいのでルート名とルートのパラメータのマッピングを持つオブジェクト型を作成する。 そのために、ルートにtypes.tsxを作成する。
作成する型はそれぞれ次の通り
RootStackParamList
Root
のBottom Tab Navigator
を配置するStack Navigator
用の型
RootTabParamList
TabOne
とTabTwo
のStack Navigator
を配置するBottom Tab Navigator
用の型
TabOneStackParamList
Alpha
とBeta
のスクリーンコンポーネントを配置するStack Navigator
用の型
TabTwoStackParamList
Gamma
とDelta
のスクリーンコンポーネントを配置するStack Navigator
用の型
TabOneStackScreenProps
TabOne
のStack Navigator
に配置されるスクリーンコンポーネントのProps
用の型
TabTwoStackScreenProps
TabTwo
のStack Navigator
に配置されるスクリーンコンポーネントのProps
用の型
TL;DR
最終的なプロジェクトはこちらから確認できます。
プロジェクトを作成する
Expo CLIを使ってReact Nativeのプロジェクトを準備する。
今回はテンプレートとしてexpo-template-blank-typescriptを使用する。
アプリ名はexpo-minimum-appとかなんとかにしておく。
expo init --template expo-template-blank-typescript
必要なパッケージのインストール
今回の実装に必要なパッケージは次の通り。
- @react-navigation/bottom-tabs
- @react-navigation/native
- @react-navigation/native-stack
yarn add @react-navigation/bottom-tabs @react-navigation/native-stack
次のパッケージはExpo CLIを使ってインストールする。
- react-native-screens
- react-native-safe-area-context
expo install react-native-screens react-native-safe-area-context
RootStackParamList
ナビゲーター(Stack Navigator
)にネストされたナビゲーター(Bottom Tab Navigator
)に含まれているスクリーンのパラメーターの型を定義するにはNavigatorScreenParams
を使用する。
// types.tsx import { NativeStackScreenProps } from "@react-navigation/native-stack"; ... export type RootStackParamList = { Root: NavigatorScreenParams<RootTabParamList> | undefined; };
RootTabParamList
RootStackParamList
で参照されているRootTabParamList
はBottom Tab Navigator
であり、その内部でそれぞれのタブでStack Navigator
をネストしているので、ここでもNavigatorScreenParams
を使用する。
// types.tsx ... export type RootTabParamList = { TabOne: NavigatorScreenParams<TabOneStackParamList>; TabTwo: NavigatorScreenParams<TabTwoStackParamList>; };
TabOneStackParamListとTabTwoStackParamList
今回実装するスクリーンでは遷移時にパラメーターを必要としないでの、undefined
として定義する。
// types.tsx ... export type TabOneStackParamList = { Alpha: undefined; Beta: undefined; }; export type TabTwoStackParamList = { Gamma: undefined; Delta: undefined; };
TabOneStackScreenProps
TabOne
やTabTwo
のStack Navigator
に配置したスクリーンコンポーネントで受け取ることができるProps
の型を定義するために、CompositeScreenProps
とBottomTabScreenProps
、そしてNativeStackScreenProps
を使用する。
// types.tsx ... export type TabOneStackScreenProps<Screen extends keyof TabOneStackParamList> = CompositeScreenProps< BottomTabScreenProps<TabOneStackParamList, Screen>, CompositeScreenProps< BottomTabScreenProps<RootTabParamList>, NativeStackScreenProps<RootStackParamList> > >; export type TabTwoStackScreenProps<Screen extends keyof TabTwoStackParamList> = CompositeScreenProps< BottomTabScreenProps<TabTwoStackParamList, Screen>, CompositeScreenProps< BottomTabScreenProps<RootTabParamList>, NativeStackScreenProps<RootStackParamList> > >;
CompositeScreenProps
スタックナビゲーターの中にボトムタブナビゲーターがネストされているような状況の時に、複数のナビゲーターからの型を簡単に組み合わせるために使用するのがCompositeScreenProps
である。
最初のパラメーターはプライマリーナビゲーション型とよばれ、このPropsが渡されるスクリーンを管理するナビゲーターの型である。このナビゲーター型をBottomTabScreenProps
などで指定する場合に、BottomTabScreenProps<TabOneStackParamList, Screen>
のようにその第二引数にスクリーン名を渡す必要がある。
2つ目のパラメーターはそのセカンダリーナビゲーション型とよばれ、プリまりナビゲーション型をネストしている親ナビゲーターの型である。
上記のようにStack > Bottom Tab > Stack
のように複数の親ナビゲーターにネストされている場合はセカンダリナビゲーション型もCompositeScreenProps
を使ってネストする必要がある。
スクリーン用のPropsをtypes.tsxでエクスポートする
上記で定義したTabOneStackParamList
やTabTwoStackParamList
を使ってそれぞれのスクリーンのProps
をtypes.tsxで定義してエクスポートする。
... // TabOne Screens export type AlphaScreenProps = TabOneStackScreenProps<"Alpha">; export type BetaScreenProps = TabOneStackScreenProps<"Beta">; // TabTwo Screens export type GammaScreenProps = TabTwoStackScreenProps<"Gamma">; export type DeltaScreenProps = TabTwoStackScreenProps<"Delta">;
実際に使用するときはtypes.tsxからそれぞれの型をインポートして使用する。
// screens/TabOne/AlphaScreen.tsx ... import { AlphaScreenProps as Props } from "../../types"; export function AlphaScreen({ navigation }: Props) { ... }
実際に定義した型を使用してアプリを構築する
まずはnavigation/index.tsxを作成する。React Navigationの機能を使用するには、アプリ全体をNavigationContainer
で一度だけ囲む必要があるので、その処理をここで行う。
// navigation/index.tsx import { NavigationContainer } from '@react-navigation/native'; import { RootNavigator } from './RootNavigator'; export function Navigation() { return ( <NavigationContainer> <RootNavigator /> </NavigationContainer> ) }
Bottom Tabs Navigator
をネストするルートのStack Navigator
をnavigation/RootNavigator.tsxに実装する。
// navigation/RootNavigator.tsx import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { RootStackParamList } from "../types"; import { BottomTabNavigator } from "./BottomTabNavigator"; const Stack = createNativeStackNavigator<RootStackParamList>(); export function RootNavigator() { return ( <Stack.Navigator> <Stack.Screen name="Root" component={BottomTabNavigator} options={{ headerShown: false }} /> </Stack.Navigator> ); }
TabOne
とTabTwo
のStack Navigator
を配置するためのBottom Tabs Navigator
を実装する。
// navigation/BottomTabNavigator.tsx import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { RootTabParamList } from "../types"; import { TabOneNavigator } from "./TabOneNavigator"; import { TabTwoNavigator } from "./TabTwoNavigator"; const BottomTab = createBottomTabNavigator<RootTabParamList>(); export function BottomTabNavigator() { return ( <BottomTab.Navigator initialRouteName="TabOne"> <BottomTab.Screen name="TabOne" component={TabOneNavigator} /> <BottomTab.Screen name="TabTwo" component={TabTwoNavigator} /> </BottomTab.Navigator> ); }
最後にTabOne
とTabTwo
という名前を担うStack Navigator
を実装する。まずはTabOneNavigator
から。
import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { TabOneStackParamList } from "../types"; import { AlphaScreen } from "../screens/TabOne/AlphaScreen"; import { BetaScreen } from "../screens/TabOne/BetaScreen"; const TabOneStack = createNativeStackNavigator<TabOneStackParamList>(); export function TabOneNavigator() { return ( <TabOneStack.Navigator screenOptions={{ headerTitleAlign: "center", }} > <TabOneStack.Screen name="Alpha" component={AlphaScreen} /> <TabOneStack.Screen name="Beta" component={BetaScreen} /> </TabOneStack.Navigator> ) }
次にTabTwoNavigator
を実装する。
import { createNativeStackNavigator } from "@react-navigation/native-stack"; import { TabTwoStackParamList } from "../types"; import { GammaScreen } from "../screens/TabTwo/GammaScreen"; import { DeltaScreen } from "../screens/TabTwo/DeltaScreen"; const TabTwoStack = createNativeStackNavigator<TabTwoStackParamList>(); export function TabTwoNavigator() { return ( <TabTwoStack.Navigator screenOptions={{ headerTitleAlign: "center", }} > <TabTwoStack.Screen name="Gamma" component={GammaScreen} /> <TabTwoStack.Screen name="Delta" component={DeltaScreen} /> </TabTwoStack.Navigator> ); }
スクリーンでは、types.tsxで定義したそれぞれのスクリーン用の型をインポートして使用する。例としてAlphaScreen
コンポーネントを実装してみる。その他のスクリーンも同じようにして実装する。
// screens/TabOne/AlphaScreen.tsx import { useNavigation } from "@react-navigation/native"; import { Box, Center } from "native-base"; import { Button } from "native-base"; import { useCallback } from "react"; import { AlphaScreenProps as Props } from "../../types"; export function AlphaScreen({ navigation }: Props) { const nav = useNavigation(); nav.navigate("Root", { screen: "TabOne", params: { screen: "Alpha", }, }); const handleNavigateToBetaButton = useCallback(() => { navigation.navigate("Beta"); }, []); const handleNavigateToGammaButton = useCallback(() => { navigation.navigate("Root", { screen: "TabTwo", params: { screen: "Gamma", }, }); }, []); const handleNavigateToDeltaButton = useCallback(() => { navigation.navigate("Root", { screen: "TabTwo", params: { screen: "Delta", }, }); }, []); return ( <Box flex={1}> <Center flex={1}> <Button onPress={handleNavigateToBetaButton} mb={4}> TabOneのBetaスクリーンに遷移します </Button> <Button onPress={handleNavigateToGammaButton} colorScheme="secondary" mb={4} > TabTwoのGammaスクリーンに遷移します </Button> <Button onPress={handleNavigateToDeltaButton} colorScheme="amber"> TabTwoのDeltaスクリーンに遷移します </Button> </Center> </Box> ); }
navigation/index.tsxで実装したNavigation
コンポーネントはMain.tsx
のMainコンポーネントでレンダリングする。
// Main.tsx import { StatusBar } from "expo-status-bar"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { Navigation } from "./navigation"; export default function Main() { return ( <SafeAreaProvider> <Navigation /> <StatusBar style="auto" /> </SafeAreaProvider> ); }
そして、エントリーポイントであるApp.tsxでMain.tsxのMainコンポーネントをインポートしてレンダリングするようにする。
// App.tsx import { StatusBar } from "expo-status-bar"; import { SafeAreaProvider } from "react-native-safe-area-context"; import { Navigation } from "./navigation"; export default function Main() { return ( <SafeAreaProvider> <Navigation /> <StatusBar style="auto" /> </SafeAreaProvider> ); }
実際の画面にするとこんな感じ。
AlphaScreen
からTabOneNavigator
のBetaScreen
、そしてTabTwoNavigator
のGammaScreen
、DeltaScreen
に遷移する。
BetaScreen
からTabOneNavigator
のAlphaScreen
、そしてTabTwoNavigator
のGammaScreen
、DeltaScreen
に遷移する。
GammaScreen
からTabOneNavigator
の AlphaScreen
とBetaScreen
、そしてTabTwoNavigator
のDeltaScreen
に遷移する。
DeltaScreen
からTabOneNavigator
の AlphaScreen
とBetaScreen
、そしてTabTwoNavigator
のGammaScreen
に遷移する。
同一ナビゲーターの兄弟スクリーンに遷移する場合はnavigation.navigate
にスクリーン名を渡すだけで良い。
const handleNavigateToBetaButton = useCallback(() => { navigation.navigate("Beta"); }, []);
同じBottom Tabs Navigator
に属する他のStack Navigator
のスクリーンに遷移する場合はscreen
キーとparams
キーを組み合わせてオブジェクトを作成してnavigation.navigate
に渡す。
const handleNavigateToGammaButton = useCallback(() => { navigation.navigate("Root", { screen: "TabTwo", params: { screen: "Gamma", }, }); }, []);
まとめ
初見だと、どの型がどれに対応していて、遷移する場合はどんな引数を渡せば良いか理解しにくいかもしれないので、ぜひこの記事を読みながら少しでも理解の手助けになってくれると嬉しい。
Coroutineを学んでみた
CoroutineはノンブロッキングプログラミングをKotlinで使えるようにした機能。非同期操作を安全に扱える機能などが含まれている。
導入
Coroutineを使うにはkotlinx.coroutines
というパッケージを使わないといけない。
build.gradle.ktsファイルを開いて、dependencies
のブロックにimplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
を追加する。
// build.gradle.kts dependencies { testImplementation(kotlin("test")) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") }
plugins
ブロックにkotlin("jvm") version "1.6.21"
が書かれていることを確認する。
// build.gradle.kts plugins { kotlin("jvm") version "1.6.21" application }
plugins
ブロックで、Kotlinの最新バージョンが使われていることを確認する。
plugins { kotlin("jvm") version "1.6.21" }
ノンブロッキング
Coroutineを利用するにはsuspend
を使ってsuspend関数を定義しないといけない。
そして、coroutineScope
を使用してCoroutineの利用範囲を指定して、内部で、launch()
にラムダを渡す。そのラムダの内部では別のsuspend関数を呼び出すことができる。
main
関数にもsuspend
を付与することを忘れない。
import kotlinx.coroutines.* suspend fun main(args: Array<String>) { coroutineTest() } suspend fun coroutineTest() = coroutineScope { launch { coroutineTest2() } println("Hello World") } suspend fun coroutineTest2() { println("Nice to meet you") }
コルーチンビルダーはcoroutineScope
も併せて3つある。
coroutineScope
- ブロック内で起動したコルーチンが完了するのを待たない
runBlocking
- ブロック内で起動したコルーチンが完了するまで処理を待つ。
withContext
- 指定したコンテキストに実行中の処理を切り替える
runBlocking
を使う際はsuspend
を使用しないで定義する。
import kotlinx.coroutines.* fun main(args: Array<String>) = runBlocking { println("Start Coroutine") launch { println("before 200") delay(200) println("after 200") } println("Finished Coroutine") }
Coroutineの実行に使用されるスレッドをlaunch
に引数を渡すことで指定できる。これによりlaunch
のラムダにあるsuspend
関数をノンブロッキングで実行することができる。
このときに渡す引数のことをディスパッチャーという。ディスパッチャーはDispatchers
を通して参照できる。
Dispatchers.Default
- 指定されない場合に使用されるディスパッチャー
Dispatchers.Main
- OSのメインスレッド
Dispatchers.Unconfined
- 実行時に最初に使用可能なスレッドを割り当てる
Dispatchers.IO
- データベースへのリクエストやファイルへのアクセスなどに使われる
import kotlinx.coroutines.* fun main(args: Array<String>) = runBlocking { println("Start Coroutine") launch(Dispatchers.IO) { println("before 200") delay(200) println("after 200") } println("Finished Coroutine") }
タイムアウトを設定できる。タイムアウトで設定した時間を超えた場合はTimeoutCancellationException
がスローされる。
import kotlinx.coroutines.* fun main(args: Array<String>) = runBlocking { println("Start Coroutine") launch { timeoutTest() } println("Finished Coroutine") } suspend fun timeoutTest() = coroutineScope{ withTimeout(timeMillis = 1_000) { println("before 200") delay(2_000) println("after 200") } }
Job
launch
の戻り値であり、この戻り値を使えばCoroutineを制御できる。
例えば、キャンセルすることもできる。
import kotlinx.coroutines.* fun main(args: Array<String>) = runBlocking { println("Start Coroutine") val job = launch(Dispatchers.IO) { println("before 200") delay(200) println("after 200") } job.cancel() println("Finished Coroutine") }
async
Coroutineで並列処理を行う場合は、async()
を使ってCoroutineを作成する。そして、async()
を使って作成したCoroutineの結果を受け取る場合は戻り値に対してawait()
を呼び出す。await()
を使うことで、スレッドをブロッキングすることなく処理の完了を待つことができる。
import kotlinx.coroutines.* fun main(args: Array<String>) = runBlocking { testAsync() } suspend fun returnNum(num: Int): Int { return num } suspend fun testAsync() = coroutineScope { val a = async { returnNum(5) } val b = async { returnNum(10) } println("a: ${a.await()}") println("b: ${b.await()}") }
ChannelとFlow
スレッド間でデータの受け渡しを行う方法として主にChannel
とFlow
がある。
Channelにおいて、データを送信する場合はsend()
を利用する。受信する場合はreceive()
を利用する。
import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel fun main(args: Array<String>) = runBlocking<Unit> { val channel = Channel<Int>() launch { sendNum(channel, 200) } launch { val result = channel.receive() println(result) } } suspend fun sendNum(ch: Channel<Int>, num: Int): Unit { delay(300) ch.send(num) }
Flowでは、データを送信する場合にemit()
を呼び出す。そして受信する際は、flow.collect
にラムダを渡して、その中でit
を経由して送信されたデータを参照する。
import kotlinx.coroutines.* import kotlinx.coroutines.flow.flow fun main(args: Array<String>) = runBlocking<Unit> { val flow = flow { val result = returnNum(200) emit(result) } flow.collect { println(it) } } suspend fun returnNum(num: Int): Int { delay(300) return num }
Nike Run Clubアプリのチャートコンポーネントぽいものを作ってみる
僕はランニングする時にNRCを愛用しています。
このアプリのグラフをReact Native Chart Kitというライブラリで再現できないか試してみた。
まずはインストールから。
yarn add react-native-chart-kit react-native-svg
実際に実装してみる。今回使用するのはBarChart
コンポーネント。
<BarChart // onDataPointClick={(data) => console.log(data)} data={data} width={screenWidth} height={220} yAxisLabel="" yAxisSuffix="" segments={3} chartConfig={{ backgroundGradientFromOpacity: 0, backgroundGradientToOpacity: 0, barRadius: 5, color: (opacity = 1) => `rgba(240, 157, 77, ${opacity})`, barPercentage: 0.7, fillShadowGradientFromOpacity: 1, fillShadowGradientToOpacity: 1, decimalPlaces: 0, propsForBackgroundLines: { strokeDasharray: 0, stroke: "#e4e3e4", strokeWidth: 1, }, propsForHorizontalLabels: { stroke: "#333333", fontWeight: "lighter", fontSize: "10", }, propsForVerticalLabels: { stroke: "#333333", fontWeight: "lighter", fontSize: "10", }, }} verticalLabelRotation={0} showBarTops={false} fromZero={true} showValuesOnTopOfBars={true} withVerticalLabels={true} withHorizontalLabels={true} />;
僕の実力だとこのくらいまでしかできなかった。横に表示されているラベルを右に表示したかったのだけど、普通には設定できないぽくて断念した。
設定に使用した各プロパティを解説する。
data
labels
は横軸に表示されるラベルの値。datasets[0].data
の値は縦軸の値。labels
とdatasets[0].data
の各要素の値は1対1で対応している。なので、要素数を合わせる必要がある。
{ labels: ["日", "月", "火", "水", "木", "金"], datasets: [ { data: [20, 45, 28, 80, 99, 43], }, ], }
width
グラフ全体が表示される幅の値。この値の取得方法はreact-native
パッケージのDimensions
を使ってDimensions.get("window").width
の値を設定する。
height
チャートの高さ。
yAxisLabel
一番左に表示されるラベルの先頭に付与する値。お金を表すチャートの場合は¥
マークを指定したりすると¥100
みたいに表示される。
yAxisSuffix
一番左に表示されるラベルの末尾に付与する値。割合を表すチャートの場合は%
マークを指定したりすると100%
みたいに表示される。
segments
背景に表示される横線の数を指定する。僕の場合は3
を指定した。
chartConfig
チャートの設定に関するオブジェクト。BarChart
に限らずいろいろなコンポーネントで共通の設定オブジェクト。
backgroundGradientFromOpacity
グラフの背景色における線形グラデーションの最初の色の不透明度をnumber
で指定する。0
から1
までを指定できる。1
に近い値を指定するほど黒に近い色になる。逆に0
に近い値を指定するほど白に近い色になる。
backgroundGradientToOpacity
グラフの背景色における線形グラデーションの2番目の色の不透明度をnumber
で指定する。0
から1
までを指定できる。1
に近い値を指定するほど黒に近い色になる。逆に0
に近い値を指定するほど白に近い色になる。
barRadius
各バーの半径をnumber
で指定する。丸みを追加できる。
color
opacity: number
を引数で受け取る関数を指定する。(opacity = 1) => `rgba(240, 157, 77, ${opacity})`,
のように文字列で作成したrgba()
を返す。
barPercentage
表示されるバーの横幅を0
から1
のnumber
で指定する。1
に近づくほど太くなり、0
に近づくほど細くなる。
fillShadowGradientFromOpacity
バーのの線形グラデーションにおける最初の色(一番上からまんなかくらいまで)の不透明度をnumber
で定義する。0
を指定すると無色透明になり、1
を指定するとはっきりと色が表示される。
fillShadowGradientToOpacity
バーのの線形グラデーションにおける2番目の色(真ん中くらいから一番下まで)の不透明度をnumber
で定義する。0
を指定すると無色透明になり、1
を指定するとはっきりと色が表示される。
decimalPlaces
縦軸の値を、小数点第何位まで表示するかをnumber
で指定する。1
を指定すると小数点第1位まで表示する。0
を指定すると小数点以降を表示しない。
propsForBackgroundLines
背景に表示されている線のスタイルを指定する。react-native-svg
のLine
コンポーネントに指定するプロパティと同じ内容を指定できる。
https://github.com/react-native-svg/react-native-svg#line
strokeDasharray
破線に表示される線の間隔をnumber
で指定する。0
を指定すると破線ではなく直線を表現できる。
SVGのプロパティを理解してアニメーションさせてみよう | 株式会社LIG(リグ)|コンサルティング・システム開発・Web制作
stroke
破線の色をstring
で16進数表記で指定できる。
strokeWidth
破線の幅をnumber
で指定できる。
propsForHorizontalLabels
一番左に表示されるラベルのスタイルを設定できる。react-native-svg
のText
コンポーネントに設定できるプロパティの値と同じ値を指定できる。
https://github.com/react-native-svg/react-native-svg#text
stroke
テキストの色をstring
で16進数表記で指定できる。
fontWeight
フォントの幅をstring
で指定できる。lighter
やbold
などを指定できる。
fontSize
フォントのサイズをnumbder
で指定できる。
propsForVerticalLabels
一番下に表示されるラベルのスタイルを設定できる。react-native-svg
のText
コンポーネントに設定できるプロパティの値と同じ値を指定できる。
https://github.com/react-native-svg/react-native-svg#text
stroke
テキストの色をstring
で16進数表記で指定できる。
fontWeight
フォントの幅をstringで指定できる。lighterやboldなどを指定できる。
fontSize
フォントのサイズをnumbderで指定できる。
verticalLabelRotation
一番下に表示されているラベルの角度をnumbder
で指定できる。正の値を指定すると右に回転する。負の値を指定すると左に回転する。
showBarTops
バーの一番上に線を表示するかどうかをboolean
で指定する。
fromZero
表示するチャートの最小値を0にするかどうかをboolean
で指定する。
showValuesOnTopOfBars
バーの一番上にそのバーの値を表示するかどうかをboolean
で指定する。
withVerticalLabels
一番下に表示されているラベルを表示するかどうかをboolean
で指定する。
withHorizontalLabels
一番左に表示されているラベルを表示するかどうかをboolean
で指定する。
Kotlinはじめてみた
積読していたKotlinの入門本をちょっとずつ読んでみる。
基礎からわかる Kotlin | 富田健二 | 工学 | Kindleストア | Amazon
環境構築
環境設定から始める。
JetBrainsの公式サイトからIntelliJ IDEAをダウンロードする。
ちなみに、僕の場合はCommunity版のApple Siliconバージョンをダウンロードした。
ダウンロード IntelliJ IDEA: JetBrains の人間工学に基づく高機能 Java IDE
プロジェクトを作成する
IntelliJ IDEAでNew Projectをクリックする。
各種設定を行って、Createボタンをクリックする。
変数
var
は再代入可能な変数を宣言するときに使う。
var text: String = "Hello" text = "World"
val
は再代入不可能な変数を宣言するときに使う。
val text: String = "Hello World"
const
は定数を宣言するのに使う。注意点として、トップレベルでの宣言かオブジェクト宣言であり基本型の定数を宣言するときのみ使用可能になる。
なのでList
型はconst
で宣言できない。
const val text: String = "Hello World"
基本型
Boolean
型true
false
Char
型- 単一の文字を表す。
- シングルクォートで囲む必要がある。
String
型- 文字列を扱う。
- ダブルクォーテーションで囲む。
- トリプルクォーテーションで囲むと複数行の文字列に対応する。これを使うときは
.trimMargin()
を利用する。 - 文字列テンプレート(
"${x + y}")
)を使うことで式や変数を文字列に展開できる。
Byte
型- 8ビット
Short
型- 16ビット
Int
型- 32ビット
Long
型- 64ビット
Float
型- 32ビット
Double
型- 64ビット
UByte
型- 符号なし8ビット
UShort
型- 符号なし16ビット
UInt
型- 符号なし32ビット
ULong
型- 符号なし64ビット
Any
型- すべてのクラスの親クラス
- 特定の値の型チェックを無効にしてコンパイルを通すために利用される。
- 極力使用しないようにする。
Unit
型- 型がまったくないことを表す。
- 何も返さない関数の返り値の型として利用される。
fun a(text: String): Unit {}
Nothing
型- すべてのクラスのサブクラス
- 存在しない値を示す。つまりインスタンスを生成できない。
- 例外をスローする関数の返り値の型として利用される。なのでNull許容型を使わずにそういった関数を実装できる。
数値型のTips
0x
や0b
をつかって16進数やバイナリリテラルを表現できる。
val a: Int = 0xAAAAAA val b: Int = 0b1111
_
を使うことで桁の多い数字も読みやすく表現できる。
val a: Int = 9_999 // 9999
符号なしをリテラルで表現する場合は末尾にU
をつける。
val a: UInt = 100U
Null許容型
Null許容型を宣言するには普段の変数んの宣言の時に型名の横に?
を付与する。
var a: String? = "Hello World"
そして、Null許容型にアクセスする際は?
を使って連結することでチェーン呼び出しができる。
println(a?.length)
標準ライブラリのrequire()
やcheck()
を使用すると、事前にNullでないことを確認できる。
require(a != null) check(a != null)
他にもエルビス演算子(?:
)を利用することで、Null
の場合に何かをするなどの指定ができる。
// aがnullの場合に出力する a ?: println("Nullです")
列挙型
列挙型はenum
を使って宣言する。
enum class Fruit { Apple, Orange, Banana, }
列挙型でordinal
プロパティを利用すると0から始まる序数を取得できる。name
プロパティを利用すると、その列挙型の名前を取得できる。
Kotlinには三項演算子がない
Kotlinには三項演算子がないので代わりにif (bar) a else b
のような式を利用する。
範囲の演算子
1..10
を使用すると1から10までの範囲を取得できる。使い道としてはin
イテレーターと一緒に利用する。
var total = 0 for (i in 1..10) { total += i } println(total)
比較演算子の違い
==
は内容が一致している場合にtrue
を返す。
===
は二つのオブジェクトが同じ参照を指していればtrue
を返す。
if式を代入する
if
は式なので、結果を変数に代入できる。
var a = if (true) { return 1 } else { return 0 }
when式とは
when
式は、条件が満たされるまですでの条件を順次照合する。switch
文みたいに使える。
if
式と同じように代入できる。
条件式の中で、変数に代入もできる。(キャプチャ機能)
すべての条件に合致しない場合のelse -> {}
も使用できる。列挙型を利用する場合は必要ない。
when(val fruit = Fruit.Apple) { Fruit.Apple -> { println("Appleです") } Fruit.Orange -> { println("Orangeです") } Fruit.Banana -> { println("Bananaです") } }
forの使い方
Kotlinのfor
文はイテレーター(in
)を用いて反復処理を行う。
for (i in 1..10) { println(i) }
example@ for
のようにラベルを利用できる。break@example
を呼び出すことで、example@
ラベルがついたfor
を抜け出すことができるようになる。
コレクション
List
は変更不可能なコレクション。
val list = listOf('a', 'b', 'c')
MutableList
は変更可能なコレクション。
val mutableList = mutableListOf('a', 'b', 'c') mutableList += 'd'
vararg
を使うと動的にサイズを変更できる配列を関数の引数として宣言できる。
fun example(vararg list: String) { for (t in list) { println(t) } } example('a', 'b', 'c') example('a', 'b', 'c', 'd')
スプレッド演算子(*
)を使うとコレクションを展開できる。
var list = mutableListOf('a', 'b', 'c') var list2 = listOf('d', 'e', 'f') var list3 = listOf(*list.toTypedArray(), *list2.toTypedArray()) println(list3)```
Set
Setは重複を防ぐことができる順序なしのコレクションである。
setOf
とmutableSetOf
でSetを生成できる。
Map
Mapはキーと値のペアのコレクションである。
mapOf
とmutableMap
で生成できる。
var map = mapOf( "name" to "Tanaka Taro", "age" to 14, ) for (element in map) { println("${element.key} : ${element.value}") }
配列
配列はarrayOf
で生成できる。
Listは複数のオブジェクトを内部で保持するが、配列は参照型なのでアドレスを保持する。
なので、arrayOf
で生成した値を出力するとアドレスが表示される。
var array = arrayOf('a', 'b', 'c') println(array) // [Ljava.lang.Character;@1e643faf
Sequence
Listと違ってmap
やfilter
を利用した場合に、順序的ではなく垂直的に評価される。
つまり、Sequenceに対してmap
とfilter
がチェーン呼出されていたら、最初の要素のみがmap
とfilter
で処理された後に次の要素がmap
とfilter
で処理されるといった流れを繰り返す。
sequenceOf
を使って生成できる。
関数
fun
を使って宣言する。
引数がある場合は必ず型を指定する。戻り値がある場合は必ず型を定義する。戻り値がなく、戻り値の型を指定しない場合はUnit
型になる。
関数名はcamelCaseを使用する。
fun main(args: Array<String>) { println(example(5)) } fun example(a: Int): String { return "hello world ${a}"; }
名前付き引数を使用することもできる。引数名 = 値
で引数を渡す。
fun main(args: Array<String>) { println(example(a = 5)) } fun example(a: Int): String { return "hello world ${a}"; }
デフォルト引数も利用できる。
fun main(args: Array<String>) { println(example()) } fun example(a: Int = 10): String { return "hello world ${a}"; }
クラス
クラスはclass
を使って宣言する。
class Fruit {}
すべてのクラスはAny
を継承している。そしてクラスはデフォルトでfinal
宣言になっているので、継承することはできないが、open
を使用することで継承できるようになる。
public open class Fruit { public open fun print(text: String) { println(text) } }
コンストラクタはconstructor
を使用する。
class クラス名
の横の(
と)
の間がプリマリーコンストラクタ。
fun main(args: Array<String>) { var user = User(text = "僕は田中太郎") println(user.text) } class User constructor(val text: String = "") {}
省略もできる。
fun main(args: Array<String>) { var user = User(text = "僕は田中太郎") println(user.text) } class User(val text: String = "") {}
プリマリーコンストラクタ以外に宣言されたconstructor
はセカンダリーコンストラクタで、this()
を使用してプリマリーコンストラクタを継承する必要がある。
イニシャライザはinit
を使ってブロック宣言する。コンストラクタよりも前に呼び出される。
プロパティはvar
とval
を使って宣言する。必ず初期化しないといけない。get()
とset()
を用いてゲッターとセッターをカスタマイズすることができる。もちろん使わなくても良い。val
で宣言したプロパティはset()
を使うことはできない。ゲッターとセッターの対象の値にはfield
を使ってアクセスする。
fun main(args: Array<String>) { var user = User() println(user.id) println(user.name) user.name = "田中太郎" println(user.name) } class User { val id: String // 変える必要がないのでvalで宣言している get() { return "[${field}]" } var name :String // 変える可能性も考えてvarで宣言している get() { return "${field}さん" } set(value) { field = value } init { this.name = "anonymous" this.id = "ランダムな値" } constructor() { } constructor(name: String): this() { this.name = name; // valで宣言しているからidの値を変更することはできない // this.id = 'test'; } }
インナークラスはinner
を使って宣言する。メリットはPhone.Android
のように外部クラスを名前空間として利用できる。そして、内部クラスから外部クラスのプロパティにアクセスできるようになる。その逆はできない。
fun main(args: Array<String>) { var b = A().B() b.print() } class A { var name = "tanaka taro" inner class B { var middleName = "grape" fun print() { println("${name} ${middleName}") } } }
クラスの代わりにobject
を利用してシングルトンクラスを生成できる。
companion object
をクラス内部で使用することでクラスの中にオブジェクトを配置することもできる。
fun main(args: Array<String>) { Obj.print() } class Obj { companion object { const val TEXT = "Hello World" fun print() = println(TEXT) } }
abstract
を使って抽象クラスを宣言できる。抽象クラスを実装するクラスはoverride
を使って中身を定義できる。
fun main(args: Array<String>) { val user = User() println(user.name) } abstract class Human { abstract val name: String } class User : Human() { override val name = "Hello World" }
データクラス
data class
を使って宣言できる。
equals()
、hashCode()
、toString()
が自動的に宣言したクラスに追加される。data class
を使って宣言したクラスはopen
を使用できないので継承できない。
fun main(args: Array<String>) { val a = DataClass(text = "Data Class Hello World") println(a.text) } data class DataClass constructor(val text: String = "") {}
シールドクラス
sealed class
を使って宣言できる。列挙型のような使い方ができる。階層関係を表現できる。
when
と一緒に活用できる。
fun main(args: Array<String>) { var feeling = Feeling.Happy(50) Feel(feeling) var feeling2 = Feeling.Sad(20, "色々あったから") Feel(feeling2) } fun Feel(feeling: Feeling) { when (feeling) { is Feeling.Happy -> { println("あなたはハッピーです") } is Feeling.Sad -> { println("あなたは悲しいです") } } } sealed class Feeling { data class Happy(val score: Int) : Feeling() data class Sad(val score: Int, val reason: String) : Feeling() }
インターフェース
interface
を使って宣言できる。インターフェースを使う場合は:
を使って実装クラスを定義できる。
デフォルト実装は保持するが、そのほかの実装を保持しない。
setter
を宣言することはできない。getter
のみ宣言できる。
fun main(args: Array<String>) { val value = B() value.print("Nice to meet you") } interface A { val text: String get() = "Hello World" fun print(newText: String) = if (newText != "") { println(newText) } else { println(text) } } class B : A {}
SAMインターフェース
メソッドを1つしか持たない抽象クラス。fun interface
を使って宣言する。
デフォルト実装は利用できない。
インターフェースを呼び出す際にラムダを使って1つしかないメソッドの中身を実装できる。
fun main(args: Array<String>) { val sam = Sam { println (it) } sam.print("HELLO") } fun interface Sam { fun print(text: String): Unit }
継承
継承元になるクラスを作成する際はopen class
を使ってクラスを定義する。理由はクラスはデフォルトでfinal
が付与されているから。そして継承元のクラスのメソッドを再定義する場合はoverride fun メソッド名
を使用するが、継承元のクラス側でopen fun メソッド名
のようにopen
を付与しておかないといけない。
あと継承する際は継承先クラス名 : 継承元クラス名()
のように継承元クラスのコンストラクタを呼び出してあげないといけない。
fun main(args: Array<String>) { val value = B() value.print("Taro") } open class A { open fun print(text: String): Unit { println(text) } } class B : A() { override fun print(text: String) { println("こんにちわ、${text}") } }
抽象クラスを宣言する場合は、open
はいらない。そのままabstract class クラス名
とabstract fun メソッド名
を利用する。
fun main(args: Array<String>) { val value = B() value.print("Taro") } abstract class A { abstract fun print(text: String): Unit } class B : A() { override fun print(text: String) { println("こんにちわ、${text}") } }
例外処理
例外はtry-catch
を使う。すべての例外はThrowable
を基本クラスとしている。
例外を明示的にスローする際はthrow Exception("message")
を使用する。
fun main(args: Array<String>) { try { throw Exception("エラーが発生しました") } catch (e: Exception) { println(e.message) } }
スマートキャスト
変数x
がある特定の型もしくはインターフェースを含んでいるかを確認する場合はis
を使用する。
強制的に型キャストする場合はas
を使用する。
fun main(args: Array<String>) { val x: Any = 'z' if (x is Int) { // このスコープではxはInt型としてコンパイラに認識される println(x.toString()) } // これは失敗する val y: String = x as String; println (y.length) }
スコープ関数
変数x
に対してx.let { it * 10 }
だったり、x.run { this * 2 }
みたいな使い方ができる。
ちなみにit
とthis
はコンテキストオブジェクトと言われていて、x
そのものを表す。
5種類スコープ関数が存在していて、それぞれでコンテキストオブジェクトがit
だったりthis
だったりと違う。ちなみに返り値もラムダ自体の実行結果が返り値になるのか、更新されたコンテキストオブジェクト自体になるのかなどさまざま。
let
- it
- ラムダの実行結果
- 拡張関数
run
- this
- ラムダの実行結果
- 拡張関数
with
- this
- ラムダの実行結果
- 拡張関数じゃない
apply
- this
- コンテキストオブジェクト自体
- 拡張関数
also
- it
- コンテキストオブジェクト自体
- 拡張関数
fun main(args: Array<String>) { val text = "Hello World" val result = text.let { // itはtext変数を表す println(it.length) "${it}. Mr. Tanaka" } println(result) }
ちなみに、this
はレシーバーで、it
はラムダパラメーターという扱い。
無名関数
関数名をつけないで関数を宣言できる。
fun main(args: Array<String>) { val anonFun = fun(text: String) { println(text) } anonFun("Hello World") }
ラムダ
ラムダを宣言するときはfun
は不要。
ブロック内でreturn
を使えない。なので、返したい値をそのまま最後に置いておく。
ちなみに、名前付き引数も使えない。
fun main(args: Array<String>) { val lambda = { text: String -> println (text) text } val result = lambda("Hello World") println (result) }
関数の引数でラムダを受け取りたい場合は、最後に関数型の引数を定義してあげると呼び出し側でラムダを渡せる。
fun main(args: Array<String>) { withLambda() { text: String -> text } } fun withLambda(action: (String) -> String) { println("関数を実行します") println("ラムダを実行します") val result = action("ラムダ実行中") println(result) println("ラムダの実行が完了しました") println("関数を終了します") }
クロージャー
外部のスコープにある変数にアクセスできるラムダをクロージャーという。 クロージャーは関数のインスタンスが再生成されるが、ラムダや無名関数の場合は再利用される。
fun main(args: Array<String>) { val outerText = "Hello World" withLambda() { text: String -> "${text} _ ${outerText}" } } fun withLambda(action: (String) -> String) { println("関数を実行します") println("ラムダを実行します") val result = action("ラムダ実行中") println(result) println("ラムダの実行が完了しました") println("関数を終了します") }
ジェネリクス
ジェネリクス<T>
は型パラメーターを作成できる機能。
fun main(args: Array<String>) { val instance = A("Text") println(instance.getValue()) } class A<T>(private val value: T) { fun getValue(): T { return this.value } }
out
で共変(サブタイプの関係)を表せる。in
で反変(スーパータイプの関係)を表せる。何もつけない場合は不変(サブタイプの関係性がない)を表せる。
どういうことかというと、in T
と型パラメーターを渡してT
にParent
クラスを渡した場合、Child
クラスをT
に渡したクラスを代入することができない。
しかし、Parent
クラスの継承元のGrandParent
クラスをT
に渡したクラスは代入することができる。
これが反変という意味。
fun main(args: Array<String>) { // これはエラー val parentInstance: A<Parent> = A<Child>(Child()) // これは成功 val grandParentInstance: A<Parent> = A<GrandParent>(GrandParent()) } data class A<in T>( private val value: T, ) open class GrandParent() {} open class Parent: GrandParent() {} class Child : Parent() {}
共変はその逆。
fun main(args: Array<String>) { // これは成功 val parentInstance: A<Parent> = A<Child>(Child()) // これはエラー val grandParentInstance: A<Parent> = A<GrandParent>(GrandParent()) } data class A<out T>( private val value: T, ) open class GrandParent() {} open class Parent: GrandParent() {} class Child : Parent() {}
不変の場合はそもそもParent
クラスとChild
クラスにサブクラスの関係性があったとしても代入できない。その逆も然り。
fun main(args: Array<String>) { // これはエラー val parentInstance: A<Parent> = A<Child>(Child()) // これもエラー val grandParentInstance: A<Parent> = A<GrandParent>(GrandParent()) } data class A<T>( private val value: T, ) open class GrandParent() {} open class Parent: GrandParent() {} class Child : Parent() {}
アクセス修飾子
public
- デフォルト
- アクセスする場所に制限無し
internal
- 同じモジュール(ファイル内)ならアクセスできる。
protected
- 同じクラス、またはサブクラスからアクセスできる。
- トップレベルからではアクセスできない。
private
- 宣言したクラスが書かれたファイル内でだけアクセスできる。
- 指定したクラスだけアクセスできる。
- 外部にあるクラスからはアクセスできない。
- 同じクラスの中だったらアクセスできる。
拡張関数
自分で定義した関数を既存のクラスに対して追加できる機能。
fun 拡張したい型.拡張関数名() {}
で定義する
拡張関数内ではthis
を通して追加先のクラスの関数やプロパティにアクセスできる。もちろん省略もできる。
既に定義されているメンバー関数と同じ名前の拡張関数を定義したときは、既に定義されているオリジナルのメンバー関数の方が優先される。
fun String.printSelf() { println(this.toString()); } fun main(args: Array<String>) { val text = "Hello World" text.printSelf() }
プロパティも拡張できる。val 拡張したい型名.新しいプロパティ名: 返したい型 get() { ... }
を使ってゲッターを定義できる。セッターは定義できない。
val String.type: String get() { return "私は文字列です" } fun main(args: Array<String>) { val text = "Hello World" println(text.type) }
Null許容型型名?
も拡張できる。
fun String?.printIfNotNull() { if (this == null) { println("僕はnullです") return } println(this.toString()) } fun main(args: Array<String>) { val text: String? = null text.printIfNotNull() }
分解宣言
分解宣言を使うとクラスで宣言したプロパティを複数の変数に分けて受け取ることができる。operator fun component1() = プロパティ
を使って実装する。
fun main(args: Array<String>) { val (a, b) = Disassembly( a = 1, b = 2 ) println(a) println(b) } class Disassembly(val a: Int, val b: Int) { operator fun component1() = a operator fun component2() = b }
データクラスを使う場合はoperator fun component1()
の形式はいらない。自動的に実装してくれる。
fun main(args: Array<String>) { val (a, b) = CustomPair<Int, Int>(1, 2) println(a) println(b) } public data class CustomPair<out A, out B>( public val a: A, public val b: B, ) { public override fun toString(): String = "($a, $b)" }
標準クラスにPair
とTriple
というクラスがある。それらもデータクラスで定義されていて、分解可能になっている。
fun main(args: Array<String>) { val (a, b) = Pair<Int, Int>(1, 2) println(a) println(b) val (c, d, e) = Triple<Int, Int, Int>(4, 5, 6) println(c) println(d) println(e) }
遅延初期化
lazy
を使うことで、変数やプロパティが実際にアクセスされた場合に初期化が行われるようにすることができる。
実行したらわかるが、リクエストを送信します
とレスポンスを受け取りました
は、最初のfetchedResponse
変数へのアクセス時のみ出力されて、それ以降の変数へのアクセス時には出力されないようになっている。
val fetchedResponse by lazy { fetchRequest() } fun fetchRequest(): Map<String, String> { println("リクエストを送信します") val result = mapOf("name" to "Tanaka Taro", "age" to "27") println("レスポンスを受け取りました") return result } fun main(args: Array<String>) { println("実行開始") println(fetchedResponse) println(fetchedResponse) println("実行終了") }
lateinit
を使えば、クラスのプロパティや変数の宣言後や作成後にその場で初期化せず、あとから任意のタイミングでそのプロパティや変数を初期化できる。注意点として、var
で宣言しないといけない。
lateinit による変数の初期化 | まくまくKotlinノート
class LateClass { lateinit var text: String } fun main(args: Array<String>) { val instance = LateClass() instance.text = "HELLO WORLD" println(instance.text) }
Type Alias
typealias
を使って既存の型の別名を定義できる。
typealias Response = Map<String, String>
関数型にもtypealias
を使える。
typealias Response = Map<String, String> typealias Handler = (status: Int, message: String) -> Response
Delegated Properties
ReadWriteProperty
インターフェース、もしくはReadOnlyProperty
インターフェースを実装したプロパティの移譲クラスを作成して、by
を使って移譲クラスでオーバーライドしたgetter
とsetter
をプロパティのゲッターとセッターとして定義できる。これにより、何度もgetter
とsetter
を定義するという冗長な実装をしなくて済む。
プロパティをval
で宣言すると移譲クラスでオーバーライドしたゲッターのみ利用可能となる。
import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty typealias Response = Map<String, String> class FetcherDelegate(val url: String): ReadWriteProperty<API, Response> { override fun getValue(thisRef: API, property: KProperty<*>): Response { println("${url}に[GET]リクエストを送信中です") val response = mapOf("name" to "Tanaka Taro", "age" to "27") println("${url}から[GET]レスポンスを受信しました") return response } override fun setValue(thisRef: API, property: KProperty<*>, value: Map<String, String>) { println("${url}に[POST]リクエストを送信中です") println("${url}から[POST]リクエストを送信しました ${value}") } } class API() { var requestPath: Response by FetcherDelegate("http://example.com/users/1") } fun main(args: Array<String>) { val api = API() api.requestPath = mapOf("name" to "Suzuki Taro", "age" to "28") println(api.requestPath) }
NestJSでパイプをつくる話
パイプは、@Injectable()
デコレーターでアノテーションされていてかつPipeTransform
インターフェースを実装しているクラスのことである。
用途は、コントローラーのルートハンドラで、@Query()
や@Param()
などで受け取った入力データを希望の型やフォーマットに変換するために使われる。
あとは、その受け取った入力データがこのメソッドの入力データとして正しいかどうかなどを検証するために使われる。正しくない場合は例外をスローする。
個人的に便利なのが、ルートメソッドで受け取る入力データは基本的に文字列なので、それをDate
型などに変換したりすることで、ルートメソッドで変換処理を実装しなくて済むようになるところ。
実際に作ってみる。パイプ名は-
で単語間を結ぶようにする。語尾に*-pipe
をつけなくて良い。自動的に付与される。
作成されるファイル名は*.pipe.ts
になる。
nest g pipe parse-date
srcディレクトリに、parse-date.pipe.tsとparse-date.pipe.spec.tsファイルが作成される。
transform
メソッドに処理内容を実装していく。
ParseTransform
の第一型引数には受け取る入力データの型を指定する。
第二型引数にはtransform
メソッドの返り値の型を指定する。
transform
メソッドの引数のvalue
はデフォルトでany
型なので、具体的な型を指定しておくのもいいと思う。返り値の型も同様。
// src/pipes/parse-date.pipe.ts import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; @Injectable() export class ParseDatePipe implements PipeTransform<string | undefined, Date | undefined> { transform( value: string | undefined, metadata: ArgumentMetadata, ): Date | undefined { if (!value) { return undefined; } if (typeof value !== 'string') { return undefined; } try { const time = parseInt(value, 10); return new Date(time); } catch (e: unknown) { if (e instanceof Error) { console.log(`e: ${e.message}`); } return undefined; } } }
テストも一緒に書いておく。
// src/pipes/parse-date.pipe.spec.ts import { ParseDatePipe } from './parse-date.pipe'; describe('ParseDatePipe', () => { it('should be defined', () => { expect(new ParseDatePipe()).toBeDefined(); }); it('ミリ秒を表す文字列を渡すとDate型に変換する', () => { const pipe = new ParseDatePipe(); const baseDate = new Date().getTime(); const result = pipe.transform(`${baseDate}`, {} as any); expect(result).toBeDefined(); expect(result.getTime()).toBe(baseDate); }); it('undefinedを渡すとエラーの代わりにundefinedを返す', () => { const pipe = new ParseDatePipe(); const result = pipe.transform(undefined, {} as any); expect(result).toBeUndefined(); }); });
使うときはルートメソッドの引数に指定したデコレーターの第二引数にクラスをそのまま渡すだけ。
@Get() async findAll( @Req() request, @Query('base_date', ParseDatePipe) baseDate: Date | undefined ) {} ...
NestJSでパラメーターを含むパスをネストしたい場合の簡単な方法
UsersController
とActivitiesController
の二つがある場合、activities
ルートをusers
ルートの子ルートとして実装したい場合がある。
例えば/users/:uid
の後に/activities/:id
のパスを繋いで/users/:uid/activities/:id
というパスでアクセスしたいみたいな感じ。
その場合は単純に@Controller
デコレーターにusers/:uid/activities
を渡せば良い。
... @Controller('users/:uid/activities') export class ActivitiesController { ...
そして、:uid
パラメーターを受け取る場合は@Param('uid')
をメソッドの引数で利用する。
... @Get(':id') async findOne(@Param('id') id: string, @Param('uid') uid: string) { const activity = await this.activitiesService.findOne({ id, userId: uid, }); return activity; } ...
NestJSでのデータベースをSQLiteからMySQLに乗り換えた話
*.controller.spec.ts
や*.e2e-spec.ts
でテストを行う際に、あえてサービスに対してモックを行わず、Prismaを通してローカルに置いてあるSQLiteを使用するようにしていた。
ところが、テストの数が多くなってくるとConnectorError
が発生し始めた。
Invalid `this.prisma.user.delete()` invocation in ... 53 async remove(where: Prisma.UserWhereUniqueInput): Promise<User> { → 54 const deletedUser = await this.prisma.user.delete( Error occurred during query execution: ConnectorError(ConnectorError { user_facing_error: None, kind: ConnectionError(Timed out during query execution.) })
DATABASE_URL
環境変数を設定する際にbusy_timeout
パラメーターやconnection_limit
パラメーターを使用してみたがうまくいかなかった。
DATABASE_URL="file:./dev.db?busy_timeout=100000&connection_limit=1&socket_timeout=100000"
なので、DockerでMySQLコンテナを立ち上げてそれに接続するようにした。
僕の環境はM1 Proなので通常のmysql:8.0
イメージを使わず、arcm64v8/mysql:8.0-oracle
イメージを利用している。
// docker-compose.yml version: "3.7" services: mysql: image: arm64v8/mysql:8.0-oracle restart: always environment: MYSQL_ROOT_HOST: "%" MYSQL_ROOT_PASSWORD: "root" MYSQL_DATABASE: "example-database" MYSQL_USER: "mysql" MYSQL_PASSWORD: "mysql" ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql
docker-compose up -d
を実行して、コンテナを立ち上げる。そして、環境変数DATABASE_URL
を設定する。
DATABASE_URL="mysql://root:root@localhost:3321/example-database"
schema.prismaでprovider
をsqlite
に設定している場合はmysql
に変更する。
generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") }
あとはnpx prisma db push
を実行して完了。
環境を整えてすべてのテストを実行してみると、warn(prisma-client) There are already 10 instances of Prisma Client actively running.
は表示されたものの、テストは無事成功した。