この記事は Recruit Engineers Advent Calendar 2020 の 9日目の記事です。
自分はAndroidエンジニアとして6年ほど開発に携わっています。
6年ほど開発をしていると、昔よく使っていたデファクトスタンダードなライブラリやコーディング記法が、今は非推奨になっていたり、新しい便利なライブラリが増えたりするので、必要に応じて保守しているコードに取り込む必要があります。
「古い書き方のコードでも動いているからいいじゃん」って方もいらっしゃるかもしれないです。短期的な目線で考えた場合は悪くはないと思いますが、非推奨になったものが突然動かなくなるリスクもありますし、チーム開発しているコードだと、もしかしたら新しいエンジニアの方は古い書き方を知らない(使わないから学習していない)からコードの保守ができない、など発生するかもしれないので、個人的には早く対応していきたいです。
今回は、Android開発において2020年に非推奨になったもの・これから非推奨になる予定だと広報されたものの中から、個人的にインパクトが大きかったものを3つ紹介し、それらの古いコードから新しいコードへの変更点などを共有したいと思います。
非推奨① : Kotlin Android Extensions
昔からある手段として、ActivityやFragmentで findViewById()
を利用して必要なViewを取得できます。
ただ、動的に変更されるView要素が増えれば増えるほど、毎回 findViewById
を呼び出すために同じようなコードを書くのが大変でした。
// 必要なViewの分だけfindViewByIdが必要
val button1: Button = view.findViewById(R.id.button1)
val button2: Button = view.findViewById(R.id.button2)
val button3: Button = view.findViewById(R.id.button3)
button1.setOnClickListener { /* some codes */ }
button2.setOnClickListener { /* some codes */ }
button3.setOnClickListener { /* some codes */ }
...
Kotlin Android Extensionsのsyntheticの登場によって、これらの findViewById()
の記述が不要になり、簡単にViewの取得ができてとても便利になりました。
自分もよく利用していました。
Kotlin Android Extensionsのsyntheticを利用するために必要な作業は主に2つでした。
まずは、 app/build.gradle
に kotlin-android-extensions
のプラグインを追加します。
plugins {
id 'kotlin-android-extensions'
}
あとは、Viewが定義してあるレイアウトファイル名に応じたインポート文を定義すれば利用できます。
<Button
android:id="@+id/main_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
import kotlinx.android.synthetic.main.fragment_main.* // 「fragment_main」の部分はViewが定義してあるxmlファイル名
class MainFragment : Fragment() {
fun someFunction() {
main_button.setOnClickListener { /* some codes */ }
}
}
ところが、 Kotlin 1.4.20 からこの便利なKotlin Android Extensionsの利用が非推奨となりました。
Kotlin 1.4.20 やGoogleから公開された Kotlin Android Extensions の未来 という記事などを見ていると、代わりに ViewBinding を利用することが推奨されています。
非推奨①の回避策 : ViewBindingを利用する
さっそく ViewBinding を使ってみます。
ViewBinding は Android Studio 3.6 以上でないと動作しないので、注意してください。
まずは、 app/build.gradle
に viewBinding の設定を追加します。
android {
viewBinding {
enabled = true
}
}
この定義を追加してあげると、レイアウトファイルの情報からViewをバインディングしたクラスが自動で生成されるようになります。
この自動で生成したクラスを利用することで、 findViewById()
を使う必要がなくなります。
自動生成されたクラスは、「レイアウトファイル名をキャメルケースに変換した名前」+「Binding」という命名規則でクラスが生成され、そのレイアウトファイルに定義されているid名をキャメルケースに変換した名前で各Viewへの参照が保持されます。
例えば、 fragment_main.xml
というレイアウトファイルからは FragmentMainBinding.kt
というクラスが自動生成されます。
<Button
android:id="@+id/main_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding // fragment_main.xmlから自動生成されたクラス
fun someFunction() {
binding.mainButton.setOnClickListener { /* some codes */ }
}
}
この自動生成されたクラスには、初期化を行うために inflate
や bind
などの function が用意されているので、 ViewBindig を利用するためにはそれらの function を利用する必要があります。
Fragmentであれば、 onCreateView
で inflate
functionを利用するのが一般的だと思います。
class MainFragment : Fragment() {
private lateinit var binding: FragmentMainBinding // fragment_main.xmlから自動生成されたクラス
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentMainBinding.inflate(layoutInflater, container, false)
return binding.root
}
fun someFunction() {
binding.mainButton.setOnClickListener { /* some codes */ }
}
}
上記の設定だけでも動作はしますが、 ViewBindingのドキュメント によると、
注: フラグメントはビューよりも持続します。フラグメントの onDestroyView() メソッドでバインディング クラスのインスタンスへの参照をすべてクリーンアップしてください。
とあるので、 ViewBindingのドキュメント のコードにあるような工夫が必要なようです。(クリーンアップができていれば良いので、別の対応でも良いと思います。)
class MainFragment : Fragment() {
private lateinit var _binding: FragmentMainBinding? = null // nullableの_bindingに保持
private val binding get() = _binding!! // ※強制アンラップの利用には注意しましょう
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = FragmentMainBinding.inflate(layoutInflater, container, false)
return binding.root
}
fun someFunction() {
// non-nullのbindingでアクセスする
binding.mainButton.setOnClickListener { /* some codes */ }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null // clean up
}
}
より詳細な情報は ViewBindingのドキュメント や Use view binding to replace findViewById などを参考にしてみてください。
非推奨② : Fragment#onActivityCreated()
androidx の Fragment を利用している方も多いと思いますが、現在 Beta Version である次期バージョン 1.3.0 の Fragment には非推奨となる機能がいくつかあります。
Fragmentには様々なライフサイクルメソッドが用意されていますが、 Activity#onCreate()
メソッドから戻ったときに呼び出される Fragment#onActivityCreated()
メソッドが バージョン1.3.0-alpha02 から非推奨となることになりました。
非推奨②の回避策
Fragmentのリリースノート には下記のように記載されています。
onActivityCreated()
メソッドは非推奨になりました。フラグメントのビューをタッチするコードはonViewCreated()
(onActivityCreated()
の直前に呼び出される)、他の初期化コードはonCreate()
で実行する必要があります。アクティビティのonCreate()
が完了した際にコールバックを受け取るには、onAttach()
のアクティビティのLifecycle
にLifeCycleObserver
を登録し、onCreate()
のコールバックを受け取ったら削除する必要があります。
onActivityCreated()
を利用する代わりに、用途に応じて適切な場所に処理を記載する必要があるということです。
ここでは、初期化処理の話と、 Activity#onCreate()
が完了した際にコールバックを受け取る話の2つについて述べられているので、ここでもサンプルコードを作ってみたいと思います。
初期化処理の移動
初期化処理の方はあまり良いサンプルが思いつきませんでしたが、
例えば、このような書き方をしている初期化処理の場合、
class OldFragment : Fragment() {
private var message: String? = null
private var textView: TextView? = null
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState) // deprecated
message = savedInstanceState?.getString("KEY")
textView = view?.findViewById(R.id.text_view)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("KEY", "save")
}
}
onActivityCreated()
を利用しない書き方に変更すると、こんな感じかと思います。
class NewFragment : Fragment() {
private var message: String? = null
private var textView: TextView? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
message = savedInstanceState?.getString("KEY") // 移動
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
textView = view?.findViewById(R.id.text_view) // 移動
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("KEY", "save")
}
}
重要なのは、それぞれの初期化処理に応じて、適切な場所に移動させてあげることです。
Activity#onCreate()が完了した通知を受け取る
次に、 Activity#onCreate()
が完了した通知を受け取る方法です。
onAttach()
のアクティビティのLifecycle
にLifeCycleObserver
を登録し、onCreate()
のコールバックを受け取ったら削除する必要があります。
とあるので、Stack Overflowを参考にしつつ、サンプルを作ってみました。
class NewFragment : Fragment() {
private lateinit var lifecycleObserver: LifecycleObserver
override fun onAttach(context: Context) {
super.onAttach(context)
lifecycleObserver = object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
fun onCreated() {
// Activity#onCreate() が完了したときにやりたい処理を書く
activity?.lifecycle?.removeObserver(this) // 解除
}
}
activity?.lifecycle?.addObserver(lifecycleObserver) // 登録
}
}
意図したどおりに動いていそうでした。
非推奨③ : startActivityForResult()/onActivityResult()
次期バージョン 1.3.0 の Fragment に非推奨となる機能でもう一つ取り上げたいのが、 Fragment#startActivityForResult()
と Fragment#onActivityResult()
が非推奨になったことです。
より細かい開発中のバージョンだと、 バージョン1.3.0-alpha04 から非推奨となることになりました。
startActivityForResult()/onActivityResult()
は Fragment だけでなく Activity にもあるので、Activityの方も バージョン1.2.0-alpha04 から非推奨になっています。
非推奨③の回避策
Fragmentのリリースノートには下記のように記載されています。
フラグメントの startActivityForResult()/onActivityResult() API と requestPermissions()/onRequestPermissionsResult() API は非推奨になりました。Activity Result API を使用してください。
Activityのリリースノート には下記のように記載されています。
ComponentActivity の startActivityForResult()/onActivityResult() API と onRequestPermissionsResult() API のサポートが終了しました。Activity Result API を使用してください。
ここでちょっと気になったのが、Fragmentの方は「非推奨になりました」で、Activityの方は「サポート終了しました」という日本語の違いだったのですが、英語で確認したところどちらも「deprecated」という単語を使っていたので、翻訳の仕方の違いだけのようでした。
Fragment、Activityともに Activity Result API
が代わりになるようです。
リリースノートでは Android Developersのサイト へのリンクが張ってあり、そこに Activity Result API
について説明されていました。
Android Developersのサイト を参考にして、サンプルコードを書いてみます。
まずは Activity Result API
を利用するためにFragmentとActivityのバージョンを更新する必要があります。
dependencies {
// 2021/02/11 修正:安定版がリリースされたので更新
implementation "androidx.activity:activity-ktx:1.2.0"
implementation "androidx.fragment:fragment-ktx:1.3.0"
}
試しにActivityとFragmentで onActivityResult()
を呼んでみたら、期待通り非推奨となっていました。
Activity Result API
を利用するためには、 FragmentとActivityで用意されている registerForActivityResult()
メソッドを利用すれば良いです。
※注意:2020/12/9時点だと、 Android Developers - アクティビティからの結果の取得 の日本語訳の情報が古いです。日本語訳のサイトでは prepareCall()
を利用するように述べられていますが、 Activity ver1.2.0-alpha04、Fragment ver1.3.0-alpha04のタイミングで、メソッド名が prepareCall()
から registerForActivityResult()
に変更になっています。他にも英語と日本語でところどころ違う箇所があるように見えたので、もしも Android Developers - アクティビティからの結果の取得 の日本語訳を参考にしながら動作しないところがあれば、言語を英語に切り替えて内容を確認することをおすすめします。まだ開発中のバージョンなので、こういった情報の整合性が取れていない状態はある程度はしょうがないですね。きっと一般で利用される頃には修正されていると思います。(安定版がリリースされましたが、2021/02/11時点だとまだ修正されていませんでした。)
val getContent = registerForActivityResult(GetContent()) { uri: Uri? ->
// Handle the returned Uri
}
registerForActivityResult()
メソッドは、
引数に ActivityResultContract
と ActivityResultCallback
の2つを受け取り、
ActivityResultLauncher
を返しています。
CallActivityResultApiSample.kt
の例と対応させると、
ActivityResultContract
が GetContent()
、
ActivityResultCallback
が { uri: Uri? -> 〜 }
、
ActivityResultLauncher
が getContent
です。
ActivityResultContract
はデフォルトで様々なクラスが用意されており、入力と出力に必要な情報を定義できます。独自クラスの定義も可能です。
ActivityResultLauncher
の launch()
を呼び出すことで、Activityが起動します。
起動されたActivityの結果を ActivityResultCallback
で受け取ります。
ここで、 Fragment#registerForActivityResult()
、ActivityResultLauncher#launch()
、ActivityResultCallback
がどのように連携されているか気になったので、実装を軽く確認してみました。
@MainThread
@NonNull
@Override
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
return prepareCallInternal(contract, new Function<Void, ActivityResultRegistry>() {
@Override
public ActivityResultRegistry apply(Void input) {
if (mHost instanceof ActivityResultRegistryOwner) {
return ((ActivityResultRegistryOwner) mHost).getActivityResultRegistry();
}
return requireActivity().getActivityResultRegistry();
}
}, callback);
}
public abstract class ActivityResultLauncher<I> {
public void launch(@SuppressLint("UnknownNullness") I input) {
launch(input, null);
}
}
public interface ActivityResultCallback<O> {
void onActivityResult(@SuppressLint("UnknownNullness") O result);
}
細かくは見てないですが、これらの実装から、
ActivityResultContract<I, O>
にある I
をもとにActivityを起動するためのメソッド ActivityResultLauncher#launch(I)
の引数(Input)が決まって、
ActivityResultContract<I, O>
にある O
をもとに、受け取る結果(Output)のクラスが変更されていることがイメージできました。
つまり、 ActivityResultContract
の実装クラスを入れ替えることで、どのような入力で、どのような結果を受け取りたいか、も変更することができるということです。
説明だけだと分かりづらいと思うので、もう少し具体的にどのように使うか例示したいと思います。
よく使いそうな例として、アプリ外部から結果を受け取る方法と、自身のアプリ内部で別Activityからの結果を受け取る方法の2つを取り上げてみました。
アプリ外部から結果を受け取る
ストレージから端末に保存されている画像のURIを取得する例を考えてみます。
URIを受け取ったところまでが確認したいことなので、受け取ったらURIをログ出力するだけのシンプルな仕様にします。
まずは非推奨である onActivityResult()
を利用したサンプルコードです。
class OldFragment : Fragment() {
private val REQUEST_CODE = 1
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
button.setOnClickListener {
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
}
startActivityForResult(intent, REQUEST_CODE)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Log.d("TAG", "uri = ${data?.data}")
}
}
}
この既存のコードに対して、 Activity Result API
を利用する場合は、 ActivityResultContract
の実装クラスとして GetContent を指定します。
class NewFragment : Fragment() {
private val activityResultLauncher = registerForActivityResult(GetContent()) { uri ->
Log.d("TAG", "uri = $uri")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
button.setOnClickListener {
activityResultLauncher.launch("image/*")
}
}
}
書いてみると結構シンプルですね。
また、 activityResultLauncher
をインスタンス変数として定義して初期化も行っているのですが、 registerForActivityResult()
はFragmentやActivityが生成されるより前から安全に呼び出すことができるから、問題ないそうです。
ただし、フラグメントまたはアクティビティの Lifecycle
が CREATED
になるまでは、ActivityResultLauncher
を起動することはできないので、 launch()
を呼び出すタイミングは注意してください。
自身のアプリ内部で別Activityからの結果を受け取る
次にアプリ内部の別のActivityを起動して、そのActivityから結果を受け取るサンプルを実装します。
まずは非推奨である onActivityResult()
を利用したサンプルコードです。
class OldFragment : Fragment() {
private val REQUEST_CODE = 1
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
button.setOnClickListener {
val intent = Intent(activity, OtherActivity::class.java)
startActivityForResult(intent, REQUEST_CODE)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
Log.d("TAG", "RESULT_OK")
}
}
}
この既存のコードに対して、 Activity Result API
を利用する場合は、 ActivityResultContract
の実装クラスとして StartActivityForResult を指定します。
class NewFragment : Fragment() {
private val activityResultLauncher = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
Log.d("TAG", "RESULT_OK")
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
button.setOnClickListener {
val intent = Intent(activity, OtherActivity::class.java)
activityResultLauncher.launch(intent)
}
}
}
ここでの result
は ActivityResult というクラスになっています。
ActivityResult は resultCode: Int
と data: Intent?
を利用できるので、これまでの onActivityResult()
と同じ引数が利用できるので、他と比べると利用のイメージがしやすいと感じました。
より丁寧に作るのであれば、 カスタム コントラクトを作成する にあるように、 ActivityResultContract
を拡張したクラスをカスタム実装したほうが良いと思います。
例えば、起動するActivityが固定で、受け取る結果をカスタムクラスにしたものも作れます。
class CustomResult(val isResultOk: Boolean = false)
class CustomActivityResultContract : ActivityResultContract<Unit, CustomResult>() {
// startActivityForResult()で起動するIntentを返す
override fun createIntent(context: Context, input: Unit?): Intent =
Intent(context, OtherActivity::class.java)
// 呼び元のクラスに結果だけ(今回の場合はCustomResult)を返す
override fun parseResult(resultCode: Int, intent: Intent?): CustomResult {
// intentから必要な要素を取り出して、CustomResultにセットすることも可能
return CustomResult(resultCode == Activity.RESULT_OK)
}
}
class NewFragment : Fragment() {
// 起動するActivityが呼び出し元では意識しなくて良くなる
private val activityResultLauncher = registerForActivityResult(CustomActivityResultContract()) { customResult ->
// Intentのキーなどを意識せず、customResultでアクセスできる
Log.d("TAG", "customResult.isResultOk = ${customResult.isResultOk}")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
button.setOnClickListener {
activityResultLauncher.launch() // activity-ktxの拡張関数を使えば、launchに引数を入れなくても呼べる
}
}
}
再利用性が高まっていて良い感じです。
動作確認環境
今回の記事を書くにあたって、利用していた動作確認環境についてもお伝えしておきます。
今回は Android Studio Arctic Fox Canary 2 を利用して、動作を確認していました。
12月1日に、Android Studio 4.2の次のバージョンにあたる、 Android Studio Arctic Fox
のCanary Versionがリリースされました。
Canary Version なのでまだ安定利用できるものではないですが、せっかくのタイミングなので、今回使ってみることにしました。
今回 Android Studio Arctic Fox を使ってはいますが、何か新機能を使って動作確認しているわけでもないので、下位バージョンでも問題なく動作すると思います。
余談ですが、Android Studio Arctic Fox はバージョン情報や、 Gradle Plugin の扱いに変更が入っており、これらも個人的にインパクトは大きかったです。(4.2の次が、4.3でも5.0でもないし、Arctic Foxというコードネームで呼ばれるようになったのも驚きでした。)
まだ本格的に利用することはないですが、気になる方は Googleからのこのあたりの記事 は目を通しておいて良いと思います。
さいごに
今回は非推奨となる Kotlin Android Extensions
、 Fragment#onActivityCreated()
、 startActivityForResult()/onActivityResult()
について紹介しました。
今回紹介した非推奨の機能の情報をざっくりまとめると下記の表になります。(2020/12/9時点情報)
非推奨となる機能 | 非推奨となった開発バージョン | 現在の安定リリースバージョン | いつから非推奨となるか |
---|---|---|---|
Kotlin Android Extensions | 1.4.20-M2 | 1.4.20 | すでに非推奨 |
Fragment#onActivityCreated() | 1.3.0-alpha02 | 1.2.5 | 今後 |
Fragment#startActivityForResult()/onActivityResult() | 1.3.0-alpha04 | 1.2.5 | 今後 |
Activity#startActivityForResult()/onActivityResult() | 1.2.0-alpha04 | 1.1.0 | 今後 |
startActivityForResult()/onActivityResult()
はまだ対応できないと思いますが、他の非推奨になる機能群は新しいバージョンに上げる前から対応することができます。
自分は今回紹介できなかったものも含め、非推奨の機能は新規に利用しないように、既存のコードを修正するときに該当機能があれば書き換えるように心がけています。(もちろんテストスコープ次第でそのタイミングでの既存コードの修正を見送ったりはします。)
2021/02/11追記:
安定版である Activity ver1.2.0 と Fragment ver1.3.0 がリリースされました。
記事を書いたときに作ったサンプルコードのバージョン指定をアップデートしてみて、さっと確認してみましたが、 Activity Result API
に関しては記事を書いた時点とは仕様変更はなさそうです。(あくまでサンプルコードでコーディングした範囲での確認です。)
一応、サンプルコードは こちら にあげておきました。
該当のライブラリをアップデートしたときや、非推奨の機能が動作しなくなってから慌てて対応するよりは、これから新しく作るとき、既存のコードを修正するときに、少しずつ修正しておいたほうが、将来対応するときに楽になり、バグのリスクも減るので、強くおすすめします。
また、今年非推奨になったものは他にも色々あります。
最新の情報をキャッチアップし、他の方(Android DeveloperやAndroid Studio)から非推奨のコードを書いて注意されることがないように、来年も頑張りましょう。
参考リンク
- Kotlin 1.4.20
- Kotlin Android Extensions の未来
- Android Developers - ビューバインディング
- Use view binding to replace findViewById
- Fragment リリースノート
- Stack Overflow - onActivityCreated is deprecated, how to properly use LifecycleObserver?
- Activity リリースノート
- Android Developers - アクティビティからの結果の取得
- Android Developers - 一般的なインテント
- Android Studio Arctic Fox Canary 2
- Android Studio Arctic Fox(2020.3.1)と Android Gradle プラグイン 7.0