From f4b6fbdcfc76b11818271a673103953994c6df69 Mon Sep 17 00:00:00 2001 From: andy Date: Tue, 8 Dec 2020 02:31:44 +0300 Subject: [PATCH 1/5] WIP --- app/build.gradle | 9 ++ app/src/main/AndroidManifest.xml | 3 +- .../graphql/query/GetActivities.graphql | 40 ++++++++ .../main/java/com/flatstack/android/Router.kt | 9 ++ .../android/activities/ActivitiesAdapter.kt | 22 +++++ .../activities/ActivitiesDiffUtilCallback.kt | 15 +++ .../activities/ActivitiesRepository.kt | 93 +++++++++++++++++++ .../activities/ActivitiesViewHolder.kt | 18 ++++ .../activities/ActivitiesViewHolderModel.kt | 14 +++ .../android/activities/ActivitiesViewModel.kt | 66 +++++++++++++ .../activities/UserActivitiesActivity.kt | 51 ++++++++++ .../flatstack/android/di/modules/netModule.kt | 4 +- .../android/di/modules/repoModule.kt | 2 + .../android/di/modules/viewModelModule.kt | 2 + .../android/profile/ProfileActivity.kt | 9 ++ app/src/main/res/layout/activity_profile.xml | 9 ++ .../res/layout/activity_user_activities.xml | 29 ++++++ app/src/main/res/layout/item_activity.xml | 56 +++++++++++ app/src/main/res/values/strings.xml | 2 + deps.gradle | 9 +- 20 files changed, 457 insertions(+), 5 deletions(-) create mode 100644 app/src/main/graphql/com/flatstack/android/graphql/query/GetActivities.graphql create mode 100644 app/src/main/java/com/flatstack/android/activities/ActivitiesAdapter.kt create mode 100644 app/src/main/java/com/flatstack/android/activities/ActivitiesDiffUtilCallback.kt create mode 100644 app/src/main/java/com/flatstack/android/activities/ActivitiesRepository.kt create mode 100644 app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolder.kt create mode 100644 app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolderModel.kt create mode 100644 app/src/main/java/com/flatstack/android/activities/ActivitiesViewModel.kt create mode 100644 app/src/main/java/com/flatstack/android/activities/UserActivitiesActivity.kt create mode 100644 app/src/main/res/layout/activity_user_activities.xml create mode 100644 app/src/main/res/layout/item_activity.xml diff --git a/app/build.gradle b/app/build.gradle index b6a859d..348c0ed 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,6 +37,8 @@ android { branchNames = ["master"] // branch names from which you can deploy, master by default remoteRepoName = "origin" // alias repository, origin by default } + + multiDexEnabled true } signingConfigs { debug { @@ -105,12 +107,19 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$versions.kotlin" implementation 'com.google.code.gson:gson:2.8.6' + implementation "androidx.paging:paging-runtime-ktx:2.1.2" + implementation "com.google.android.material:material:1.2.1" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" + implementation 'com.android.support:multidex:1.0.3' + implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" implementation androidLibs implementation apolloLibs implementation coroutinesLibs implementation kodeinLibs implementation roomLibs + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' kapt compilerLibs testImplementation unitTestLibs diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aafc5de..678d0d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="AllowBackup,GoogleAppIndexingWarning"> + @@ -31,4 +32,4 @@ - \ No newline at end of file + diff --git a/app/src/main/graphql/com/flatstack/android/graphql/query/GetActivities.graphql b/app/src/main/graphql/com/flatstack/android/graphql/query/GetActivities.graphql new file mode 100644 index 0000000..f76c6e5 --- /dev/null +++ b/app/src/main/graphql/com/flatstack/android/graphql/query/GetActivities.graphql @@ -0,0 +1,40 @@ +query GetActivities($after: String, $before: String, $events: [ActivityEvent!], $first: Int, $last: Int) { + activities( + after: $after, + before: $before, + events: $events, + first: $first, + last: $last + ) { + ...ActivityConnectionFragment + } +} + +fragment ActivityConnectionFragment on ActivityConnection { + edges { + node { + ...ActivityFragment + } + } + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } +} + + +fragment ActivityFragment on Activity { + body, + createdAt, + event, + id, + title, + user { + firstName, + lastName, + email, + avatarUrl + } +} \ No newline at end of file diff --git a/app/src/main/java/com/flatstack/android/Router.kt b/app/src/main/java/com/flatstack/android/Router.kt index ae126e4..fedf287 100644 --- a/app/src/main/java/com/flatstack/android/Router.kt +++ b/app/src/main/java/com/flatstack/android/Router.kt @@ -2,6 +2,7 @@ package com.flatstack.android import android.content.Context import android.content.Intent +import com.flatstack.android.activities.UserActivitiesActivity import com.flatstack.android.login.LoginActivity import com.flatstack.android.profile.ProfileActivity @@ -23,4 +24,12 @@ class Router( } }) } + + fun activities(context: Context, clearStack: Boolean = false) { + context.startActivity(Intent(context, UserActivitiesActivity::class.java).apply { + if (clearStack) { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + }) + } } diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesAdapter.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesAdapter.kt new file mode 100644 index 0000000..0f389eb --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesAdapter.kt @@ -0,0 +1,22 @@ +package com.flatstack.android.activities + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import com.flatstack.android.R + +class ActivitiesAdapter : + PagedListAdapter(ActivitiesDiffUtilCallback()) { + + private val activities = mutableListOf() + + override fun getItemCount(): Int = activities.count() + + override fun onBindViewHolder(holder: ActivitiesViewHolder, position: Int) { + holder.bind(activities[position]) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ActivitiesViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_activity, parent, false) + ) +} diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesDiffUtilCallback.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesDiffUtilCallback.kt new file mode 100644 index 0000000..3a4f3bb --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesDiffUtilCallback.kt @@ -0,0 +1,15 @@ +package com.flatstack.android.activities + +import androidx.recyclerview.widget.DiffUtil + +class ActivitiesDiffUtilCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: ActivitiesViewHolderModel, + newItem: ActivitiesViewHolderModel + ): Boolean = oldItem == newItem + + override fun areContentsTheSame( + oldItem: ActivitiesViewHolderModel, + newItem: ActivitiesViewHolderModel + ): Boolean = oldItem == newItem +} diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesRepository.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesRepository.kt new file mode 100644 index 0000000..e7525cb --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesRepository.kt @@ -0,0 +1,93 @@ +package com.flatstack.android.activities + +import androidx.paging.DataSource +import androidx.paging.PageKeyedDataSource +import com.apollographql.apollo.ApolloClient +import com.apollographql.apollo.api.Input +import com.apollographql.apollo.coroutines.toDeferred +import com.flatstack.android.fragment.ActivityConnectionFragment.Edge +import com.flatstack.android.fragment.ActivityConnectionFragment.PageInfo +import com.flatstack.android.graphql.query.GetActivitiesQuery +import com.flatstack.android.type.ActivityEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ActivitiesRepository( + private val apolloClient: ApolloClient +) { + fun getPagedUserActivities(coroutineScope: CoroutineScope, events: List) = + object : DataSource.Factory() { + override fun create(): DataSource = + object : PageKeyedDataSource() { + override fun loadInitial( + params: LoadInitialParams, + callback: LoadInitialCallback + ) { + coroutineScope.launch(Dispatchers.IO) { + apolloClient.query( + GetActivitiesQuery( + events = Input.fromNullable(events), + first = Input.fromNullable(15) + ) + ).toDeferred() + .await().data?.activities?.fragments?.activityConnectionFragment?.let { + it.edges?.let { edges -> + callback.onResult( + edges, + null, + it.pageInfo + ) + } + } + } + } + + override fun loadAfter( + params: LoadParams, + callback: LoadCallback + ) { + coroutineScope.launch(Dispatchers.IO) { + apolloClient.query( + GetActivitiesQuery( + after = Input.fromNullable(params.key.endCursor), + events = Input.fromNullable(events), + first = Input.fromNullable(15) + ) + ).toDeferred() + .await().data?.activities?.fragments?.activityConnectionFragment?.let { + it.edges?.let { edges -> + callback.onResult( + edges, + it.pageInfo + ) + } + } + } + } + + override fun loadBefore( + params: LoadParams, + callback: LoadCallback + ) { + coroutineScope.launch(Dispatchers.IO) { + apolloClient.query( + GetActivitiesQuery( + before = Input.fromNullable(params.key.startCursor), + events = Input.fromNullable(events), + first = Input.fromNullable(15) + ) + ).toDeferred() + .await().data?.activities?.fragments?.activityConnectionFragment?.let { + it.edges?.let { edges -> + callback.onResult( + edges, + it.pageInfo + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolder.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolder.kt new file mode 100644 index 0000000..b5d43d7 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolder.kt @@ -0,0 +1,18 @@ +package com.flatstack.android.activities + +import android.view.View +import com.flatstack.android.R +import com.flatstack.android.util.recyclerview.BaseHolder +import kotlinx.android.synthetic.main.item_activity.view.* + +class ActivitiesViewHolder(itemView: View) : BaseHolder(itemView) { + + override fun bind(item: ActivitiesViewHolderModel) { + with(containerView) { + tv_event.text = item.title + tv_body.text = item.body + tv_created_at.text = item.createdAt + tv_username.text = item.userName + } + } +} diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolderModel.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolderModel.kt new file mode 100644 index 0000000..1417e45 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolderModel.kt @@ -0,0 +1,14 @@ +package com.flatstack.android.activities + +import com.flatstack.android.profile.entities.Profile +import com.flatstack.android.type.ActivityEvent +import java.util.* + +data class ActivitiesViewHolderModel( + val body: String, + val createdAt: String, + val event: ActivityEvent, + val id: String, + val title: String, + val userName: String +) diff --git a/app/src/main/java/com/flatstack/android/activities/ActivitiesViewModel.kt b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewModel.kt new file mode 100644 index 0000000..eae6ddb --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewModel.kt @@ -0,0 +1,66 @@ +package com.flatstack.android.activities + +import androidx.lifecycle.* +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import com.flatstack.android.model.entities.Resource +import com.flatstack.android.type.ActivityEvent +import com.flatstack.android.type.ActivityEvent.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +class ActivitiesViewModel( + private val activitiesRepository: ActivitiesRepository +) : ViewModel() { + + private val events = MutableLiveData>() + + init { + events.postValue( + listOf( + RESET_PASSWORD_REQUESTED, + USER_LOGGED_IN, + USER_REGISTERED, + USER_RESET_PASSWORD, + USER_UPDATED + ) + ) + } + + val activities: LiveData>> = events.switchMap { events -> + LivePagedListBuilder( + activitiesRepository.getPagedUserActivities(viewModelScope, events) + .mapByPage { edges -> + edges.map { edge -> + edge.node?.fragments?.activityFragment?.let { fragment -> + ActivitiesViewHolderModel( + body = fragment.body, + createdAt = fragment.createdAt.toString(), + event = fragment.event, + id = fragment.id, + title = fragment.title, + userName = fragment.id + ) + } + } + }, + PagedList.Config.Builder() + .setEnablePlaceholders(false) + .setPageSize(15) + .build() + ).build().asFlow() + .flowOn(Dispatchers.IO) + .map { Resource.success(it) } + .onStart { emit(Resource.loading()) } + .catch { error -> + error.message?.let { + emit(Resource.error(it)) + } + } + .asLiveData(viewModelScope.coroutineContext) + } +} + diff --git a/app/src/main/java/com/flatstack/android/activities/UserActivitiesActivity.kt b/app/src/main/java/com/flatstack/android/activities/UserActivitiesActivity.kt new file mode 100644 index 0000000..d1e3985 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/UserActivitiesActivity.kt @@ -0,0 +1,51 @@ +package com.flatstack.android.activities + +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.paging.PagedList +import com.flatstack.android.R +import com.flatstack.android.util.observeBy +import com.flatstack.android.util.provideViewModel +import kotlinx.android.synthetic.main.activity_user_activities.* +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.kodein + +class UserActivitiesActivity : AppCompatActivity(), KodeinAware { + + override val kodein: Kodein by kodein() + + private val viewModel: ActivitiesViewModel by provideViewModel() + private val adapter = ActivitiesAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_user_activities) + initRecycler() + + viewModel.activities.observeBy( + this, + onNext = ::fetchActivities, + onLoading = ::showProgress, + onError = ::showError + ) + } + + private fun fetchActivities(pagedList: PagedList) { + adapter.submitList(pagedList) + } + + private fun showProgress(isLoading: Boolean) { + pb_progress.isVisible = isLoading + } + + private fun showError(errorMessage: String) { + Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show() + } + + private fun initRecycler() { + rv_activities.adapter = adapter + } +} diff --git a/app/src/main/java/com/flatstack/android/di/modules/netModule.kt b/app/src/main/java/com/flatstack/android/di/modules/netModule.kt index d13c1d1..563b68c 100644 --- a/app/src/main/java/com/flatstack/android/di/modules/netModule.kt +++ b/app/src/main/java/com/flatstack/android/di/modules/netModule.kt @@ -4,6 +4,7 @@ import com.apollographql.apollo.ApolloClient import com.flatstack.android.BuildConfig import okhttp3.Interceptor import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import org.kodein.di.Kodein import org.kodein.di.generic.bind import org.kodein.di.generic.instance @@ -13,7 +14,8 @@ val netModule = Kodein.Module(name = "netModule") { bind() with singleton { AuthorizationInterceptor(instance()) } bind() with singleton { OkHttpClient.Builder() - .addInterceptor(instance()) + .addInterceptor(AuthorizationInterceptor(instance())) + .addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) .build() } bind() with singleton { diff --git a/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt b/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt index d3f702f..1171606 100644 --- a/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt +++ b/app/src/main/java/com/flatstack/android/di/modules/repoModule.kt @@ -1,5 +1,6 @@ package com.flatstack.android.di.modules +import com.flatstack.android.activities.ActivitiesRepository import com.flatstack.android.login.LoginRepository import com.flatstack.android.profile.ProfileRepository import org.kodein.di.Kodein @@ -10,4 +11,5 @@ import org.kodein.di.generic.provider val repoModule = Kodein.Module(name = "repoModule") { bind() with provider { LoginRepository(instance(), instance(), instance(), instance()) } bind() with provider { ProfileRepository(instance(), instance(), instance()) } + bind() with provider { ActivitiesRepository(instance()) } } diff --git a/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt b/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt index c69b1d3..c567ab5 100644 --- a/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt +++ b/app/src/main/java/com/flatstack/android/di/modules/viewModelModule.kt @@ -1,6 +1,7 @@ package com.flatstack.android.di.modules import androidx.lifecycle.ViewModelProvider +import com.flatstack.android.activities.ActivitiesViewModel import com.flatstack.android.login.LoginViewModel import com.flatstack.android.profile.ProfileViewModel import com.flatstack.android.util.ViewModelFactory @@ -16,4 +17,5 @@ val viewModelModule = Kodein.Module(name = "viewModelModule") { bindViewModel() with provider { LoginViewModel(instance(), instance()) } bindViewModel() with provider { ProfileViewModel(instance(), instance()) } + bindViewModel() with provider { ActivitiesViewModel(instance()) } } diff --git a/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt b/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt index 8fbdfaa..00af6ca 100644 --- a/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt +++ b/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt @@ -41,6 +41,15 @@ class ProfileActivity : AppCompatActivity(), KodeinAware, OnRefreshListener { }, onError = ::showError, onLoading = ::visibleProgress) + + initListeners() + } + + private fun initListeners() { + bt_go_to_activities.setOnClickListener { + val router by kodein.instance() + router.activities(context = this, clearStack = true) + } } override fun onCreateOptionsMenu(menu: Menu?): Boolean { diff --git a/app/src/main/res/layout/activity_profile.xml b/app/src/main/res/layout/activity_profile.xml index a447517..e3f0bce 100644 --- a/app/src/main/res/layout/activity_profile.xml +++ b/app/src/main/res/layout/activity_profile.xml @@ -40,6 +40,15 @@ app:layout_constraintTop_toBottomOf="@id/tv_first_name" tools:text="@string/last_name_hint" /> +