1
/
5

【Kotlin】MVVM + Room + CalendarView でカレンダーアプリを作成してみた

こんにちは!ナディアのエンジニア Sです。
今回は自分自身の技術取得のために、MVVMでカレンダーでスケジュール管理する簡単なアプリを作成しました。
Roomを使ったCRUD(主に作成(Create)、読み出し(Read))の実装方法や、MVVMアーキテクチャのシンプルな実装方法を知ることができる内容となっています。

何をするアプリか

カレンダーに自分が登録したスケジュール、ToDoの確認などを行うためのアプリです。

カレンダーアプリの設計について

アーキテクチャーは概ねAndroid Developersにて推奨されているアーキテクチャーに従っています。

アプリ アーキテクチャ ガイド | Android デベロッパー | Android Developers
このガイドでは、品質の高い堅牢なアプリを作成するためのおすすめの方法と 推奨アーキテクチャ を紹介します。 このページは、読者が Android フレームワークの基本について熟知していることを前提としています。Android アプリの開発経験がない場合は、 注: Android の基本コース でアプリの開発の基本を学習したうえで、このガイドで取り上げているコンセプトの詳細を確認してください。 標準的な Android アプリは、さまざまな アプリ コンポーネント( アクティビティ、 フラグメント、 サービ
https://developer.android.com/jetpack/guide?hl=ja#recommended-app-arch

デザインに関しては、さまざまなテーマに関するイメージやデザインのアイデアを共有するプラットフォームのPinterestから検索したdribbbleのデザインを参考にしました。

Pinterest
Discover recipes, home ideas, style inspiration and other ideas to try.
https://www.pinterest.jp/

一部のデザイン作成には、自分でも画像作成が出来るfigmaを利用しました。
Material 3 Design Kitにも対応し、コミュニティーもあるので参考になる画像も見つかります。

Material 3 Design Kit | Figma Community
Introducing Material Design 3 Meet Material Design 3, Material Design's most personal design system yet. The Material 3 Design Kit provides a comprehensive introduction to the design system, with styles and components to help you get started. Visualize dy
https://www.figma.com/community/file/1035203688168086460/material-3-design-kit

カレンダー機能は、サードパーティーのライブラリーを使用しています。
これを選択した理由は、カレンダーの機能を1から全部自前で作成するとコストがかかるため、デザインや機能、ソースなどを見てカスタムしやすいと思い選択しました。

RoomのCRUD(主に作成(Create)、読み出し(Read))、StateFlowを利用したデータ取得から画面表示

DB機能としてRoomライブラリを使用します。
Database、Entity、DAO(Data Access Object)を用いてDB(SQLite)上でデータを管理します。 クラスやインターフェースなどにこれらのアノテーションをつけることで、Roomが提供する各コンポーネントとして振舞うようになります。
Repositoryを利用しているのはDIすることで以下のような利点があります。ただし、このアプリに関してはテーブル、画面数も多くないのでほとんど効果はないです。 DIを実装し、あるデータの処理に関するメソッドを1つのRepositoryに集約することで、そのデータを扱う各ViewModelの肥大化を抑えることができます。

参考サイト

【Android】はじめてのRoom - Qiita
【Android】分かった気になれる!アーキテクチャ・MVVM概説 ではアーキテクチャ・MVVMの概要をコードを記述せずに概念のみ説明してみました。その後の投稿では、実践編として各ライブラリを実際...
https://qiita.com/iTakahiro/items/7e0d63140ae4dac10d18

DBのテーブル定義

Entity(エンティティクラス)の作成

  @Entity(tableName = "schedule")                        
data class Schedule(

@PrimaryKey(autoGenerate = true)
@ColumnInfo
var id: Int = 0,

@ColumnInfo
var memo: String = "",

@ColumnInfo
var time: Date = Date(),

//0ー>すべて、1ー>スケジュール、2ー>ToDo
@ColumnInfo
var type: Int = 0,

) : Serializable {

} @Entity(tableName = "schedule")
data class Schedule(

@PrimaryKey(autoGenerate = true)
@ColumnInfo
var id: Int = 0,

@ColumnInfo
var memo: String = "",

@ColumnInfo
var time: Date = Date(),

//0ー>すべて、1ー>スケジュール、2ー>ToDo
@ColumnInfo
var type: Int = 0,

) : Serializable {

}

参考サイト

Room を使用してローカル データベースにデータを保存する | デベロッパー向け Android | Android Developers
Room ライブラリを使用してデータを簡単に永続化する方法について学びます。
https://developer.android.com/training/data-storage/room?hl=ja

DBクエリー

DAO(Data Access Object)の作成

   @Dao                                                
interface ScheduleDao {

@Query("select * from schedule")
suspend fun selectScheduleList(): List<Schedule>

@Query("select * from schedule where type = :type order by time desc")
suspend fun selectScheduleListByOrder(type: Int): List<Schedule>

@Query("select * from schedule where type = :type order by time asc")
suspend fun selectScheduleListByOrderAsc(type: Int): List<Schedule>

@Query("SELECT * FROM schedule WHERE time = :targetDate")
suspend fun selectScheduleListOnDate(targetDate: Date): List<Schedule>


@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertResult(data: Schedule): Long

}

@Dao
interface ScheduleDao {

@Query("select * from schedule")
suspend fun selectScheduleList(): List<Schedule>

@Query("select * from schedule where type = :type order by time desc")
suspend fun selectScheduleListByOrder(type: Int): List<Schedule>

@Query("select * from schedule where type = :type order by time asc")
suspend fun selectScheduleListByOrderAsc(type: Int): List<Schedule>

@Query("SELECT * FROM schedule WHERE time = :targetDate")
suspend fun selectScheduleListOnDate(targetDate: Date): List<Schedule>


@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertResult(data: Schedule): Long

}

DBの設定

Roomデータベース(Database)の追加

 @Database(                                                
//テーブルを追加する場合、entitiesにテーブルクラスを追加する
entities = [
Schedule::class
],
version = AppDatabase.DATABASE_VERSION
)
@TypeConverters(DateConverters::class)
abstract class AppDatabase() : RoomDatabase() {

// DAO
abstract fun ScheduleDao() : ScheduleDao

//クラス内に作成されるシングルトンのことです。
//クラスのインスタンスを生成せずにクラスのメソッドにアクセスすることができ、
//データベースが1つしか存在しないようにできます。
companion object {

const val DATABASE_VERSION: Int = 1

@Volatile
private var INSTANCE: AppDatabase? = null

fun getDatabase(context: Context): AppDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"calendar_app_database"
)
// Wipes and rebuilds instead of migrating if no Migration object.
// Migration is not part of this codelab.
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
// return instance
instance
}
}

}

}
@Database(
//テーブルを追加する場合、entitiesにテーブルクラスを追加する
entities = [
Schedule::class
],
version = AppDatabase.DATABASE_VERSION
)
@TypeConverters(DateConverters::class)
abstract class AppDatabase() : RoomDatabase() {

// DAO
abstract fun ScheduleDao() : ScheduleDao

//クラス内に作成されるシングルトンのことです。
//クラスのインスタンスを生成せずにクラスのメソッドにアクセスすることができ、
//データベースが1つしか存在しないようにできます。
companion object {

const val DATABASE_VERSION: Int = 1

@Volatile
private var INSTANCE: AppDatabase? = null

fun getDatabase(context: Context): AppDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"calendar_app_database"
)
// Wipes and rebuilds instead of migrating if no Migration object.
// Migration is not part of this codelab.
.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
// return instance
instance
}
}

}

}

Repositoryの作成 - DI機能(Dependency Injection)

  class ScheduleRepositoryRoom(                                                        
private val scheduleDao: ScheduleDao,

) {

//メモ一覧を取得する
suspend fun selectScheduleList(): List<Schedule>{
return scheduleDao.selectScheduleList()
}

//メモの種類を指定しメモ一覧を取得する
suspend fun selectScheduleListByOrder(type: Int): List<Schedule>{
return scheduleDao.selectScheduleListByOrder(type)
}

//メモの種類を指定しメモ一覧を取得する
suspend fun selectScheduleListByOrderAsc(type: Int): List<Schedule>{
return scheduleDao.selectScheduleListByOrderAsc(type)
}

//日付に紐づくメモ一覧を取得する
suspend fun selectScheduleListOnDate(targetDate: Date): List<Schedule>{
return scheduleDao.selectScheduleListOnDate(targetDate)
}

//登録したメモを追加する
suspend fun insertResult(data: Schedule): Long{
return scheduleDao.insertResult(data)
}

}
class ScheduleRepositoryRoom(
private val scheduleDao: ScheduleDao,

) {

//メモ一覧を取得する
suspend fun selectScheduleList(): List<Schedule>{
return scheduleDao.selectScheduleList()
}

//メモの種類を指定しメモ一覧を取得する
suspend fun selectScheduleListByOrder(type: Int): List<Schedule>{
return scheduleDao.selectScheduleListByOrder(type)
}

//メモの種類を指定しメモ一覧を取得する
suspend fun selectScheduleListByOrderAsc(type: Int): List<Schedule>{
return scheduleDao.selectScheduleListByOrderAsc(type)
}

//日付に紐づくメモ一覧を取得する
suspend fun selectScheduleListOnDate(targetDate: Date): List<Schedule>{
return scheduleDao.selectScheduleListOnDate(targetDate)
}

//登録したメモを追加する
suspend fun insertResult(data: Schedule): Long{
return scheduleDao.insertResult(data)
}

}

ViewModelの作成

     class DialogViewModel(                                                                                
private val repository: ScheduleRepositoryRoom,
) : ViewModel() {

//メニュー登録のダイアログからのコールバック
val state = MutableLiveData<DialogState<SimpleDialogFragment>>()

//メニュー登録のダイアログからのコールバック
private val _purchaseState = MutableLiveData<PurchaseState?>()
val purchaseState: LiveData<PurchaseState?> get() = _purchaseState


//一般的に、クラスのカプセル化を意識して、MutableLiveDataプロパティを非公開にし、LiveDataプロパティのみを公開します。
//StateFlow
private val _scheduleList: MutableStateFlow<Resource<List<Schedule>>>
= MutableStateFlow(Resource.Loading())
val scheduleList: StateFlow<Resource<List<Schedule>>>
get() = _scheduleList.asStateFlow()



/** * DBにメモを追加する */
fun insertScheduleData(memo: String, time: String, type: Int){
_purchaseState.value = PurchaseState.PURCHASE_IN_PROGRESS

viewModelScope.launch {

try {
val schedule = Schedule().apply {
this.id = 0
this.memo = memo

val replaceDate: String = time.replace("-", "/")

val formatter: DateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.JAPANESE)
val date: Date = formatter.parse(replaceDate) as Date
this.time = date

val formatter2: DateFormat = SimpleDateFormat("yyyy/MM", Locale.JAPANESE)
val date2: Date = formatter2.parse(replaceDate) as Date
this.time2 = date2

this.type = type
}
val count: Long = repository.insertResult(schedule)

_purchaseState.postValue(PurchaseState.PURCHASE_SUCCESSFUL)

}catch (e: Exception){
_purchaseState.value = PurchaseState.PURCHASE_ERROR

}
}

}



/** * 日付(年月)に紐づくスケジュール一覧を取得する。 */
fun selectScheduleListOnDate(targetDate: Date){

viewModelScope.launch {
//データ取得
try {
//これが無いと、呼び出し側の lifecycle.repeatOnLifecycle が呼ばれない
_scheduleList.value = Resource.Success(repository.selectScheduleListOnDate(targetDate))
}catch (e: Exception){
println("### e : " +e.message.toString())
_scheduleList.value = Resource.Error(e.message.toString())
}
}

}

////////////////////////////////////////////////////////////
// inner class
////////////////////////////////////////////////////////////
class DialogViewModelFactory(
private val repository: ScheduleRepositoryRoom,

) : ViewModelProvider.NewInstanceFactory() {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(DialogViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return DialogViewModel(repository) as T
}
throw IllegalArgumentException("!!! Unknown DialogViewModel class !!!")
}

}

}
class DialogViewModel(
private val repository: ScheduleRepositoryRoom,
) : ViewModel() {

//メニュー登録のダイアログからのコールバック
val state = MutableLiveData<DialogState<SimpleDialogFragment>>()

//メニュー登録のダイアログからのコールバック
private val _purchaseState = MutableLiveData<PurchaseState?>()
val purchaseState: LiveData<PurchaseState?> get() = _purchaseState


//一般的に、クラスのカプセル化を意識して、MutableLiveDataプロパティを非公開にし、LiveDataプロパティのみを公開します。
//StateFlow
private val _scheduleList: MutableStateFlow<Resource<List<Schedule>>>
= MutableStateFlow(Resource.Loading())
val scheduleList: StateFlow<Resource<List<Schedule>>>
get() = _scheduleList.asStateFlow()



/** * DBにメモを追加する */
fun insertScheduleData(memo: String, time: String, type: Int){
_purchaseState.value = PurchaseState.PURCHASE_IN_PROGRESS

viewModelScope.launch {

try {
val schedule = Schedule().apply {
this.id = 0
this.memo = memo

val replaceDate: String = time.replace("-", "/")

val formatter: DateFormat = SimpleDateFormat("yyyy/MM/dd", Locale.JAPANESE)
val date: Date = formatter.parse(replaceDate) as Date
this.time = date

val formatter2: DateFormat = SimpleDateFormat("yyyy/MM", Locale.JAPANESE)
val date2: Date = formatter2.parse(replaceDate) as Date
this.time2 = date2

this.type = type
}
val count: Long = repository.insertResult(schedule)

_purchaseState.postValue(PurchaseState.PURCHASE_SUCCESSFUL)

}catch (e: Exception){
_purchaseState.value = PurchaseState.PURCHASE_ERROR

}
}

}



/** * 日付(年月)に紐づくスケジュール一覧を取得する。 */
fun selectScheduleListOnDate(targetDate: Date){

viewModelScope.launch {
//データ取得
try {
//これが無いと、呼び出し側の lifecycle.repeatOnLifecycle が呼ばれない
_scheduleList.value = Resource.Success(repository.selectScheduleListOnDate(targetDate))
}catch (e: Exception){
println("### e : " +e.message.toString())
_scheduleList.value = Resource.Error(e.message.toString())
}
}

}

////////////////////////////////////////////////////////////
// inner class
////////////////////////////////////////////////////////////
class DialogViewModelFactory(
private val repository: ScheduleRepositoryRoom,

) : ViewModelProvider.NewInstanceFactory() {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(DialogViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return DialogViewModel(repository) as T
}
throw IllegalArgumentException("!!! Unknown DialogViewModel class !!!")
}

}

}

Viewの作成 - 追加機能(一部抜粋)

      class SimpleDialogFragment() : DialogFragment() {                                                                        

private lateinit var viewModel: DialogViewModel


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

//ViewModelProviderの第一引数にrequireActivityを指定してあげると
// Fragmentを使っているActivityでも同じViewModelのインスタンスをシェアできる。
viewModel = ViewModelProvider(requireActivity(), DialogViewModel.DialogViewModelFactory(
(activity?.application as CalendarAppApplication).scheduleRepositoryRoom,
)).get(DialogViewModel::class.java)

}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {

val v = inflater.inflate(R.layout.fragment_simple_dialog, container, false)

purchaseButton.setOnClickListener {
//スケジュール登録を行う。
viewModel.insertScheduleData(memoEditText.text.toString(), date, type)
observePurchaseState()
}

cancelButton.setOnClickListener {
viewModel.state.value = DialogState.Cancel(this@SimpleDialogFragment)
dismissAllowingStateLoss()
}

return v
}

override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)

viewModel.state.value = DialogState.Cancel(this@SimpleDialogFragment)

}

/** * ダイアログのコールバック処理 */
private fun observePurchaseState(){
viewModel.purchaseState.observe(this) { purchaseState ->
when (purchaseState) {
PurchaseState.PURCHASE_IN_PROGRESS -> {
purchaseButton.isEnabled = false
showProgressBar()
}

PurchaseState.PURCHASE_SUCCESSFUL -> {
hideProgressBar()
// The purchase was successful! Show a message and dismiss the dialog.
Toast.makeText(requireContext(), R.string.purchase_successful, Toast.LENGTH_SHORT).show()
viewModel.state.value = DialogState.Ok(this@SimpleDialogFragment)
dismissAllowingStateLoss()
}

PurchaseState.PURCHASE_ERROR -> {
hideProgressBar()
purchaseButton.isEnabled = true // enable so that the user can try again
Toast.makeText(requireContext(), R.string.purchase_error, Toast.LENGTH_SHORT).show()
}

else -> {
//nothing
}
}
}
}
}
class SimpleDialogFragment() : DialogFragment() {

private lateinit var viewModel: DialogViewModel


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

//ViewModelProviderの第一引数にrequireActivityを指定してあげると
// Fragmentを使っているActivityでも同じViewModelのインスタンスをシェアできる。
viewModel = ViewModelProvider(requireActivity(), DialogViewModel.DialogViewModelFactory(
(activity?.application as CalendarAppApplication).scheduleRepositoryRoom,
)).get(DialogViewModel::class.java)

}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {

val v = inflater.inflate(R.layout.fragment_simple_dialog, container, false)

purchaseButton.setOnClickListener {
//スケジュール登録を行う。
viewModel.insertScheduleData(memoEditText.text.toString(), date, type)
observePurchaseState()
}

cancelButton.setOnClickListener {
viewModel.state.value = DialogState.Cancel(this@SimpleDialogFragment)
dismissAllowingStateLoss()
}

return v
}

override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog)

viewModel.state.value = DialogState.Cancel(this@SimpleDialogFragment)

}

/** * ダイアログのコールバック処理 */
private fun observePurchaseState(){
viewModel.purchaseState.observe(this) { purchaseState ->
when (purchaseState) {
PurchaseState.PURCHASE_IN_PROGRESS -> {
purchaseButton.isEnabled = false
showProgressBar()
}

PurchaseState.PURCHASE_SUCCESSFUL -> {
hideProgressBar()
// The purchase was successful! Show a message and dismiss the dialog.
Toast.makeText(requireContext(), R.string.purchase_successful, Toast.LENGTH_SHORT).show()
viewModel.state.value = DialogState.Ok(this@SimpleDialogFragment)
dismissAllowingStateLoss()
}

PurchaseState.PURCHASE_ERROR -> {
hideProgressBar()
purchaseButton.isEnabled = true // enable so that the user can try again
Toast.makeText(requireContext(), R.string.purchase_error, Toast.LENGTH_SHORT).show()
}

else -> {
//nothing
}
}
}
}
}

Viewの作成 - 表示機能(一部抜粋)

        class CalendarFragment : Fragment() {                                                                

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

//ViewModelProviderの第一引数にrequireActivityを指定してあげると
// Fragmentを使っているActivityでも同じViewModelのインスタンスをシェアできる。
dialogViewModel = ViewModelProvider(requireActivity(), DialogViewModel.DialogViewModelFactory(
(activity?.application as CalendarAppApplication).scheduleRepositoryRoom,
)).get(DialogViewModel::class.java)

//ダイアログからのコールバック処理
dialogViewModel.state.observe(this) {
when (it) {
is DialogState.Ok -> {
//画面に表示するデータを取得する
dialogViewModel.selectScheduleListOnDate2(date)
}

is DialogState.Cancel -> {
}

else -> {

}
}
} //dialogViewModel


}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {

//監視
viewLifecycleOwner.lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
dialogViewModel.scheduleList.collect(){
when(it){
is Resource.Loading -> {
}

is Resource.Success -> {
//ここで取得したデータを画面に表示する
}

is Resource.Error -> {
}

else -> {
}
}
}
}
} //viewLifecycleOwner


return binding.root

}

} class CalendarFragment : Fragment() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

//ViewModelProviderの第一引数にrequireActivityを指定してあげると
// Fragmentを使っているActivityでも同じViewModelのインスタンスをシェアできる。
dialogViewModel = ViewModelProvider(requireActivity(), DialogViewModel.DialogViewModelFactory(
(activity?.application as CalendarAppApplication).scheduleRepositoryRoom,
)).get(DialogViewModel::class.java)

//ダイアログからのコールバック処理
dialogViewModel.state.observe(this) {
when (it) {
is DialogState.Ok -> {
//画面に表示するデータを取得する
dialogViewModel.selectScheduleListOnDate2(date)
}

is DialogState.Cancel -> {
}

else -> {

}
}
} //dialogViewModel


}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {

//監視
viewLifecycleOwner.lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED){
dialogViewModel.scheduleList.collect(){
when(it){
is Resource.Loading -> {
}

is Resource.Success -> {
//ここで取得したデータを画面に表示する
}

is Resource.Error -> {
}

else -> {
}
}
}
}
} //viewLifecycleOwner


return binding.root

}

}

DBに追加、取得時の結果を返す - UI 状態を管理機能

       sealed class Resource<T>(                                                        
val data: T? = null,
val message: String? = null

) {
/** * 処理結果が成功時のイベント */
class Success<T>(data: T) : Resource<T>(data)

/** * 処理結果がエラー時のイベント */
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)

/** * 処理中のイベント */
class Loading<T> : Resource<T>()

} sealed class Resource<T>(
val data: T? = null,
val message: String? = null

) {
/** * 処理結果が成功時のイベント */
class Success<T>(data: T) : Resource<T>(data)

/** * 処理結果がエラー時のイベント */
class Error<T>(message: String, data: T? = null) : Resource<T>(data, message)

/** * 処理中のイベント */
class Loading<T> : Resource<T>()

}

参考サイト

アプリ アーキテクチャ ガイド | Android デベロッパー | Android Developers
このガイドでは、品質の高い堅牢なアプリを作成するためのおすすめの方法と 推奨アーキテクチャ を紹介します。 このページは、読者が Android フレームワークの基本について熟知していることを前提としています。Android アプリの開発経験がない場合は、 注: Android の基本コース でアプリの開発の基本を学習したうえで、このガイドで取り上げているコンセプトの詳細を確認してください。 標準的な Android アプリは、さまざまな アプリ コンポーネント( アクティビティ、 フラグメント、 サービ
https://developer.android.com/jetpack/guide?hl=ja
Retrofit with ViewModel in Kotlin (Part-1)
Using Retrofit with ViewModel is really goo practice. This article explains it in very simple manner.
https://medium.com/dsc-sastra-deemed-to-be-university/retrofit-with-viewmodel-in-kotlin-part-1-f9e705e77144

ダイアログイベントの結果を返す - UI 状態を管理機能

        sealed class DialogState<T : DialogFragment> {                                                        
/** * ダイアログのOKボタンタップイベント */
data class Ok<T : DialogFragment>(val dialog: T) : DialogState<T>()

/** * ダイアログのCancelボタンタップイベント */
data class Cancel<T : DialogFragment>(val dialog: T) : DialogState<T>()

/** * 読み込み処理イベント */
data class Loading<T : DialogFragment>(val dialog: T) : DialogState<T>()
}


enum class PurchaseState {

PURCHASE_IN_PROGRESS,
PURCHASE_SUCCESSFUL,
PURCHASE_ERROR

} sealed class DialogState<T : DialogFragment> {
/** * ダイアログのOKボタンタップイベント */
data class Ok<T : DialogFragment>(val dialog: T) : DialogState<T>()

/** * ダイアログのCancelボタンタップイベント */
data class Cancel<T : DialogFragment>(val dialog: T) : DialogState<T>()

/** * 読み込み処理イベント */
data class Loading<T : DialogFragment>(val dialog: T) : DialogState<T>()
}


enum class PurchaseState {

PURCHASE_IN_PROGRESS,
PURCHASE_SUCCESSFUL,
PURCHASE_ERROR

}

参考サイト

android DialogFragment を ViewModel と LiveDataで実装 | 40代中間管理職 [sakony] のブログ
レガシーcallbackスタイルからの脱却 Androidでダイアログのcallbackを受け取るのってめんどくさいですよね。お作法的にはcallback interfaceを定...
https://sakony.jp/dev/post-109/

実装完成イメージ

苦労した点、工夫した点など

アプリ作成時、技術的に以下の点で苦慮しました。
1)ダイアログでメモ登録イベント(OKボタン、Cancelボタン)のコールバック実装方法。
2)ダイアログでメモを登録した後、カレンダー画面に戻っても画面が更新されない。
ネットでQiita、ZENNなど技術情報を記事にしているサイトを見ると、いろんな実装方法があり、どの方法がベストプラクティスかひとつずつ確認していきました。
時間はかかりましたが、そのお陰で技術的な知見は広がりました。
1の問題がクリア出来れば、2も同時に解決ができました。
DBに追加、取得時の結果を返す - UI 状態を管理機能を利用することとViewModelProviderの生成方法を変更しました。

        //ViewModelProviderの第一引数にrequireActivityを指定してあげると                                                                
// Fragmentを使っているActivityでも同じViewModelのインスタンスをシェアできる。
dialogViewModel = ViewModelProvider(requireActivity(), DialogViewModel.DialogViewModelFactory(
(activity?.application as CalendarAppApplication).scheduleRepositoryRoom,
)).get(DialogViewModel::class.java) //ViewModelProviderの第一引数にrequireActivityを指定してあげると
// Fragmentを使っているActivityでも同じViewModelのインスタンスをシェアできる。
dialogViewModel = ViewModelProvider(requireActivity(), DialogViewModel.DialogViewModelFactory(
(activity?.application as CalendarAppApplication).scheduleRepositoryRoom,
)).get(DialogViewModel::class.java)

3)カレンダー画面の日付に対して、メモを登録した該当日付にドットを表示する。
既存のままだと、該当日付をタップした時にドットが表示される処理になっていたので、既存処理をカスタムする必要がありました。
既存の動作を確認する為、ログなどを仕込みながら処理を理解して行きました。
今度は、カレンダーの該当日付に対して、タップしなくてもドットを表示する為には、どうすれば良いかを考え、DBからメモに紐づく日付の一覧を取得し、 カレンダーの日付とマッチングさせて該当日付にドットを表示するように修正しました。
4)アプリに表示するデザイン、画像を自作で作る。
カレンダーアプリは有象無象にあるので短期間である程度の機能を網羅するデザインをどこから探せば良いか悩みました。
chat GPTに質問したら、以下のサイトをお勧めされたので、そのサイトを参考にしました。(本文のURL参照)
画像も自分のスキルアップの為、figmaを使って画像を作成してみました。 (本文のURL参照)

最後に

このアプリを通して、Android、Kotlinでアーキテクチャーなど使い所を理解する必要があると思いました。 今後もよりAndroid、Kotlinを深掘りして行きます。

株式会社ナディアからお誘い
この話題に共感したら、メンバーと話してみませんか?
株式会社ナディアでは一緒に働く仲間を募集しています
1 いいね!
1 いいね!

同じタグの記事

今週のランキング

Endoh Yukieさんにいいねを伝えよう
Endoh Yukieさんや会社があなたに興味を持つかも