Kotlin で Result

どう書くべきでしょうか、リポジトリが返す Result。

data クラス で書きますか?

data class Result<out T>(val status: Status, val data: T?, val message: String?) {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T): Result<T> {
return Result(Status.SUCCESS, data, null)
}
fun <T> error(message: String, data: T? = null): Result<T> {
return Result(Status.ERROR, data, message)
}
fun <T> loading(data: T? = null): Result<T> {
return Result(Status.LOADING, data, null)
}
}
}
view raw Result.kt hosted with ❤ by GitHub

それとも、Kotlin ビルトインのを使いますか?

👉 Result - Kotlin Programming Language 

 

Sealed クラス で書くべし

enum の拡張的なイメージで使いましょう、

👉 Sealed Classes - Kotlin Programming Language 

sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
object Loading : Result<Nothing>()
object Error : Result<Nothing>()
}
view raw Result.kt hosted with ❤ by GitHub

/**
* A generic class that holds a value with its loading status.
* @param <T>
*/
sealed class Result<out R> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
override fun toString(): String {
return when (this) {
is Success<*> -> "Success[data=$data]"
is Error -> "Error[exception=$exception]"
Loading -> "Loading"
}
}
}
/**
* `true` if [Result] is of type [Success] & holds non-null [Success.data].
*/
val Result<*>.succeeded
get() = this is Success && data != null
view raw Result.kt hosted with ❤ by GitHub

👉 architecture-samples/Result.kt

できるだけ長く広く便利に使えるものがいいですよね。

👉 【MVVM】Flow vs LiveData 


【MVVM】Flow vs LiveData

👉 Using LiveData & Flow in MVVM — Part I - ProAndroidDev 

Kotlin Flow の登場で盛り上がってきました。

どれにします? どの流れにします?

Repository

Result を返す。

class WeatherForecastRepository @Inject constructor() {
suspend fun fetchWeatherForecast(): Result<Int> {
// Since you can only return one value from suspend function
// you have to set data loading before calling fetchWeatherForecast
// Fake api call
delay(1000)
// Return fake success data
return Result.Success((0..20).random())
}
}

Flow<Result>を返す。

class WeatherForecastRepository @Inject constructor() {
/**
* This methods is used to make one shot request to get
* fake weather forecast data
*/
fun fetchWeatherForecast() = flow {
emit(Result.Loading)
// Fake api call
delay(1000)
// Send a random fake weather forecast data
emit(Result.Success((0..20).random()))
}
/**
* This method is used to get data stream of fake weather
* forecast data in real time
*/
fun fetchWeatherForecastRealTime() = flow {
emit(Result.Loading)
// Fake data stream
while (true) {
delay(1000)
// Send a random fake weather forecast data
emit(Result.Success((0..20).random()))
}
}
}

ViewModel

Result を受けて、LiveData<Result> を渡す。

class WeatherForecastOneShotViewModel @Inject constructor(
val weatherForecastRepository: WeatherForecastRepository
) : ViewModel() {
private var _weatherForecast = MutableLiveData<Result<Int>>()
val weatherForecast: LiveData<Result<Int>>
get() = _weatherForecast
fun fetchWeatherForecast() {
// Set value as loading
_weatherForecast.value = Result.Loading
viewModelScope.launch {
// Fetch and update weather forecast LiveData
_weatherForecast.value = weatherForecastRepository.fetchWeatherForecast()
}
}
}

Flow<Result> を受けて、LiveData<Result> を渡す。

class WeatherForecastOneShotViewModel @Inject constructor(
weatherForecastRepository: WeatherForecastRepository
) : ViewModel() {
private val _weatherForecast = weatherForecastRepository
.fetchWeatherForecast()
.asLiveData(viewModelScope.coroutineContext) // Use viewModel scope for auto cancellation
val weatherForecast: LiveData<Result<Int>>
get() = _weatherForecast
}

class WeatherForecastDataStreamViewModel @Inject constructor(
weatherForecastRepository: WeatherForecastRepository
) : ViewModel() {
private val _weatherForecast = weatherForecastRepository
.fetchWeatherForecastRealTime()
.map {
// Do some heavy operation. This operation will be done in the
// scope of this flow collected. In our case it is the scope
// passed to asLiveData extension function
// This operation will not block the UI
delay(1000)
it
}
.asLiveData(
// Use Default dispatcher for CPU intensive work and
// viewModel scope for auto cancellation when viewModel
// is destroyed
Dispatchers.Default + viewModelScope.coroutineContext
)
val weatherForecast: LiveData<Result<Int>>
get() = _weatherForecast
}

Fragment

LiveData<Result> を受け取る。

class WeatherForecastDataStreamFragment : DaggerFragment() {
...
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
// Obtain viewModel
viewModel = ViewModelProviders.of(
this,
viewModelFactory
).get(WeatherForecastDataStreamViewModel::class.java)
// Observe weather forecast data stream
viewModel.weatherForecast.observe(viewLifecycleOwner, Observer {
when (it) {
Result.Loading -> {
Toast.makeText(context, "Loading", Toast.LENGTH_SHORT).show()
}
is Result.Success -> {
// Update weather data
tvDegree.text = it.data.toString()
}
Result.Error -> {
Toast.makeText(context, "Error", Toast.LENGTH_SHORT).show()
}
}
})
lifecycleScope.launch {
while (true) {
delay(1000)
// Update text
tvDegree.text = "Not blocking"
}
}
}
}

Flow<Result> を受け取る。


override fun onActivityCreated(savedInstanceState: Bundle?) {
  super.onActivityCreated(savedInstanceState)

  viewModel = ViewModelProviders.of(
      this,
      viewModelFactory
  ).get(WeatherForecastDataStreamFlowViewModel::class.java)

  // Consume data when fragment is started
  lifecycleScope.launchWhenStarted {

    // Since collect is a suspend function it needs to be called
    // from a coroutine scope
    viewModel.weatherForecast.collect {
      when (it) {
        Result.Loading -> {
          Toast.makeText(context, "Loading", Toast.LENGTH_SHORT).show()
        }
        is Result.Success -> {
          tvDegree.text = it.data.toString()
        }
        Result.Error -> {
          Toast.makeText(context, "Error", Toast.LENGTH_SHORT).show()
        }
      }
    }
  }
}

WeatherForecastDataStreamFlowFragment #L47-L75

まとめ

とはいえ、今はまだ、完全に LiveData は捨てれんよの。

👉 Kotlin で Result 
👉 Kotlin Flow vs Android LiveData - Stack Overflow 
👉 From RxJava 2 to Kotlin Flow: Threading - ProAndroidDev 

追記: ホットな Flow が登場したので以下。

👉 【MVVM】 Kotlin Flow で使える5つの利用パターン 


SoundFlowerからBlackHoleに移行してOBS接続おさらい

仮想オーディオデバイスと各音声の流れを整理します。

macOS版OBSでは「デスクトップ音声」を受け取ることができません。

一方、「マイク音声」は「Built-in Microphone」として受け取ることができます。

なので、配信時の音声は、「マイク音声」のみとなります。

簡単な図にするとこうなります。

デスクトップ音声も流したいですよね。

そこで、仮想オーディオデバイスを使います。

OBSは仮想オーディオデバイスを受け取ることができます。

今回は仮想オーディオデバイスとして SoundFlower の代替として BlackHole を使います。

👉 ExistentialAudio/BlackHole: BlackHole is a modern macOS virtual audio driver that allows applications to pass audio to other applications with zero additional latency. 

インストールすると、BlackHoleを経由したデスクトップ音声をマイク音声2として受け取れるようになります。

この状態では、パソコンのスピーカーからデスクトップの音声は聞こえないので、AUDIO MIDI設定から複数出力装置を作成して内蔵出力(パソコンのスピーカー)にも流れるように分岐します。

これでマイクとデスクトップの音声をOBS経由で配信や録画することができるようになりました。

大まかに各音声の流れを掴みながら細かい設定をしていくと混乱せずに設定していくことができます。

詳細設定は画面キャプを参考にいけると思います。



👉 OBS出力から 仮想カメラ デバイス を作成する【macOS】 
👉 【OBS】複数のURL/APIキーを管理させる - YouTube 
👉 Soundflower と Audio MIDI設定 
👉 How to setup OBS on macOS Catalina using BlackHole - YouTube 

👉 このサイト内で「OBS」で検索する