- 投稿日:2021-06-23T22:26:38+09:00
SerializableのserialVersionUIDが原因で発生するInvalidClassExceptionの対処方法
serialVersionUIDとは serialVersionUIDはSerializableなオブジェクトに定義するバージョン番号のようなものです。データのシリアライズ/デシリアライズする際に、互換性を確認するためにこのserialVersionUIDを使用します。 また、serialVersionUIDを定義しない場合、自動で計算されることになります。 InvalidClassException これは、Serializableなオブジェクトをデシリアライズする際、serialVersionUIDが異なる場合に発生します。 java.io.InvalidClassException com.sample.data.SampleSerializableData; Incompatible class (SUID): com.sample.data.SampleSerializableData: static final long serialVersionUID =4092797215335171425L; but expected com.sample.data.SampleSerializableData: static final long serialVersionUID =5140160798577733211L; 今回、SerializableなJavaクラスをKotlin化した際、誤ってserialVersionUIDを削除してしまったため、アプリバージョンアップの際にデータのデシリアライズができなくなってしまいました。 回避方法 今回、既にアプリのバージョンアップを何度か挟んでしまっていたため、serialVersionUIDを復活させてしまうと、今後はKotlin版のSerializableオブジェクトがデシリアライズできなくなってしまいます。そのため、以下の記事を参考にデシリアライザをカスタマイズすることで回避しました。 カスタムInputStream ObjectInputStream をextendsして readClassDescriptorをoverrideしています。戻り値の ObjectStreamClass はSerializationの情報を持つオブジェクトです。クラス名とserialVersionUIDを含んでいます。 内容はコード内のコメントに記載しました。 CustomInputStream class CustomInputStream(inputStream: InputStream) : ObjectInputStream(inputStream) { @Throws(IOException::class, ClassNotFoundException::class) override fun readClassDescriptor(): ObjectStreamClass? { // streamからクラス記述子を読み出します val resultClassDescriptor = super.readClassDescriptor() ?: return null // Class名を取得 val localClass = try { Class.forName(resultClassDescriptor.name) } catch (e: ClassNotFoundException) { return resultClassDescriptor } // クラス名を、現在のアプリバイナリの中から探します val localClassDescriptor = ObjectStreamClass.lookup(localClass) // only if class implements serializable if (localClassDescriptor != null) { // 現在のアプリバイナリと、streamの中のそれぞれのserialVersionUIDを比較します val localSUID = localClassDescriptor.serialVersionUID val streamSUID = resultClassDescriptor.serialVersionUID // serialVersionUIDが異なる場合 if (streamSUID != localSUID) { // streamのserialVersionUIDが正として、localClassDescriptorを使います return localClassDescriptor } } return resultClassDescriptor } } 利用するとき val objectInputStream = CustomInputStream(byteArrayInputStream) val data = objectInputStream.readObject() as? SampleSerializableData かなり強引な技ではあるので、どうにも回避方法がなくなってしまった場合の奥の手として使うのが良いと思います。また、利用箇所も限定的にして、他のSerializableには影響が出ないようにしましょう。
- 投稿日:2021-06-23T17:28:46+09:00
【Kotlin研修15日目】リサイクラービューの実装
リサイクラービュー(RecyclerView) 参考1: RecyclerViewでのリスト作成 参考2: 研修14日目(CoordinatorLayout) CoordinatorLayoutでスクロール連動させるために必要なNestedScrollingChildインタフェースを実装したListView。 リサイクラービューが保持するレイアウトマネージャを通じてリストのレイアウトを決定し、 アダプタクラスによって生成されるビューホルダを通じて、アイテム内ビューにリストデータが埋め込まれる。 RecyclerViewとListView 参考: 研修2日目 リスト形式のビューを提供するRecyclerViewとListViewの違いは、以下の通り。 ビュー スクロール連動 レイアウトの柔軟性 アダプタクラス 区切り線 リスナインタフェース RecyclerView o o x x o ListView x x o o o レイアウトマネージャ リサイクラービューのレイアウト配置とアイテム管理を行うクラス。 標準でLinearLayoutManager・GridLayoutManager・StaggeredGridLayoutManagerの3つのレイアウトマネージャが用意されている。 また、RecyclerView.LayoutManagerを実装することで、独自定義したレイアウトマネージャを作成することもできる。 LinearLayoutManager アイテムを縦のリストに並べるレイアウトマネージャ。 GridLayoutManager アイテムを格子状に並べるレイアウトマネージャ。 StaggeredGridLayoutManager アイテムをスタッガード格子状に並べるレイアウトマネージャ。 アダプタ 参考: 研修2日目(Adapter) アクティビティクラスで記述したリストデータをビューホルダが管理するビューに紐づける(=バインド)、 RecyclerView.Adapterクラスの実装クラス。 ListViewとは異なり、リサイクラービューにはアダプタクラスが用意されていないため、 RecyclerView.Adapterクラスを実装したアダプタクラスを独自に定義する必要がある。 RecyclerView.Adapter リサイクラービューのアダプタを定義する抽象クラス。 アイテム内ビューを管理するビューホルダを生成するメソッド(=onCreateViewHolder())、 ビューホルダが管理するビューにリストデータをバインドするメソッド(=onBindViewHolder())、 また、リサイクラービューが表示するリストデータの件数を取得するメソッド(=getItemCount())を抽象的に定義する。 ビューホルダ リストデータを表示するリストアイテム内のビューを管理する、RecyclerView.ViewHolderクラスの実装クラス。 RecyclerView.ViewHolder リストデータを表示するリストアイテム内のビューを管理する抽象クラス。 リサイクラービューの実装 リサイクラービューを実装する手順は、以下の通り。 リストデータを定義・返却するメソッドを記述 「リストデータを表示するビュー」を定義するビューホルダクラスの作成 リストデータをリサイクラービューに表示するアダプタクラスの作成 リサイクラービューにレイアウトマネージャとアダプタをセット リストデータの定義 リサイクラービューに表示するリストデータを定義する。 サンプルコード MainActivity.kt // RecyclerViewのItemのリスト定義・作成 private fun createSetMealList(): MutableList<MutableMap<String, Any>> { // MutableList val menuList: MutableList<MutableMap<String, Any>> = mutableListOf() // --- リストデータの定義開始 --- // MutableMapの定義 var menu = mutableMapOf<String, Any>( "name" to "item 1", "price" to 750, "desc" to "This description is about item 1." ) // MutableListに追加 menuList.add(menu) // MutableMapの定義 menu = mutableMapOf<String, Any>( "name" to "item 2", "price" to 900, "desc" to "This description is about item 2." ) // MutableListに追加 menuList.add(menu) ... // 以降繰り返し // --- リストデータの定義終了 --- // 定義したMutableListを返却 return menuList } ビューホルダの作成 リストデータを表示するビューを定義する、RecyclerView.ViewHolderの実装クラス(=ビューホルダ)を作成する。 ただし、RecyclerView.ViewHolderのクラスコンストラクタは引数にViewオブジェクトをとるため、 ビューホルダのクラスコンストラクタも引数にViewオブジェクトをとるように定義する必要がある。 なお、ここでのViewオブジェクトとは、リサイクラービューのアイテムビューを指す。 定義 RecyclerView.ViewHolder(@NonNull itemView: View) サンプルコード MainActivity.kt // アイテム内ビュー(=TextView)を保持するビューホルダ private inner class RecyclerListViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { // Item内に表示される1つ目のTextView var _tvMenuNameRow: TextView // Item内に表示される2つ目のTextView var _tvMenuPriceRow: TextView // 初期化時に呼び出される処理(=イニシャライザ) init { // Item内のTextView // -> アダプタクラスのonBindViewHolder()メソッドによってリストデータが反映される _tvMenuNameRow = itemView.findViewById(R.id.tvMenuNameRow) _tvMenuPriceRow = itemView.findViewById(R.id.tvMenuPriceRow) } } アダプタクラスの作成 ビューホルダが保持するビューにリストデータを紐づける、RecyclerView.Adapterの実装クラス(=アダプタクラス)を作成する。 アイテム内ビューを管理するビューホルダを生成するonCreateViewHolder()メソッドでは、 ビューホルダのクラスコンストラクタの引数にアイテムビューが必要となるため、 リサイクラービューのレイアウトファイルのインフレートを行う必要がある。 インフレート(inflate) 参考: 研修4日目 レイアウトファイル(=XMLファイル)をJavaオブジェクトのビューとして実体化すること。 LayoutInflaterクラスのfrom()メソッドを用いてコンテキストからLayoutInflaterオブジェクトを取得し、 inflate()メソッドを用いてインフレートを行う。 LayoutInflater UI部品をインフレートするメソッドを定義するクラス。 定義 参考: LayoutInflater // LayoutInflaterオブジェクトの取得 LayoutInflater.from(context: Context!): LayoutInflater! // パラメータ // context: LayoutInflaterを利用するコンテキスト // レイアウトファイルのインフレート LayoutInflater.inflate( resource: Int, root: ViewGroup? attachToRoot: Boolean ): View! // パラメータ // resource: インフレートするレイアウトファイル名のID(R値) // root: レイアウトファイルの実体化先となるビューグループ // attachToRoot: rootパラメータのビューグループを親レイアウト部品として設定するかどうか 定義 RecyclerView.Adapterの抽象メソッド // RecyclerViewによって最初に呼び出される処理 @NonNull RecyclerView.Adapter.onCreateViewHolder( @NonNull parent: ViewGroup, viewType: Int ): <ビューホルダ> // パラメータ // parent: ビューホルダが保持するビューの親ビューグループ(=アイテムビュー) // viewType: アイテムビューのビュータイプを識別する整数値 // RecyclerViewがアダプタからビューホルダを受け取った際にRecyclerViewから呼び出される処理 RecyclerView.Adapter.onBindViewHolder( @NonNull holder: <ビューホルダ>, position: Int ): Unit // パラメータ // holder: ビューホルダオブジェクト // position: リストデータがバインドされるアイテムのポジション番号(≒行番号) // リストデータの項目数を取得 RecyclerView.Adapter.getItemCount(): Int サンプルコード MainActivity.kt // 独自定義するアダプタクラス // <- RecyclerView.Adapterクラスの実装クラス // <- クラスコンストラクタは一度のみ呼ばれ、クラス内でしか利用しない(=機密性を持たせる)ため、 // "private val"を付与 private inner class RecyclerListAdapter( private val _listData: MutableList<MutableMap<String, Any>> ): RecyclerView.Adapter<RecyclerListViewHolder>() { // RecyclerViewによって最初に呼び出される処理 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerListViewHolder { // LayoutInflaterオブジェクト val inflater = LayoutInflater.from(this@MainActivity) // レイアウトファイルのインフレート(=アイテムビュー) val view = inflater.inflate(R.layout.row, parent, false) // ビューホルダの生成 val holder = RecyclerListViewHolder(view) // ビューホルダを返却 return holder } // RecyclerViewがアダプタからビューホルダを受け取った際にRecyclerViewから呼び出される処理 override fun onBindViewHolder(holder: RecyclerListViewHolder, position: Int) { // リストデータのアイテム val item = _listData[position] // 指定したキーと一致するリストデータ val menuName = item["name"] as String val menuPrice = item["price"] as Int // Int型 → String型 への変換 val menuPriceStr = menuPrice.toString() // ビューホルダが保持するビューにデータを反映 holder._tvMenuNameRow.text = menuName holder._tvMenuPriceRow.text = menuPriceStr } // リストデータの項目数を取得 override fun getItemCount(): Int { // リストデータの項目数を返却 // <- リストデータはクラスコンストラクタの引数から取得 return _listData.size } } レイアウトマネージャ・アダプタのセット 参考: 研修2日目 レイアウトマネージャはRecyclerViewオブジェクトのlayoutManagerプロパティ、 アダプタはRecyclerViewオブジェクトのadapterプロパティにセットする。 定義 // LinearLayoutManagerオブジェクトの生成 LinearLayoutManager(context: Context!) // パラメータ // context: LinearLayoutManagerを利用するコンテキスト サンプルコード MainActivity.kt class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... // RecyclerView val lvMenu = findViewById<RecyclerView>(R.id.lvMenu) // レイアウト配置とリストアイテムを管理するLinearLayoutManager(=レイアウトマネージャ) val layout = LinearLayoutManager(this@MainActivity) // RecyclerViewにLinearLayoutManagerをセット lvMenu.layoutManager = layout // リストデータ val menuList = createSetMealList() // リストデータを紐づけるRecyclerListAdapter val adapter = RecyclerListAdapter(menuList) // RecyclerViewにRecyclerListAdapterをセット lvMenu.adapter = adapter } ... } LinearLayoutManagerのリサイクラービューへの区切り線の追加 レイアウトマネージャにLinearLayoutManagerを採用するリサイクラービューは、 リストアイテム間の区切り線(=Divider)を標準で用意していないため、 区切り線を表示したい場合は、DividerItemDecorationオブジェクトを利用する必要がある。 DividerItemDecoration LinearLayoutManagerを採用するリサイクルビューへの区切り線を追加する、 RecyclerView.ItemDecorationクラスの実装クラス。 RecyclerView.ItemDecoration アイテムビューの装飾スタイルを定義する抽象クラス。 定義 // DividerItemDecorationオブジェクトの生成 DividerItemDecoration(context: Context!, orientation: Int) // パラメータ // context: DividerItemDecorationを利用するコンテキスト // orientation: 区切り線の方向(=垂直または水平方向)を表すDividerItemDecorationクラス定数 // <- LinearLayoutManagerのorientationプロパティを利用するのが一般的 // リサイクラービューへの区切り線の追加 RecyclerView.addItemDecoration( @NonNull decor: RecyclerView.ItemDecoration ): Unit // パラメータ // decor: アイテムビューに追加するItemDecorationオブジェクト 区切り線の方向を表すDividerItemDecorationクラス定数 定数名 区切り線の方向 HORIZONTAL 水平 VERTICAL 垂直 サンプルコード MainActivity.kt // RecyclerView val lvMenu = findViewById<RecyclerView>(R.id.lvMenu) // 区切り線を表示するDividerItemDecorationオブジェクト val decorator = DividerItemDecoration(this@MainActivity, layout.orientation) // リサイクラービューへの区切り線の追加 lvMenu.addItemDecoration(decorator) リサイクラービューへのリスナ定義 リサイクラービューはListViewとは異なり、専用のリスナインタフェースが用意されていないため、 View.OnClickListenerインタフェースを実装したリスナクラスを独自定義する必要がある。 また、アイテムビューに対してリスナとしてリスナクラスオブジェクトをセットする必要があるため、 アダプタクラスのonCreateViewHolder()メソッド内でインフレートしたレイアウトファイル(=アイテムビュー)に対して、リスナ定義を行う。 サンプルコード MainActivity.kt class MainActivity : AppCompatActivity() { // アダプタクラス // <- RecyclerView.Adapterクラスの実装クラス private inner class RecyclerListAdapter( private val _listData: MutableList<MutableMap<String, Any>> ): RecyclerView.Adapter<RecyclerListViewHolder>() { // RecyclerViewによって最初に呼び出される処理 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerListViewHolder { ... // LayoutInflaterオブジェクト val inflater = LayoutInflater.from(this@MainActivity) // レイアウトファイルのインフレート(=アイテムビュー) val view = inflater.inflate(R.layout.row, parent, false) // インフレートしたアイテムビューをリスナとしてセット view.setOnClickListener(ItemClickListener()) ... } ... } // リサイクラービューのアイテムの"タップ"イベントを検知するリスナクラス // <- View.OnClickListenerインタフェースの実装クラス private inner class ItemClickListener: View.OnClickListener { // "タップ"イベント検知時に呼び出される処理(イベントハンドラ) override fun onClick(view: View?) { ... // タップイベント検知時の処理 } } }
- 投稿日:2021-06-23T14:36:31+09:00
対策例:startActivityForResult(Intent,int) in Fragment has been deprecated
背景 startActivityForResult(Intent,int) での画像選択コードが deprecated と言われるので、修正前後のコード例を残しておく。 コード 修正前 public class MainActivity extends AppCompatActivity implements View.OnClickListener { private final static int RESULT_PICK_IMAGEFILE = 1000; @Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { super.onActivityResult(requestCode, resultCode, resultData); if (requestCode == RESULT_PICK_IMAGEFILE && resultCode == RESULT_OK) { if (resultData != null) { Uri uri = resultData.getData(); // Do something here. } } } private void onSelectImage() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("image/*"); intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); startActivityForResult(intent, RESULT_PICK_IMAGEFILE); // deprecated!! } } 修正後 public class MainActivity extends AppCompatActivity implements View.OnClickListener { ActivityResultLauncher<Intent> _launcherSelectSingleImage = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() { @Override public void onActivityResult(ActivityResult result) { if (result.getResultCode() == RESULT_OK) { Intent resultData = result.getData(); if (resultData != null) { Uri uri = resultData.getData(); // Do something here. } } } }); private void onSelectImage() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("image/*"); intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true); Intent chooserIntent = Intent.createChooser(intent, "単一画像の選択"); _launcherSelectSingleImage.launch(chooserIntent); } } 参考リンク android - Java solution for "startActivityForResult(Intent,int) in Fragment has been deprecated" when opening external URL? - Stack Overflow startActivityForResult()とonActivityResult()がDeprecatedになった対処法 / startActivityForResult() and onActivityResult() has Deprecated - Speaker Deck