coroutine の使い方 - Android Architecture Blueprints v2 #1

気がついたら「v2」です。MVVM のみとなって、 RxJava は姿を消していますが。

👉 googlesamples/android-architecture: A collection of samples to discuss and showcase different architectural tools and patterns for Android apps. 



現在、4つのバリエーションが公開されていますが、以下は共通。


- kotlin
- coroutine
- single activity
- architecture component
- navigation component + fragment
- presentetion layer(per page) = fragment + view model
- reactive ui = live data + data binding
- data layer = repositpory + local(room) + remote
- datalayer one shot operations(no listener or data streams)

非同期処理には、どのバリエーションも「coroutine」使う。

どのように coroutine を使っているか、を定型化しておきたい。

関数 coroutineScope

「kotlinx-coroutines-core」の関数の「coroutineScope」 が目についたので見ておく。


suspend fun <R> coroutineScope(
    block: suspend CoroutineScope.() → R
): R (source)

CoroutineScope を作成し、このスコープで指定された suspend ブロックを呼び出します。その外側のスコープから coroutineContext を継承しますが、そのコンテキストのJobをオーバーライドします。

この関数は、処理の並列分解用に設計されています。 このスコープ内のいずれかの子コルーチンが失敗すると、このスコープは失敗し、残りのすべての子はキャンセルされます(supervisorScope との違いを参照してください)。 与えられたブロックとそのすべての子コルーチンが完了するとすぐに戻ります。
スコープの使用例は次のようになります。


suspend fun showSomeData() = coroutineScope {

  val data = async(Dispatchers.IO) { // <- extension on current scope
     // ... load some UI data for the Main thread ...
  }

  withContext(Dispatchers.Main) {
    doSomeWork()
    val result = data.await()
    display(result)
  }
}

この例のスコープの意味は次のとおりです。

1. showSomeDataは、データがロードされてUIに表示されるとすぐに戻ります。
2. doSomeWorkが例外をスローすると、非同期タスクはキャンセルされ、showSomeDataはその例外を再スローします。
3. showSomeData の外部スコープが取り消されると、開始された async ブロックとwithContext ブロックの両方が取り消されます。
4. 非同期ブロックが失敗すると、withContext はキャンセルされます。

現在のジョブが外部でキャンセルされた場合、メソッドは CancellationException をスローします。このスコープ内に未処理の例外がある場合(たとえば、このスコープ内の起動で開始されたクラッシュコルーチンから)は、対応する未処理のThrowableをスローします。

coroutineScope - kotlinx-coroutines-core

実際の記述

coroutine を使った非同期処理は 各 ViewModel を起点に記述されています。


class TasksViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() {

    fun loadTasks(forceUpdate: Boolean) {
        viewModelScope.launch {

            // 対応する Repository メソッドをコール
            // val tasksResult = tasksRepository.getTasks(forceUpdate)

        }
    }

}

👉 android-architecture/TasksViewModel.kt at master · googlesamples/android-architecture 

Repository を展開して、スコープ周りを抜き出してみると、


class TasksViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() {

    fun loadTasks(forceUpdate: Boolean) {
        viewModelScope.launch {

            withContext(ioDispatcher) {
            
                // ...            

            }

        }
    }

}

というこれまで通り withContext() を使った dispatcher の切り換え。

他には、

前述の coroutineScope 関数を使った記述が、単独、並列、入れ子の3種類。


override suspend fun saveTask(task: Task) {
    coroutineScope {
        launch { tasksRemoteDataSource.saveTask(it) }
        launch { tasksLocalDataSource.saveTask(it) }
    }
}


override suspend fun clearCompletedTasks() {
    coroutineScope {
        launch { tasksRemoteDataSource.clearCompletedTasks() }
        launch { tasksLocalDataSource.clearCompletedTasks() }
    }
    withContext(ioDispatcher) {
        cachedTasks?.entries?.removeAll { it.value.isCompleted }
    }
}


override suspend fun deleteAllTasks() {
    withContext(ioDispatcher) {
        coroutineScope {
            launch { tasksRemoteDataSource.deleteAllTasks() }
            launch { tasksLocalDataSource.deleteAllTasks() }
        }
    }
}

👉 android-architecture/DefaultTasksRepository.kt at master · googlesamples/android-architecture 

今後、さらに簡潔に記述されていくのだろうが、意図は今のが理解しやすいと思いメモ。




Android Paging Library と Retrofit

例えば、このパターン、Network only。

ここでは、PositionalDataSource を拡張するが、他のタイプのDataSource拡張でも同じ。

compositeDisposable のように viewModelScope をはるばる持ってきたにもかかわらず、


class RemoteDataSource(
  private val coroutineScope: CoroutineScope,
  private val service: RemoteService,
  private val s: String
) : PositionalDataSource<Item>() {

  @ExperimentalCoroutinesApi
  override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<Item>) {
    Timber.d("RemoteDataSource#loadInitial: ${Thread.currentThread().name}")
    ...

loadInitial() 内は、メインスレッドではなく、別スレッドで実行されている。


Timber.d("RemoteDataSource#loadInitial: ${Thread.currentThread().name}")


D/RemoteDataSource: RemoteDataSource#loadInitial: arch_disk_io_0
D/RemoteDataSource: RemoteDataSource#loadInitial: arch_disk_io_1
D/RemoteDataSource: RemoteDataSource#loadInitial: arch_disk_io_3

これらは、Android Architecture Component が作成した独自スレッド。

よって、同期なRetrofitの実行処理で良い、となる。

If I use enqueue with Retrofit 2.3 it will doesn't work but if i do a .execute() the LiveData is correctly triggered

Retrofit 2.3で、enqueue() を使用しても機能しませんが、execute() を実行するとLiveDataが正しくトリガーされます。

Android Paging Library LiveData> is triggered before the end of the api call

公式リファレンスにも記述はあるという。

To display data from a backend server, use the synchronous version of the Retrofit API to load information into your own custom DataSource object.

バックエンドサーバーからのデータを表示するには、同期バージョンのRetrofit APIを使用して、独自のカスタムDataSourceオブジェクトに情報をロードします。

Network only - Paging library overview

しかし、DiffUtilを使ったリフレッシュなアニメーションが実行されない。

「手間がかかる」でなく「沼にハマる」ことが最近は多くなった。

APIの仕様がおせっかいすぎやしないか。

しきいを下げようとして、余計に混乱させるばかり。

👉 あなたは Android Architecture Component をどう思いますか? 
👉 Fragment と Toolbar の歴史の話 - Qiita 


【Kotlin】バッキング・フィールド/プロパティ

フィールド

Fields cannot be declared directly in Kotlin classes.
Kotlin クラスでは、直接にフィールドを宣言することはできません。

プロパティ


// Kotlin
class Human {
    val age = 20
}


// Java
public final class Human {
   private final int age = 20;

   public final int getAge() {
      return this.age;
   }
}

バッキング・フィールド

ゲッターやセッターにカスタムロジックを入れたい場合は、「バッキング・フィールド」を使うことができます。

カスタムゲッターやセッターのスコープ内で「field」にアクセスできます。


// Kotlin
var counter = 0
    set(value) {
        if (value >= 0) field = value
    }


// Kotlin
class Human {
    val age = 20
        get() {
            println("Age is: $field")
            return field
        }
}


// Java
public final class Human {
   private final int age = 20;

   public final int getAge() {
      String var1 = "Age is: " + this.age;
      System.out.println(var1);
      return this.age;
   }
}

バッキング・プロパティ

ゲッターやセッターの外でフィールドに直接アクセスしたいときなどに使います。

private なフィールドに_を付けて明示します。


// Kotlin
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // Type parameters are inferred
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }


// Kotlin
class Human {
    private val _age: Int = 20
    val age: Int
        get() {
            return _age
        }

     val printAge = {
         println("Age is: $_age")
     }
}


// Java
public final class Human {
   private final int _age = 20;
   @NotNull
   private final Function0 printAge = (Function0)(new Function0() {
      public Object invoke() {
         this.invoke();
         return Unit.INSTANCE;
      }

      public final void invoke() {
         String var1 = "Age is: " + Human.this._age;
         System.out.println(var1);
      }
   });

   public final int getAge() {
      return this._age;
   }

   @NotNull
   public final Function0 getPrintAge() {
      return this.printAge;
   }
}

まとめ

Java にデコンパイルした状況を確認しながら進むと理解しやすいように思います。

👉【Kotlin】Properties and Fields: Getters, Setters
👉 Properties and Fields: Getters, Setters, const, lateinit - Kotlin Programming Language
👉 Backing properties in Kotlin – ProAndroidDev
👉【MVVM】 ViewModel の_プロパティ記述


【MVVM】ViewModel インスタンスの取得

以下の記述ですが、


private val viewModel: MainViewModel by lazy {
  ViewModelProviders.of(this).get<MainViewModel>()
}

Activityで にしろ、Fragmentで にしろ、


private val viewModel: MainViewModel by viewModels()

でいける。

Fragment内で呼び出す場合は、


dependencies {
  implementation 'androidx.fragment:fragment-ktx:1.1.0-alpha08'
}

で、今現在。

コードをみてみる。


@MainThread
inline fun <reified VM : ViewModel> Fragment.viewModels(
    noinline ownerProducer: () -> ViewModelStoreOwner = { this },
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { ownerProducer().viewModelStore }, factoryProducer)

@MainThread
inline fun <reified VM : ViewModel> Fragment.activityViewModels(
    noinline factoryProducer: (() -> Factory)? = null
) = createViewModelLazy(VM::class, { requireActivity().viewModelStore }, factoryProducer)

@MainThread
fun <VM : ViewModel> Fragment.createViewModelLazy(
    viewModelClass: KClass<VM>,
    storeProducer: () -> ViewModelStore,
    factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
    val factoryPromise = factoryProducer ?: {
        val application = activity?.application ?: throw IllegalStateException(
            "ViewModel can be accessed only when Fragment is attached"
        )
        AndroidViewModelFactory.getInstance(application)
    }
    return ViewModelLazy(viewModelClass, storeProducer, factoryPromise)
}

使ったほうが良いですよね。

はまらなくてすむ。

👉【MVVM】 ViewModel の_プロパティ記述

androidx.fragment.app  |  Android Developers


Android Q beta3 で感じる Navigation Bar (ナビゲーションバー) の方向性

Android端末で、ナビゲーションバーが担当する操作は、以下の5つです。

- ホームへ (Home)
- アプリドロワー表示 (App drawer)
- アプリ切り替え (App switcher)
- 戻る (Back)
- アシスタントアプリ起動 (Assistant)

2019-05-05付で公開された Android Q beta3 では、


「設定」

  |

「システム」

  |

「操作」

  |

「System Navigation」

3タイプに分かれています。

それぞれ試してみましょう。

 

3-button navigation

これまでの、馴染みやすいレガシーなスタイルと言えるでしょう。



これが一番使いやすいと思う私は固執しすぎの保守な人間ですかね?

 

2-button navigation

実はこれが慣れると一番使えるのでは?



現在普及割合を増やしてる「Android P」 とはボタン数は同じだが、アプリ切り替えが少し違う。それがすごく良い。

 

Fully gestural navigation

問題なのはこれ。

今回、最新版の操作として登場してます。



「画面両端からの中向きスワイプ」が「戻る(バック)ボタン」という直感的でない操作に。

それ、各アプリ内ナビゲーションメニューやホームアプリなどと衝突しますし。

「薄っぺらい一つのボタン」を採用することにより、コンテンツスペースを縦方向最大限に確保できてるとはいえどうなんでしょうか。

 

まとめ

フルジェスチャーな1ボタンは辛くねえですか?




スペース空いてるんだし、2ボタンか3ボタンが馴染みやすいように思いますが、果たしてどうなるのか。

👉Android Pie から始まる「ナビゲーションバー」の混乱と操作の覚え方

👉Android Q Beta 3 hands-on: Dark theme, new gestures, and more! - Android Authority