Kotlin使って(まだまだしょぼいけど)QiitaのAndroidクライアントアプリ Qiitlin 作ったよ
こんにちは、@kgmyshinです。
最近Kotlin流行ってますね
いろんなところのブログやイベントでKotlinに関するLTがあったりして盛り上がってますよね。
実は自分の感想としては、正直まだ仕事ではいいかなってのが本音でした。
ただ、Kotlinを開発しているのがAndroid StudioのベースになっているIntelliJ IDEAを開発しているjetbrainsなので、Kotlinが主流になっていく可能性も考慮して抑えておいたほうがいいのは確かです。
ちょうどゴールデンウィークで時間もあったので、この時間を使ってQiitaのクライアントアプリを作ってみました。
まだ、貧弱なRSSリーダーレベルの機能しかありませんが。。。
ソースも全部公開しているので参考にしてもらえたら幸いです。
最近のライブラリを使いつつ、実際自分がアプリを作る設計で作ってみました。
ほぼ箇条書きレベルのメモみたいな記事ですが、情報を共有できたらと思いブログを書きます。
導入は簡単
下記を見れば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 object
とplatformStatic
を使います。
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なのはどうかしたら直ったりするんですかね
雑感
落とし穴に落ちまくりました。
やっぱり情報が少ないのでなかなか落とし穴から抜けられない印象です。
ルール決めて使えば、拡張関数はきっと便利ですね。
テストも普通にかけました。
リスナー系でonClickとかは関数リテラルでスッキリかけるけど、関数が複数あるOnScrollListenerなどのinterfaceの場合は今まで通りな上にオートコンプリート走らないのでむしろきつい。
- 名前が可愛い。そして可愛いは正義。