ページ

ラベル Rx の投稿を表示しています。 すべての投稿を表示
ラベル Rx の投稿を表示しています。 すべての投稿を表示

2019年7月18日木曜日

UILabelをタップ可能にする(小ネタ)

UILabelは rx.tap が使えない

そこで、UITapGestureRecognizer を使ってタップに対応させる
swift
        @IBOutlet private var profileDescription: UILabel!

        ...

        let gesture = UITapGestureRecognizer()
        profileDescription.addGestureRecognizer(gesture)
        gesture.rx.event.asSignal()
            .emit(onNext: { [weak self] state in
                guard let self = self else { return }
                let viewController = XXXXX
                self.navigationController?.pushViewController(viewController, animated: true)
            }).disposed(by: disposeBag)
:warning: 事前に User Interaction Enabled をONにしとかないといけない!
image.png (60.1 kB)

2018年7月1日日曜日

Retrofit + Rxを試す(Android)

Retrofit+Rx

RetrofitのRx化に関しては Jake神作のAdapterを使っていたらしい。。が
現在本家のretrofitにAdapterが出来てるみたいで Jake神作のはDEPRECATEDになっている。
こちらを使う:eyes:

:computer: 環境構築


app/build.gradleに以下を追加
    implementation 'io.reactivex.rxjava2:rxjava:2.1.6'
    implementation 'io.reactivex.rxjava2:rxkotlin:2.1.0'
    implementation 'com.squareup.moshi:moshi:1.5.0'
    implementation 'com.squareup.retrofit2:retrofit:2.4.0'
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
    implementation 'com.squareup.retrofit2:converter-moshi:2.4.0'  

:pencil: 実装


簡単なサンプル

サンプルとしても多い、GithubのAPIを使ってユーザー情報を取得して見ます。
  • Userクラス (ユーザー情報を格納するデータクラス)

    import com.squareup.moshi.Json
    
    data class User(
        @Json(name = "name")
        var name: String,
        @Json(name = "login")
        var login: String,
        @Json(name = "blog")
        var blog: String,
        @Json(name = "type")
        var type: String
    )
    
  • GithubApiクラス (interfaceを定義)

    import io.reactivex.Observable
    import retrofit2.http.GET
    import retrofit2.http.Path
    
    interface GithubApi {
    
        companion object {
            const val BASE_URL = "https://api.github.com"
        }
    
        @GET("users/{username}")
        fun getUser(@Path("username") user: String): Observable<User>
    }
    
  • MainActivityクラス

    class MainActivity : AppCompatActivity() {
    
        companion object {
            val TAG = MainActivity::class.java.simpleName
        }
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            val retrofit = Retrofit.Builder()
                    .baseUrl(GithubApi.BASE_URL)
                    .addConverterFactory(MoshiConverterFactory.create(Moshi.Builder().build()))
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
                    .build()
    
            val service = retrofit.create(GithubApi::class.java)
            service.getUser("Slowhand0309").subscribe({ ret ->
                Log.d(TAG, "ret $ret")
            }, { error ->
                Log.e(TAG, error.message)
            })
        }
    }
    
AndroidManifestに
<uses-permission android:name="android.permission.INTERNET"/> を追加するのを忘れずに

戻り値に関して

RetrofitのRxJava2 Adapterは戻り値として
Observable, Single, Maybe, Completable
を提供しているが、それぞれの違いがわからないので調べてみる。
  • Observable: いつもの
  • Single
    • onNext+onComplete = onSuccess でonSuccessは一回しか呼べない
  • Maybe
    • onSuccessかonErrorかonCompleteのどれかが呼ばれる、または全く呼ばれない
  • Completable
    • onErrorかonCompleteのどれかが呼ばれる、または全く呼ばれない

エラー処理に関して

200番台以外のレスポンスの場合 HttpException として onError が呼ばれる
実際のソースコード
エラー処理に関しては↓のように捌けば良さそう。
        disposable += ApiClient.createUser()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({ res ->
                    Log.d(TAG, "create user success ${res.id}")
                }, { error ->
                    (error as? HttpException)?.let {
                        Log.e(TAG, "code: ${it.code()}")
                        Log.e(TAG, "message: ${it.message()}")
                        Log.e(TAG, "response: ${it.response()}")
                    }
                })
401が返ってきた場合の実際の出力
E/MainActivity: code: 401
    message: 
E/MainActivity: response: Response{protocol=h2, code=401, message=, url=https://....} 

:bomb: バッドノウハウ


単純にmoshiを使うだけだと、dataクラスのnon-nullのプロパティにnullが入ってしまう!
参考URL
そこでmoshiのkotlin extensionを導入して、non-nullにnullを入れうようとしたら
例外を投げるように修正
app/build.gradleに以下を追加
implementation 'com.squareup.moshi:moshi-kotlin:1.5.0'
MainActivityのmoshiを生成している箇所をいかに変更
        val moshi = Moshi.Builder()
                    .add(KotlinJsonAdapterFactory())
                    .build()

        val retrofit = Retrofit.Builder()
                .baseUrl(GithubApi.BASE_URL)
                .addConverterFactory(MoshiConverterFactory.create(moshi))
                .addCallAdapterFactory(RxJava2CallAdapterFactory.createWithScheduler(Schedulers.io()))
                .build()
これでnon-null / null をきっちり管理してくれます:+1:

:link: 関連リンク


2018年3月17日土曜日

RxKotlinとRxBindingでフォームの入力チェック


RxBinding

AndroidのUIコンポーネントをRx~でよろしく触れるようにしたもの

:computer:環境構築


今回はKotlinを使うのでRxBindingもKotlin用をインストール
app/build.gradle
    // RxKotlin
    compile "io.reactivex.rxjava2:rxkotlin:$rxkotlin_version"

    // RxBinding
    compile "com.jakewharton.rxbinding2:rxbinding-kotlin:$rxbinding_version"

:pencil: 実装


簡単なサンプル

よくある以下のようなフォーム画面を作成してみる。
  • 入力フィールドに正しい値が設定されている時のみSubmitボタンが押せるようにする
レイアウトはこんな感じ
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.v7.widget.Toolbar
        android:id="@+id/profileToolbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        app:theme="@style/ToolbarTheme"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="5dp"
        android:orientation="vertical">

        <android.support.design.widget.TextInputLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            app:errorEnabled="true"
            app:hintEnabled="true"
            app:hintAnimationEnabled="true">

            <EditText
                android:id="@+id/profileName"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:maxLines="1"
                android:maxLength="20"
                android:inputType="text"
                android:hint="@string/hint_name" />

        </android.support.design.widget.TextInputLayout>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/hint_area"/>

        <Spinner
            android:id="@+id/spinnerArea"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:entries="@array/areas" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:paddingStart="50dp"
            android:paddingEnd="50dp"
            android:gravity="center">

            <Button
                android:id="@+id/profileSubmit"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@drawable/round_shape_disabled"
                android:enabled="false"
                android:text="@string/save"
                android:textColor="@android:color/white"/>

        </LinearLayout>

    </LinearLayout>
</LinearLayout>
image.png (43.9 kB)
名前とエリアを選択するフォーム画面
名前やエリアが未選択の場合Submitボタンが押せない状態にする
kotlin
        val nameChanges = profileName.textChanges()
        val areaChanges = spinnerArea.itemSelections()

        disposable += Observables.combineLatest(nameChanges, areaChanges)
                    {name, area -> name.isNotEmpty() && area != AdapterView.INVALID_POSITION}
                    .subscribe { isValid ->
                        profileSubmit.isEnabled = isValid
                        profileSubmit.setBackgroundResource(
                                if (isValid)
                                    R.drawable.round_shape_enabled
                                else
                                    R.drawable.round_shape_disabled)
                    }
名前とエリアのObservableをcombineLatestで結合し、
両方の値チェックでOKだった場合のみボタンを有効化しています。
name.isNotEmpty() && area != AdapterView.INVALID_POSITION
-> 名前が空でなく、spinnerの選択位置が空でない

オペレータ

  • distinctUntilChanged
    連続して重複したデータを通知しない
    kotlin
        disposable += Observable.fromArray(1, 2, 2, 3, 4, 5, 5, 6)
                .distinctUntilChanged()
                .subscribe(::println) // 1,2,3,4,5,6
    
  • combineLatest
    どちらか1つの送信される時、指定された関数によって、各Observableから送信される最新のアイテムを結合し、この関数の評価に基づいたアイテムを送信する
    kotlin
            val o1 = PublishSubject.create<Int>()
            val o2 = PublishSubject.create<String>()
    
            disposable += Observables.combineLatest(
                                    o1, o2
                                    ) { n, s -> "${n}/${s}"}
                                    .subscribe(::println)
            o1.onNext(1)
            o2.onNext("a")
            o1.onNext(2)
            o1.onNext(3)
            o2.onNext("b")
            // 1/a
            // 2/a
            // 3/a
            // 3/b
    

:bomb: バッドノウハウ


自分だけかもしれませんが、Observables.combineLatestObservable.combineLatest
(Observable(s)の違い)と書くとエラーになります。。