diff --git a/app/build.gradle b/app/build.gradle index b6a859d..b3ee4c9 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:3.12.8" 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..bc92766 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,6 +14,9 @@ android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="AllowBackup,GoogleAppIndexingWarning"> + @@ -31,4 +34,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..7fdfeeb --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesAdapter.kt @@ -0,0 +1,18 @@ +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()) { + + override fun onBindViewHolder(holder: ActivitiesViewHolder, position: Int) { + getItem(position)?.let { holder.bind(it) } + } + + 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..27f22b8 --- /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.id == newItem.id + + 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..e0e21cc --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesRepository.kt @@ -0,0 +1,125 @@ +package com.flatstack.android.activities + +import androidx.lifecycle.asFlow +import androidx.paging.DataSource +import androidx.paging.LivePagedListBuilder +import androidx.paging.PageKeyedDataSource +import androidx.paging.PagedList +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.profile.entities.Profile +import com.flatstack.android.type.ActivityEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ActivitiesRepository( + private val apolloClient: ApolloClient +) { + @Suppress("LongMethod") + fun getPagedUserActivities(coroutineScope: CoroutineScope, events: List) = + LivePagedListBuilder( + 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.optional(events), + first = Input.optional(PAGE_SIZE) + ) + ).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.optional(params.key.endCursor), + events = Input.optional(events), + first = Input.optional(PAGE_SIZE) + ) + ).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.optional(params.key.startCursor), + events = Input.optional(events), + first = Input.optional(PAGE_SIZE) + ) + ).toDeferred() + .await().data?.activities?.fragments?.activityConnectionFragment?.let { + it.edges?.let { edges -> + callback.onResult( + edges, + it.pageInfo + ) + } + } + } + } + } + } + .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, + user = Profile( + firstName = fragment.user.firstName ?: "", + lastName = fragment.user.lastName ?: "" + ) + ) + } + } + }, + PagedList.Config.Builder() + .setEnablePlaceholders(false) + .setPageSize(PAGE_SIZE) + .build() + ).build().asFlow() + + companion object { + private const val PAGE_SIZE = 15 + } +} 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..635a9d2 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolder.kt @@ -0,0 +1,27 @@ +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.* +import java.text.SimpleDateFormat +import java.util.* + +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 = SimpleDateFormat("HH:mm, dd MMM yyyy", Locale.ENGLISH).format( + SimpleDateFormat("yyyy-mm-dd'T'HH:MM:ss'Z'", Locale.ENGLISH).parse(item.createdAt) + ) + tv_username.text = itemView.context.getString( + R.string.username_mask, + item.user.firstName, + item.user.lastName + ) + tv_id.text = itemView.context.getString(R.string.id_mask, item.id) + } + } +} 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..3bba719 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewHolderModel.kt @@ -0,0 +1,13 @@ +package com.flatstack.android.activities + +import com.flatstack.android.profile.entities.Profile +import com.flatstack.android.type.ActivityEvent + +data class ActivitiesViewHolderModel( + val id: String, + val body: String, + val createdAt: String, + val event: ActivityEvent, + val title: String, + val user: Profile +) 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..96ed517 --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/ActivitiesViewModel.kt @@ -0,0 +1,45 @@ +package com.flatstack.android.activities + +import androidx.lifecycle.* +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 -> + activitiesRepository.getPagedUserActivities(viewModelScope, events) + .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..305fccd --- /dev/null +++ b/app/src/main/java/com/flatstack/android/activities/UserActivitiesActivity.kt @@ -0,0 +1,51 @@ +package com.flatstack.android.activities + +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +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..7c7ef0f 100644 --- a/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt +++ b/app/src/main/java/com/flatstack/android/profile/ProfileActivity.kt @@ -23,7 +23,7 @@ class ProfileActivity : AppCompatActivity(), KodeinAware, OnRefreshListener { override val kodein: Kodein by kodein() private val viewModel: ProfileViewModel by provideViewModel() - + private val router by kodein.instance() private val refreshLayout: SwipeRefreshLayout by lazy { swipe_layout } override fun onCreate(savedInstanceState: Bundle?) { @@ -41,6 +41,14 @@ class ProfileActivity : AppCompatActivity(), KodeinAware, OnRefreshListener { }, onError = ::showError, onLoading = ::visibleProgress) + + initListeners() + } + + private fun initListeners() { + bt_go_to_activities.setOnClickListener { + 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..304a592 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" /> +