?) {
54 | handler.post {
55 | newData?.let {
56 | dataList = it
57 | notifyDataSetChanged()
58 | }
59 | }
60 | }
61 |
62 | fun clearData(isNotify: Boolean = true) {
63 | dataList.clear()
64 | if (isNotify) notifyDataSetChanged()
65 | }
66 |
67 | fun addItem(data: T, position: Int) {
68 | dataList.add(position, data)
69 | notifyItemInserted(position)
70 | }
71 |
72 | fun removeItem(position: Int, isNotifyAll: Boolean = false) {
73 | if (position < 0 || position >= dataList.size) {
74 | return
75 | }
76 | dataList.removeAt(position)
77 | if (isNotifyAll) notifyDataSetChanged() else notifyItemChanged(position)
78 | }
79 |
80 | fun replaceItem(item: T, position: Int, isNotifyAll: Boolean = false) {
81 | if (position < 0 || position >= dataList.size) {
82 | return
83 | }
84 | dataList[position] = item
85 | if (isNotifyAll) notifyDataSetChanged() else notifyItemChanged(position)
86 | }
87 |
88 | fun registerItemClickListener(onItemClickListener: (T) -> Unit) {
89 | itemClickListener = onItemClickListener
90 | }
91 |
92 | fun unRegisterItemClickListener() {
93 | itemClickListener = null
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.di
2 |
3 | import android.app.Application
4 | import com.squareup.moshi.Moshi
5 | import com.thuanpx.view_mvvm_architecture.BuildConfig
6 | import com.thuanpx.view_mvvm_architecture.data.local.datastore.PreferenceDataStore
7 | import com.thuanpx.view_mvvm_architecture.data.remote.api.ApiService
8 | import com.thuanpx.view_mvvm_architecture.data.remote.api.middleware.DefaultInterceptor
9 | import dagger.Module
10 | import dagger.Provides
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.components.SingletonComponent
13 | import okhttp3.Cache
14 | import okhttp3.Interceptor
15 | import okhttp3.OkHttpClient
16 | import okhttp3.logging.HttpLoggingInterceptor
17 | import retrofit2.Retrofit
18 | import retrofit2.converter.moshi.MoshiConverterFactory
19 | import java.util.concurrent.TimeUnit
20 | import javax.inject.Singleton
21 |
22 | /**
23 | * Created by ThuanPx on 8/7/20.
24 | */
25 |
26 | @Module
27 | @InstallIn(SingletonComponent::class)
28 | object NetworkModule {
29 |
30 | @Singleton
31 | @Provides
32 | fun provideRetrofit(moshi: Moshi, okHttpClient: OkHttpClient): Retrofit {
33 | return Retrofit.Builder()
34 | .baseUrl(BuildConfig.END_POINT)
35 | .addConverterFactory(MoshiConverterFactory.create(moshi))
36 | .client(okHttpClient)
37 | .build()
38 | }
39 |
40 | @Singleton
41 | @Provides
42 | fun provideOkHttpCache(app: Application): Cache {
43 | val cacheSize: Long = 10 * 1024 * 1024 // 10 MiB
44 | return Cache(app.cacheDir, cacheSize)
45 | }
46 |
47 | @Singleton
48 | @Provides
49 | fun provideOkHttpClient(cache: Cache, interceptor: Interceptor): OkHttpClient {
50 | val httpClientBuilder = OkHttpClient.Builder()
51 | httpClientBuilder.cache(cache)
52 | httpClientBuilder.addInterceptor(interceptor)
53 |
54 | httpClientBuilder.readTimeout(
55 | READ_TIMEOUT, TimeUnit.SECONDS
56 | )
57 | httpClientBuilder.writeTimeout(
58 | WRITE_TIMEOUT, TimeUnit.SECONDS
59 | )
60 | httpClientBuilder.connectTimeout(
61 | CONNECTION_TIMEOUT, TimeUnit.SECONDS
62 | )
63 |
64 | if (BuildConfig.DEBUG) {
65 | val logging = HttpLoggingInterceptor()
66 | httpClientBuilder.addInterceptor(logging)
67 | logging.level = HttpLoggingInterceptor.Level.BODY
68 | }
69 |
70 | return httpClientBuilder.build()
71 | }
72 |
73 | @Singleton
74 | @Provides
75 | fun provideInterceptor(preferenceDataStore: PreferenceDataStore): Interceptor {
76 | return DefaultInterceptor(preferenceDataStore)
77 | }
78 |
79 | @Singleton
80 | @Provides
81 | fun provideApi(retrofit: Retrofit): ApiService {
82 | return retrofit.create(ApiService::class.java)
83 | }
84 |
85 | private const val READ_TIMEOUT: Long = 30
86 | private const val WRITE_TIMEOUT: Long = 30
87 | private const val CONNECTION_TIMEOUT: Long = 30
88 | }
89 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Base project Android with Hilt, Coroutines, Flow, Jetpack (Room, ViewModel), and Material Design based on MVVM architecture.
8 |
9 |
10 |
11 |
12 |
13 |
14 | # Tech stack & Open-source libraries
15 | - Minimum SDK level 21
16 | - [Kotlin](https://kotlinlang.org/) based, [Coroutines](https://github.com/Kotlin/kotlinx.coroutines) + [Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/) for asynchronous.
17 | - [Hilt](https://dagger.dev/hilt/) for dependency injection.
18 | - Jetpack
19 | - Lifecycle - Observe Android lifecycles and handle UI states upon the lifecycle changes.
20 | - ViewModel - Manages UI-related data holder and lifecycle aware. Allows data to survive configuration changes such as screen rotations.
21 | - ViewBinding - Is a feature that allows you to more easily write code that interacts with views. Once view binding is enabled in a module, it generates a binding class for each XML layout file present in that module. An instance of a binding class contains direct references to all views that have an ID in the corresponding layout.
22 | - Room Persistence - Constructs Database by providing an abstraction layer over SQLite to allow fluent database access.
23 | - Architecture
24 | - MVVM Architecture (Model - View - ViewModel)
25 | - [Repository Pattern](https://developer.android.com/codelabs/basic-android-kotlin-training-repository-pattern#0)
26 | - [Retrofit2 & OkHttp3](https://github.com/square/retrofit) - Construct the REST APIs.
27 | - [Sandwich](https://github.com/skydoves/Sandwich) - Construct a lightweight and modern response interface to handle network payload for Android.
28 | - [Gson](https://github.com/google/gson) - A modern JSON library for Kotlin and Java.
29 | - [Glide](https://github.com/bumptech/glide) - Loading images from network.
30 | - [Timber](https://github.com/JakeWharton/timber) - A logger with a small, extensible API.
31 | - [Material-Components](https://github.com/material-components/material-components-android) - Material design components for building ripple animation, and CardView.
32 |
33 | # License
34 | ```xml
35 | Designed and developed by 2022 ThuanPx
36 |
37 | Licensed under the Apache License, Version 2.0 (the "License");
38 | you may not use this file except in compliance with the License.
39 | You may obtain a copy of the License at
40 |
41 | http://www.apache.org/licenses/LICENSE-2.0
42 |
43 | Unless required by applicable law or agreed to in writing, software
44 | distributed under the License is distributed on an "AS IS" BASIS,
45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
46 | See the License for the specific language governing permissions and
47 | limitations under the License.
48 | ```
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/DataResult.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2019 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.thuanpx.view_mvvm_architecture.utils
18 |
19 | import androidx.lifecycle.LiveData
20 |
21 | /**
22 | * A generic class that holds a value with its loading status.
23 | * @param
24 | */
25 | sealed class DataResult {
26 |
27 | data class Success(val data: T) : DataResult()
28 | data class Error(val exception: Exception) : DataResult()
29 |
30 | inline fun executeIfSucceed(block: (data: R) -> Unit): DataResult {
31 | if (this is Success) block(this.data)
32 | return this
33 | }
34 |
35 | inline fun executeIfFailed(block: (ex: Exception) -> Unit): DataResult {
36 | if (this is Error) block(this.exception)
37 | return this
38 | }
39 |
40 | inline fun map(block: (R) -> M): DataResult {
41 | return when (this) {
42 | is Success -> Success(block(data))
43 | is Error -> Error(exception)
44 | }
45 | }
46 |
47 | inline fun mapWithoutResult(success: (R) -> M): M? {
48 | return when (this) {
49 | is Success -> success(data)
50 | else -> null // Ignore loading and failed for synchronize code
51 | }
52 | }
53 |
54 | /**
55 | * Get data of result by status
56 | * If succeeded return the data
57 | * else return null
58 | */
59 | fun getResultData(): R? = when (this) {
60 | is Success -> data
61 | else -> null // Ignore loading and failed for synchronize code
62 | }
63 |
64 | fun isCompleted() = this is Success || this is Error
65 |
66 | override fun toString(): String {
67 | return when (this) {
68 | is Success<*> -> "Success[data=$data]"
69 | is Error -> "Error[exception=$exception]"
70 | }
71 | }
72 | }
73 |
74 | /**
75 | * `true` if [DataResult] is of type [Success] & holds non-null [Success.data].
76 | */
77 | val DataResult<*>.succeeded get() = this is DataResult.Success && data != null
78 |
79 | val LiveData>.dataOfResult: T? get() = if (value is DataResult.Success) (value as DataResult.Success).data else null
80 |
81 | /**
82 | * A observable list items include success case and error case
83 | */
84 | typealias LiveResultItems = LiveData>>
85 |
86 | /**
87 | * A observable a item include success case and error case
88 | */
89 | typealias LiveResult = LiveData>
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/base/BaseDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.base
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import com.thuanpx.view_mvvm_architecture.model.response.BaseResponse
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.delay
8 | import kotlinx.coroutines.flow.collect
9 | import kotlinx.coroutines.flow.flow
10 | import kotlinx.coroutines.flow.flowOn
11 | import retrofit2.HttpException
12 | import java.io.IOException
13 |
14 | /**
15 | * Created by ThuanPx on 4/3/22.
16 | */
17 |
18 | abstract class BaseDataSource : PagingSource() {
19 |
20 | companion object {
21 | private const val STARTING_PAGE_INDEX = 0
22 | private const val LOAD_DELAY_MILLIS = 2_000L
23 | }
24 |
25 | abstract suspend fun requestMore(nextPage: Int): BaseResponse>
26 |
27 | override suspend fun load(params: LoadParams): LoadResult {
28 |
29 | val pageNumber = params.key ?: STARTING_PAGE_INDEX
30 | if (pageNumber != STARTING_PAGE_INDEX) delay(LOAD_DELAY_MILLIS)
31 |
32 | return try {
33 | var prevKey: Int? = null
34 | var nextKey: Int? = null
35 | var items = listOf()
36 | flow>> {
37 | items = requestMore(nextPage = pageNumber).data ?: emptyList()
38 | // Since 0 is the lowest page number, return null to signify no more pages should
39 | prevKey = if (pageNumber == STARTING_PAGE_INDEX) null else pageNumber.minus(1)
40 | // data, we return `null` to signify no more pages should be loaded
41 | nextKey = if (items.isNotEmpty()) pageNumber + 1 else null
42 | }
43 | .flowOn(Dispatchers.IO)
44 | .collect()
45 |
46 | LoadResult.Page(
47 | data = items,
48 | prevKey = prevKey,
49 | nextKey = nextKey
50 | )
51 | } catch (e: IOException) {
52 | // IOException for network failures.
53 | return LoadResult.Error(e)
54 | } catch (e: HttpException) {
55 | // HttpException for any non-2xx HTTP status codes.
56 | return LoadResult.Error(e)
57 | } catch (e: Throwable) {
58 | return LoadResult.Error(e)
59 | }
60 | }
61 |
62 | override fun getRefreshKey(state: PagingState): Int? {
63 | // Try to find the page key of the closest page to anchorPosition, from
64 | // either the prevKey or the nextKey, but you need to handle nullability
65 | // here:
66 | // * prevKey == null -> anchorPage is the first page.
67 | // * nextKey == null -> anchorPage is the last page.
68 | // * both prevKey and nextKey null -> anchorPage is the initial page, so
69 | // just return null.
70 | return state.anchorPosition?.let { anchorPosition ->
71 | val anchorPage = state.closestPageToPosition(anchorPosition)
72 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
22 |
23 |
29 |
30 |
37 |
38 |
58 |
59 |
60 |
67 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/base/fragment/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.base.fragment
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import androidx.fragment.app.createViewModelLazy
9 | import androidx.lifecycle.Lifecycle
10 | import androidx.lifecycle.lifecycleScope
11 | import androidx.lifecycle.repeatOnLifecycle
12 | import androidx.viewbinding.ViewBinding
13 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity
14 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel
15 | import com.thuanpx.view_mvvm_architecture.feature.MainActivity
16 | import com.thuanpx.view_mvvm_architecture.widget.ProgressDialog
17 | import kotlinx.coroutines.flow.Flow
18 | import kotlinx.coroutines.launch
19 | import kotlin.reflect.KClass
20 |
21 | /**
22 | * Created by ThuanPx on 8/5/20.
23 | *
24 | * @viewModel -> view model
25 | * @viewModelClass -> class view model
26 | * @viewBinding -> class binding
27 | * @initialize -> init UI, adapter, listener...
28 | * @onSubscribeObserver -> subscribe observer live data
29 | *
30 | */
31 |
32 | abstract class BaseFragment(viewModelClass: KClass) :
33 | Fragment() {
34 |
35 | protected val viewModel by createViewModelLazy(viewModelClass, { viewModelStore })
36 | private var _viewBinding: viewBinding? = null
37 | protected val viewBinding get() = _viewBinding!! // ktlint-disable
38 |
39 | abstract fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): viewBinding
40 |
41 | protected var progressDialog: ProgressDialog? = null
42 |
43 | fun getMainActivity(): MainActivity? = activity as? MainActivity
44 |
45 | protected abstract fun initialize()
46 |
47 | override fun onCreateView(
48 | inflater: LayoutInflater,
49 | container: ViewGroup?,
50 | savedInstanceState: Bundle?
51 | ): View? {
52 | _viewBinding = inflateViewBinding(inflater, container)
53 | return viewBinding.root
54 | }
55 |
56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
57 | super.onViewCreated(view, savedInstanceState)
58 | progressDialog = ProgressDialog(requireContext())
59 | initialize()
60 | onSubscribeObserver()
61 | }
62 |
63 | /**
64 | * Fragments outlive their views. Make sure you clean up any references to
65 | * the binding class instance in the fragment's onDestroyView() method.
66 | */
67 | override fun onDestroyView() {
68 | super.onDestroyView()
69 | _viewBinding = null
70 | }
71 |
72 | fun showLoading(isShow: Boolean) {
73 | (activity as? BaseActivity<*, *>)?.showLoading(isShow)
74 | }
75 |
76 | open fun onSubscribeObserver() {
77 | viewModel.run {
78 | isLoading.launchAndCollect {
79 | showLoading(it)
80 | }
81 | error.launchAndCollect {
82 | (activity as? BaseActivity<*, *>)?.handleApiError(it)
83 | }
84 | }
85 | }
86 |
87 | protected inline infix fun Flow.launchAndCollect(crossinline action: (T) -> Unit) {
88 | viewLifecycleOwner.lifecycleScope.launch {
89 | viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
90 | collect { action(it) }
91 | }
92 | }
93 | }
94 | }
95 |
96 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/feature/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.feature
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import androidx.activity.OnBackPressedCallback
6 | import androidx.activity.enableEdgeToEdge
7 | import androidx.viewpager2.widget.ViewPager2
8 | import com.thuanpx.mvvm_architecture.widget.KeyboardUtils.hideSoftKeyboard
9 | import com.thuanpx.view_mvvm_architecture.R
10 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity
11 | import com.thuanpx.view_mvvm_architecture.databinding.ActivityMainBinding
12 | import dagger.hilt.android.AndroidEntryPoint
13 |
14 | @AndroidEntryPoint
15 | class MainActivity : BaseActivity(MainViewModel::class) {
16 |
17 | companion object {
18 | const val TAB1 = 0
19 | const val TAB2 = 1
20 | }
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | }
25 |
26 | override fun inflateViewBinding(inflater: LayoutInflater): ActivityMainBinding {
27 | return ActivityMainBinding.inflate(inflater)
28 | }
29 |
30 | override fun initialize() {
31 | onBackPressedDispatcher.addCallback(this, object: OnBackPressedCallback(true) {
32 | override fun handleOnBackPressed() {
33 | if (viewBinding.viewPager.currentItem != 0) {
34 | viewBinding.viewPager.setCurrentItem(0, false)
35 | } else {
36 | finish()
37 | }
38 | }
39 |
40 | })
41 | initBottomNav()
42 | initViewPager()
43 | }
44 |
45 | private fun initViewPager() {
46 | viewBinding.run {
47 | viewPager.apply {
48 | isUserInputEnabled = false
49 | adapter = MainViewPagerAdapter(this@MainActivity)
50 | offscreenPageLimit = 3
51 | registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
52 | override fun onPageSelected(position: Int) {
53 | super.onPageSelected(position)
54 | selectedBottomNav(position)
55 | }
56 | })
57 | }
58 | }
59 | }
60 |
61 | private fun selectedBottomNav(position: Int) {
62 | when (position) {
63 | 0 -> {
64 | viewBinding.bottomNav.post {
65 | viewBinding.bottomNav.menu.findItem(R.id.tab1).isChecked = true
66 | }
67 | }
68 | 1 -> {
69 | viewBinding.bottomNav.post {
70 | viewBinding.bottomNav.menu.findItem(R.id.tab2).isChecked = true
71 | }
72 | }
73 | }
74 | viewBinding.viewPager.setCurrentItem(position, false)
75 | }
76 |
77 | private fun initBottomNav() {
78 | viewBinding.run {
79 |
80 | bottomNav.setOnItemSelectedListener { item ->
81 | hideSoftKeyboard(bottomNav)
82 | when (item.itemId) {
83 | R.id.tab1 -> {
84 | viewBinding.viewPager.setCurrentItem(0, false)
85 | }
86 |
87 | R.id.tab2 -> {
88 | viewBinding.viewPager.setCurrentItem(1, false)
89 | }
90 |
91 | }
92 | return@setOnItemSelectedListener true
93 | }
94 | }
95 | }
96 |
97 | override fun onSubscribeObserver() {
98 | super.onSubscribeObserver()
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.base
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.lifecycle.Lifecycle
7 | import androidx.lifecycle.ViewModelLazy
8 | import androidx.lifecycle.lifecycleScope
9 | import androidx.lifecycle.repeatOnLifecycle
10 | import androidx.viewbinding.ViewBinding
11 | import com.thuanpx.view_mvvm_architecture.base.network.AppErrors
12 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel
13 | import com.thuanpx.view_mvvm_architecture.utils.extension.boolean.isNotTrue
14 | import com.thuanpx.view_mvvm_architecture.utils.extension.boolean.isTrue
15 | import com.thuanpx.view_mvvm_architecture.utils.extension.string.toIntOrZero
16 | import com.thuanpx.view_mvvm_architecture.utils.extension.widget.dialog
17 | import com.thuanpx.view_mvvm_architecture.widget.ProgressDialog
18 | import kotlinx.coroutines.flow.Flow
19 | import kotlinx.coroutines.launch
20 | import java.net.HttpURLConnection
21 | import kotlin.reflect.KClass
22 |
23 | /**
24 | * Created by ThuanPx on 8/5/20.
25 | *
26 | * @viewModel -> view model
27 | * @viewModelClass -> class view model
28 | * @viewBinding -> class binding
29 | * @initialize -> init UI, adapter, listener...
30 | * @onSubscribeObserver -> subscribe observer
31 | *
32 | */
33 |
34 | abstract class BaseActivity(viewModelClass: KClass) :
35 | AppCompatActivity() {
36 |
37 | protected val viewModel by ViewModelLazy(
38 | viewModelClass,
39 | { viewModelStore },
40 | { defaultViewModelProviderFactory },
41 | { this.defaultViewModelCreationExtras })
42 | protected lateinit var viewBinding: viewBinding
43 | abstract fun inflateViewBinding(inflater: LayoutInflater): viewBinding
44 |
45 | protected var progressDialog: ProgressDialog? = null
46 |
47 | protected abstract fun initialize()
48 |
49 | override fun onCreate(savedInstanceState: Bundle?) {
50 | super.onCreate(savedInstanceState)
51 | viewBinding = inflateViewBinding(layoutInflater)
52 | progressDialog = ProgressDialog(this)
53 | setContentView(viewBinding.root)
54 | initialize()
55 | onSubscribeObserver()
56 | }
57 |
58 | fun showLoading(isShow: Boolean) {
59 | if (isShow && progressDialog?.isShowing.isNotTrue()) {
60 | progressDialog?.show()
61 | } else if (progressDialog?.isShowing.isTrue()) {
62 | progressDialog?.dismiss()
63 | }
64 | }
65 |
66 | open fun onSubscribeObserver() {
67 | viewModel.run {
68 | isLoading.launchAndCollect {
69 | showLoading(it)
70 | }
71 | error.launchAndCollect {
72 | handleApiError(it)
73 | }
74 | }
75 | }
76 |
77 | fun handleApiError(throwable: Throwable) {
78 | val networkError = AppErrors.fromThrowable(throwable)
79 | if (networkError?.errorCode?.toIntOrZero() == HttpURLConnection.HTTP_UNAUTHORIZED) {
80 | dialog {
81 | message = networkError.message ?: throwable.message ?: "Unknown"
82 | }
83 | return
84 | }
85 | dialog {
86 | message = networkError?.message ?: throwable.message ?: "Unknown"
87 | }
88 | }
89 |
90 | protected inline infix fun Flow.launchAndCollect(crossinline action: (T) -> Unit) {
91 | with(this) {
92 | lifecycleScope.launch {
93 | repeatOnLifecycle(Lifecycle.State.STARTED) {
94 | collect { action(it) }
95 | }
96 | }
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/context/TimeAgoExt.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.utils.extension.context
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.text.format.DateUtils
6 | import com.thuanpx.view_mvvm_architecture.R
7 | import java.text.ParseException
8 | import java.text.SimpleDateFormat
9 | import java.time.Instant
10 | import java.time.LocalDateTime
11 | import java.time.ZoneId
12 | import java.time.format.DateTimeFormatter
13 | import java.util.Date
14 | import java.util.TimeZone
15 | import kotlin.math.abs
16 |
17 | private const val SECOND_MILLIS = 1000
18 | private const val MINUTE_MILLIS = 60 * SECOND_MILLIS
19 | private const val HOUR_MILLIS = 60 * MINUTE_MILLIS
20 | private const val DAY_MILLIS = 24 * HOUR_MILLIS
21 |
22 | @Throws(ParseException::class)
23 | fun getLocalTime(timestamp: String?, simpleDateFormat: String?): Date? {
24 | val formatter = SimpleDateFormat(simpleDateFormat).apply {
25 | timeZone = TimeZone.getTimeZone("UTC")
26 | }
27 | return timestamp?.let { formatter.parse(it) }
28 | }
29 |
30 | @Throws(ParseException::class)
31 | fun timestampToMilli(timestamp: String?, simpleDateFormat: String?): Long {
32 | val dateUtc = getLocalTime(timestamp, simpleDateFormat)
33 | val dateFormatter =
34 | SimpleDateFormat(simpleDateFormat).apply { timeZone = TimeZone.getDefault() }
35 | val localTimeString = dateUtc?.let { dateFormatter.format(it) }
36 |
37 | val date = localTimeString?.let { SimpleDateFormat(simpleDateFormat).parse(it) }
38 | return date!!.time
39 | }
40 |
41 | @SuppressLint("NewApi")
42 | fun milliToStringTime(milli: Long): String {
43 | val instant = Instant.ofEpochMilli(milli)
44 | val date = LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
45 | val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")
46 | return formatter.format(date)
47 | }
48 |
49 | fun Context.getTimeAgo(
50 | timeString: String? = "",
51 | pattern: String = "yyyy-MM-dd'T'HH:mm:ss"
52 | ): String {
53 | if (timeString.isNullOrEmpty() ) {
54 | return resources.getString(R.string.just_now)
55 | }
56 | try {
57 | var time = timestampToMilli(timeString, pattern)
58 | if (time < 1000000000000L) {
59 | time *= 1000
60 | }
61 | val diff: Long = abs(System.currentTimeMillis() - time)
62 | return if (diff < MINUTE_MILLIS) {
63 | resources.getString(R.string.just_now)
64 | } else if (diff < 2 * MINUTE_MILLIS) {
65 | resources.getString(R.string.min_ago)
66 | } else if (diff < 50 * MINUTE_MILLIS) {
67 | val minutes = diff / MINUTE_MILLIS
68 | "$minutes " + resources.getString(R.string.mins_ago)
69 | } else if (diff < 90 * MINUTE_MILLIS) {
70 | resources.getString(R.string.hour_ago)
71 | } else if (diff < 24 * HOUR_MILLIS) {
72 | val hours = (diff / HOUR_MILLIS).toString()
73 | "$hours " + resources.getString(R.string.hours_ago)
74 | } else if (diff < 7 * DAY_MILLIS) {
75 | if ((diff / DAY_MILLIS) == 1L) {
76 | resources.getString(R.string.day_ago)
77 | } else {
78 | val day = diff / DAY_MILLIS
79 | "$day " + resources.getString(R.string.days_ago)
80 | }
81 | } else if (diff < 4 * DateUtils.WEEK_IN_MILLIS) {
82 | if (diff / DateUtils.WEEK_IN_MILLIS == 1L) {
83 | resources.getString(R.string.week_ago)
84 | } else {
85 | val week = diff / DateUtils.WEEK_IN_MILLIS
86 | "$week " + resources.getString(R.string.weeks_ago)
87 | }
88 | } else {
89 | resources.getString(R.string.more_than_months_ago)
90 | }
91 | } catch (e: Exception) {
92 | return resources.getString(R.string.just_now)
93 | }
94 | }
95 |
96 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/base/fragment/BaseDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.base.fragment
2 |
3 | import android.graphics.Color
4 | import android.graphics.drawable.ColorDrawable
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.fragment.app.DialogFragment
10 | import androidx.fragment.app.createViewModelLazy
11 | import androidx.lifecycle.Lifecycle
12 | import androidx.lifecycle.lifecycleScope
13 | import androidx.lifecycle.repeatOnLifecycle
14 | import androidx.viewbinding.ViewBinding
15 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity
16 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel
17 | import com.thuanpx.view_mvvm_architecture.R
18 | import com.thuanpx.view_mvvm_architecture.widget.ProgressDialog
19 | import kotlinx.coroutines.flow.Flow
20 | import kotlinx.coroutines.launch
21 | import kotlin.reflect.KClass
22 |
23 | /**
24 | * Created by ThuanPx on 8/5/20.
25 | *
26 | * @viewModel -> name view model
27 | * @classViewModel -> class view model
28 | * @viewBinding -> class binding
29 | * @initialize -> init UI, adapter, listener...
30 | * @onSubscribeObserver -> subscribe observer
31 | *
32 | */
33 |
34 | abstract class BaseDialogFragment(viewModelClass: KClass) :
35 | DialogFragment() {
36 |
37 | protected val viewModel by createViewModelLazy(viewModelClass, { viewModelStore })
38 | private var _viewBinding: viewBinding? = null
39 | protected val viewBinding get() = _viewBinding!! // ktlint-disable
40 | protected var progressDialog: ProgressDialog? = null
41 |
42 | abstract fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): viewBinding
43 |
44 | protected abstract fun initialize()
45 |
46 | override fun onCreateView(
47 | inflater: LayoutInflater,
48 | container: ViewGroup?,
49 | savedInstanceState: Bundle?
50 | ): View? {
51 | _viewBinding = inflateViewBinding(inflater, container)
52 | return viewBinding.root
53 | }
54 |
55 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
56 | super.onViewCreated(view, savedInstanceState)
57 | progressDialog = ProgressDialog(requireContext())
58 | initialize()
59 | onSubscribeObserver()
60 | }
61 |
62 | override fun onCreate(savedInstanceState: Bundle?) {
63 | super.onCreate(savedInstanceState)
64 | setStyle(STYLE_NORMAL, R.style.AppTheme_Dialog)
65 | }
66 |
67 | override fun onStart() {
68 | super.onStart()
69 | dialog?.let {
70 | dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
71 | val width = ViewGroup.LayoutParams.MATCH_PARENT
72 | val height = ViewGroup.LayoutParams.MATCH_PARENT
73 | it.window?.setLayout(width, height)
74 | }
75 | }
76 |
77 | /**
78 | * Fragments outlive their views. Make sure you clean up any references to
79 | * the binding class instance in the fragment's onDestroyView() method.
80 | */
81 | override fun onDestroyView() {
82 | super.onDestroyView()
83 | _viewBinding = null
84 | }
85 |
86 | private fun showLoading(isShow: Boolean) {
87 | (activity as? BaseActivity<*, *>)?.showLoading(isShow)
88 | }
89 |
90 | open fun onSubscribeObserver() {
91 | viewModel.run {
92 | isLoading.launchAndCollect {
93 | showLoading(it)
94 | }
95 | error.launchAndCollect {
96 | (activity as? BaseActivity<*, *>)?.handleApiError(it)
97 | }
98 | }
99 | }
100 |
101 | protected inline infix fun Flow.launchAndCollect(crossinline action: (T) -> Unit) {
102 | with(viewLifecycleOwner) {
103 | lifecycleScope.launch {
104 | repeatOnLifecycle(Lifecycle.State.STARTED) {
105 | collect { action(it) }
106 | }
107 | }
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/context/ActivityExt.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.utils.extension.context
2 |
3 | import android.content.Intent
4 | import android.graphics.Color
5 | import android.net.Uri
6 | import android.os.Build
7 | import android.view.View
8 | import androidx.annotation.ColorRes
9 | import androidx.annotation.IdRes
10 | import androidx.core.content.ContextCompat
11 | import androidx.fragment.app.Fragment
12 | import androidx.fragment.app.FragmentActivity
13 | import androidx.lifecycle.Lifecycle
14 | import androidx.lifecycle.lifecycleScope
15 | import androidx.lifecycle.repeatOnLifecycle
16 | import com.thuanpx.mvvm_architecture.utils.extension.AnimationType
17 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_LEFT
18 | import kotlinx.coroutines.CoroutineScope
19 | import kotlinx.coroutines.launch
20 | import kotlin.reflect.KClass
21 |
22 | /**
23 | * Created by ThuanPx on 3/15/20.
24 | */
25 |
26 | /**
27 | * Launches a new coroutine and repeats `block` every time the Activity's viewLifecycleOwner
28 | * is in and out of `minActiveState` lifecycle state.
29 | * Source: https://medium.com/androiddevelopers/repeatonlifecycle-api-design-story-8670d1a7d333
30 | */
31 | inline fun FragmentActivity.launchAndRepeatWithViewLifecycle(
32 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
33 | crossinline block: suspend CoroutineScope.() -> Unit
34 | ) {
35 | lifecycleScope.launch {
36 | repeatOnLifecycle(minActiveState) {
37 | block()
38 | }
39 | }
40 | }
41 |
42 | fun FragmentActivity.replaceFragment(
43 | @IdRes containerId: Int,
44 | fragment: Fragment,
45 | addToBackStack: Boolean = true,
46 | tag: String = fragment::class.java.simpleName,
47 | @AnimationType animateType: Int = SLIDE_LEFT
48 | ) {
49 | supportFragmentManager.transact(animateType) {
50 | if (addToBackStack) {
51 | addToBackStack(tag)
52 | }
53 | replace(containerId, fragment, tag)
54 | }
55 | }
56 |
57 | fun FragmentActivity.addFragment(
58 | @IdRes containerId: Int,
59 | fragment: Fragment,
60 | addToBackStack: Boolean = true,
61 | tag: String = fragment::class.java.simpleName,
62 | @AnimationType animateType: Int = SLIDE_LEFT
63 | ) {
64 | supportFragmentManager.transact(animateType) {
65 | if (addToBackStack) {
66 | addToBackStack(tag)
67 | }
68 | add(containerId, fragment, tag)
69 | }
70 | }
71 |
72 | fun FragmentActivity.isVisibleFragment(tag: String): Boolean {
73 | val fragment = supportFragmentManager.findFragmentByTag(tag)
74 | return fragment?.isAdded ?: false && fragment?.isVisible ?: false
75 | }
76 |
77 | inline fun FragmentActivity.getFragment(clazz: KClass): T? {
78 | val tag = clazz.java.simpleName
79 | return supportFragmentManager.findFragmentByTag(tag) as? T?
80 | }
81 |
82 | /**
83 | * val test = extra("test")
84 | * */
85 | inline fun FragmentActivity.extra(key: String, default: T? = null) = lazy {
86 | val value = intent?.extras?.get(key)
87 | if (value is T) value else default
88 | }
89 |
90 | fun FragmentActivity.getCurrentFragment(@IdRes containerId: Int): Fragment? {
91 | return supportFragmentManager.findFragmentById(containerId)
92 | }
93 |
94 | fun FragmentActivity.setTransparentStatusBar(isDarkBackground: Boolean = false) {
95 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
96 | window.statusBarColor = Color.TRANSPARENT
97 | window.decorView.systemUiVisibility = if (isDarkBackground)
98 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
99 | else
100 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
101 | }
102 | }
103 |
104 | fun FragmentActivity.setStatusBarColor(@ColorRes color: Int, isDarkColor: Boolean = false) {
105 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
106 | window?.apply {
107 | decorView.systemUiVisibility = if (isDarkColor) 0 else View.SYSTEM_UI_FLAG_VISIBLE
108 | statusBarColor = ContextCompat.getColor(context, color)
109 | }
110 | }
111 | }
112 |
113 | fun FragmentActivity.openWithUrl(url: String) {
114 | val defaultBrowser =
115 | Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER)
116 | defaultBrowser.data = Uri.parse(url)
117 | this.startActivity(defaultBrowser)
118 | }
119 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/base/fragment/BaseBottomSheetFragment.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.base.fragment
2 |
3 | import android.app.Dialog
4 | import android.os.Bundle
5 | import android.os.Handler
6 | import android.os.Looper
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import android.widget.FrameLayout
11 | import androidx.core.content.ContextCompat
12 | import androidx.fragment.app.createViewModelLazy
13 | import androidx.lifecycle.Lifecycle
14 | import androidx.lifecycle.lifecycleScope
15 | import androidx.lifecycle.repeatOnLifecycle
16 | import androidx.viewbinding.ViewBinding
17 | import com.google.android.material.bottomsheet.BottomSheetBehavior
18 | import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
19 | import com.google.android.material.bottomsheet.BottomSheetDialog
20 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
21 | import com.thuanpx.view_mvvm_architecture.base.BaseActivity
22 | import com.thuanpx.view_mvvm_architecture.base.viewmodel.BaseViewModel
23 | import com.thuanpx.view_mvvm_architecture.R
24 | import kotlinx.coroutines.flow.Flow
25 | import kotlinx.coroutines.launch
26 | import kotlin.reflect.KClass
27 |
28 |
29 | /**
30 | * Copyright © 2020 Neolab VN.
31 | * Created by ThuanPx on 8/5/20.
32 | *
33 | * @viewModel -> view model
34 | * @viewModelClass -> class view model
35 | * @viewBinding -> class binding
36 | * @initialize -> init UI, adapter, listener...
37 | * @onSubscribeObserver -> subscribe observer
38 | *
39 | */
40 |
41 | abstract class BaseBottomSheetFragment(
42 | viewModelClass: KClass
43 | ) : BottomSheetDialogFragment() {
44 |
45 | protected val viewModel by createViewModelLazy(viewModelClass, { viewModelStore })
46 | private var _viewBinding: viewBinding? = null
47 | protected val viewBinding get() = _viewBinding!! // ktlint-disable
48 |
49 | abstract fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): viewBinding
50 |
51 | protected abstract fun initialize()
52 |
53 | override fun onCreateView(
54 | inflater: LayoutInflater,
55 | container: ViewGroup?,
56 | savedInstanceState: Bundle?
57 | ): View? {
58 | _viewBinding = inflateViewBinding(inflater, container)
59 | return viewBinding.root
60 | }
61 |
62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
63 | super.onViewCreated(view, savedInstanceState)
64 | initialize()
65 | onSubscribeObserver()
66 | }
67 |
68 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
69 | val dialog = super.onCreateDialog(savedInstanceState)
70 | dialog.setOnShowListener {
71 | Handler(Looper.getMainLooper()).post {
72 | val bottomSheet = (dialog as? BottomSheetDialog)?.findViewById(com.google.android.material.R.id.design_bottom_sheet) as? FrameLayout
73 | bottomSheet?.let {
74 | BottomSheetBehavior.from(it).state = STATE_EXPANDED
75 | bottomSheet.background = ContextCompat.getDrawable(requireContext(),
76 | R.drawable.bg_bottom_sheet)
77 | }
78 | }
79 |
80 | }
81 | return dialog
82 | }
83 |
84 | /**
85 | * Fragments outlive their views. Make sure you clean up any references to
86 | * the binding class instance in the fragment's onDestroyView() method.
87 | */
88 | override fun onDestroyView() {
89 | super.onDestroyView()
90 | _viewBinding = null
91 | }
92 |
93 | private fun showLoading(isShow: Boolean) {
94 | (activity as? BaseActivity<*, *>)?.showLoading(isShow)
95 | }
96 |
97 | open fun onSubscribeObserver() {
98 | viewModel.run {
99 | isLoading.launchAndCollect {
100 | showLoading(it)
101 | }
102 | error.launchAndCollect {
103 | (activity as? BaseActivity<*, *>)?.handleApiError(it)
104 | }
105 | }
106 | }
107 |
108 | protected inline infix fun Flow.launchAndCollect(crossinline action: (T) -> Unit) {
109 | with(viewLifecycleOwner) {
110 | lifecycleScope.launch {
111 | repeatOnLifecycle(Lifecycle.State.STARTED) {
112 | collect { action(it) }
113 | }
114 | }
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/context/FragmentExt.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.utils.extension.context
2 |
3 | import android.os.Bundle
4 | import androidx.annotation.IdRes
5 | import androidx.fragment.app.Fragment
6 | import androidx.fragment.app.FragmentManager
7 | import androidx.fragment.app.FragmentTransaction
8 | import androidx.lifecycle.Lifecycle
9 | import androidx.lifecycle.lifecycleScope
10 | import androidx.lifecycle.repeatOnLifecycle
11 | import com.thuanpx.mvvm_architecture.utils.extension.AnimationType
12 | import com.thuanpx.mvvm_architecture.utils.extension.FADE
13 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_DOWN
14 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_LEFT
15 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_RIGHT
16 | import com.thuanpx.mvvm_architecture.utils.extension.SLIDE_UP
17 | import com.thuanpx.view_mvvm_architecture.R
18 | import kotlinx.coroutines.CoroutineScope
19 | import kotlinx.coroutines.launch
20 |
21 | /**
22 | * Created by ThuanPx on 3/15/20.
23 | */
24 |
25 | /**
26 | * Launches a new coroutine and repeats `block` every time the Fragment's viewLifecycleOwner
27 | * is in and out of `minActiveState` lifecycle state.*
28 | * Source: https://medium.com/androiddevelopers/repeatonlifecycle-api-design-story-8670d1a7d333
29 | */
30 | inline fun Fragment.launchAndRepeatWithViewLifecycle(
31 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED,
32 | crossinline block: suspend CoroutineScope.() -> Unit
33 | ) {
34 | viewLifecycleOwner.lifecycleScope.launch {
35 | viewLifecycleOwner.repeatOnLifecycle(minActiveState) {
36 | block()
37 | }
38 | }
39 | }
40 |
41 | fun Fragment.addOrReplaceFragment(
42 | @IdRes containerId: Int,
43 | fragmentManager: FragmentManager? = parentFragmentManager,
44 | fragment: Fragment,
45 | isAddFrag: Boolean,
46 | addToBackStack: Boolean = true,
47 | @AnimationType animateType: Int,
48 | tag: String = fragment::class.java.simpleName
49 | ) {
50 | fragmentManager?.transact(animateType) {
51 | if (addToBackStack) {
52 | addToBackStack(tag)
53 | }
54 |
55 | if (isAddFrag) {
56 | add(containerId, fragment, tag)
57 | } else {
58 | replace(containerId, fragment, tag)
59 | }
60 | }
61 | }
62 |
63 | fun Fragment.replaceFragment(
64 | @IdRes containerId: Int,
65 | fragment: Fragment,
66 | addToBackStack: Boolean = true,
67 | tag: String = fragment::class.java.simpleName,
68 | @AnimationType animateType: Int = SLIDE_LEFT
69 | ) {
70 | childFragmentManager.transact(animateType) {
71 | if (addToBackStack) {
72 | addToBackStack(tag)
73 | }
74 | replace(containerId, fragment, tag)
75 | }
76 | }
77 |
78 | fun Fragment.addFragment(
79 | @IdRes containerId: Int,
80 | fragment: Fragment,
81 | addToBackStack: Boolean = true,
82 | tag: String = fragment::class.java.simpleName,
83 | @AnimationType animateType: Int = SLIDE_LEFT
84 | ) {
85 | childFragmentManager.transact(animateType) {
86 | if (addToBackStack) {
87 | addToBackStack(tag)
88 | }
89 | add(containerId, fragment, tag)
90 | }
91 | }
92 |
93 | fun Fragment.generateTag(): String {
94 | return this::class.java.simpleName
95 | }
96 |
97 | fun Fragment.popBackFragment(): Boolean {
98 | with(parentFragmentManager) {
99 | val isShowPreviousPage = this.backStackEntryCount > 0
100 | if (isShowPreviousPage) {
101 | this.popBackStackImmediate()
102 | }
103 | return isShowPreviousPage
104 | }
105 | }
106 |
107 | fun FragmentManager.isExitFragment(tag: String): Boolean {
108 | return this.findFragmentByTag(tag) != null
109 | }
110 |
111 | fun T.withArgs(argsBuilder: Bundle.() -> Unit): T =
112 | this.apply { arguments = Bundle().apply(argsBuilder) }
113 |
114 | /**
115 | * Runs a FragmentTransaction, then calls commitAllowingStateLoss().
116 | */
117 | inline fun FragmentManager.transact(
118 | @AnimationType animateType: Int = SLIDE_LEFT,
119 | action: FragmentTransaction.() -> Unit,
120 | ) {
121 | beginTransaction().apply {
122 | setAnimations(animateType)
123 | action()
124 | }.commitAllowingStateLoss()
125 | }
126 |
127 | fun FragmentTransaction.setAnimations(@AnimationType animateType: Int) {
128 | when (animateType) {
129 | FADE -> {
130 | setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
131 | }
132 | SLIDE_DOWN -> {
133 | setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
134 | }
135 | SLIDE_UP -> {
136 | setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
137 | }
138 | SLIDE_LEFT -> {
139 | setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_right, 0, 0)
140 | }
141 | SLIDE_RIGHT -> {
142 | setCustomAnimations(R.anim.slide_in_right, 0, 0, R.anim.slide_out_right)
143 | }
144 | else -> {
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/widget/DialogBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.utils.extension.widget
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.widget.TextView
7 | import androidx.appcompat.app.AlertDialog
8 | import androidx.core.content.ContextCompat
9 | import androidx.fragment.app.Fragment
10 | import androidx.fragment.app.FragmentActivity
11 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
12 | import com.thuanpx.view_mvvm_architecture.R
13 | import com.thuanpx.view_mvvm_architecture.utils.extension.view.clicks
14 | import com.thuanpx.view_mvvm_architecture.utils.extension.view.gone
15 |
16 | /**
17 | * Created by ThuanPx on 15/09/2021.
18 | */
19 |
20 | @DslDialog
21 | fun Fragment.dialog(setup: DialogBuilder.() -> Unit) {
22 | val builder = DialogBuilder(requireContext(), setup = setup)
23 | builder.build().show()
24 | }
25 |
26 | @DslDialog
27 | fun FragmentActivity.dialog(setup: DialogBuilder.() -> Unit) {
28 | val builder = DialogBuilder(this, setup = setup)
29 | builder.build().show()
30 | }
31 |
32 | data class DialogOptions(
33 | val title: String,
34 | val message: String,
35 | val positiveText: String,
36 | val negativeText: String,
37 | val positiveListener: (() -> Unit)? = null,
38 | val negativeListener: (() -> Unit)? = null,
39 | var positiveColor: Int,
40 | var negativeColor: Int,
41 | var messageColor: Int,
42 | var titleColor: Int,
43 | val cancelable: Boolean,
44 | val isShowNegative: Boolean
45 | )
46 |
47 | @DslMarker
48 | annotation class DslDialog
49 |
50 | @DslDialog
51 | class DialogBuilder(
52 | private val context: Context,
53 | val setup: DialogBuilder.() -> Unit = {}
54 | ) {
55 |
56 | var title: String = ""
57 | var message: String = ""
58 | var titleColor: Int = android.R.color.black
59 | var messageColor: Int = android.R.color.black
60 | var positiveText: String = "OK"
61 | var negativeText: String = "No"
62 | var positiveListener: (() -> Unit)? = null
63 | var negativeListener: (() -> Unit)? = null
64 | var positiveColor: Int = R.color.blue_700
65 | var negativeColor: Int = R.color.blue_700
66 | var cancelable: Boolean = false
67 | var isShowNegative = false
68 | private lateinit var dialog: AlertDialog
69 |
70 | fun build(): AlertDialog {
71 | setup()
72 | if (message.isEmpty()) {
73 | throw IllegalArgumentException("You should fill all mandatory fields in the options")
74 | }
75 | val options = DialogOptions(
76 | title = title,
77 | message = message,
78 | positiveText = positiveText,
79 | negativeText = negativeText,
80 | positiveListener = positiveListener,
81 | negativeListener = negativeListener,
82 | cancelable = cancelable,
83 | isShowNegative = isShowNegative,
84 | titleColor = titleColor,
85 | messageColor = messageColor,
86 | negativeColor = negativeColor,
87 | positiveColor = positiveColor
88 | )
89 |
90 | dialog = setupCustomAlertDialog(options)
91 |
92 | return dialog
93 | }
94 |
95 | private fun setupCustomAlertDialog(options: DialogOptions): AlertDialog {
96 | val view = LayoutInflater.from(context).inflate(R.layout.dialog_custom, null)
97 |
98 | val alertDialog =
99 | MaterialAlertDialogBuilder(context, R.style.DialogCustomTheme)
100 | .setView(view)
101 | .setCancelable(options.cancelable)
102 | .create()
103 |
104 | alertDialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
105 |
106 | val tvTitle = view.findViewById(R.id.tvTitle)
107 | tvTitle.text = options.title
108 | tvTitle.setTextColor(ContextCompat.getColor(context, options.titleColor))
109 | tvTitle.gone(isGone = options.title.isEmpty())
110 |
111 | val tvMessage = view.findViewById(R.id.tvMessage)
112 | tvMessage.text = options.message
113 | tvMessage.setTextColor(ContextCompat.getColor(context, options.messageColor))
114 |
115 | val buttonNegative = view.findViewById(R.id.btNegative)
116 | buttonNegative.setTextColor(ContextCompat.getColor(context, options.negativeColor))
117 | buttonNegative.visibility = if (isShowNegative) View.VISIBLE else View.GONE
118 | buttonNegative.text = options.negativeText
119 | buttonNegative.clicks {
120 | options.negativeListener?.invoke()
121 | if (alertDialog.isShowing) {
122 | alertDialog.dismiss()
123 | }
124 | }
125 |
126 | val buttonPositive = view.findViewById(R.id.btPositive)
127 | buttonPositive.setTextColor(ContextCompat.getColor(context, options.positiveColor))
128 | buttonPositive.text = options.positiveText
129 | buttonPositive.clicks {
130 | options.positiveListener?.invoke()
131 | if (alertDialog.isShowing) {
132 | alertDialog.dismiss()
133 | }
134 | }
135 |
136 | return alertDialog
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_custom.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
24 |
25 |
40 |
41 |
56 |
57 |
65 |
66 |
80 |
81 |
89 |
90 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.text.SimpleDateFormat
2 | import java.util.Calendar
3 |
4 | plugins {
5 | id("com.android.application")
6 | id("org.jetbrains.kotlin.android")
7 | kotlin("kapt")
8 | id("com.google.dagger.hilt.android")
9 | }
10 |
11 | android {
12 | namespace = "com.thuanpx.view_mvvm_architecture"
13 | compileSdk = 34
14 | flavorDimensions += "default"
15 |
16 | defaultConfig {
17 | applicationId = "com.thuanpx.view_mvvm_architecture"
18 | minSdk = 25
19 | targetSdk = 34
20 | versionCode = 1
21 | versionName = "1.0"
22 |
23 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
24 | }
25 |
26 | productFlavors {
27 | create("dev") {
28 | applicationIdSuffix = ".dev"
29 | versionCode = 1
30 | versionName = "1.0.0"
31 |
32 | buildConfigField("String", "END_POINT", "\"https://pokeapi.co/api/v2/\"")
33 | }
34 |
35 | create("prod") {
36 | versionCode = 1
37 | versionName = "1.0.0"
38 |
39 | buildConfigField("String", "END_POINT", "\"https://pokeapi.co/api/v2/\"")
40 | }
41 | }
42 |
43 | buildTypes {
44 | release {
45 | isMinifyEnabled = true
46 | isShrinkResources = false
47 | proguardFiles(
48 | getDefaultProguardFile("proguard-android-optimize.txt"),
49 | file("proguard-rules.pro")
50 | )
51 | proguardFile("proguard/proguard-google-play-services.pro")
52 | proguardFile("proguard/proguard-square-okhttp.pro")
53 | proguardFile("proguard/proguard-square-retrofit.pro")
54 | proguardFile("proguard/proguard-google-analytics.pro")
55 | proguardFile("proguard/proguard-facebook.pro")
56 | proguardFile("proguard/proguard-project.pro")
57 | proguardFile("proguard/proguard-hilt.pro")
58 | proguardFile("proguard/proguard-support-v7-appcompat.pro")
59 | proguardFile("proguard/okhttp3.pro")
60 | proguardFile("proguard/kotlin.pro")
61 | proguardFile("proguard/retrofit2.pro")
62 | proguardFile("proguard/proguard-testfairy.pro")
63 | }
64 | }
65 |
66 | compileOptions {
67 | sourceCompatibility = JavaVersion.VERSION_1_8
68 | targetCompatibility = JavaVersion.VERSION_1_8
69 | }
70 |
71 | kotlinOptions {
72 | jvmTarget = "1.8"
73 | }
74 |
75 | buildFeatures {
76 | viewBinding = true
77 | buildConfig = true
78 | }
79 |
80 | applicationVariants.all {
81 | val outputFileName = name +
82 | "_versionName_$versionName" +
83 | "_versionCode_$versionCode" +
84 | "_time_${SimpleDateFormat("HH_mm_dd_MM_yyyy").format(Calendar.getInstance().time)}.apk"
85 | outputs.all {
86 | val output = this as? com.android.build.gradle.internal.api.BaseVariantOutputImpl
87 | output?.outputFileName = outputFileName
88 | }
89 | }
90 |
91 | }
92 |
93 | kapt {
94 | useBuildCache = true
95 | correctErrorTypes = true
96 | }
97 |
98 | dependencies {
99 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
100 | // App compat & design
101 | implementation("androidx.appcompat:appcompat:1.6.1")
102 | implementation("com.google.android.material:material:1.11.0")
103 | implementation("androidx.constraintlayout:constraintlayout:2.1.4")
104 | // Coroutines
105 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
106 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
107 | // Retrofit
108 | implementation("com.squareup.retrofit2:retrofit:2.9.0")
109 | implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
110 | // Okhttp
111 | implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.3")
112 | implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.3")
113 | // Glide
114 | implementation("com.github.bumptech.glide:glide:4.16.0")
115 | annotationProcessor("com.github.bumptech.glide:compiler:4.15.1")
116 | kapt("com.github.bumptech.glide:compiler:4.15.1")
117 | // Gson
118 | implementation("com.google.code.gson:gson:2.10.1")
119 | // Moshi
120 | implementation("com.squareup.moshi:moshi-kotlin:1.15.0")
121 | // Leak canary
122 | // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.0")
123 | // Timber
124 | implementation("com.jakewharton.timber:timber:5.0.1")
125 | // KTX
126 | implementation("androidx.core:core-ktx:1.12.0")
127 | implementation("androidx.fragment:fragment-ktx:1.6.2")
128 | implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
129 | implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
130 | implementation("androidx.activity:activity-ktx:1.8.2")
131 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
132 | // Hilt
133 | implementation("com.google.dagger:hilt-android:2.50")
134 | kapt("com.google.dagger:hilt-android-compiler:2.50")
135 | // Lottie
136 | implementation("com.airbnb.android:lottie:6.1.0")
137 | // DataStore
138 | implementation("androidx.datastore:datastore-preferences:1.0.0")
139 | // Paging
140 | implementation("androidx.paging:paging-runtime-ktx:3.2.1")
141 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 1dp
4 | 2dp
5 | 3dp
6 | 4dp
7 | 5dp
8 | 6dp
9 | 7dp
10 | 8dp
11 | 9dp
12 | 10dp
13 | 11dp
14 | 12dp
15 | 13dp
16 | 14dp
17 | 15dp
18 | 16dp
19 | 17dp
20 | 18dp
21 | 19dp
22 | 20dp
23 | 21dp
24 | 22dp
25 | 23dp
26 | 24dp
27 | 25dp
28 | 26dp
29 | 27dp
30 | 28dp
31 | 29dp
32 | 30dp
33 | 31dp
34 | 32dp
35 | 33dp
36 | 34dp
37 | 35dp
38 | 36dp
39 | 37dp
40 | 38dp
41 | 39dp
42 | 40dp
43 | 42dp
44 | 44dp
45 | 45dp
46 | 47dp
47 | 48dp
48 | 50dp
49 | 51dp
50 | 53dp
51 | 55dp
52 | 56dp
53 | 58dp
54 | 60dp
55 | 61dp
56 | 62dp
57 | 68dp
58 | 72dp
59 | 74dp
60 | 76dp
61 | 77dp
62 | 80dp
63 | 84dp
64 | 85dp
65 | 86dp
66 | 89dp
67 | 90dp
68 | 92dp
69 | 96dp
70 | 98dp
71 | 99dp
72 | 100dp
73 | 104dp
74 | 110dp
75 | 114dp
76 | 120dp
77 | 125dp
78 | 140dp
79 | 150dp
80 | 154dp
81 | 158dp
82 | 180dp
83 | 187dp
84 | 196dp
85 | 204dp
86 | 254dp
87 | 270dp
88 | 290dp
89 | 300dp
90 | 320dp
91 | 358dp
92 | 360dp
93 | 400dp
94 | 480dp
95 | 488dp
96 | 8sp
97 | 9sp
98 | 10sp
99 | 11sp
100 | 12sp
101 | 13sp
102 | 14sp
103 | 15sp
104 | 16sp
105 | 17sp
106 | 18sp
107 | 20sp
108 | 22sp
109 | 23sp
110 | 24sp
111 | 28sp
112 | 32sp
113 | 53sp
114 | 228dp
115 | 264dp
116 | 357dp
117 | 63dp
118 | 46dp
119 | 34sp
120 | 250dp
121 | 25sp
122 | 192dp
123 | 66dp
124 | 200dp
125 | 43dp
126 | 510dp
127 | 78dp
128 | 82dp
129 | 160dp
130 | 169dp
131 | 41dp
132 | 167dp
133 | 175dp
134 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/material_wave_loading.json:
--------------------------------------------------------------------------------
1 | {"v":"4.6.8","fr":29.9700012207031,"ip":0,"op":40.0000016292334,"w":256,"h":256,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 3","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":20,"s":[208.6,127.969,0],"e":[208.6,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":30,"s":[208.6,88,0],"e":[208.6,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":40.0000016292334}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.9843137,0.5490196,0,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":15,"s":[168.6,128,0],"e":[168.6,88,0],"to":[0,-6.66666650772095,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":25,"s":[168.6,88,0],"e":[168.6,128,0],"to":[0,0,0],"ti":[0,-6.66666650772095,0]},{"t":35.0000014255792}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.9921569,0.8470588,0.2078431,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 1","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":10,"s":[128.594,127.969,0],"e":[128.594,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":20,"s":[128.594,88,0],"e":[128.594,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":30.0000012219251}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.2627451,0.627451,0.2784314,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 4","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":5,"s":[88.6,127.969,0],"e":[88.6,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":15,"s":[88.6,88,0],"e":[88.6,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":25.0000010182709}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.1176471,0.5333334,0.8980392,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 5","ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":0,"s":[48.6,127.969,0],"e":[48.6,88,0],"to":[0,-6.66145849227905,0],"ti":[0,-0.00520833348855,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"n":"0p667_1_0p333_0","t":10,"s":[48.6,88,0],"e":[48.6,128,0],"to":[0,0.00520833348855,0],"ti":[0,-6.66666650772095,0]},{"t":20.0000008146167}]},"a":{"a":0,"k":[-70,-0.5,0]},"s":{"a":0,"k":[75,75,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[33.75,34.5]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse"},{"ty":"fl","c":{"a":0,"k":[0.8980392,0.2235294,0.2078431,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[-70.125,-0.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":300.00001221925,"st":0,"bm":0,"sr":1}]}
--------------------------------------------------------------------------------
/app/src/main/java/com/thuanpx/view_mvvm_architecture/utils/extension/date/DateExt.kt:
--------------------------------------------------------------------------------
1 | package com.thuanpx.view_mvvm_architecture.utils.extension.date
2 |
3 | import android.text.TextUtils
4 | import com.thuanpx.view_mvvm_architecture.app.Constant
5 | import java.text.ParseException
6 | import java.text.SimpleDateFormat
7 | import java.util.Calendar
8 | import java.util.Date
9 | import java.util.Locale
10 | import java.util.TimeZone
11 |
12 | fun String.convertUiFormatToDataFormat(
13 | inputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC,
14 | outputFormat: String,
15 | locale: Locale = Locale.JAPAN
16 | ): String? {
17 | if (this.isEmpty()) {
18 | return ""
19 | }
20 | val gmtTime = TimeZone.getTimeZone(Constant.KTEXT_TIME_ZONE_UTC)
21 | val sdf = SimpleDateFormat(inputFormat, locale)
22 | sdf.timeZone = gmtTime
23 | val newSdf = SimpleDateFormat(outputFormat, locale)
24 | newSdf.timeZone = gmtTime
25 | return try {
26 | newSdf.format(sdf.parse(this))
27 | } catch (e: ParseException) {
28 | null
29 | }
30 | }
31 |
32 | fun String.convertToUTC(
33 | inputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC,
34 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC,
35 | locale: Locale = Locale.JAPAN
36 | ): String? {
37 | if (TextUtils.isEmpty(this)) {
38 | return ""
39 | }
40 | val gmtTime = TimeZone.getTimeZone(Constant.KTEXT_TIME_ZONE_UTC)
41 | val sdf = SimpleDateFormat(inputFormat, locale)
42 | val newSdf = SimpleDateFormat(outputFormat, locale)
43 | newSdf.timeZone = gmtTime
44 | return try {
45 | newSdf.format(sdf.parse(this))
46 | } catch (e: ParseException) {
47 | null
48 | }
49 | }
50 |
51 | fun Date.convertUiFormatToDataFormat(
52 | outputFormat: String,
53 | locale: Locale = Locale.JAPAN
54 | ): String? {
55 | val sdf = SimpleDateFormat(outputFormat, locale)
56 | return try {
57 | sdf.format(this.time)
58 | } catch (e: ParseException) {
59 | null
60 | }
61 | }
62 |
63 | fun Calendar.getDateTime(
64 | outputFormat: String = Constant.KTEXT_TIME_FORMAT_HH_MM,
65 | locale: Locale = Locale.JAPAN
66 | ): String? {
67 | return this.time.convertDateToString(outputFormat, locale)
68 | }
69 |
70 | fun Calendar.getCurrentDate(
71 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_YYYY_MM_DD_EN,
72 | locale: Locale = Locale.JAPAN
73 | ): String? {
74 | val calendar = Calendar.getInstance(locale)
75 | return calendar.time.convertDateToString(outputFormat)
76 | }
77 |
78 | fun Date.getCurrentDate(
79 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_YYYY_MM_DD,
80 | locale: Locale = Locale.JAPAN
81 | ): String? {
82 | return convertDateToString(outputFormat, locale)
83 | }
84 |
85 | fun Date.convertDateToDate(
86 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC,
87 | locale: Locale = Locale.JAPAN
88 | ): Date? {
89 | val df = SimpleDateFormat(outputFormat, locale)
90 | return df.format(this).convertStringToDate(outputFormat, locale)
91 | }
92 |
93 | fun Date.convertDateToString(
94 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC,
95 | locale: Locale = Locale.JAPAN
96 | ): String? {
97 | val df = SimpleDateFormat(outputFormat, locale)
98 | return df.format(this)
99 | }
100 |
101 | fun String.convertStringToDate(
102 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC,
103 | locale: Locale = Locale.JAPAN
104 | ): Date {
105 | val parser = SimpleDateFormat(outputFormat, locale)
106 | return try {
107 | parser.parse(this)
108 | } catch (e: ParseException) {
109 | Date()
110 | }
111 | }
112 |
113 | fun Date.getDayOfWeek(locale: Locale = Locale.JAPAN): String {
114 | return this.convertDateToString(Constant.KTEXT_DAY_OF_WEEK, locale).toString()
115 | }
116 |
117 | fun Date.getDayOfMonth(locale: Locale = Locale.JAPAN): String {
118 | val calendar = Calendar.getInstance(locale)
119 | calendar.time = this
120 | return calendar.get(Calendar.DAY_OF_MONTH).toString()
121 | }
122 |
123 | fun Date.getMonthOfYear(locale: Locale = Locale.JAPAN): String {
124 | val calendar = Calendar.getInstance(locale)
125 | calendar.time = this
126 | return (calendar.get(Calendar.MONTH) + 1).toString()
127 | }
128 |
129 | fun String.getFirstDayOfWeek(locale: Locale = Locale.JAPAN): Date {
130 | val calendar = Calendar.getInstance(locale)
131 | calendar.time = this.convertStringToDate()
132 | while (calendar.get(Calendar.DAY_OF_WEEK) != Calendar.MONDAY) {
133 | calendar.add(Calendar.DATE, -1)
134 | }
135 | return calendar.time
136 | }
137 |
138 | fun String.isValidDateFormat(format: String, locale: Locale = Locale.JAPAN): Boolean {
139 | val formatter = SimpleDateFormat(format, locale)
140 | formatter.isLenient = false
141 | return try {
142 | formatter.parse(this)
143 | true
144 | } catch (e: ParseException) {
145 | false
146 | }
147 | }
148 |
149 | fun Date.isSameDay(expectedDay: Int, locale: Locale = Locale.JAPAN): Boolean {
150 | val calendar = Calendar.getInstance(locale)
151 | calendar.time = this
152 | return calendar.get(Calendar.DAY_OF_WEEK) == expectedDay
153 | }
154 |
155 | fun String.convertDateStringWithPlusTime(
156 | plusTime: Long,
157 | outputFormat: String = Constant.KTEXT_DATE_TIME_FORMAT_UTC,
158 | locale: Locale = Locale.JAPAN
159 | ): String {
160 | val date = this.convertStringToDate(outputFormat)
161 | val calendar = Calendar.getInstance(locale)
162 | calendar.time = date
163 | return Date(calendar.timeInMillis + plusTime).convertDateToString().toString()
164 | }
165 |
166 | fun Date.convertDateWithPlusTime(plusTime: Long, locale: Locale = Locale.JAPAN): Date {
167 | val calendar = Calendar.getInstance(locale)
168 | calendar.time = this
169 | return Date(calendar.timeInMillis + plusTime)
170 | }
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/proguard/proguard-project.pro:
--------------------------------------------------------------------------------
1 | # AndroidAnnotations
2 | -dontwarn org.androidannotations.api.rest.*
3 |
4 |
5 | -optimizationpasses 5
6 | -dontusemixedcaseclassnames
7 | -dontskipnonpubliclibraryclasses
8 | -dontskipnonpubliclibraryclassmembers
9 | -dontpreverify
10 | -verbose
11 | -optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
12 |
13 | -allowaccessmodification
14 | -keepattributes *Annotation*
15 | -renamesourcefileattribute SourceFile
16 | -keepattributes SourceFile,LineNumberTable
17 | -repackageclasses ''
18 |
19 | -dontwarn android.support.**
20 | -dontwarn com.atinternet.**
21 | -dontwarn org.apache.**
22 | -dontwarn javax.annotation.**
23 | -dontwarn com.google.protobuf.**
24 |
25 | -keepattributes InnerClasses
26 | -keepattributes *Annotation*
27 | -keepattributes Signature
28 | -keepattributes EnclosingMethod
29 |
30 | -dontwarn com.sothree.**
31 | -keep class com.sothree.**
32 | -keep interface com.sothree.**
33 |
34 | -dontwarn org.xmlpull.v1.**
35 |
36 | -keepclasseswithmembernames class * {
37 | native ;
38 | }
39 |
40 | -keepclasseswithmembers class * {
41 | public (android.content.Context, android.util.AttributeSet);
42 | }
43 |
44 | -keepclasseswithmembers class * {
45 | public (android.content.Context, android.util.AttributeSet, int);
46 | }
47 |
48 | # keep setters in Views so that animations can still work.
49 | # see http://proguard.sourceforge.net/manual/examples.html#beans
50 | -keepclassmembers public class * extends android.view.View {
51 | void set*(***);
52 | *** get*();
53 | }
54 |
55 | -keepclassmembers class * extends android.app.Activity {
56 | public void *(android.view.View);
57 | }
58 |
59 | -keepclassmembers enum * {
60 | public static **[] values();
61 | public static ** valueOf(java.lang.String);
62 | }
63 |
64 | -keep class * implements android.os.Parcelable {
65 | public static final android.os.Parcelable$Creator *;
66 | }
67 |
68 | -dontwarn java.awt.**
69 | -dontwarn **CompatHoneycomb
70 | -keep class android.support.v4.** { *; }
71 |
72 | -dontwarn uk.co.senab.photoview.**
73 | -keep class uk.co.senab.photoview.** { *; }
74 |
75 | -keep class com.crashlytics.** { *; }
76 | -keep class com.crashlytics.android.**
77 | -keepattributes SourceFile,LineNumberTable
78 | -dontwarn com.crashlytics.**
79 | -keep public class * extends java.lang.Exception
80 |
81 | -keep class android.support.v4.view.ViewPager
82 | -keepclassmembers class android.support.v4.view.ViewPager$LayoutParams { *; }
83 | -keep class android.support.v4.app.Fragment { *; }
84 |
85 | -keep class com.mixpanel.android.mpmetrics.MixpanelAPI { *;}
86 | -keep class com.google.android.gms.analytics.Tracker { *; }
87 | -keep class com.google.analytics.tracking.android.Tracker { *; }
88 | -keep class com.flurry.android.FlurryAgent { *; }
89 | -keep class com.omniture.AppMeasurementBase { *;}
90 | -keep class com.adobe.adms.measurement.ADMS_Measurement { *;}
91 |
92 | ##---------------Begin: proguard configuration common for all Android apps ----------
93 |
94 | # Explicitly preserve all serialization members. The Serializable interface
95 | # is only a marker interface, so it wouldn't save them.
96 | -keepclassmembers class * implements java.io.Serializable {
97 | static final long serialVersionUID;
98 | private static final java.io.ObjectStreamField[] serialPersistentFields;
99 | private void writeObject(java.io.ObjectOutputStream);
100 | private void readObject(java.io.ObjectInputStream);
101 | java.lang.Object writeReplace();
102 | java.lang.Object readResolve();
103 | }
104 |
105 | # Preserve all native method names and the names of their classes.
106 | -keepclasseswithmembernames class * {
107 | native ;
108 | }
109 |
110 | -keepclasseswithmembernames class * {
111 | public (android.content.Context, android.util.AttributeSet);
112 | }
113 |
114 | -keepclasseswithmembernames class * {
115 | public (android.content.Context, android.util.AttributeSet, int);
116 | }
117 |
118 | # Preserve static fields of inner classes of R classes that might be accessed
119 | # through introspection.
120 | -keepclassmembers class **.R$* {
121 | public static ;
122 | }
123 |
124 | # Preserve the special static methods that are required in all enumeration classes.
125 | -keepclassmembers enum * {
126 | public static **[] values();
127 | public static ** valueOf(java.lang.String);
128 | }
129 |
130 | -keep class * implements android.os.Parcelable {
131 | public static final android.os.Parcelable$Creator *;
132 | }
133 | ##---------------End: proguard configuration common for all Android apps ----------
134 |
135 | ##---------------Begin: proguard configuration for Gson ----------
136 | # Gson uses generic type information stored in a class file when working with fields. Proguard
137 | # removes such information by default, so configure it to keep all of it.
138 | -keepattributes Signature
139 |
140 | # For using GSON @Expose annotation
141 | -keepattributes *Annotation*
142 |
143 | # Gson specific classes
144 | -keep class sun.misc.Unsafe { *; }
145 | #-keep class com.google.gson.stream.** { *; }
146 |
147 | # Application classes that will be serialized/deserialized over Gson
148 | -keep class net.itify.cookpad.model.** { *; }
149 | -keep class net.itify.cookpad.base.network.** { *; }
150 |
151 | # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
152 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
153 | -keep class * extends com.google.gson.TypeAdapter
154 | -keep class * implements com.google.gson.TypeAdapterFactory
155 | -keep class * implements com.google.gson.JsonSerializer
156 | -keep class * implements com.google.gson.JsonDeserializer
157 |
158 | # Prevent R8 from leaving Data object members always null
159 | -keepclassmembers,allowobfuscation class * {
160 | @com.google.gson.annotations.SerializedName ;
161 | }
162 |
163 | # Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
164 | -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
165 | -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
166 |
167 | # https://github.com/google/gson/issues/2069
168 | -keep class com.google.gson.reflect.TypeToken
169 | -keep class * extends com.google.gson.reflect.TypeToken
170 | -keep public class * implements java.lang.reflect.Type
171 |
172 | ##---------------End: proguard configuration for Gson ----------
173 |
174 |
175 | ##---------------Begin: proguard configuration for appboy ----------
176 | -dontwarn com.amazon.device.messaging.**
177 | -dontwarn bo.app.**
178 | -dontwarn com.google.android.gms.**
179 | -dontwarn com.appboy.ui.**
180 | -keep class bo.app.** { *; }
181 | -keep class com.appboy.** { *; }
182 | ##---------------End: proguard configuration for appboy ----------
183 |
184 | ##---------------Begin: proguard configuration for Tealium ----------
185 | -keepclassmembers class fqcn.of.javascript.interface.for.webview {
186 | public *;
187 | }
188 |
189 | # Allow obfuscation of android.support.v7.internal.view.menu.**
190 | # to avoid problem on Samsung 4.2.2 devices with appcompat v21
191 | # see https://code.google.com/p/android/issues/detail?id=78377
192 | -keep class !android.support.v7.internal.view.menu.**,android.support.** {*;}
193 | -keep interface !android.support.v7.internal.view.menu.**,android.support.** {*;}
194 |
195 | # Config for Google Play Services: http://developer.android.com/google/play-services/setup.html#Setup
196 | -keep class * extends java.util.ListResourceBundle {
197 | protected Object[][] getContents();
198 | }
199 |
200 | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {
201 | public static final *** NULL;
202 | }
203 |
204 | -keepnames @com.google.android.gms.common.annotation.KeepName class *
205 | -keepclassmembernames class * {
206 | @ccom.google.android.gms.common.annotation.KeepName *;
207 | }
208 |
209 | -keepnames class * implements android.os.Parcelable {
210 | public static final ** CREATOR;
211 | }
212 |
213 | -dontwarn com.google.android.gms.**
214 |
215 | -keep class com.tealium.library.* {
216 | public (...);
217 | ;
218 | }
219 |
220 | -dontwarn com.tealium.**
221 | ##---------------End: proguard configuration for Tealium ----------
222 |
223 | ##---------------Begin: proguard configuration for Newrelic ----------
224 |
225 | -keepattributes Exceptions, Signature, InnerClasses, LineNumberTable
226 |
227 | -keep class com.google.firebase.** { *; }
228 | #-dontwarn com.google.j2objc.annotations.**
229 |
230 | # moshi
231 | -keep class com.squareup.moshi.** { *; }
232 | -keep interface com.squareup.moshi.** { *; }
233 | -dontwarn com.squareup.moshi.**
234 | -dontwarn okio.**
235 |
236 | # ThreeTen-Backport
237 | -keep class org.threeten.bp.zone.*
238 | -dontwarn org.threeten.bp.chrono.JapaneseEra
239 |
240 | ###### Fix exception: Module with the Main dispatcher is missing. Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'
241 | -dontwarn kotlinx.atomicfu.AtomicBoolean
242 | # ServiceLoader support
243 | -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
244 | -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
245 | -keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {}
246 | -keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {}
247 |
248 | # Most of volatile fields are updated with AFU and should not be mangled
249 | -keepclassmembernames class kotlinx.** {
250 | volatile ;
251 | }
252 |
253 | # Amplitude
254 | -keep class com.google.android.gms.ads.** { *; }
255 | -dontwarn okio.**
256 |
257 | #######
258 |
259 | # Glide
260 | -keep public class * implements com.bumptech.glide.module.GlideModule
261 | -keep public class * extends com.bumptech.glide.module.AppGlideModule
262 | -keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
263 | **[] $VALUES;
264 | public *;
265 | }
266 |
267 | # for DexGuard only
268 | # -keepresourcexmlelements manifest/application/meta-data@value=GlideModule
269 | -dontwarn com.chotot.vn.dashboard.fragments.**
270 |
271 | -dontwarn org.json.JSONStringer
272 |
273 | #Jsoup
274 | -keep public class org.jsoup.** {
275 | public *;
276 | }
277 | -keeppackagenames org.jsoup.nodes
278 | #zalo sign-in
279 | -keep class com.zing.zalo.**{ *; }
280 | -keep enum com.zing.zalo.**{ *; }
281 | -keep interface com.zing.zalo.**{ *; }
282 | #google sign-in
283 | -keep class com.google.googlesignin.** { *; }
284 | -keepnames class com.google.googlesignin.* { *; }
285 |
286 | -keep class com.google.android.gms.auth.** { *; }
287 |
288 | # Protobuf
289 | -keep public class * extends com.google.protobuf.GeneratedMessageLite { *; }
290 | -keep class CxSvcProto.** { *; }
291 |
--------------------------------------------------------------------------------