Prechádzať zdrojové kódy

add CRUD operation for persons

MrOzOn 5 rokov pred
rodič
commit
58bddce0d0
32 zmenil súbory, kde vykonal 798 pridanie a 31 odobranie
  1. 10 16
      app/src/main/java/com/mrozon/healthdiary/di/NavigationModule.kt
  2. 14 0
      app/src/main/java/com/mrozon/healthdiary/navigation/EditPersonNavigatorImpl.kt
  3. 17 0
      app/src/main/java/com/mrozon/healthdiary/navigation/ListPersonNavigatorImpl.kt
  4. 25 1
      app/src/main/res/navigation/nav_graph.xml
  5. 12 0
      core_api/src/main/java/com/mrozon/core_api/db/HealthDiaryDao.kt
  6. 8 0
      core_api/src/main/java/com/mrozon/core_api/navigation/EditPersonNavigator.kt
  7. 8 0
      core_api/src/main/java/com/mrozon/core_api/navigation/ListPersonNavigator.kt
  8. 2 0
      core_api/src/main/java/com/mrozon/core_api/navigation/NavigatorProvider.kt
  9. 11 0
      core_api/src/main/java/com/mrozon/core_api/network/HealthDiaryService.kt
  10. 1 0
      feature_auth/src/main/res/layout/fragment_registration.xml
  11. 19 0
      feature_person/src/main/java/com/mrozon/feature_person/data/PersonRemoteDataSource.kt
  12. 71 0
      feature_person/src/main/java/com/mrozon/feature_person/data/PersonRepository.kt
  13. 33 0
      feature_person/src/main/java/com/mrozon/feature_person/di/EditPersonFragmentComponent.kt
  14. 21 0
      feature_person/src/main/java/com/mrozon/feature_person/di/EditPersonFragmentModule.kt
  15. 206 0
      feature_person/src/main/java/com/mrozon/feature_person/presentation/EditPersonFragment.kt
  16. 123 0
      feature_person/src/main/java/com/mrozon/feature_person/presentation/EditPersonFragmentViewModel.kt
  17. 13 5
      feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonAdapter.kt
  18. 21 2
      feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonFragment.kt
  19. 5 0
      feature_person/src/main/res/drawable/ic_baseline_delete_outline_24.xml
  20. 5 0
      feature_person/src/main/res/drawable/ic_check_white_24dp.xml
  21. 111 0
      feature_person/src/main/res/layout/fragment_edit_person.xml
  22. 1 1
      feature_person/src/main/res/layout/fragment_list_person.xml
  23. 0 3
      feature_person/src/main/res/layout/item_person.xml
  24. 16 0
      feature_person/src/main/res/menu/add_person_menu.xml
  25. 8 0
      feature_person/src/main/res/values/strings.xml
  26. 4 1
      utils/src/main/AndroidManifest.xml
  27. 4 1
      utils/src/main/java/com/mrozon/utils/base/BaseDataSource.kt
  28. 8 0
      utils/src/main/java/com/mrozon/utils/extension/FragmentExt.kt
  29. 8 1
      utils/src/main/java/com/mrozon/utils/extension/TypeExt.kt
  30. 8 0
      utils/src/main/java/com/mrozon/utils/extension/ViewExt.kt
  31. 4 0
      utils/src/main/java/com/mrozon/utils/network/Result.kt
  32. 1 0
      utils/src/main/res/values/strings.xml

+ 10 - 16
app/src/main/java/com/mrozon/healthdiary/di/NavigationModule.kt

@@ -1,24 +1,10 @@
 package com.mrozon.healthdiary.di
 
-import androidx.databinding.ViewDataBinding
-import androidx.navigation.NavController
-import androidx.navigation.fragment.findNavController
-import com.mrozon.core_api.db.HealthDiaryDao
-import com.mrozon.core_api.mapper.UserToUserDbMapper
-import com.mrozon.core_api.navigation.LoginNavigator
-import com.mrozon.core_api.navigation.RegistrationNavigator
-import com.mrozon.core_api.navigation.SplashNavigator
-import com.mrozon.feature_splash.repository.LocalUser
-import com.mrozon.feature_splash.repository.LocalUserImp
-import com.mrozon.healthdiary.navigation.LoginNavigatorImpl
-import com.mrozon.healthdiary.navigation.RegistrationNavigatorImpl
-import com.mrozon.healthdiary.navigation.SplashNavigatorImpl
-import com.mrozon.utils.base.BaseFragment
+import com.mrozon.core_api.navigation.*
+import com.mrozon.healthdiary.navigation.*
 import dagger.Binds
 import dagger.Module
-import dagger.Provides
 import dagger.Reusable
-import javax.inject.Singleton
 
 @Module
 interface NavigationModule {
@@ -34,4 +20,12 @@ interface NavigationModule {
     @Reusable
     @Binds
     fun registrationNavigator(navigator: RegistrationNavigatorImpl): RegistrationNavigator
+
+    @Reusable
+    @Binds
+    fun listPersonNavigator(navigator: ListPersonNavigatorImpl): ListPersonNavigator
+
+    @Reusable
+    @Binds
+    fun editPersonNavigator(navigator: EditPersonNavigatorImpl): EditPersonNavigator
 }

+ 14 - 0
app/src/main/java/com/mrozon/healthdiary/navigation/EditPersonNavigatorImpl.kt

@@ -0,0 +1,14 @@
+package com.mrozon.healthdiary.navigation
+
+import androidx.navigation.NavController
+import com.mrozon.core_api.navigation.EditPersonNavigator
+import com.mrozon.healthdiary.R
+import javax.inject.Inject
+
+class EditPersonNavigatorImpl @Inject constructor()
+    : EditPersonNavigator {
+
+    override fun navigateToListPerson(navController: NavController) {
+        navController.navigate(R.id.action_editPersonFragment_to_listPersonFragment)
+    }
+}

+ 17 - 0
app/src/main/java/com/mrozon/healthdiary/navigation/ListPersonNavigatorImpl.kt

@@ -0,0 +1,17 @@
+package com.mrozon.healthdiary.navigation
+
+import androidx.core.os.bundleOf
+import androidx.navigation.NavController
+import androidx.navigation.fragment.findNavController
+import com.mrozon.core_api.navigation.ListPersonNavigator
+import com.mrozon.healthdiary.R
+import javax.inject.Inject
+
+class ListPersonNavigatorImpl @Inject constructor()
+    :ListPersonNavigator{
+
+    override fun navigateToEditPerson(navController: NavController, title: String, id: Long) {
+        val bundle = bundleOf("title" to title, "id" to id)
+        navController.navigate(R.id.action_listPersonFragment_to_editPersonFragment, bundle)
+    }
+}

+ 25 - 1
app/src/main/res/navigation/nav_graph.xml

@@ -78,6 +78,30 @@
         android:id="@+id/listPersonFragment"
         android:name="com.mrozon.feature_person.presentation.ListPersonFragment"
         android:label="ListPersonFragment"
-        tools:layout="@layout/fragment_list_person"/>
+        tools:layout="@layout/fragment_list_person">
+        <action
+            android:id="@+id/action_listPersonFragment_to_editPersonFragment"
+            app:destination="@id/editPersonFragment"
+            app:enterAnim="@anim/slide_in_right"
+            app:exitAnim="@anim/slide_out_left"
+            app:popEnterAnim="@anim/slide_in_left"
+            app:popExitAnim="@anim/slide_out_right"/>
+    </fragment>
+
+    <fragment
+        android:id="@+id/editPersonFragment"
+        android:name="com.mrozon.feature_person.presentation.EditPersonFragment"
+        tools:layout="@layout/fragment_edit_person"
+        android:label="{title}" >
+        <argument
+            android:name="title"
+            app:argType="string" />
+        <argument
+            android:name="id"
+            app:argType="long" />
+        <action
+            android:id="@+id/action_editPersonFragment_to_listPersonFragment"
+            app:destination="@id/listPersonFragment" />
+    </fragment>
 
 </navigation>

+ 12 - 0
core_api/src/main/java/com/mrozon/core_api/db/HealthDiaryDao.kt

@@ -26,6 +26,18 @@ interface HealthDiaryDao {
     @Query("SELECT * FROM person_table")
     fun getPersons(): LiveData<List<PersonDb>>
 
+    @Query("SELECT * FROM person_table WHERE person_id=:id LIMIT 1")
+    suspend fun getPerson(id: Long): PersonDb
+
     @Insert(onConflict = OnConflictStrategy.REPLACE)
     suspend fun insertAllPerson(persons: List<PersonDb>)
+
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    suspend fun insertPerson(person: PersonDb)
+
+    @Query("DELETE FROM person_table WHERE person_id=:id")
+    suspend fun deletePerson(id: Long)
+
+    @Query("DELETE FROM person_table")
+    suspend fun deleteAllPerson()
 }

+ 8 - 0
core_api/src/main/java/com/mrozon/core_api/navigation/EditPersonNavigator.kt

@@ -0,0 +1,8 @@
+package com.mrozon.core_api.navigation
+
+import androidx.navigation.NavController
+
+interface EditPersonNavigator {
+
+    fun navigateToListPerson(navController: NavController)
+}

+ 8 - 0
core_api/src/main/java/com/mrozon/core_api/navigation/ListPersonNavigator.kt

@@ -0,0 +1,8 @@
+package com.mrozon.core_api.navigation
+
+import androidx.navigation.NavController
+
+interface ListPersonNavigator {
+
+    fun navigateToEditPerson(navController: NavController, title: String, id: Long)
+}

+ 2 - 0
core_api/src/main/java/com/mrozon/core_api/navigation/NavigatorProvider.kt

@@ -4,4 +4,6 @@ interface NavigatorProvider {
     fun provideSplashNavigator(): SplashNavigator
     fun provideLoginNavigator(): LoginNavigator
     fun provideRegistrationNavigator(): RegistrationNavigator
+    fun provideListPersonNavigator(): ListPersonNavigator
+    fun provideEditPersonNavigator(): EditPersonNavigator
 }

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

@@ -19,6 +19,17 @@ interface HealthDiaryService {
     @GET("patients/")
     suspend fun getPersons(@Header("Authorization") token: String): Response<List<PersonResponse>>
 
+    @POST("patients/")
+    suspend fun addPerson(@Header("Authorization") token: String, @Body body: PersonRequest): Response<PersonResponse>
+
+    @DELETE("patients/{id}/")
+    suspend fun deletePerson(@Header("Authorization") token: String, @Path("id") id: String): Response<Unit>
+
+    @PUT("patients/{id}/")
+    suspend fun editPerson(@Header("Authorization") token: String,
+                           @Path("id") id: String,
+                           @Body body: PersonRequest): Response<PersonResponse>
+
 //    @GET("lego/themes/")
 //    suspend fun getThemes(@Query("page") page: Int? = null,
 //                          @Query("page_size") pageSize: Int? = null,

+ 1 - 0
feature_auth/src/main/res/layout/fragment_registration.xml

@@ -36,6 +36,7 @@
                 android:hint="@string/etUsername"
                 android:inputType="textEmailAddress"
                 app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintHorizontal_bias="0.0"
                 app:layout_constraintStart_toStartOf="parent"
                 app:layout_constraintTop_toTopOf="@+id/guideline" />
 

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

@@ -2,6 +2,7 @@ package com.mrozon.feature_person.data
 
 import com.mrozon.core_api.db.HealthDiaryDao
 import com.mrozon.core_api.network.HealthDiaryService
+import com.mrozon.core_api.network.model.PersonRequest
 import com.mrozon.utils.base.BaseDataSource
 import javax.inject.Inject
 
@@ -13,4 +14,22 @@ class PersonRemoteDataSource @Inject constructor(private val service: HealthDiar
                 val token = "Token "+dao.getAccessToken()
                 service.getPersons(token)
             }
+
+    suspend fun addPersons(personRequest: PersonRequest)
+            = getResult {
+        val token = "Token "+dao.getAccessToken()
+        service.addPerson(token, personRequest)
+    }
+
+    suspend fun deletePerson(id: Long)
+            = getResult {
+        val token = "Token "+dao.getAccessToken()
+        service.deletePerson(token, id.toString())
+    }
+
+    suspend fun editPerson(id: Long, personRequest: PersonRequest)
+            = getResult {
+        val token = "Token "+dao.getAccessToken()
+        service.editPerson(token, id.toString(), personRequest)
+    }
 }

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

@@ -2,9 +2,20 @@ package com.mrozon.feature_person.data
 
 import androidx.lifecycle.Transformations
 import com.mrozon.core_api.db.HealthDiaryDao
+import com.mrozon.core_api.db.model.PersonDb
+import com.mrozon.core_api.entity.Gender
+import com.mrozon.core_api.entity.Person
 import com.mrozon.core_api.mapper.PersonToPersonDbMapper
+import com.mrozon.core_api.network.model.PersonRequest
 import com.mrozon.core_api.network.model.toPerson
 import com.mrozon.core_api.resultLiveData
+import com.mrozon.utils.extension.toDateString
+import com.mrozon.utils.network.Result
+import com.mrozon.utils.network.Result.Companion.error
+import com.mrozon.utils.network.Result.Companion.loading
+import com.mrozon.utils.network.Result.Companion.success
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -27,8 +38,68 @@ class PersonRepository @Inject constructor(private val dao: HealthDiaryDao,
                 personResponse.toPerson()
             }
             val personsDb = mapper.map(persons)
+            dao.deleteAllPerson()
             dao.insertAllPerson(personsDb)
         }
     )
 
+    fun addPerson(person: Person): Flow<Result<Person>> {
+        return flow {
+            emit(loading())
+            var gender = true
+            if (person.gender==Gender.FEMALE) gender = false
+            val personRequest = PersonRequest(name=person.name, gender = gender, born = person.born.toDateString() )
+            val response = personRemoteDataSource.addPersons(personRequest)
+            if (response.status == Result.Status.SUCCESS) {
+                val personDb = mapper.map(response.data?.toPerson())
+                dao.insertPerson(personDb!!)
+                emit(success(response.data!!.toPerson()))
+            } else if (response.status == Result.Status.ERROR) {
+                emit(error(response.message!!))
+            }
+        }
+    }
+
+    fun getPerson(id: Long): Flow<Result<Person>> {
+        return flow {
+            try {
+                emit(loading())
+                val response = dao.getPerson(id)
+                emit(success(mapper.reverseMap(response)!!))
+            }
+            catch (e: Exception){
+                emit(error(e.message!!))
+            }
+        }
+    }
+
+    fun deletePerson(id: Long): Flow<Result<Unit>> {
+        return flow {
+            val response = personRemoteDataSource.deletePerson(id)
+            if (response.status == Result.Status.SUCCESS) {
+                dao.deletePerson(id)
+                emit(success())
+            } else if (response.status == Result.Status.ERROR) {
+                emit(error(response.message!!))
+            }
+        }
+    }
+
+    fun editPerson(person: Person): Flow<Result<Person>> {
+        return flow {
+            emit(loading())
+            var gender = true
+            if (person.gender==Gender.FEMALE) gender = false
+            val personRequest = PersonRequest(name=person.name, gender = gender, born = person.born.toDateString() )
+            val response = personRemoteDataSource.editPerson(person.id, personRequest)
+            if (response.status == Result.Status.SUCCESS) {
+                val personDb = mapper.map(response.data?.toPerson())
+                dao.insertPerson(personDb!!)
+                emit(success(response.data!!.toPerson()))
+            } else if (response.status == Result.Status.ERROR) {
+                emit(error(response.message!!))
+            }
+        }
+    }
+
 }

+ 33 - 0
feature_person/src/main/java/com/mrozon/feature_person/di/EditPersonFragmentComponent.kt

@@ -0,0 +1,33 @@
+package com.mrozon.feature_person.di
+
+import com.mrozon.core_api.providers.AppWithFacade
+import com.mrozon.core_api.providers.ProvidersFacade
+import com.mrozon.core_api.viewmodel.ViewModelsFactoryProvider
+import com.mrozon.feature_person.presentation.EditPersonFragment
+import com.mrozon.feature_person.presentation.ListPersonFragment
+import dagger.Component
+import javax.inject.Singleton
+
+@Singleton
+@Component(
+    modules = [EditPersonFragmentModule::class],
+    dependencies = [ProvidersFacade::class]
+)
+interface EditPersonFragmentComponent: ViewModelsFactoryProvider {
+
+    companion object {
+        fun create(providersFacade: ProvidersFacade): EditPersonFragmentComponent {
+            return DaggerEditPersonFragmentComponent.builder()
+                .providersFacade(providersFacade)
+                .build()
+        }
+        fun injectFragment(fragment: EditPersonFragment): EditPersonFragmentComponent  {
+            val component = create((fragment.activity?.application
+                    as AppWithFacade).getFacade())
+            component.inject(fragment)
+            return component
+        }
+    }
+
+    fun inject(fragment: EditPersonFragment)
+}

+ 21 - 0
feature_person/src/main/java/com/mrozon/feature_person/di/EditPersonFragmentModule.kt

@@ -0,0 +1,21 @@
+package com.mrozon.feature_person.di
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.mrozon.core_api.viewmodel.ViewModelKey
+import com.mrozon.feature_person.presentation.EditPersonFragmentViewModel
+import com.mrozon.feature_person.presentation.ListPersonFragmentViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+interface EditPersonFragmentModule {
+    @Binds
+    @IntoMap
+    @ViewModelKey(EditPersonFragmentViewModel::class)
+    fun bindViewModel(viewmodel: EditPersonFragmentViewModel): ViewModel
+
+    @Binds
+    fun viewModelFactory(factory: DaggerViewModelFactory): ViewModelProvider.Factory
+}

+ 206 - 0
feature_person/src/main/java/com/mrozon/feature_person/presentation/EditPersonFragment.kt

@@ -0,0 +1,206 @@
+package com.mrozon.feature_person.presentation
+
+import android.content.Context
+import android.os.Bundle
+import android.view.*
+import androidx.core.app.ActivityCompat.invalidateOptionsMenu
+import androidx.core.view.get
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import com.mrozon.core_api.entity.Gender
+import com.mrozon.core_api.navigation.EditPersonNavigator
+import com.mrozon.core_api.navigation.ListPersonNavigator
+import com.mrozon.feature_person.R
+import com.mrozon.feature_person.databinding.FragmentEditPersonBinding
+import com.mrozon.feature_person.di.EditPersonFragmentComponent
+import com.mrozon.utils.base.BaseFragment
+import com.mrozon.utils.extension.*
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.FlowPreview
+import timber.log.Timber
+import java.util.*
+import javax.inject.Inject
+
+
+class EditPersonFragment : BaseFragment<FragmentEditPersonBinding>() {
+
+    override fun getLayoutId(): Int = R.layout.fragment_edit_person
+
+    @Inject
+    lateinit var viewModelFactory: ViewModelProvider.Factory
+
+    @Inject
+    lateinit var navigator: EditPersonNavigator
+
+    private val viewModel by viewModels<EditPersonFragmentViewModel> { viewModelFactory }
+
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        EditPersonFragmentComponent.injectFragment(this)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        setHasOptionsMenu(true)
+        return super.onCreateView(inflater, container, savedInstanceState)
+    }
+
+    @ExperimentalCoroutinesApi
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        binding?.radioGroup?.setOnCheckedChangeListener { _, id ->
+            if(id==R.id.rbMale)
+                viewModel.setMaleGender(true)
+            else
+                viewModel.setMaleGender(false)
+        }
+
+        binding?.etPersonName?.offer(viewModel.personNameChannel)
+
+        binding?.pickerBoD?.init(2020,1,1)
+        { _, year, month, day ->
+            val date = "$year-${month+1}-$day"
+//            Timber.d("string = $date date=${date.toSimpleDate().toDateString()}")
+            viewModel.personDobChannel.offer(date.toSimpleDate())
+        }
+        val calendar = Calendar.getInstance()
+        calendar.time = Date()
+        binding?.pickerBoD?.updateDate(calendar.get(Calendar.YEAR),
+            calendar.get(Calendar.MONTH),
+            calendar.get(Calendar.DAY_OF_MONTH))
+
+        val id = arguments?.getLong("id",0)?:0
+        if(id>0){
+            viewModel.initValue(id)
+        }
+    }
+
+    @ExperimentalCoroutinesApi
+    @FlowPreview
+    override fun subscribeUi() {
+        viewModel.male.observe(viewLifecycleOwner, Observer { isMale ->
+            if (isMale)
+                binding?.ivGenger?.setImageResource(R.drawable.ic_male_avatar)
+            else
+                binding?.ivGenger?.setImageResource(R.drawable.ic_female_avatar)
+        })
+
+        viewModel.enableAdding.observe(viewLifecycleOwner, Observer { enabled ->
+            invalidateOptionsMenu(activity)
+        })
+
+        viewModel.person.observe(viewLifecycleOwner, Observer { result ->
+            if(result!=null){
+                when (result.status) {
+                    Result.Status.LOADING -> {
+                        binding?.progressBar?.visible(true)
+                    }
+                    Result.Status.SUCCESS -> {
+                        binding?.progressBar?.visible(false)
+                        navigator.navigateToListPerson(findNavController())
+                    }
+                    Result.Status.ERROR -> {
+                        binding?.progressBar?.visible(false)
+                        showError(result.message!!) {}
+                    }
+                }
+            }
+        })
+
+        viewModel.initPerson.observe(viewLifecycleOwner, Observer { result ->
+            if(result!=null){
+                when (result.status) {
+                    Result.Status.LOADING -> {
+                        binding?.progressBar?.visible(true)
+                    }
+                    Result.Status.SUCCESS -> {
+                        arguments?.remove("id")
+                        binding?.progressBar?.visible(false)
+                        binding?.etPersonName?.setText(result.data?.name)
+                        if (result.data?.gender==Gender.FEMALE)
+                            binding?.radioGroup?.check(R.id.rbFemale)
+                        else
+                            binding?.radioGroup?.check(R.id.rbMale)
+                        val calendar = Calendar.getInstance()
+                        calendar.time = result.data?.born?:Date()
+                        binding?.pickerBoD?.updateDate(calendar.get(Calendar.YEAR),
+                            calendar.get(Calendar.MONTH),
+                            calendar.get(Calendar.DAY_OF_MONTH))
+                        viewModel.initDone()
+                        arguments?.putLong("current_id",result.data?.id?:-1)
+                        invalidateOptionsMenu(activity)
+                    }
+                    Result.Status.ERROR -> {
+                        binding?.progressBar?.visible(false)
+                        showError(result.message!!) {}
+                    }
+                }
+            }
+        })
+
+        viewModel.deletedPerson.observe(viewLifecycleOwner, Observer { result ->
+            if(result!=null){
+                when (result.status) {
+                    Result.Status.LOADING -> {
+                        binding?.progressBar?.visible(true)
+                    }
+                    Result.Status.SUCCESS -> {
+                        binding?.progressBar?.visible(false)
+                        navigator.navigateToListPerson(findNavController())
+                    }
+                    Result.Status.ERROR -> {
+                        binding?.progressBar?.visible(false)
+                        showError(result.message!!) {}
+                    }
+                }
+            }
+        })
+    }
+
+    @ExperimentalCoroutinesApi
+    @FlowPreview
+    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+        inflater.inflate(R.menu.add_person_menu, menu)
+        val saveMenuItem = menu.findItem(R.id.addPerson)
+        viewModel.enableAdding.value.let {
+            saveMenuItem.isVisible = it ?: false
+        }
+        val deleteMenuItem = menu.findItem(R.id.deletePerson)
+        val current_id = arguments?.getLong("current_id",-1)?:-1
+        deleteMenuItem.isVisible = current_id>0
+        return super.onCreateOptionsMenu(menu, inflater)
+    }
+
+    @ExperimentalCoroutinesApi
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        hideKeyboard()
+        if (!isActiveNetwork()){
+            showError(getString(R.string.network_inactive)) {}
+            return false
+        }
+        val current_id = arguments?.getLong("current_id",-1)?:-1
+        when(item.itemId){
+            R.id.addPerson -> {
+                if(current_id>0){
+                    viewModel.editPerson(current_id)
+                }
+                else {
+                    viewModel.addPerson()
+                }
+            }
+            R.id.deletePerson -> {
+                viewModel.deletePerson(current_id)
+            }
+        }
+        return false
+    }
+
+
+}

+ 123 - 0
feature_person/src/main/java/com/mrozon/feature_person/presentation/EditPersonFragmentViewModel.kt

@@ -0,0 +1,123 @@
+package com.mrozon.feature_person.presentation
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.viewModelScope
+import com.mrozon.core_api.entity.Gender
+import com.mrozon.core_api.entity.Person
+import com.mrozon.feature_person.data.PersonRepository
+import com.mrozon.utils.base.BaseViewModel
+import com.mrozon.utils.network.Result
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.ConflatedBroadcastChannel
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import java.util.*
+import javax.inject.Inject
+
+class EditPersonFragmentViewModel @Inject constructor(val repository: PersonRepository): BaseViewModel() {
+
+    private val _male = MutableLiveData<Boolean>(true)
+    val male: LiveData<Boolean>
+        get() = _male
+
+    private var _person = MutableLiveData<Result<Person>?>(null)
+    val person: LiveData<Result<Person>?>
+        get() = _person
+
+    private var _initPerson = MutableLiveData<Result<Person>?>(null)
+    val initPerson: LiveData<Result<Person>?>
+        get() = _initPerson
+
+    private var _deletedPerson = MutableLiveData<Result<Unit>?>(null)
+    val deletedPerson: LiveData<Result<Unit>?>
+        get() = _deletedPerson
+
+    @ExperimentalCoroutinesApi
+    val personNameChannel = ConflatedBroadcastChannel<String>()
+
+    @ExperimentalCoroutinesApi
+    val personDobChannel = ConflatedBroadcastChannel<Date>()
+
+    @ExperimentalCoroutinesApi
+    @FlowPreview
+    val enableAdding = object: MutableLiveData<Boolean>() {
+
+        override fun onActive() {
+            value?.let { return }
+            viewModelScope.launch {
+                var job: Deferred<Unit>? = null
+                personNameChannel.asFlow()
+                    .combine(personDobChannel.asFlow()) { name, date ->
+                        Pair(name, date)
+                    }
+                    .collect {
+                        job?.cancel()
+                        job = async(Dispatchers.Main) {
+                            value = it.first.isNotEmpty() and it.second.before(Date())
+                        }
+                    }
+            }
+        }
+    }
+
+    fun setMaleGender(isMale: Boolean) {
+        _male.value = isMale
+    }
+
+    @ExperimentalCoroutinesApi
+    fun addPerson() {
+        var gender = Gender.MALE
+        if(_male.value==false)
+            gender = Gender.FEMALE
+        val personEntity = Person(name = personNameChannel.value, gender = gender, born = personDobChannel.value )
+        viewModelScope.launch(Dispatchers.IO) {
+            repository.addPerson(personEntity).collect {
+                withContext(Dispatchers.Main) {
+                    _person.value = it
+                }
+            }
+        }
+    }
+
+    fun initValue(id: Long) {
+        viewModelScope.launch(Dispatchers.IO) {
+            repository.getPerson(id).collect {
+                withContext(Dispatchers.Main) {
+                    _initPerson.value = it
+                }
+            }
+        }
+    }
+
+    fun initDone() {
+        _initPerson.value = null
+    }
+
+    fun deletePerson(id: Long) {
+        viewModelScope.launch(Dispatchers.IO) {
+            repository.deletePerson(id).collect {
+                withContext(Dispatchers.Main) {
+                    _deletedPerson.value = it
+                }
+            }
+        }
+    }
+
+    @ExperimentalCoroutinesApi
+    fun editPerson(id: Long) {
+        var gender = Gender.MALE
+        if(_male.value==false)
+            gender = Gender.FEMALE
+        val personEntity = Person(id = id, name = personNameChannel.value, gender = gender, born = personDobChannel.value )
+        viewModelScope.launch(Dispatchers.IO) {
+            repository.editPerson(personEntity).collect {
+                withContext(Dispatchers.Main) {
+                    _person.value = it
+                }
+            }
+        }
+    }
+
+}

+ 13 - 5
feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonAdapter.kt

@@ -8,7 +8,7 @@ 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()){
+class ListPersonAdapter(private val clickListener: ListPersonClickListener): ListAdapter<Person, ListPersonAdapter.ViewHolder>(ListPersonDiffCallback()){
 
     override fun onCreateViewHolder(
         parent: ViewGroup,
@@ -23,9 +23,16 @@ class ListPersonAdapter(private val clickListener: ListPersonListener): ListAdap
     class ViewHolder private constructor(val binding: ItemPersonBinding ): RecyclerView.ViewHolder(binding.root){
         fun bind(
             item: Person,
-            clickListener: ListPersonListener) {
+            clickListener: ListPersonClickListener) {
             binding.person = item
-            binding.listener = clickListener
+//            binding.listener = clickListener
+            binding.cvPerson.setOnClickListener {
+                clickListener.onClick(item)
+            }
+            binding.cvPerson.setOnLongClickListener{
+                clickListener.onLongClick(item)
+                true
+            }
             binding.executePendingBindings()
         }
 
@@ -50,8 +57,9 @@ class ListPersonAdapter(private val clickListener: ListPersonListener): ListAdap
         }
     }
 
-    class ListPersonListener(val clickListener:(person: Person) -> Unit) {
-        fun onClick(person: Person) = clickListener(person)
+    interface ListPersonClickListener {
+        fun onClick(person: Person)
+        fun onLongClick(person: Person)
     }
 
 }

+ 21 - 2
feature_person/src/main/java/com/mrozon/feature_person/presentation/ListPersonFragment.kt

@@ -3,10 +3,15 @@ package com.mrozon.feature_person.presentation
 import android.content.Context
 import android.os.Bundle
 import android.view.View
+import androidx.core.os.bundleOf
 import androidx.fragment.app.viewModels
 import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
 import androidx.recyclerview.widget.LinearLayoutManager
+import com.mrozon.core_api.entity.Person
+import com.mrozon.core_api.navigation.ListPersonNavigator
+import com.mrozon.core_api.navigation.LoginNavigator
 import com.mrozon.feature_person.R
 import com.mrozon.feature_person.databinding.FragmentListPersonBinding
 import com.mrozon.feature_person.di.ListPersonFragmentComponent
@@ -22,6 +27,9 @@ class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
     @Inject
     lateinit var viewModelFactory: ViewModelProvider.Factory
 
+    @Inject
+    lateinit var navigator: ListPersonNavigator
+
     private val viewModel by viewModels<ListPersonFragmentViewModel> { viewModelFactory }
 
     private lateinit var adapter: ListPersonAdapter
@@ -33,12 +41,23 @@ class ListPersonFragment : BaseFragment<FragmentListPersonBinding>() {
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
-        adapter = ListPersonAdapter(ListPersonAdapter.ListPersonListener { person ->
-            Timber.d("click to ${person.name}")
+        adapter = ListPersonAdapter(object : ListPersonAdapter.ListPersonClickListener {
+            override fun onClick(person: Person) {
+                Timber.d("click to ${person.name}")
+            }
+
+            override fun onLongClick(person: Person) {
+                Timber.d("long click to ${person.name}")
+                navigator.navigateToEditPerson(findNavController(),getString(R.string.edit_person),person.id)
+            }
         })
         binding?.rvPerson?.adapter = adapter
         val manager = LinearLayoutManager(context)
         binding?.rvPerson?.layoutManager = manager
+
+        binding?.fabAddPerson?.setOnClickListener {
+            navigator.navigateToEditPerson(findNavController(),getString(R.string.add_person),0)
+        }
     }
 
     override fun subscribeUi() {

+ 5 - 0
feature_person/src/main/res/drawable/ic_baseline_delete_outline_24.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8,9h8v10L8,19L8,9zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
+</vector>

+ 5 - 0
feature_person/src/main/res/drawable/ic_check_white_24dp.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24.0" android:viewportWidth="24.0"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
+</vector>

+ 111 - 0
feature_person/src/main/res/layout/fragment_edit_person.xml

@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout 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">
+
+    <data>
+
+    </data>
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content">
+
+            <androidx.constraintlayout.widget.Guideline
+                android:id="@+id/guideline"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal"
+                app:layout_constraintGuide_percent="0.3" />
+
+            <EditText
+                android:id="@+id/etPersonName"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="8dp"
+                android:layout_marginEnd="16dp"
+                android:ems="10"
+                android:hint="@string/hint_person_name"
+                android:imeOptions="actionDone"
+                android:inputType="textPersonName"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@+id/ivGenger"
+                app:layout_constraintTop_toTopOf="@id/guideline"
+                tools:text="Ivan Petrov" />
+
+            <ImageView
+                android:id="@+id/ivGenger"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="16dp"
+                app:layout_constraintBottom_toBottomOf="@+id/etPersonName"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="@+id/etPersonName"
+                app:srcCompat="@drawable/ic_male_avatar" />
+
+            <RadioGroup
+                android:id="@+id/radioGroup"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="16dp"
+                android:checkedButton="@id/rbMale"
+                android:orientation="horizontal"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@id/etPersonName">
+
+                <RadioButton
+                    android:id="@+id/rbMale"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@string/male" />
+
+                <RadioButton
+                    android:id="@+id/rbFemale"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:text="@string/female" />
+
+            </RadioGroup>
+
+            <DatePicker
+                android:id="@+id/pickerBoD"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:calendarViewShown="false"
+                android:datePickerMode="spinner"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toBottomOf="@+id/textView3" />
+
+            <TextView
+                android:id="@+id/textView3"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="16dp"
+                android:text="@string/tvDOB"
+                app:layout_constraintStart_toStartOf="@+id/etPersonName"
+                app:layout_constraintTop_toBottomOf="@id/radioGroup" />
+
+            <ProgressBar
+                android:id="@+id/progressBar"
+                style="?android:attr/progressBarStyle"
+                android:layout_width="128dp"
+                android:layout_height="128dp"
+                android:indeterminate="true"
+                android:visibility="invisible"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:visibility="visible" />
+
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+    </ScrollView>
+</layout>

+ 1 - 1
feature_person/src/main/res/layout/fragment_list_person.xml

@@ -12,7 +12,7 @@
         android:layout_height="match_parent">
 
         <com.google.android.material.floatingactionbutton.FloatingActionButton
-            android:id="@+id/floatingActionButton"
+            android:id="@+id/fabAddPerson"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_marginEnd="32dp"

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

@@ -7,9 +7,6 @@
         <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

+ 16 - 0
feature_person/src/main/res/menu/add_person_menu.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/deletePerson"
+        android:icon="@drawable/ic_baseline_delete_outline_24"
+        android:title="@string/delete_person"
+        android:visible="false"
+        app:showAsAction="ifRoom" />
+
+    <item
+        android:id="@+id/addPerson"
+        android:icon="@drawable/ic_check_white_24dp"
+        android:title="@string/add_person"
+        app:showAsAction="always" />
+</menu>

+ 8 - 0
feature_person/src/main/res/values/strings.xml

@@ -2,4 +2,12 @@
     <!-- TODO: Remove or change this placeholder text -->
     <string name="hello_blank_fragment">Hello blank fragment</string>
     <string name="ivGender">gender\'s person</string>
+    <string name="add_person">Add person</string>
+    <string name="hint_person_name">Type person name</string>
+    <string name="male">Male</string>
+    <string name="female">Female</string>
+    <string name="etBorn">Type date of born</string>
+    <string name="tvDOB">Date of birth</string>
+    <string name="edit_person">Edit person</string>
+    <string name="delete_person">Delete person</string>
 </resources>

+ 4 - 1
utils/src/main/AndroidManifest.xml

@@ -1,2 +1,5 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-    package="com.mrozon.utils" />
+    package="com.mrozon.utils" >
+
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+</manifest>

+ 4 - 1
utils/src/main/java/com/mrozon/utils/base/BaseDataSource.kt

@@ -13,7 +13,10 @@ abstract class BaseDataSource {
             //TODO response.errorBody()
             if (response.isSuccessful) {
                 val body = response.body()
-                if (body != null) return Result.success(body)
+                if (body != null)
+                    return Result.success(body)
+                else
+                    return Result.success()
             }
             var errMessage = response.message()
             val errorBody= response.errorBody()

+ 8 - 0
utils/src/main/java/com/mrozon/utils/extension/FragmentExt.kt

@@ -1,6 +1,8 @@
 package com.mrozon.utils.extension
 
 import android.content.Context
+import android.net.ConnectivityManager
+import android.net.NetworkInfo
 import android.view.WindowManager
 import android.view.inputmethod.InputMethodManager
 import androidx.appcompat.app.AppCompatActivity
@@ -20,4 +22,10 @@ fun Fragment.hideKeyboard() {
     if (activity is AppCompatActivity) {
         activity.hideKeyboard()
     }
+}
+
+fun Fragment.isActiveNetwork(): Boolean {
+    val cm = context?.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+    val activeNetwork: NetworkInfo? = cm.activeNetworkInfo
+    return activeNetwork?.isConnectedOrConnecting == true
 }

+ 8 - 1
utils/src/main/java/com/mrozon/utils/extension/TypeExt.kt

@@ -4,6 +4,13 @@ import java.text.SimpleDateFormat
 import java.util.Date
 import java.util.Locale
 
-fun String.toSimpleDate(format: String = "YYYY-MM-DD"): Date {
+const val defaultFormat = "yyyy-MM-dd"
+
+fun String.toSimpleDate(format: String = defaultFormat): Date {
     return SimpleDateFormat(format, Locale.getDefault()).parse(this)!!
+}
+
+fun Date.toDateString(format: String = defaultFormat): String {
+    val dateFormat = SimpleDateFormat(format, Locale.getDefault())
+    return dateFormat.format(this)
 }

+ 8 - 0
utils/src/main/java/com/mrozon/utils/extension/ViewExt.kt

@@ -1,10 +1,12 @@
 package com.mrozon.utils.extension
 
 import android.view.View
+import android.widget.DatePicker
 import android.widget.EditText
 import androidx.core.widget.doOnTextChanged
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.ConflatedBroadcastChannel
+import java.util.*
 
 @ExperimentalCoroutinesApi
 fun EditText.offer(channel: ConflatedBroadcastChannel<String>) {
@@ -23,4 +25,10 @@ fun View.visible(show: Boolean, isGone: Boolean = false) {
         else
             visibility = View.INVISIBLE
     }
+}
+
+fun DatePicker.getDate(): Date {
+    val calendar = Calendar.getInstance()
+    calendar.set(year, month, dayOfMonth)
+    return calendar.time
 }

+ 4 - 0
utils/src/main/java/com/mrozon/utils/network/Result.kt

@@ -13,6 +13,10 @@ data class Result<out T>(val status: Status, val data: T?, val message: String?)
             return Result(Status.SUCCESS, data, null)
         }
 
+        fun <T> success(): Result<T> {
+            return Result(Status.SUCCESS, null, null)
+        }
+
         fun <T> error(message: String, data: T? = null): Result<T> {
             return Result(Status.ERROR, data, message)
         }

+ 1 - 0
utils/src/main/res/values/strings.xml

@@ -1,4 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
     <string name="Ok">Ok</string>
+    <string name="network_inactive">Sorry, no internet access!</string>
 </resources>