Browse Source

add logic for reading persons from both type datasource

MrOzOn 5 năm trước cách đây
mục cha
commit
086268bbb9

+ 8 - 0
core_api/build.gradle

@@ -23,6 +23,13 @@ android {
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
         }
     }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
 
 }
 
@@ -39,6 +46,7 @@ dependencies {
     implementation okhttp
     implementation converterGson
     implementation navigationFragment
+    implementation lifecycleLivedata
 
     implementation fileTree(dir: 'libs', include: ['*.jar'])
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

+ 24 - 0
core_api/src/main/java/com/mrozon/core_api/SingleSourceOfTruthStrategy.kt

@@ -0,0 +1,24 @@
+package com.mrozon.core_api
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.liveData
+import androidx.lifecycle.map
+import kotlinx.coroutines.Dispatchers
+import com.mrozon.utils.network.Result
+
+fun <T, A> resultLiveData(databaseQuery: () -> LiveData<T>,
+                          networkCall: suspend () -> Result<A>,
+                          saveCallResult: suspend (A) -> Unit): LiveData<Result<T>> =
+        liveData(Dispatchers.IO) {
+            emit(Result.loading<T>())
+            val source = databaseQuery.invoke().map { Result.success(it) }
+            emitSource(source)
+
+            val responseStatus = networkCall.invoke()
+            if (responseStatus.status == Result.Status.SUCCESS) {
+                saveCallResult(responseStatus.data!!)
+            } else if (responseStatus.status == Result.Status.ERROR) {
+                emit(Result.error<T>(responseStatus.message!!))
+                emitSource(source)
+            }
+        }

+ 10 - 1
core_api/src/main/java/com/mrozon/core_api/db/HealthDiaryDao.kt

@@ -8,6 +8,7 @@ import com.mrozon.core_api.db.model.UserDb
 @Dao
 interface HealthDiaryDao {
 
+    // USER
     @Query("SELECT * FROM user_table LIMIT 1")
     fun getUser(): LiveData<UserDb>
 
@@ -17,6 +18,14 @@ interface HealthDiaryDao {
     @Delete
     suspend fun deleteUser(userDb: UserDb)
 
+    // TOKEN
+    @Query("SELECT user_token from user_table LIMIT 1")
+    fun getAccessToken(): String
+
+    // PERSON
     @Query("SELECT * FROM person_table")
-    fun getPersons(): LiveData<PersonDb>
+    fun getPersons(): LiveData<List<PersonDb>>
+
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    suspend fun insertAllPerson(persons: List<PersonDb>)
 }

+ 4 - 4
core_api/src/main/java/com/mrozon/core_api/entity/Person.kt

@@ -3,10 +3,10 @@ package com.mrozon.core_api.entity
 import java.util.Date
 
 data class Person (
-    var id: Long = 0L,
-    var name: String,
-    var gender: Gender = Gender.MALE,
-    var born: Date
+    val id: Long = 0L,
+    val name: String,
+    val gender: Gender = Gender.MALE,
+    val born: Date
 )
 
 enum class Gender(val code: Int) {

+ 1 - 1
core_api/src/main/java/com/mrozon/core_api/network/HealthDiaryService.kt

@@ -17,7 +17,7 @@ interface HealthDiaryService {
     suspend fun registerUser(@Body body: RegisterRequest): Response<RegisterResponse>
 
     @GET("patients/")
-    suspend fun getPatients(): Response<List<PersonResponse>>
+    suspend fun getPersons(@Header("Authorization") token: String): Response<List<PersonResponse>>
 
 //    @GET("lego/themes/")
 //    suspend fun getThemes(@Query("page") page: Int? = null,

+ 16 - 0
feature_person/src/main/java/com/mrozon/feature_person/data/PersonRemoteDataSource.kt

@@ -0,0 +1,16 @@
+package com.mrozon.feature_person.data
+
+import com.mrozon.core_api.db.HealthDiaryDao
+import com.mrozon.core_api.network.HealthDiaryService
+import com.mrozon.utils.base.BaseDataSource
+import javax.inject.Inject
+
+class PersonRemoteDataSource @Inject constructor(private val service: HealthDiaryService,
+        private val dao: HealthDiaryDao): BaseDataSource() {
+
+    suspend fun getPersons()
+            = getResult {
+                val token = "Token "+dao.getAccessToken()
+                service.getPersons(token)
+            }
+}

+ 34 - 0
feature_person/src/main/java/com/mrozon/feature_person/data/PersonRepository.kt

@@ -0,0 +1,34 @@
+package com.mrozon.feature_person.data
+
+import androidx.lifecycle.Transformations
+import com.mrozon.core_api.db.HealthDiaryDao
+import com.mrozon.core_api.mapper.PersonToPersonDbMapper
+import com.mrozon.core_api.network.model.toPerson
+import com.mrozon.core_api.resultLiveData
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class PersonRepository @Inject constructor(private val dao: HealthDiaryDao,
+        private val personRemoteDataSource: PersonRemoteDataSource,
+        private val mapper: PersonToPersonDbMapper
+) {
+
+    fun getPersons() = resultLiveData(
+        databaseQuery = {
+            val personDb = dao.getPersons()
+            Transformations.map(personDb) {
+                mapper.reverseMap(it)
+            }
+        },
+        networkCall = { personRemoteDataSource.getPersons() },
+        saveCallResult = {
+            val persons = it.map { personResponse ->
+                personResponse.toPerson()
+            }
+            val personsDb = mapper.map(persons)
+            dao.insertAllPerson(personsDb)
+        }
+    )
+
+}

+ 40 - 0
feature_person/src/main/java/com/mrozon/feature_person/presentation/BindingUtils.kt

@@ -0,0 +1,40 @@
+package com.mrozon.feature_person.presentation
+
+import android.annotation.SuppressLint
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.databinding.BindingAdapter
+import com.mrozon.core_api.entity.Gender
+import com.mrozon.core_api.entity.Person
+import com.mrozon.feature_person.R
+import java.time.Period
+import java.util.*
+
+@BindingAdapter("gender")
+fun ImageView.setGender(item: Person) {
+    if (item.gender==Gender.MALE) {
+        setImageResource(R.drawable.ic_male_avatar)
+    }
+    else
+    {
+        setImageResource(R.drawable.ic_female_avatar)
+    }
+}
+
+@SuppressLint("SetTextI18n")
+@BindingAdapter("name_with_age")
+fun TextView.setNameAge(item: Person) {
+    val age = getAge(item.born)
+    text = "${item.name} ($age y.o.)"
+}
+
+private fun getAge(born: Date): Int {
+    val dob: Calendar = Calendar.getInstance()
+    val today: Calendar = Calendar.getInstance()
+    dob.timeInMillis = born.time
+    var age: Int = today.get(Calendar.YEAR) - dob.get(Calendar.YEAR)
+    if (today.get(Calendar.DAY_OF_YEAR) < dob.get(Calendar.DAY_OF_YEAR)) {
+        age--
+    }
+    return age
+}

+ 59 - 0
feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonAdapter.kt

@@ -0,0 +1,59 @@
+package com.mrozon.feature_person.presentation
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.mrozon.core_api.entity.Person
+import com.mrozon.feature_person.databinding.ItemPersonBinding
+
+class ListPersonAdapter(private val clickListener: ListPersonListener): ListAdapter<Person, ListPersonAdapter.ViewHolder>(ListPersonDiffCallback()){
+
+    override fun onCreateViewHolder(
+        parent: ViewGroup,
+        viewType: Int
+    ): ViewHolder  = ViewHolder.from(parent)
+
+    override fun onBindViewHolder(holder: ListPersonAdapter.ViewHolder, position: Int) {
+        val item = getItem(position)
+        holder.bind(item, clickListener)
+    }
+
+    class ViewHolder private constructor(val binding: ItemPersonBinding ): RecyclerView.ViewHolder(binding.root){
+        fun bind(
+            item: Person,
+            clickListener: ListPersonListener) {
+            binding.person = item
+            binding.listener = clickListener
+            binding.executePendingBindings()
+        }
+
+        companion object {
+            fun from(parent: ViewGroup): ViewHolder {
+                val layoutInflater = LayoutInflater.from(parent.context)
+                val binding =
+                    ItemPersonBinding.inflate(layoutInflater, parent, false)
+                return ViewHolder(binding)
+            }
+        }
+    }
+
+    class ListPersonDiffCallback : DiffUtil.ItemCallback<Person>() {
+
+        override fun areItemsTheSame(oldItem: Person, newItem: Person): Boolean {
+            return oldItem.id == newItem.id
+        }
+
+        override fun areContentsTheSame(oldItem: Person, newItem: Person): Boolean {
+            return oldItem == newItem
+        }
+    }
+
+    class ListPersonListener(val clickListener:(person: Person) -> Unit) {
+        fun onClick(person: Person) = clickListener(person)
+    }
+
+}
+
+

+ 33 - 1
feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonFragment.kt

@@ -1,12 +1,18 @@
 package com.mrozon.feature_person.presentation
 
 import android.content.Context
+import android.os.Bundle
+import android.view.View
 import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
 import com.mrozon.feature_person.R
 import com.mrozon.feature_person.databinding.FragmentListPersonBinding
 import com.mrozon.feature_person.di.ListPersonFragmentComponent
 import com.mrozon.utils.base.BaseFragment
+import com.mrozon.utils.network.Result
+import timber.log.Timber
 import javax.inject.Inject
 
 class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
@@ -18,13 +24,39 @@ class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
 
     private val viewModel by viewModels<ListPersonFragmentViewModel> { viewModelFactory }
 
+    private lateinit var adapter: ListPersonAdapter
+
     override fun onAttach(context: Context) {
         super.onAttach(context)
         ListPersonFragmentComponent.injectFragment(this)
     }
 
-    override fun subscribeUi() {
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        adapter = ListPersonAdapter(ListPersonAdapter.ListPersonListener { person ->
+            Timber.d("click to ${person.name}")
+        })
+        binding?.rvPerson?.adapter = adapter
+        val manager = LinearLayoutManager(context)
+        binding?.rvPerson?.layoutManager = manager
+    }
 
+    override fun subscribeUi() {
+        viewModel.persons.observe(viewLifecycleOwner, Observer { result ->
+            when (result.status) {
+                Result.Status.LOADING -> {
+                    binding?.srlPerson?.isRefreshing = true
+                }
+                Result.Status.SUCCESS -> {
+                    binding?.srlPerson?.isRefreshing = false
+                    adapter.submitList(result.data)
+                }
+                Result.Status.ERROR -> {
+                    binding?.srlPerson?.isRefreshing = false
+                    showError(result.message!!) { }
+                }
+            }
+        })
     }
 
 }

+ 4 - 1
feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonFragmentViewModel.kt

@@ -1,7 +1,10 @@
 package com.mrozon.feature_person.presentation
 
+import com.mrozon.feature_person.data.PersonRepository
 import com.mrozon.utils.base.BaseViewModel
 import javax.inject.Inject
 
-class ListPersonFragmentViewModel @Inject constructor(): BaseViewModel() {
+class ListPersonFragmentViewModel @Inject constructor(repository: PersonRepository): BaseViewModel() {
+
+    val persons by lazy { repository.getPersons() }
 }

+ 4 - 0
feature_person/src/main/res/drawable/ic_female_avatar.xml

@@ -0,0 +1,4 @@
+<vector android:height="64dp" android:viewportHeight="612"
+    android:viewportWidth="612" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M507.921,445.34c-34.766,-24.516 -76.772,-41.947 -122.504,-51.296c10.094,-7.34 19.397,-16.031 27.721,-25.86l142.126,-66.532c7.149,-3.346 11.108,-11.128 9.6,-18.876c-0.558,-2.85 -14.024,-70.631 -51.66,-139.402C461.871,49.578 390.223,0 306.001,0c-84.219,0 -155.87,49.578 -207.205,143.375c-37.635,68.769 -51.102,136.551 -51.659,139.402c-1.508,7.748 2.45,15.53 9.599,18.876l142.129,66.532c8.324,9.83 17.627,18.521 27.721,25.86c-45.735,9.349 -87.738,26.78 -122.505,51.293c-55.989,39.478 -86.822,92.553 -86.822,149.448c0,9.506 7.705,17.212 17.212,17.212h543.06c9.508,0 17.212,-7.707 17.212,-17.212C594.744,537.892 563.911,484.817 507.921,445.34zM456.912,577.575v-48.378c0,-9.506 -7.704,-17.212 -17.212,-17.212c-9.506,0 -17.212,7.707 -17.212,17.212v48.378H189.511v-48.378c0,-9.506 -7.704,-17.212 -17.212,-17.212c-9.506,0 -17.212,7.707 -17.212,17.212v48.378H52.922c5.637,-38.98 30.348,-75.439 70.998,-104.101c19.173,-13.52 40.908,-24.625 64.443,-33.117c15.582,13.655 61.154,48.746 118.007,48.746c0.871,0 1.757,-0.009 2.631,-0.026c39.181,-0.749 75.213,-18.014 107.16,-51.323c26.424,8.774 50.729,20.775 71.924,35.719c40.647,28.662 65.359,65.12 70.996,104.101H456.912V577.575zM229.62,428.557c24.39,-5.276 50.068,-8.015 76.383,-8.015c24.326,0 48.104,2.345 70.831,6.866c-21.331,17.583 -44.213,26.735 -68.265,27.247C276.77,455.34 248.285,441.027 229.62,428.557zM189.514,243.933c0,-9.506 -7.707,-17.212 -17.212,-17.212c-9.508,0 -17.212,7.707 -17.212,17.212c0,25.581 4.676,49.903 13.071,71.872L83.93,276.375c5.57,-22.162 19.338,-69.469 45.065,-116.472C174.562,76.641 234.117,34.425 306.001,34.425c71.674,0 131.099,41.984 176.624,124.782c25.892,47.09 39.818,94.865 45.438,117.171l-84.222,39.427c8.395,-21.969 13.074,-46.291 13.074,-71.872c0,-9.506 -7.707,-17.212 -17.212,-17.212c-9.508,0 -17.212,7.707 -17.212,17.212c0,78.4 -52.257,142.185 -116.487,142.185C241.771,386.118 189.514,322.333 189.514,243.933z"/>
+</vector>

+ 4 - 0
feature_person/src/main/res/drawable/ic_male_avatar.xml

@@ -0,0 +1,4 @@
+<vector android:height="64dp" android:viewportHeight="400.613"
+    android:viewportWidth="400.613" android:width="64dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M277.664,208.01h-16.123c31.146,-20.152 51.804,-55.194 51.804,-94.972C313.345,50.708 262.637,0 200.307,0S87.269,50.708 87.269,113.038c0,39.778 20.656,74.82 51.804,94.972H122.95c-41.434,0 -75.143,33.707 -75.143,75.141v104.963c0,6.904 5.597,12.5 12.5,12.5h280c6.903,0 12.5,-5.596 12.5,-12.5V283.15C352.807,241.717 319.098,208.01 277.664,208.01zM112.269,113.038c0,-48.544 39.493,-88.038 88.038,-88.038c48.544,0 88.038,39.494 88.038,88.038c0,48.543 -39.494,88.036 -88.038,88.036C151.762,201.074 112.269,161.581 112.269,113.038zM236.88,219.998l-36.573,36.574l-36.573,-36.573c11.477,3.935 23.779,6.075 36.573,6.075S225.404,223.934 236.88,219.998zM327.807,375.613h-34v-60.059c0,-6.904 -5.597,-12.5 -12.5,-12.5s-12.5,5.596 -12.5,12.5v60.059h-137v-60.059c0,-6.904 -5.597,-12.5 -12.5,-12.5s-12.5,5.596 -12.5,12.5v60.059h-34V283.15c0,-27.647 22.494,-50.141 50.143,-50.141h18.439l50.079,50.078c2.441,2.441 5.64,3.661 8.839,3.661s6.396,-1.22 8.839,-3.661l50.079,-50.078h18.439c27.648,0 50.143,22.492 50.143,50.141V375.613z"/>
+</vector>

+ 10 - 3
feature_person/src/main/res/layout/item_person.xml

@@ -4,7 +4,12 @@
     xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <data>
-
+        <variable
+            name="person"
+            type="com.mrozon.core_api.entity.Person" />
+        <variable
+            name="listener"
+            type="com.mrozon.feature_person.presentation.ListPersonAdapter.ListPersonListener" />
     </data>
 
     <androidx.cardview.widget.CardView
@@ -24,10 +29,12 @@
                 android:id="@+id/ivGender"
                 android:layout_width="40dp"
                 android:layout_height="40dp"
-                android:layout_marginBottom="8dp"
                 android:layout_marginStart="8dp"
                 android:layout_marginTop="8dp"
+                android:layout_marginBottom="8dp"
+                android:alpha="0.75"
                 android:contentDescription="@string/ivGender"
+                app:gender="@{person}"
                 app:layout_constraintBottom_toBottomOf="parent"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toTopOf="parent"
@@ -42,7 +49,7 @@
                 android:layout_marginRight="16dp"
                 android:layout_toEndOf="@id/ivGender"
                 android:paddingTop="16dp"
-                android:text="textview_name"
+                app:name_with_age="@{person}"
                 android:textAppearance="@style/TextAppearance.AppCompat.Large"
                 android:textColor="#000000"
                 android:textSize="20sp"

+ 5 - 0
scripts/deps_versions.gradle

@@ -18,6 +18,7 @@ ext {
     cardviewVersion = '1.0.0'
     recyclerviewViersion = '1.2.0-alpha05'
     materialVersion = '1.2.1'
+    lifecycleVersion = '2.3.0-alpha07'
 
 //    retrofit = "com.squareup.retrofit2:retrofit:$retrofitVersion"
 //    jsr330 = "javax.inject:javax.inject:$jsr330Version"
@@ -56,4 +57,8 @@ ext {
     cardview =  "androidx.cardview:cardview:$cardviewVersion"
     recyclerview = "androidx.recyclerview:recyclerview:$recyclerviewViersion"
     material = "com.google.android.material:material:$materialVersion"
+    // Livecycle
+    lifecycleExtensions = "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
+    lifecycleLivedata = "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
+    lifecycleViewmodel = "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
 }