add login and board_list #26

Manually merged
norangebit merged 38 commits from feature_integrate_login into develop 2020-01-14 19:53:09 +00:00
21 changed files with 650 additions and 61 deletions
Showing only changes of commit c0200cf445 - Show all commits

View File

@ -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,21 +23,51 @@ 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"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
// TESTING
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation "com.squareup.retrofit2:retrofit:2.6.2"
implementation "com.squareup.retrofit2:converter-gson:2.6.2"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.androidxArchVersion"
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"
}

View File

@ -0,0 +1,22 @@
package it.unisannio.ding.ids.wedroid.app
import androidx.lifecycle.*
class OneTimeObserver<T>(private val handler: (T) -> Unit) : Observer<T>, LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun getLifecycle(): Lifecycle = lifecycle
override fun onChanged(t: T) {
handler(t)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
val observer = OneTimeObserver(handler = onChangeHandler)
observe(observer, observer)
}

View File

@ -0,0 +1,103 @@
package it.unisannio.ding.ids.wedroid.app.data.dao
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
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.observeOnce
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.runBlocking
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import java.io.IOException
@RunWith(AndroidJUnit4::class)
class BoardDaoTest {
private lateinit var dao: BoardDao
private lateinit var db: BoardDatabase
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun createDb() {
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(
context, BoardDatabase::class.java
).build()
dao = db.boardDao()
}
@After
@Throws(IOException::class)
fun closeDb() {
db.close()
}
@Test
fun emptyDatabaseOnCreation() {
dao.getAllBoard().observeOnce {
assertEquals(0, it.size)
}
}
@Test
fun insert() {
val board = Board("id", "title")
runBlocking {
dao.insert(board)
}
dao.getAllBoard().observeOnce {
assertEquals(1, it.size)
assertEquals(board, it[0])
}
}
fun getInAscendingOrder() {
val board0 = Board("id0", "title0")
val board1 = Board("id1", "title1")
runBlocking {
dao.insert(board1)
}
runBlocking {
dao.insert(board0)
}
dao.getAllBoard().observeOnce {
assertEquals(2, it.size)
assertEquals(board0, it[0])
assertEquals(board1, it[1])
}
}
fun delete() {
val board = Board("id", "title")
runBlocking {
dao.insert(board)
}
runBlocking {
dao.delete(board)
}
dao.getAllBoard().observeOnce {
assertEquals(0, it.size)
}
}
}

View File

@ -10,8 +10,13 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".LoginActivity"></activity>
<activity android:name=".MainActivity">
<activity
android:name=".view.BoardsListsActivity"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -1,23 +0,0 @@
package it.unisannio.ding.ids.wedroid.app
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import it.unisannio.ding.ids.wedroid.wrapper.api.BoardService
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val service : BoardService? = null
}
fun login(v: View) {
startActivity(
Intent(this, LoginActivity::class.java)
)
}
}

View File

@ -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<List<Board>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(board: Board)
@Delete
suspend fun delete(board: Board)
}

View File

@ -0,0 +1,30 @@
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)
public abstract class BoardDatabase extends RoomDatabase {
private static volatile BoardDatabase INSTANCE;
public abstract BoardDao boardDao();
public static BoardDatabase getDatabase(Context context) {
if (INSTANCE != null)
return INSTANCE;
synchronized (BoardDatabase.class) {
INSTANCE = Room.databaseBuilder(
context.getApplicationContext(),
BoardDatabase.class,
"board_database"
).build();
return INSTANCE;
}
}
}

View File

@ -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)
}

View File

@ -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<it.unisannio.ding.ids.wedroid.wrapper.entity.Board> {
override fun onFailure(
call: Call<it.unisannio.ding.ids.wedroid.wrapper.entity.Board>,
t: Throwable
) = logNetworkError(t.message)
override fun onResponse(
call: Call<it.unisannio.ding.ids.wedroid.wrapper.entity.Board>,
response: Response<it.unisannio.ding.ids.wedroid.wrapper.entity.Board>
) {
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<MutableList<it.unisannio.ding.ids.wedroid.wrapper.entity.Board>> {
override fun onFailure(
call: Call<MutableList<it.unisannio.ding.ids.wedroid.wrapper.entity.Board>>,
t: Throwable
) = logNetworkError(t.message)
override fun onResponse(
call: Call<MutableList<it.unisannio.ding.ids.wedroid.wrapper.entity.Board>>,
response: Response<MutableList<it.unisannio.ding.ids.wedroid.wrapper.entity.Board>>
) {
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<Void> {
override fun onFailure(call: Call<Void>, t: Throwable) {
logNetworkError(t.message)
}
override fun onResponse(call: Call<Void>, response: Response<Void>) {
if (!response.isSuccessful) {
logNetworkError("${response.code()}, ${response.message()}")
return
}
CoroutineScope(Dispatchers.IO).launch {
dao.delete(Board(id))
}
}
})
}
private fun addNewBoardToDb(boards: Collection<Board>) {
boards.forEach {
CoroutineScope(Dispatchers.IO).launch {
dao.insert(it)
}
}
}
private fun removeOldBoardsFromDb(boards: Collection<Board>) {
allBoards.value?.minus(boards)
?.forEach {
CoroutineScope(Dispatchers.IO).launch {
dao.delete(it)
}
}
}
private fun logNetworkError(message: String?) {
Log.e("RETROFIT", message)
}
}

View File

@ -5,9 +5,10 @@ import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object ServicesFactory {
class ServicesFactory private constructor(
reader: PreferenceReader
) {
private val retrofit: Retrofit
private lateinit var reader: PreferenceReader //TODO
init {
val httpClient = OkHttpClient.Builder()
@ -55,5 +56,23 @@ object ServicesFactory {
val userService by lazy {
retrofit.create(UserService::class.java)
}
companion object {
@Volatile
private var INSTANCE: ServicesFactory? = null
fun getInstance(reader: PreferenceReader): ServicesFactory {
val tempInstance = INSTANCE
if (tempInstance != null)
return tempInstance
synchronized(this) {
val instance = ServicesFactory(reader)
INSTANCE = instance
return instance
}
}
}
}

View File

@ -0,0 +1,71 @@
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.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.BoardsListViewModel
import kotlinx.android.synthetic.main.activity_boards_lists.*
import kotlinx.android.synthetic.main.content_boards_lists.*
class BoardsListsActivity : AppCompatActivity() {
private lateinit var viewModel: BoardsListViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_boards_lists)
setSupportActionBar(toolbar)
viewModel = ViewModelProvider(this).get(BoardsListViewModel::class.java)
val adapter = BoardsListAdapter(this)
boardList.adapter = adapter
boardList.layoutManager = LinearLayoutManager(this)
swipeLeftToDelete()
pullToRefresh.setColorSchemeColors(getColor(R.color.colorAccent))
viewModel.allBoards.observe(this, Observer {
it.let { adapter.setBoards(it) }
pullToRefresh.isRefreshing = false
})
fab.setOnClickListener {
viewModel.insertBoard("New board")
}
pullToRefresh.setOnRefreshListener {
viewModel.refresh()
}
}
private fun swipeLeftToDelete() {
val swipeToDelete = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(
0, ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val pos = viewHolder.adapterPosition
viewModel.deleteBoard(pos)
}
})
swipeToDelete.attachToRecyclerView(boardList)
}
}

View File

@ -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<BoardsListAdapter.BoardViewHolder>() {
private val inflater = LayoutInflater.from(context)
private var boards = emptyList<Board>()
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<Board>) {
this.boards = boards
notifyDataSetChanged()
}
}

View File

@ -0,0 +1,48 @@
package it.unisannio.ding.ids.wedroid.app.viewmodel;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import java.util.List;
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.PreferenceReader;
import it.unisannio.ding.ids.wedroid.app.util.ServicesFactory;
public class BoardsListViewModel extends AndroidViewModel {
private BoardRepository repository;
private LiveData<List<Board>> allBoards;
public BoardsListViewModel(@NonNull Application application) {
super(application);
PreferenceReader reader = null; //TODO
repository = new BoardRepository(
BoardDatabase.getDatabase(application).boardDao(),
ServicesFactory.Companion.getInstance(reader).getBoardService(),
reader
);
allBoards = repository.getAllBoards();
}
public LiveData<List<Board>> getAllBoards() {
return allBoards;
}
public void insertBoard(String title) {
repository.insertBoard(title);
}
public void deleteBoard(int position) {
repository.deleteBoard(allBoards.getValue().get(position).getId());
}
public void refresh() {
repository.synchronize();
}
}

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".view.BoardsListsActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_boards_lists" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/fab_margin"
app:srcCompat="@android:drawable/ic_dialog_email" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="login"
android:text="Button"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/boardTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView" />
</LinearLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".view.BoardsListsActivity"
tools:showIn="@layout/activity_boards_lists">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/pullToRefresh"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="0dp"
android:layout_height="0dp"
android:id="@+id/boardList"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,3 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@ -1,3 +1,5 @@
<resources>
<string name="app_name">wedroid</string>
<string name="title_activity_boards_list">BoardsListActivity</string>
<string name="title_activity_boards_lists">Boards</string>
</resources>

View File

@ -8,4 +8,13 @@
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

View File

@ -23,6 +23,15 @@ allprojects {
}
}
ext {
roomVersion = '2.2.2'
archLifecycleVersion = '2.2.0-rc02'
androidxArchVersion = '2.1.0'
coreTestingVersion = "2.1.0"
coroutines = '1.3.2'
materialVersion = "1.0.0"
}
task clean(type: Delete) {
delete rootProject.buildDir
}