From 41505a0ca6f4ac552ebfeadbc9dedbe4a8293a6a Mon Sep 17 00:00:00 2001 From: norangebit Date: Sat, 30 Nov 2019 11:51:09 +0100 Subject: [PATCH 1/5] init BoardsListActivity - add room database for Board - add BoardsListActivity - add BoardsListViewModel - add BoardRepository - add insertBoard - add deleteBoard - add syncronize --- app/build.gradle | 37 ++++- app/src/main/AndroidManifest.xml | 7 + .../ding/ids/wedroid/app/MainActivity.kt | 11 +- .../ding/ids/wedroid/app/data/dao/BoardDao.kt | 17 +++ .../app/data/database/BoardDatabase.kt | 35 +++++ .../ding/ids/wedroid/app/data/entity/Board.kt | 16 ++ .../app/data/repository/BoardRepository.kt | 138 ++++++++++++++++++ .../wedroid/app/view/BoardsListsActivity.kt | 38 +++++ .../app/view/adapter/BoardsListAdapter.kt | 44 ++++++ .../app/viewmodel/BoardListViewModel.kt | 25 ++++ .../main/res/layout/activity_boards_lists.xml | 33 +++++ app/src/main/res/layout/activity_main.xml | 12 ++ .../main/res/layout/board_recycle_item.xml | 12 ++ .../main/res/layout/content_boards_lists.xml | 19 +++ app/src/main/res/values/dimens.xml | 3 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 9 ++ build.gradle | 9 ++ 18 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDao.kt create mode 100644 app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/BoardDatabase.kt create mode 100644 app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/Board.kt create mode 100644 app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/BoardRepository.kt create mode 100644 app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivity.kt create mode 100644 app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/BoardsListAdapter.kt create mode 100644 app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/BoardListViewModel.kt create mode 100644 app/src/main/res/layout/activity_boards_lists.xml create mode 100644 app/src/main/res/layout/board_recycle_item.xml create mode 100644 app/src/main/res/layout/content_boards_lists.xml create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/build.gradle b/app/build.gradle index 5fb3211..1112cf7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,8 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' + android { compileSdkVersion 29 buildToolsVersion "29.0.2" @@ -21,19 +23,44 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + packagingOptions { + exclude 'META-INF/atomicfu.kotlin_module' + } } dependencies { - // retrofit - implementation "com.squareup.retrofit2:retrofit:2.6.2" - implementation "com.squareup.retrofit2:converter-gson:2.6.2" - - implementation project(':wrapper') + // standard implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.0.0' + // wrapper + implementation project(':wrapper') + // retrofit + implementation "com.squareup.retrofit2:retrofit:2.6.2" + implementation "com.squareup.retrofit2:converter-gson:2.6.2" + // room database + implementation "androidx.room:room-runtime:$rootProject.roomVersion" + implementation "androidx.room:room-ktx:$rootProject.roomVersion" + kapt "androidx.room:room-compiler:$rootProject.roomVersion" + // lifecycle components + implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion" + //noinspection LifecycleAnnotationProcessorWithJava8 + kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion" + // ViewModel Kotlin support + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion" + // Coroutines + api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines" + // UI + implementation "com.google.android.material:material:$rootProject.materialVersion" + + + // TESTING testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion" + androidTestImplementation "androidx.arch.core:core-testing:$rootProject.androidxArchVersion" + androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 253fb67..e589c5d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + + + diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/MainActivity.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/MainActivity.kt index e5390c0..4f79a49 100644 --- a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/MainActivity.kt +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/MainActivity.kt @@ -1,14 +1,21 @@ package it.unisannio.ding.ids.wedroid.app +import android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle -import it.unisannio.ding.ids.wedroid.wrapper.api.BoardService +import android.view.View +import it.unisannio.ding.ids.wedroid.app.view.BoardsListsActivity class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - val service : BoardService? = null + } + + fun open(v: View) { + startActivity( + Intent(this, BoardsListsActivity::class.java) + ) } } diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDao.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDao.kt new file mode 100644 index 0000000..ca88fa4 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/dao/BoardDao.kt @@ -0,0 +1,17 @@ +package it.unisannio.ding.ids.wedroid.app.data.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import it.unisannio.ding.ids.wedroid.app.data.entity.Board + +@Dao +interface BoardDao { + @Query("SELECT * from board_table ORDER BY title ASC") + fun getAllBoard(): LiveData> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(board: Board) + + @Delete + suspend fun delete(board: Board) +} \ No newline at end of file diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/BoardDatabase.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/BoardDatabase.kt new file mode 100644 index 0000000..e99fc50 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/database/BoardDatabase.kt @@ -0,0 +1,35 @@ +package it.unisannio.ding.ids.wedroid.app.data.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import it.unisannio.ding.ids.wedroid.app.data.dao.BoardDao +import it.unisannio.ding.ids.wedroid.app.data.entity.Board + +@Database(entities = [Board::class], version = 1, exportSchema = false) +abstract class BoardDatabase : RoomDatabase() { + + abstract fun boardDao(): BoardDao + + companion object { + @Volatile + private var INSTANCE: BoardDatabase? = null + + fun getDatabase(context: Context): BoardDatabase { + val tempInstance = INSTANCE + if (tempInstance != null) { + return tempInstance + } + synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + BoardDatabase::class.java, + "board_database" + ).build() + INSTANCE = instance + return instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/Board.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/Board.kt new file mode 100644 index 0000000..0cd1f70 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/entity/Board.kt @@ -0,0 +1,16 @@ +package it.unisannio.ding.ids.wedroid.app.data.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "board_table") +data class Board( + @PrimaryKey @ColumnInfo(name = "id") val id: String, + @ColumnInfo(name = "title") val title: String = "" +) + +fun it.unisannio.ding.ids.wedroid.wrapper.entity.Board.convert(): Board { + return Board(this.id, this.title) +} + diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/BoardRepository.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/BoardRepository.kt new file mode 100644 index 0000000..22179c0 --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/data/repository/BoardRepository.kt @@ -0,0 +1,138 @@ +package it.unisannio.ding.ids.wedroid.app.data.repository + +import android.util.Log +import it.unisannio.ding.ids.wedroid.app.data.dao.BoardDao +import it.unisannio.ding.ids.wedroid.app.data.entity.Board +import it.unisannio.ding.ids.wedroid.app.data.entity.convert +import it.unisannio.ding.ids.wedroid.app.util.PreferenceReader +import it.unisannio.ding.ids.wedroid.wrapper.api.BoardService +import it.unisannio.ding.ids.wedroid.wrapper.entity.BoardBackgroundColor +import it.unisannio.ding.ids.wedroid.wrapper.entity.BoardPrototype +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response + +class BoardRepository( + private val dao: BoardDao, + private val service: BoardService, + private val reader: PreferenceReader +) { + val allBoards by lazy { + dao.getAllBoard() + } + + init { + synchronize() + } + + fun insertBoard(title: String) { + service.newBoard( + BoardPrototype.Builder() + .setOwner(reader.userId) + .setTitle(title) + .setBackgroundColor(BoardBackgroundColor.LIMEGREEN) + .build() + ).enqueue(object : Callback { + override fun onFailure( + call: Call, + t: Throwable + ) = logNetworkError(t.message) + + override fun onResponse( + call: Call, + response: Response + ) { + if (!response.isSuccessful) { + logNetworkError("${response.code()} ${response.message()}") + return + } + + val board = response.body() + + if (board == null) { + logNetworkError("empty body") + return + } + + CoroutineScope(Dispatchers.IO).launch { + dao.insert(Board(board.id, title)) + } + } + }) + } + + fun synchronize() { + service.getBoardsFromUser(reader.userId) + .enqueue(object : + Callback> { + + override fun onFailure( + call: Call>, + t: Throwable + ) = logNetworkError(t.message) + + override fun onResponse( + call: Call>, + response: Response> + ) { + if (!response.isSuccessful) { + logNetworkError("${response.code()} ${response.message()}") + return + } + + // read boards from the body + val boards = (response.body() ?: return) + .map { it.convert() } + + addNewBoardToDb(boards) + + removeOldBoardsFromDb(boards) + } + }) + } + + fun deleteBoard(id: String) { + service.deleteBoard(id).enqueue( + object : Callback { + override fun onFailure(call: Call, t: Throwable) { + logNetworkError(t.message) + } + + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + logNetworkError("${response.code()}, ${response.message()}") + return + } + + CoroutineScope(Dispatchers.IO).launch { + dao.delete(Board(id)) + } + } + }) + } + + private fun addNewBoardToDb(boards: Collection) { + boards.forEach { + CoroutineScope(Dispatchers.IO).launch { + dao.insert(it) + } + } + } + + private fun removeOldBoardsFromDb(boards: Collection) { + allBoards.value?.minus(boards) + ?.forEach { + CoroutineScope(Dispatchers.IO).launch { + dao.delete(it) + } + } + } + + private fun logNetworkError(message: String?) { + Log.e("RETROFIT", message) + } +} + diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivity.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivity.kt new file mode 100644 index 0000000..702e7ac --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/BoardsListsActivity.kt @@ -0,0 +1,38 @@ +package it.unisannio.ding.ids.wedroid.app.view + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import it.unisannio.ding.ids.wedroid.app.R +import it.unisannio.ding.ids.wedroid.app.view.adapter.BoardsListAdapter +import it.unisannio.ding.ids.wedroid.app.viewmodel.BoardListViewModel + +import kotlinx.android.synthetic.main.activity_boards_lists.* +import kotlinx.android.synthetic.main.content_boards_lists.* + +class BoardsListsActivity : AppCompatActivity() { + private lateinit var viewModel: BoardListViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_boards_lists) + setSupportActionBar(toolbar) + viewModel = ViewModelProvider(this).get(BoardListViewModel::class.java) + + val adapter = BoardsListAdapter(this) + boardList.adapter = adapter + boardList.layoutManager = LinearLayoutManager(this) + + viewModel.allBoards.observe(this, Observer { + it.let { adapter.setBoards(it) } + }) + + fab.setOnClickListener { view -> + viewModel.insertBoard("New board") + } + } + +} + diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/BoardsListAdapter.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/BoardsListAdapter.kt new file mode 100644 index 0000000..56d5ddb --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/view/adapter/BoardsListAdapter.kt @@ -0,0 +1,44 @@ +package it.unisannio.ding.ids.wedroid.app.view.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import it.unisannio.ding.ids.wedroid.app.R +import it.unisannio.ding.ids.wedroid.app.data.entity.Board + + +class BoardsListAdapter internal constructor( + context: Context +) : RecyclerView.Adapter() { + + private val inflater = LayoutInflater.from(context) + private var boards = emptyList() + + inner class BoardViewHolder( + view: View + ) : RecyclerView.ViewHolder(view) { + val boardTitle: TextView = view.findViewById(R.id.boardTitle) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BoardViewHolder { + val view = inflater.inflate(R.layout.board_recycle_item, parent, false) + return BoardViewHolder(view) + } + + override fun getItemCount(): Int { + return boards.size + } + + override fun onBindViewHolder(holder: BoardViewHolder, position: Int) { + val board = boards[position] + holder.boardTitle.text = board.title + } + + internal fun setBoards(boards: List) { + this.boards = boards + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/BoardListViewModel.kt b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/BoardListViewModel.kt new file mode 100644 index 0000000..582051d --- /dev/null +++ b/app/src/main/java/it/unisannio/ding/ids/wedroid/app/viewmodel/BoardListViewModel.kt @@ -0,0 +1,25 @@ +package it.unisannio.ding.ids.wedroid.app.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import it.unisannio.ding.ids.wedroid.app.util.ServicesFactory +import it.unisannio.ding.ids.wedroid.app.data.database.BoardDatabase +import it.unisannio.ding.ids.wedroid.app.data.entity.Board +import it.unisannio.ding.ids.wedroid.app.data.repository.BoardRepository +import it.unisannio.ding.ids.wedroid.app.util.DumReader + +class BoardListViewModel(application: Application) : AndroidViewModel(application) { + + private val repository: BoardRepository = BoardRepository( + BoardDatabase.getDatabase(application).boardDao(), + ServicesFactory.boardService, + DumReader() + ) + + val allBoards: LiveData> = repository.allBoards + + fun insertBoard(title: String) = repository.insertBoard(title) + + fun deleteBoard(id: String) = repository.deleteBoard(id) +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_boards_lists.xml b/app/src/main/res/layout/activity_boards_lists.xml new file mode 100644 index 0000000..985531a --- /dev/null +++ b/app/src/main/res/layout/activity_boards_lists.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4fc2444..dca2774 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,6 +7,7 @@ tools:context=".MainActivity"> +