読者です 読者をやめる 読者になる 読者になる

アナログ金木犀

つれづれなるまままにつれづれする

Kotlin使って(まだまだしょぼいけど)QiitaのAndroidクライアントアプリ Qiitlin 作ったよ

こんにちは、@kgmyshinです。

最近Kotlin流行ってますね

いろんなところのブログやイベントでKotlinに関するLTがあったりして盛り上がってますよね。

実は自分の感想としては、正直まだ仕事ではいいかなってのが本音でした。

ただ、Kotlinを開発しているのがAndroid StudioのベースになっているIntelliJ IDEAを開発しているjetbrainsなので、Kotlinが主流になっていく可能性も考慮して抑えておいたほうがいいのは確かです。

ちょうどゴールデンウィークで時間もあったので、この時間を使ってQiitaのクライアントアプリを作ってみました。

まだ、貧弱なRSSリーダーレベルの機能しかありませんが。。。

ソースも全部公開しているので参考にしてもらえたら幸いです。

Qiitlin キートリン

最近のライブラリを使いつつ、実際自分がアプリを作る設計で作ってみました。

ほぼ箇条書きレベルのメモみたいな記事ですが、情報を共有できたらと思いブログを書きます。

導入は簡単

下記を見ればOKです。特にはまらずすんなりいけました。

Getting Started with IntelliJ IDEA Getting started with Android and Kotlin

各種ライブラリを使う

普段使っていたライブラリ使う際の導入方法や書き方をちらっと紹介していきます。

Kotterknife(Kotlin版ButterKnife)

Jake神がButterKnifeのKotlin用の物を用意してくれています。

導入

repositories {
    :
    maven {
        url 'https://oss.sonatype.org/content/repositories/snapshots/'
    }
    :
}

dependencies {
    :
    compile 'com.jakewharton:kotterknife:0.1.0-SNAPSHOT'
}

使い方

    <ListView
        android:id="@+id/article_list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />
   val listView: ListView by bindView(R.id.article_list_view)

injectメソッドとか呼ばなくていいんですね。と思ってたら、少し落とし穴というか注意することがあります。

Fragment使用時の話です。butterknifeではjavaの場合は下記のように書きますよね。

public class FancyFragment extends Fragment {
  @InjectView(R.id.button1) Button button1;
  @InjectView(R.id.button2) Button button2;

  @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fancy_fragment, container, false);
    ButterKnife.inject(this, view);
    // TODO Use "injected" views...
    return view;
  }

  @Override public void onDestroyView() {
    super.onDestroyView();
    ButterKnife.reset(this);
  }
}

ポイントとしてinjectするときにviewを渡すことと、onDestroyViewでresetすることの2つです。

しかしKotterKnifeでは、injectメソッドがそもそもなくviewを渡すことができません。 なので結論から言うと fragmentにviewが紐づくのはonCreateViewでViewをリターンした後なので、 onCreateViewではbindViewしていてもそのインスタンスのアクセス時にぬるぽが発生します。

WorkaroundとしてonActivityCreatedでinjectするか初期化済みフラグとか用意してonResumeとかでやるしかないかなっていう感じです。

public class ArticlesFragment : Fragment() {

    val listView: ListView by bindView(R.id.article_list_view)
    var adapter : ArticleAdapter? = null


    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_articles, container, false)
        // ↓これはおちる
        // adapter = ArticleAdapter(getActivity())
        //listView.setAdapter(adapter)
        return view
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        adapter = ArticleAdapter(getActivity())
        listView.setAdapter(adapter)
    }
    
}

またresetはどうなの?についてはisssueがあがってます。

(Jakeさんの「I don't use fragments」ってセリフ、いいっすね)

Dagger2

Dagger2を使ってみます。最初に言っておくと、現時点でDagger2との併用において フルコットリンは無理です

導入

apply plugin: 'com.neenbedankt.android-apt'

buildscript {
    :
    dependencies {
        :
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4'
    }
}

dependencies {
    :
    compile 'com.google.dagger:dagger:2.0'
    apt 'com.google.dagger:dagger-compiler:2.0'
}

使い方

全部をkotlinで書くことはまだできないそうです。(2015/05/01 現在)

kotlin-dagger-exampleに下記はknown issueだよって言ってます。

Classes and interfaces that Dagger use for generating implementations must be kept in Java. If it's moved to Kotlin it won't be generated e.g. AndroidModule, ApplicationComponent

(ただ、kotlinの方みても、dagger2の方をみてもそういうissueは見つけられませんでした。 innerクラスにComponent作ると、kotlinでだめな$のせいで動かない。。ってのはありますが、、、誰か詳細知ってる方は教えてください。)

いろいろ試してみた結果も加えてまとめると、下記の症状は確認できました。

  • kotlinでComponentとかModuleとか書いてもgenerateされない
  • javaで書いてGenerateされたDaggerHogeComponentがkotlinから見えない

とりあえず、 現状はModuleやComponentはjavaで作る必要があります。

なので使うならwork around的にjavaで数カ所書く必要があります。

以下、例です。

というかModule, Componentは普通にjavaで書きます。

@Module
public class AppModule {

    private Qiitlin app;

    public AppModule(Qiitlin app) {
        this.app = app;
    }

    @Provides
    @Singleton
    public Application provideApplication() {
        return app;
    }

}
@Singleton
@Component(
        modules = {
                AppModule.class,
                DomainModule.class,
                InfraModule.class
        }
)
public interface AppComponent {

    void inject(Qiitlin instance);

}

次に、kotlin側でDaggerで生成されたコンポーネントクラスが見えないため、そのインスタンスを使えるようにBaseApplicationみたいなものをjavaでつくってメソッドで渡します。

public abstract class BaseApplication extends Application {

    protected AppComponent createAppComponent() {
        return DaggerAppComponent
                .builder()
                .appModule(new AppModule(this))
                .domainModule(new DomainModule())
                .infraModule(new InfraModule())
                .infraModule(new InfraModule())
                .build();
    }

}

kotlin側にApplicationクラスをつくってBaseApplicationを継承して、コンポーネントインスタンスを持ちます。

public class Qiitlin : BaseApplication() {

    public var component : AppComponent? = null

    override fun onCreate() {
        super.onCreate()
        component = createAppComponent()
    }

}

あとは好きなところでinjectします。

    var hoge: Hoge? = null
        [Inject] set

    override fun onCreate() {
        :
        (getApplication() as Qiitlin).component.inject(this)
    }

[Inject] setはプロパティのセッターにアノテーションを付け加えるイメージです。

フルコットリン待ってます。

Retrofit

導入

    compile 'com.squareup.retrofit:retrofit:1.9.0'

使い方

方法は一つじゃないと思いますが、自分がやったやり方を紹介します。落とし穴はないです。

Clientを作成。

public class ApiClient {

    companion object {
        val BASE_URL = "http://qiita.com/api/v2"
    }

    protected fun getService<T>(cls: java.lang.Class<T>) : T {
        val restAdapter = RestAdapter.Builder().setEndpoint(BASE_URL).setConverter(GsonConverter(Gson())).build()
        return restAdapter.create(cls)
    }

    public fun getQiitaApi() : QiitaApi {
        return getService(javaClass<QiitaApi>())
    }

}

Apiをtraitで作成。

public trait QiitaApi {

    [GET("/items")]
    fun getArticles() : List<Article>

    [GET("/items")]
    fun getArticles([Query("page")] page:Int) : List<Article>

    [GET("/items/{item_id}/comments")]
    fun getComments([Path("item_id")] itemId: String) : List<Comment>

    [GET("/items/{item_id}/comments")]
    fun getComments([Path("item_id")] itemId: String, [Query("page")] page:Int) : List<Comment>

    [GET("/tags")]
    fun getTags() : List<Tag>

    [GET("/tags")]
    fun getTags([Query("page")] page:Int) : List<Tag>
}

取得するEntityはdataを使うと楽で、 下記のように宣言するところに[SerializedName("rendered_body")]とやってもちゃんと動きました。 (dataはprimary constructorの引数を元にequalsとかtoStringを自動生成してくれる便利なやつです。ただParcelableなど使う時は、空のコンストラクタをprimary constructorにしたかったりするので、その場合はdataの恩恵が得られないので注意です。)

data public class Article(
        val id:String,
        val body:String,
        [SerializedName("rendered_body")] val renderedBody:String,
        val tags: ArrayList<Tag>,
        val title:String,
        val url:String,
        val user:User
)

こんな風に宣言して行って、下記のようにすれば記事一覧(List

なインスタンス)を取得できます。

        val client = ApiClient()
        val articles = client.getQiitaApi().getArticles()

ちなみにアノテーションの[GET("/hoge")][]抜きのGET("/hoge")だけでも良いみたいです。

EventBus

javaの時と同じです。

導入

    compile 'de.greenrobot:eventbus:2.4.0'

使い方

Eventをpostする

   EventBus.getDefault().post(event)

register, unregister

   EventBus.getDefault().register(this)
   
   EventBus.getDefault().unregister(this)

Try and Trouble

基本的には referenceを見ればいいのですが、いくつかピックアップをば。

static methodを実装したい

こういうstatic methodを実装したい場合、

public class ArticleFragment extends Fragment {

    public static Fragment newInstance() {
        return new AritcleFragment();
    }

}

kotlinではcompanion objectplatformStaticを使います。

public class ArticleFragment : Fragment() {

    companion object  {
        platformStatic fun newInstance(): Fragment =  ArticlesFragment()
    }

}

platformStaticがない場合、試してはないのですが、KotlinではArticleFragment. newInstance()できるんですが、javaではこれでエラーが出るようです。参照:static-methods-and-fields

定数を定義したい

下記のような定数を定義したい場合も

public class ApiClient {

    public static Fragment newInstance() {
        return new AritcleFragment();
    }

}

kotlinではcompanion objectを使います。

public class ApiClient {
    companion object {
        val BASE_URL = "http://qiita.com/api/v2"
    }
}

interfaceを実装したい

下記のようなinterfaceを実装したい場合は

public interface ArticleRepository {

    ArrayList<Article> findAll();

}

traitを使います。

public trait ArticleRepository {

    fun findAll() : ArrayList<Article>

}

日付を文字列にするユーティリティ作りたい

Utilityクラスつくるより、拡張関数作った方がいいかもしれないです。

fun Date.str() : String {
    val sdf = SimpleDateFormat("MM/dd HH:mm");
    return sdf.format(this)
}

コンストラクタでスーパークラスのコンストラクタに値を渡してごにょごにょする

たとえば下記みたいなArticleImageViewを実装するとして、

public class ArticleImageView extends RelativeLayout {

    public ArticleImageView(Context contesxt) {
        super(context);
        init();
    }
    
    public ArticleImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }
    
    private void init() {
        LayoutInflater.from(getContext()).inflate(R.layout.view_article_item, this);
    }

}

Kotlinでは下記のように実装しました。

public class ArticleItemView : RelativeLayout {

    constructor(ctx: Context) : super(ctx) {
    }

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
    }

    init {
        LayoutInflater.from(getContext()).inflate(R.layout.view_article_item, this)
    }
    :
}

initは便利かもしれない。

匿名クラスを使いたい

trait(interface)にabstratctな関数が複数ある場合はSAM変換(Single Abstract Method)が効かない ので匿名クラスを実装することがあると思います。

その時はobjectを使います。

        listView.setOnScrollListener(object : AbsListView.OnScrollListener {
            override fun onScroll(absListView:AbsListView, firstVisibleItem:Int, visibleItemCount:Int, totalItemCount:Int) {

            }
            override fun onScrollStateChanged(absListView:AbsListView, i:Int) {

            }
        })

javaの場合だと実装していない関数実装する?みたいに聞かれてオートコンプリート走るんですけど、kotlinではwarningでこの関数を実装してないよ!とは出るもののオートコンプリートが効かずに自分でoverride fun ...と書かないといけないので、少し面倒でした。

数値 to 数値

kotlinでは数値のキャストができません。as使ってもできません。 そのため、toFloat(), toInt()などで変換します。

画面遷移

javaでは下記のように書きますよね。

        Intent intent = new Intent(this, HogeActivity.class);
        startActivity(intent);

kotlinでは.classが使えないので、下記のように書きます。

        val intent = Intent(this, javaClass<ArticleActivity>())
        startActivity(intent)

Parcelableつかいたい

実装例です。primary constructorを空にして、parcel用のconstructorを設定しています。primary constructorが空なのでdataを使ってもあまり意味は得られないです。

public class Tag() : Parcelable {

    var id: String? = null
    var name: String? = null

    constructor(parcelIn: Parcel) : this() {
        id = parcelIn.readString()
        name = parcelIn.readString()
    }

    companion object {
        val CREATOR = object : Parcelable.Creator<kgmyshin.qiitlin.domain.entity.Tag> {
            override fun createFromParcel(parcel: Parcel): Tag {
                return Tag(parcel)
            }

            override fun newArray(size: Int): Array<Tag> {
                return Array<Tag>(size, { i -> Tag() })
            }
        }
    }

    override fun writeToParcel(dest: Parcel, flags: Int) {
        dest.writeString(id)
        dest.writeString(name)
    }

    override fun describeContents(): Int {
        return 0
    }

}

困ってること

自動生成されるlambda式がdeprecated

自動生成されるlambda式がdeprecatedなのはどうかしたら直ったりするんですかね

スクリーンショット 2015-05-04 16.47.33.png

雑感

  • 落とし穴に落ちまくりました。

  • やっぱり情報が少ないのでなかなか落とし穴から抜けられない印象です。

  • ルール決めて使えば、拡張関数はきっと便利ですね。

  • テストも普通にかけました。

  • リスナー系でonClickとかは関数リテラルでスッキリかけるけど、関数が複数あるOnScrollListenerなどのinterfaceの場合は今まで通りな上にオートコンプリート走らないのでむしろきつい。

  • 名前が可愛い。そして可愛いは正義。