コベリンの最近の取り組みとして、業務などで役立ちそうな知見を共有する会を開催することになりました。 そのついでに発表に使ったアジェンダもそのまま公開してしまおうという豪快な企画です。
※ アジェンダをそのままコピペして公開したものなので若干見にくい箇所もあるかもしれませんが、ご了承ください。
- ViewModelがFatになっていない? @numa08
- Swift Concurrency @mironal
- VRM Live Viewer で MMDのモーションを取り込む @takkumattsu
ViewModelがFatになっていない? @numa08
Androidの話。
今までの歴史
Activity/Fragmentの実装を集中してしまう問題が発生(2010年代中盤)。Fat Activityとか呼ばれていた時代 ↓ DataBinding/ViewBindingでViewのコードを簡略化 ↓ ViewModelの導入で、ロジックとViewの切り分けに成功(2010年代末) ↓ ViewModelになんでもかんでも実装して、ViewModelが肥大化してない?(個人の感想です)(現代)
例えば、Twitterのプロフィール画面を作るとき。(注意:画面の例としてTwitterを挙げただけで、Twitterを例にしているわけではない)
画面 | 簡単な仕様 | |
---|---|---|
自分のプロフィール | ヘッダー画像 名前、自己紹介文 プロフィールを編集するボタン ツイートのリスト |
|
他人のプロフィール | ヘッダー画像 名前、自己紹介文 フォロー状態ボタン ツイートのリスト |
共通する仕様 : Fragment の起動パラメータに渡されたidのプロフィールを表示する。BottomNavigationBarから遷移するときは、自分のプロフィールを表示する。このときパラメータのidはnull
画面仕様が微妙に異なるが、構成要素はほぼ同じなので1つのViewModelで実装をしよう
class ProfileViewModel() :ViewModel() { val isMyProfile: LiveData<Boolean> val profile: LiveData<Profile> val posts: LiveData<List<Posts>>
<Button android:text="@{viewModel.isMyProfile ? R.string.edit_profile : viewModel.followState}" />
APIのエンドポイントはこんな感じ
- /users/me : 自分のプロフィールを返す
- /users/{id} : 他人のプロフィールを返す
- /users/{id}/posts : 特定ユーザーの投稿一覧を返す
Repositoryを定義して、ViewModelからデータを取り扱えるようにしよう
- UserRepository
- myId: Flow\
- myProfile: Flow\
- usersProfile(userId): Flow\
- myId: Flow\
PostRepository
- posts(userId): Flow<List\
>
- posts(userId): Flow<List\
議論: 特定ユーザーの投稿一覧はUserRepositoryとPostRepositoryのどちらにあるべき?
- UserRepository派 : ユーザーの持っているデータだから、UserRepository
- PostRepository派: 投稿データだから PostRepository
ログイン状態は別のサービスを使って管理しているので、Repositoryを分けておこう
- AuthRepository
- authState: Flow\
ViewModelの実装をする
class ProfileViewModel( private val userRepository: UserRepository, private val postRepository: PostRepository, private val authRepository: AuthRepository savedStateBundle: SavedStateBundle) : ViewModel () { private val args: ProfileFragmentArgs.fromBundle(savedStateBundle) // navigatio graphで定義されているFragmentの起動パラメータ private val isMyProfileFlow: Flow<Boolean> = if(args.userId == null) { flowOf(true) } else { authRepository .authState .mapNotNull { it as? Authorized } .map { it.id == args.userId } } val isMyProfile: LiveData<Boolean> = isMyProfileFlow.asLiveData() val profileFlow: Flow<Profile> =isMyProfile .flatMapLatest { myProfile -> if (myProfile) userRepository.myProfile() else userRepository.profile(requireNotNull(args.userId)) } val profile = profileFlow.asLiveData()
isMyProfileFlow
とかprofileFlow
の生成をテストしたいなー
class ProfileViewModelTest() { @Mockk lateinit var userRepository: UserRepository @Mockk lateinit var authRepository: AuthRepository @Mockk lateinit var postRepository: PostRepository @Test fun showMyProfile(){ val args = ProfileFragmentArgs(userId == null).toBundle() val viewModel = ProfileViewModel(userRepository, postRepository, authRepository, args) asserEqualst(viewModel.isMyProfile.value, true) } @Test fun showMyProfileWithId() val myId = "1111" val args = ProfileFragmentArgs(userId = myId).toBundle() every { authRepository.authState } returns flowOf(AuthState.Authorized(userId = myId) } val viewModel = ProfileViewModel(userRepository, postRepository, authRepository, args) asserEqualst(viewModel.isMyProfile.value, true) }
isMyProfile
をテストするのに、PostRepositoryは不要だがモックは必要!!
- 今後ViewModelに機能が加わって、依存関係が増えた(減った)ときテストコード全体の修正が必要
- ViewModelの実装によっては、テストに関係ないモックインスタンスの呼び出しが発生するため、適切にRelaxedなどの設定が必要
- そもそも、多くのモジュールに依存している状態でunittestとは言えないのでは?
どうすれば良いんだー?
みたいな構造を作る? 参考DroidKaigi/conference-app-2019: The Official Conference App for DroidKaigi 2019 Tokyo
class Dispatcher { private val actionFlow = ShareFlow<Action>() val action: Flow<Action> fun send(action: Action) { actionFlow.value = action } } class ProfileActionCreator( private val dispatcher: Dispatcher private val userRepository: UserRepository, private val authRepository: AuthRepository, ) { suspend fun showProfile( userId: String?) { if (userId == null) { userRepository.myProfile().collect { dispatcher.send(ProfileAction::ShowMyPrfoile) return } authRepository .mapNotNull { (it as? Authorized)?.userId } .flatMap { if (it == userId) { userRepository.myProfile().map(ProfileAction::ShowMyProfile) } else { userRepository.profile(userId).map(ProfileAction::ShowOthersProfile) } .collect(dispatcher::send) } class ProfileViewModel( private val dispatcher: Dispatcher) : ViewModel() { val isMyProfile = dispatcher.action.filter(it is ProfileAction).map { it is ProfileAction.ShowMyProfile }.asLiveData() val profile = dispatcher.action.filter(it is ProfileAction).map { it.profile }.asLiveData() } class ProfileFragment() : Fragment{ @Inject lateinit var profileActionCreator: ProfileActionCreator val viewModel: ProfileViewModel by { viewModels } override fun onViewCreated(view: View) { binding.viewModel = viewModel profileActionCreator.showProfile() }
- 自分のプロフィール表示については、ProfileActionCreatorのテストだけで完結する
- 適切にActionCreatorを実装できれば、不必要なモックの挿入は不要になる
- VIewModelはViewの状態に専念できる
Swift Concurrency @mironal
Swift Concurrency とは?
https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html
// Serial let hoge = await asyncFunction() let huga = await asyncFunction2() // asyncFunction -> asyncFunction2 の順番で実行される // Parallel async let hoge = asyncFunction() async let huga = asyncFunction2() let results = await [hoge, huga] // asyncFunction と asyncFunction2 が同時に実行される. // AsyncSequence や AsyncStream などの戻り値の場合は for で loop できる for await value = asyncStreamFunc() { print(value) } // Sequence なので filter とかできる for await value = asyncStreamFunc().filter{...}.map {...} { print(value) } // async な context じゃない場合に await したいときは Task を使う // むしろ await は `Task {}` のシンタックスシュガーみたいなもの let task = Task { await hogehoge() } // task を保持しておけば後でキャンセルとかできる // ViewController で cancel ボタン押したときとか. task.cancel() // XCTest でも async できる func testHoge() async throws { let a = awit asyncFunc() }
既存の callback(closure) style のやつを対応させるには?
- withTaskCancellationHandler
- withCheckedContinuation
とかを使う.
失敗する可能性のあるなしなどでいろんなメソッドがあるので適切なものを使う.
## 罠
Xcode 13.2.1 (今現在の最新版) でビルドしたバイナリは iOS 12.5.5 でクラッシュする.
https://forums.swift.org/t/swift-concurrency-back-deploy-issue/53917
こんな感じで #if
で区切っていても xcode のバグで libswift_Concurrency をリンクしようとして、起動時にクラッシュする.
- Xcode 13.3 では直るらしい
- iOS 13系では発生しない模様
難しいところ
- await の前後で thread が何なのかわかりにくい
- 解決: @MainActor 使う
- ミスってちゃんと task を終わらせないとある行でハングしてるように見える
- callback でも同じこと起こるけど async await は慣れてないからミスりがち
VRM Live Viewer で MMDのモーションを取り込む @takkumattsu
Magica Voxelで作っていたモデルをVRMに出力することが出来たので何か遊べないかと思って見つけたVRM Live Viewer。MMDのモーションが利用できるみたいなので遊んでみた時の備忘録
- モーションを探す
- カメラのモーションもある
- モデルを読み込み
- それぞれのモデルにモーション設定
- カメラのモーション
- 音楽の設定
モーションを探す
MMDと言ったらニコニコ。
ニコニコは昔からこの辺のモーションなどを公開してくれている人がたくさんいます。「mmdモーション配布あり」などのタグで検索すると色々出てくるので公開してくれているモーションをダウンロードしましょう
今回はダンスのモーションは https://www.nicovideo.jp/watch/sm28405062 をお借りしました
カメラのモーションもある
カメラの動きもモーションとして読み込めるので今回は https://www.nicovideo.jp/watch/sm28615575 をお借りしました
モデルを読み込み
ファイル読み込みの横が青いプラスボタンになっていることを確認して、vrmを読み込みます(VRoid Hubでも可)
わかりにくいけど二体読み込めています
それぞれのモデルにモーション設定
プラスボタンの下の人のアイコンをクリックすると現状選択しているモデルが表示されます
いったん画面を閉じてオレンジの...をクリックします
モーションの部分をクリックして落としてきたvmdファイルを読み込みます
そのあとプラスボタンの下の人のアイコンをクリックしてモデルを切り替え
同じようにvmdを読み込みます。これでモーションの設定は完了
カメラのモーション
次に右上のモーションのフォルダをクリックして
下のフォルダボタンを押してカメラのモーションを読み込みます
そうするとこんな感じで視点が変わるかと思います
音楽の設定
右下の音符マークをクリックして、フォルダから音楽を読み込むことで好きな音楽を設定できます。
完成
あとは再生ボタンを押せば踊りだします!
音楽やモデルに著作権があるので個人で楽しむ分にはいいですが公開するときなどはしっかり許諾を得てから楽しみましょう。
イメージ図
自分が美少女に見えてきたな(幻覚) pic.twitter.com/6PePBTvKhE
— TakkuMattsu (@NorsteinBekkler) 2022年2月24日