.onValueChange(callback: (T) -> Unit) {
7 | val that = this
8 | this.addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
9 | override fun onPropertyChanged(p0: Observable?, p1: Int) {
10 | that.get()?.let { callback.invoke(it) }
11 | }
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/common/livedata/SingleLiveEvent.java:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.common.livedata;
2 |
3 |
4 | /*
5 | * Copyright 2017 Google Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | */
19 |
20 | import androidx.lifecycle.LifecycleOwner;
21 | import androidx.lifecycle.MutableLiveData;
22 | import androidx.lifecycle.Observer;
23 | import androidx.annotation.MainThread;
24 | import androidx.annotation.Nullable;
25 | import android.util.Log;
26 |
27 | import java.util.concurrent.atomic.AtomicBoolean;
28 |
29 | /**
30 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like
31 | * navigation and Snackbar messages.
32 | *
33 | * This avoids a common problem with events: on configuration change (like rotation) an update
34 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an
35 | * explicit call to setValue() or call().
36 | *
37 | * Note that only one observer is going to be notified of changes.
38 | */
39 | public class SingleLiveEvent extends MutableLiveData {
40 |
41 | private static final String TAG = "SingleLiveEvent";
42 |
43 | private final AtomicBoolean mPending = new AtomicBoolean(false);
44 |
45 | @MainThread
46 | public void observe(LifecycleOwner owner, final Observer super T> observer) {
47 |
48 | if (hasActiveObservers()) {
49 | Log.w(TAG, "Multiple observers registered but only one will be notified of changes.");
50 | }
51 |
52 | // Observe the internal MutableLiveData
53 | super.observe(owner, new Observer() {
54 | @Override
55 | public void onChanged(@Nullable T t) {
56 | if (mPending.compareAndSet(true, false)) {
57 | observer.onChanged(t);
58 | }
59 | }
60 | });
61 | }
62 |
63 | @MainThread
64 | public void setValue(@Nullable T t) {
65 | mPending.set(true);
66 | super.setValue(t);
67 | }
68 |
69 | /**
70 | * Used for cases where T is Void, to make calls cleaner.
71 | */
72 | @MainThread
73 | public void call() {
74 | setValue(null);
75 | }
76 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/common/view/SquareImageView.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.common.view
2 |
3 | import android.content.Context
4 | import androidx.annotation.Nullable
5 | import androidx.appcompat.widget.AppCompatImageView
6 | import android.util.AttributeSet
7 |
8 | class SquareImageView : AppCompatImageView {
9 |
10 | constructor(context: Context) : super(context)
11 |
12 | constructor(context: Context, @Nullable attrs: AttributeSet) : this(context, attrs, 0)
13 |
14 | constructor(context: Context, @Nullable attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
15 |
16 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
17 | super.onMeasure(widthMeasureSpec, widthMeasureSpec)
18 | }
19 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/di/NetworkModule.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.di
2 |
3 | import android.content.Context
4 | import com.google.gson.FieldNamingPolicy
5 | import com.google.gson.GsonBuilder
6 | import news.ta.com.news.BuildConfig
7 | import okhttp3.Cache
8 | import okhttp3.Interceptor
9 | import okhttp3.OkHttpClient
10 | import okhttp3.logging.HttpLoggingInterceptor
11 | import okhttp3.logging.HttpLoggingInterceptor.Level.BODY
12 | import okhttp3.logging.HttpLoggingInterceptor.Level.NONE
13 | import org.koin.android.ext.koin.androidContext
14 | import org.koin.dsl.module
15 | import retrofit2.Retrofit
16 | import retrofit2.converter.gson.GsonConverterFactory
17 | import java.io.File
18 |
19 | open class NetworkModule {
20 | fun getModule() = module {
21 | single {
22 | GsonBuilder()
23 | .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
24 | .setDateFormat("dd-MM-yyyy")
25 | .create()
26 | }
27 | single {
28 | Retrofit
29 | .Builder()
30 | .baseUrl(BuildConfig.BASE_URL)
31 | .addConverterFactory(GsonConverterFactory.create(get()))
32 | .client(createClient(androidContext()))
33 | .build()
34 | }
35 | }
36 |
37 | private fun createClient(context: Context): OkHttpClient {
38 | val cacheDir = File(context.cacheDir, "responses")
39 |
40 | return OkHttpClient.Builder()
41 | .cache(Cache(cacheDir, 10 * 1024 * 1024)) //10Mb
42 | .addInterceptor(log(BuildConfig.DEBUG))
43 | .addInterceptor(addHeaderKey())
44 | .build()
45 | }
46 |
47 | private fun log(enabled: Boolean): Interceptor {
48 | val logging = HttpLoggingInterceptor()
49 | logging.level = if (enabled) BODY else NONE
50 | return logging
51 | }
52 |
53 | private fun addHeaderKey() = Interceptor { chain ->
54 | var request = chain.request()
55 |
56 | request = request.newBuilder()
57 | .header("Authorization", "Bearer " + BuildConfig.NEWS_API_KEY)
58 | .build()
59 |
60 | chain.proceed(request)
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.di
2 |
3 | import news.ta.com.news.feature.newslist.NewsRepository
4 | import news.ta.com.news.feature.newslist.NewsRepositoryImpl
5 | import org.koin.dsl.module
6 |
7 | open class RepositoryModule {
8 |
9 | fun getModule() = module {
10 | single { NewsRepositoryImpl(get()) }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/di/ServicesModule.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.di
2 |
3 | import news.ta.com.news.services.NewsService
4 | import org.koin.dsl.module
5 | import retrofit2.Retrofit
6 |
7 | class ServicesModule {
8 |
9 | fun getModule() = module {
10 | single { provideNewsService(get()) }
11 | }
12 |
13 | private fun provideNewsService(retrofit: Retrofit): NewsService = retrofit.create(NewsService::class.java)
14 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/NewsApplication.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature
2 |
3 | import androidx.multidex.MultiDexApplication
4 | import news.ta.com.news.di.NetworkModule
5 | import news.ta.com.news.di.RepositoryModule
6 | import news.ta.com.news.di.ServicesModule
7 | import news.ta.com.news.feature.newslist.NewsItem
8 | import org.koin.android.ext.koin.androidContext
9 | import org.koin.android.ext.koin.androidLogger
10 | import org.koin.core.context.startKoin
11 |
12 | class NewsApplication : MultiDexApplication() {
13 |
14 | companion object {
15 | var news: NewsItem? = null
16 | }
17 |
18 | override fun onCreate() {
19 | super.onCreate()
20 | startKoin {
21 | androidContext(this@NewsApplication)
22 | androidLogger()
23 | modules(
24 | listOf(NetworkModule().getModule(),
25 | RepositoryModule().getModule(),
26 | ServicesModule().getModule()
27 | )
28 | )
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/main/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.main
2 |
3 | import androidx.databinding.DataBindingUtil
4 | import android.os.Bundle
5 | import android.util.Log
6 | import androidx.appcompat.app.AppCompatActivity
7 | import news.ta.com.news.R
8 | import news.ta.com.news.databinding.ActivityMainBinding
9 | import org.koin.android.scope.currentScope
10 | import org.koin.core.context.loadKoinModules
11 | import org.koin.core.context.unloadKoinModules
12 | import org.koin.core.module.Module
13 |
14 | class MainActivity : AppCompatActivity() {
15 |
16 | var module: Module? = null
17 | override fun onCreate(savedInstanceState: Bundle?) {
18 | super.onCreate(savedInstanceState)
19 | val binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
20 | Log.d("ROTATE", "Create")
21 | module = MainModule().getModule(this, binding)
22 | module?.let {
23 | loadKoinModules(it)
24 | }
25 | val binder: MainBinder = currentScope.get()
26 | binder.bindTo(this)
27 | }
28 |
29 | override fun onDetachedFromWindow() {
30 | super.onDetachedFromWindow()
31 | module?.let {
32 | unloadKoinModules(it)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/main/MainBinder.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.main
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.Observer
5 | import news.ta.com.news.feature.newsdetail.NewsDetailsViewModel
6 |
7 | class MainBinder(val viewModel: NewsDetailsViewModel, val view: MainView) {
8 | init {
9 | view.showList()
10 | view.showDetail()
11 | }
12 |
13 | fun bindTo(owner: LifecycleOwner) {
14 | viewModel.selectedEvent.observe(owner, Observer { view.scrollDetailToTop() })
15 | }
16 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/main/MainModule.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.main
2 |
3 | import news.ta.com.news.databinding.ActivityMainBinding
4 | import news.ta.com.news.feature.newsdetail.NewsDetailsViewModel
5 | import org.koin.android.viewmodel.dsl.viewModel
6 | import org.koin.core.qualifier.named
7 | import org.koin.dsl.module
8 |
9 | class MainModule {
10 | fun getModule(activity: MainActivity, binding: ActivityMainBinding) = module {
11 | scope(named()) {
12 | factory { MainBinder(get(), get()) }
13 | factory { MainViewImpl(binding, activity.supportFragmentManager) }
14 | viewModel { NewsDetailsViewModel() }
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/main/MainView.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.main
2 |
3 | import androidx.core.widget.NestedScrollView
4 | import androidx.fragment.app.FragmentManager
5 | import news.ta.com.news.R
6 | import news.ta.com.news.common.replaceWith
7 | import news.ta.com.news.databinding.ActivityMainBinding
8 | import news.ta.com.news.feature.newsdetail.NewsDetailFragment
9 | import news.ta.com.news.feature.newslist.NewsListFragment
10 |
11 | interface MainView {
12 | fun scrollDetailToTop()
13 | fun showList()
14 | fun showDetail()
15 | }
16 |
17 | class MainViewImpl(val binding: ActivityMainBinding?, private val fm: FragmentManager) : MainView {
18 | override fun scrollDetailToTop() {
19 | val scrollView = binding?.detail?.findViewById(R.id.scrollContainer)
20 | scrollView?.scrollTo(0, 0)
21 | }
22 |
23 | override fun showList() {
24 | fm.replaceWith(binding?.listItem?.id, NewsListFragment.newInstance(binding?.detail != null))
25 | }
26 |
27 | override fun showDetail() {
28 | fm.replaceWith(binding?.detail?.id, NewsDetailFragment.newInstance())
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newsdetail/NewsDetailActivity.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newsdetail
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import androidx.databinding.DataBindingUtil
6 | import android.os.Bundle
7 | import androidx.appcompat.app.AppCompatActivity
8 | import android.view.Window
9 | import news.ta.com.news.R
10 | import news.ta.com.news.databinding.ActivityNewsDetailsBinding
11 | import news.ta.com.news.feature.newslist.NewsItem
12 | import org.koin.android.scope.currentScope
13 | import org.koin.core.context.loadKoinModules
14 | import org.koin.core.context.unloadKoinModules
15 | import org.koin.core.module.Module
16 |
17 | class NewsDetailActivity : AppCompatActivity() {
18 | var binding: ActivityNewsDetailsBinding? = null
19 | var binder: NewsDetailActivityBinder? = null
20 | var module: Module? = null
21 |
22 | companion object {
23 |
24 | val EXTRA_NEWS_ITEM = "news.ta.com.news.feature.newsdetail.EXTRA_NEWS_ITEM"
25 |
26 | fun route(context: Context, item: NewsItem) {
27 | val i = Intent(context, NewsDetailActivity::class.java)
28 | i.putExtra(EXTRA_NEWS_ITEM, item)
29 | context.startActivity(i)
30 | }
31 | }
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 | requestWindowFeature(Window.FEATURE_ACTION_BAR)
36 | binding = DataBindingUtil.setContentView(this, R.layout.activity_news_details)
37 | module = NewsDetailModule().getModule(this, binding)
38 | module?.let {
39 | loadKoinModules(it)
40 | }
41 | binder = currentScope.get()
42 | }
43 |
44 | override fun onSupportNavigateUp(): Boolean {
45 | onBackPressed()
46 | return true
47 | }
48 |
49 | override fun onDetachedFromWindow() {
50 | super.onDetachedFromWindow()
51 | module?.let {
52 | unloadKoinModules(it)
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newsdetail/NewsDetailActivityBinder.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newsdetail
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import news.ta.com.news.databinding.ActivityNewsDetailsBinding
5 |
6 | class NewsDetailActivityBinder(activity: AppCompatActivity, binding: ActivityNewsDetailsBinding?, viewModel: NewsDetailsViewModel) {
7 |
8 | val view: NewsDetailActivityView
9 | init {
10 | view = NewsDetailActivityViewImpl(activity, binding, viewModel)
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newsdetail/NewsDetailActivityView.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newsdetail
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import news.ta.com.news.common.replaceWith
5 | import news.ta.com.news.databinding.ActivityNewsDetailsBinding
6 | import news.ta.com.news.feature.newslist.NewsItem
7 |
8 | interface NewsDetailActivityView
9 |
10 | class NewsDetailActivityViewImpl(activity: AppCompatActivity, binding: ActivityNewsDetailsBinding?, viewModel: NewsDetailsViewModel) : NewsDetailActivityView {
11 | private val newsItem = activity.intent?.getSerializableExtra(NewsDetailActivity.EXTRA_NEWS_ITEM) as NewsItem
12 |
13 | init {
14 | with(activity) {
15 | supportFragmentManager.replaceWith(binding?.detail?.id, NewsDetailFragment.newInstance())
16 | setSupportActionBar(binding?.toolbar)
17 | with(this.supportActionBar) {
18 | this?.setDisplayHomeAsUpEnabled(true)
19 | this?.setDisplayShowHomeEnabled(true)
20 | }
21 | }
22 |
23 | viewModel.item.set(newsItem)
24 | binding?.title = newsItem.headline
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newsdetail/NewsDetailBinder.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newsdetail
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.Observer
5 | import news.ta.com.news.databinding.FragmentNewsDetailsBinding
6 |
7 | class NewsDetailBinder(binding: FragmentNewsDetailsBinding?,
8 | private val router: NewsDetailRouter,
9 | private val viewModel: NewsDetailsViewModel) {
10 |
11 | init {
12 | binding?.viewModel = viewModel
13 | }
14 |
15 | fun bindTo(owner: LifecycleOwner) {
16 | viewModel.clickReadMoreEvent.observe(owner, Observer { router.openWebBrowser(it) })
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newsdetail/NewsDetailFragment.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newsdetail
2 |
3 | import androidx.databinding.DataBindingUtil
4 | import android.os.Bundle
5 | import androidx.fragment.app.Fragment
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import news.ta.com.news.R
10 | import news.ta.com.news.databinding.FragmentNewsDetailsBinding
11 | import org.koin.android.scope.currentScope
12 | import org.koin.core.context.loadKoinModules
13 | import org.koin.core.context.unloadKoinModules
14 |
15 | class NewsDetailFragment : Fragment() {
16 | private lateinit var binder: NewsDetailBinder
17 | companion object {
18 | fun newInstance(): Fragment = NewsDetailFragment()
19 | }
20 |
21 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
22 | val binding: FragmentNewsDetailsBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_news_details, container, false)
23 | val module = NewsDetailModule().getModule(this.activity!!, binding)
24 | unloadKoinModules(module)
25 | loadKoinModules(module)
26 | binder = currentScope.get()
27 | return binding.root
28 | }
29 |
30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
31 | binder.bindTo(this)
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newsdetail/NewsDetailModule.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newsdetail
2 |
3 | import androidx.activity.viewModels
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.fragment.app.FragmentActivity
6 | import news.ta.com.news.databinding.ActivityNewsDetailsBinding
7 | import news.ta.com.news.databinding.FragmentNewsDetailsBinding
8 | import org.koin.android.viewmodel.dsl.viewModel
9 | import org.koin.core.qualifier.named
10 | import org.koin.dsl.module
11 |
12 | class NewsDetailModule {
13 | fun getModule(activity: FragmentActivity, binding: FragmentNewsDetailsBinding?) = module {
14 | scope(named()) {
15 | factory { NewsDetailBinder(binding, get(), get()) }
16 | factory { NewsDetailRouterImpl(activity) }
17 | viewModel {
18 | val viewModel: NewsDetailsViewModel by activity.viewModels()
19 | viewModel
20 | }
21 | }
22 | }
23 |
24 | fun getModule(activity: AppCompatActivity, binding: ActivityNewsDetailsBinding?) = module {
25 | scope(named()) {
26 | factory { NewsDetailActivityBinder(activity, binding, get()) }
27 | viewModel {
28 | val viewModel: NewsDetailsViewModel by activity.viewModels()
29 | viewModel
30 | }
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newsdetail/NewsDetailRouter.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newsdetail
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 |
7 | interface NewsDetailRouter {
8 | fun openWebBrowser(url: String?)
9 | }
10 |
11 | class NewsDetailRouterImpl(val context: Context) : NewsDetailRouter {
12 |
13 | override fun openWebBrowser(url: String?) {
14 | val i = Intent(Intent.ACTION_VIEW)
15 | i.data = Uri.parse(url)
16 | context.startActivity(i)
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newsdetail/NewsDetailsViewModel.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newsdetail
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.databinding.ObservableField
5 | import android.view.View
6 | import news.ta.com.news.common.livedata.SingleLiveEvent
7 | import news.ta.com.news.common.onValueChange
8 | import news.ta.com.news.feature.newslist.NewsItem
9 |
10 | class NewsDetailsViewModel : ViewModel() {
11 | val item = ObservableField()
12 | val clickReadMoreEvent = SingleLiveEvent()
13 | val onClickReadMore = View.OnClickListener { clickReadMoreEvent.value = item.get()?.link ?: "" }
14 | val selectedEvent = SingleLiveEvent()
15 | val blankContent = ObservableField(true)
16 |
17 | init {
18 | item.onValueChange { selectedEvent.value = true; blankContent.set(false) }
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newslist/NewsListAdapter.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import androidx.databinding.DataBindingUtil
4 | import androidx.recyclerview.widget.RecyclerView
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.recyclerview.selection.SelectionTracker
9 | import news.ta.com.news.R
10 | import news.ta.com.news.databinding.ItemNewsBinding
11 |
12 | class NewsListAdapter(val viewModel: NewsListViewModel, var selectionTracker: SelectionTracker? = null) : RecyclerView.Adapter() {
13 |
14 | var items: List = emptyList()
15 | set(value) {
16 | field = value
17 | notifyDataSetChanged()
18 | }
19 |
20 | init {
21 | setHasStableIds(true)
22 | }
23 |
24 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NewsViewHolder {
25 | val view = LayoutInflater.from(parent.context).inflate(R.layout.item_news, parent, false)
26 | return NewsViewHolder(view)
27 | }
28 |
29 | override fun getItemCount(): Int = items.size
30 |
31 | override fun onBindViewHolder(holder: NewsViewHolder, position: Int) {
32 | holder.bind(selectionTracker, items[position])
33 | }
34 |
35 | override fun getItemId(position: Int): Long = items[position].id.toLong()
36 |
37 | inner class NewsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
38 | val binding: ItemNewsBinding? = itemView.let { DataBindingUtil.bind(it) }
39 |
40 | fun bind(selectionTracker: SelectionTracker?, item: NewsItem) {
41 | binding?.item = item
42 | binding?.listener = View.OnClickListener {
43 | viewModel.itemClickEvent.value = item
44 | }
45 |
46 | binding?.wrapper?.isSelected = selectionTracker?.isSelected(item.id.toLong()) ?: false
47 | viewModel.selectedCount.set(selectionTracker?.selection?.size().toString())
48 | }
49 | }
50 | }
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newslist/NewsListBinder.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.Observer
6 | import news.ta.com.news.databinding.FragmentNewsListBinding
7 | import news.ta.com.news.feature.newsdetail.NewsDetailsViewModel
8 |
9 | class NewsListBinder(val fragment: Fragment,
10 | val binding: FragmentNewsListBinding,
11 | val viewModel: NewsListViewModel,
12 | private val detailViewModel: NewsDetailsViewModel,
13 | private val router: NewsListRouter) {
14 |
15 | var view: NewsListView
16 |
17 | init {
18 | binding.viewModel = viewModel
19 | view = NewsListViewImpl(fragment, binding)
20 | }
21 |
22 | fun bindTo(owner: LifecycleOwner) {
23 | viewModel.items.observe(owner, Observer { view.setItems(it); viewModel.setStatic(it) })
24 | viewModel.showDetailMediator.observe(owner, Observer { view.setSelectedItem(it!!.id.toLong()); router.showDetail(detailViewModel, it) })
25 | viewModel.gotoDetailMediator.observe(owner, Observer { router.gotoDetail(it) })
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newslist/NewsListFragment.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import androidx.databinding.DataBindingUtil
4 | import android.os.Bundle
5 | import androidx.fragment.app.Fragment
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import news.ta.com.news.R
10 | import news.ta.com.news.databinding.FragmentNewsListBinding
11 | import org.koin.android.scope.currentScope
12 | import org.koin.core.context.loadKoinModules
13 | import org.koin.core.context.unloadKoinModules
14 | import org.koin.core.module.Module
15 |
16 | class NewsListFragment : Fragment() {
17 |
18 | private lateinit var binder: NewsListBinder
19 | var module: Module? = null
20 |
21 | companion object {
22 |
23 | val HAS_DETAIL = "news.ta.com.news.feature.newslist.HAS_DETAIL"
24 |
25 | fun newInstance(hasDetail: Boolean): Fragment {
26 | val fragment = NewsListFragment()
27 | val bundle = Bundle()
28 | bundle.putSerializable(HAS_DETAIL, hasDetail)
29 | fragment.arguments = bundle
30 | return fragment
31 | }
32 | }
33 |
34 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
35 | val binding: FragmentNewsListBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_news_list, container, false)
36 | module = NewsListModule().getModule(this, binding)
37 | module?.let {
38 | unloadKoinModules(it)
39 | loadKoinModules(it)
40 | }
41 | binder = currentScope.get()
42 | return binding.root
43 | }
44 |
45 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
46 | binder.bindTo(this)
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newslist/NewsListModule.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import androidx.activity.viewModels
4 | import androidx.fragment.app.Fragment
5 | import news.ta.com.news.databinding.FragmentNewsListBinding
6 | import news.ta.com.news.feature.newsdetail.NewsDetailsViewModel
7 | import org.koin.android.viewmodel.dsl.viewModel
8 | import org.koin.core.module.Module
9 | import org.koin.core.qualifier.named
10 | import org.koin.dsl.module
11 |
12 | class NewsListModule {
13 | fun getModule(fragment: Fragment, binding: FragmentNewsListBinding): Module = module {
14 | scope(named()) {
15 | factory { binding }
16 | factory { NewsListBinder(fragment, binding, get(), get(), get()) }
17 | viewModel {
18 | val viewModel: NewsDetailsViewModel by fragment.activity!!.viewModels()
19 | viewModel
20 | }
21 | viewModel { NewsListViewModel(get()) }
22 | factory { NewsListRouterImpl(fragment.context!!) }
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newslist/NewsListRouter.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import android.content.Context
4 | import news.ta.com.news.feature.newsdetail.NewsDetailActivity
5 | import news.ta.com.news.feature.newsdetail.NewsDetailsViewModel
6 |
7 | interface NewsListRouter {
8 | fun showDetail(detailViewModel: NewsDetailsViewModel, item: NewsItem?)
9 | fun gotoDetail(item: NewsItem?)
10 | }
11 |
12 | class NewsListRouterImpl(val context: Context) : NewsListRouter {
13 | override fun showDetail(detailViewModel: NewsDetailsViewModel, item: NewsItem?) {
14 | detailViewModel.item.set(item)
15 | }
16 |
17 | override fun gotoDetail(item: NewsItem?) {
18 | if (item == null) return
19 | NewsDetailActivity.route(context, item)
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newslist/NewsListView.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import android.view.MotionEvent
4 | import android.view.MotionEvent.ACTION_UP
5 | import androidx.fragment.app.Fragment
6 | import androidx.core.content.ContextCompat
7 | import androidx.recyclerview.selection.ItemDetailsLookup
8 | import androidx.recyclerview.selection.SelectionTracker
9 | import androidx.recyclerview.selection.StableIdKeyProvider
10 | import androidx.recyclerview.selection.StorageStrategy
11 | import androidx.recyclerview.widget.GridLayoutManager
12 | import androidx.recyclerview.widget.LinearLayoutManager
13 | import androidx.recyclerview.widget.RecyclerView
14 | import com.dgreenhalgh.android.simpleitemdecoration.linear.DividerItemDecoration
15 | import com.dgreenhalgh.android.simpleitemdecoration.linear.EndOffsetItemDecoration
16 | import com.dgreenhalgh.android.simpleitemdecoration.linear.StartOffsetItemDecoration
17 | import news.ta.com.news.R
18 | import news.ta.com.news.databinding.FragmentNewsListBinding
19 | import news.ta.com.news.feature.newslist.NewsListFragment.Companion.HAS_DETAIL
20 |
21 | interface NewsListView {
22 | fun setItems(items: List?)
23 | fun setSelectedItem(id: Long)
24 | }
25 |
26 | class NewsListViewImpl(fragment: Fragment, val binding: FragmentNewsListBinding) : NewsListView {
27 |
28 | init {
29 | var hasDetailView = false
30 | fragment.arguments?.let {
31 | hasDetailView = it.getBoolean(HAS_DETAIL, false)
32 | }
33 |
34 | with(binding.list) {
35 | isNestedScrollingEnabled = false
36 |
37 | val pixelSize = context.resources.getDimensionPixelSize(R.dimen.gap_m)
38 | addItemDecoration(StartOffsetItemDecoration(pixelSize))
39 | addItemDecoration(EndOffsetItemDecoration(pixelSize))
40 |
41 | val drawable = ContextCompat.getDrawable(context, R.drawable.decor_m)
42 | addItemDecoration(DividerItemDecoration(drawable))
43 | setHasFixedSize(false)
44 |
45 | layoutManager = when (hasDetailView) {
46 | true -> GridLayoutManager(context, 2)
47 | else -> LinearLayoutManager(context)
48 | }
49 |
50 | adapter = NewsListAdapter(binding.viewModel!!)
51 |
52 | if (hasDetailView) {
53 | val stableIdKeyProvider = StableIdKeyProvider(this)
54 | val selectionTracker = SelectionTracker.Builder(
55 | "news-selection",
56 | this,
57 | stableIdKeyProvider,
58 | MyItemsLookUp(this, binding.viewModel!!),
59 | StorageStrategy.createLongStorage())
60 | .build()
61 |
62 | (adapter as NewsListAdapter).selectionTracker = selectionTracker
63 | }
64 | }
65 |
66 | binding.viewModel?.hasViewDetail = hasDetailView
67 | }
68 |
69 | override fun setItems(items: List?) {
70 | (binding.list.adapter as NewsListAdapter).items = items ?: emptyList()
71 | }
72 |
73 | override fun setSelectedItem(id: Long) {
74 | (binding.list.adapter as NewsListAdapter).selectionTracker?.select(id)
75 | }
76 | }
77 |
78 | class MyItemsLookUp(private val recyclerView: RecyclerView, val viewModel: NewsListViewModel) : ItemDetailsLookup() {
79 | override fun getItemDetails(event: MotionEvent): ItemDetails? {
80 | val view = recyclerView.findChildViewUnder(event.x, event.y)
81 |
82 | if (view != null) {
83 | val viewHolder = recyclerView.getChildViewHolder(view)
84 |
85 | if (event.action == ACTION_UP) {
86 | val newsItem = (recyclerView.adapter as NewsListAdapter).items[viewHolder.adapterPosition]
87 | if (viewModel.itemClickEvent.value != newsItem) {
88 | viewModel.itemClickEvent.value = newsItem
89 | }
90 | }
91 |
92 | if (viewHolder is NewsListAdapter.NewsViewHolder) {
93 | return object : ItemDetails() {
94 | override fun getSelectionKey(): Long? = viewHolder.itemId
95 | override fun getPosition(): Int = viewHolder.adapterPosition
96 | }
97 | }
98 | }
99 |
100 | return null
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newslist/NewsListViewModel.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import androidx.annotation.VisibleForTesting
4 | import androidx.databinding.ObservableField
5 | import androidx.lifecycle.LiveData
6 | import androidx.lifecycle.MediatorLiveData
7 | import androidx.lifecycle.ViewModel
8 | import news.ta.com.news.common.livedata.SingleLiveEvent
9 | import news.ta.com.news.feature.NewsApplication
10 | import java.io.Serializable
11 | class NewsItem(val id: Int = 0,
12 | val thumbnail: String = "",
13 | val headline: String = "--",
14 | val description: String = "--",
15 | val link: String = "",
16 | val source: String = "--") : Serializable
17 |
18 | class NewsListViewModel(val repository: NewsRepository) : ViewModel() {
19 |
20 | val itemClickEvent = SingleLiveEvent()
21 |
22 | var hasViewDetail = false
23 |
24 | val items: LiveData>
25 | get() = repository.getNews()
26 |
27 | val showDetailMediator = MediatorLiveData()
28 | val gotoDetailMediator = MediatorLiveData()
29 |
30 | val selectedCount = ObservableField("0")
31 |
32 | init {
33 | showDetailMediator.addSource(itemClickEvent) { if (hasViewDetail) showDetailMediator.value = it }
34 | gotoDetailMediator.addSource(itemClickEvent) { if (!hasViewDetail) gotoDetailMediator.value = it; afterGotoDetail() }
35 | }
36 |
37 | @VisibleForTesting
38 | fun afterGotoDetail() {
39 | gotoDetailMediator.value = null
40 | }
41 |
42 | fun setStatic(list: List?) {
43 | NewsApplication.news = list?.get(0)
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/feature/newslist/NewsRepository.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import news.ta.com.news.model.ArticleDTO
6 | import news.ta.com.news.services.NewsService
7 | import news.ta.com.news.services.enqueueWithProcessing
8 |
9 | interface NewsRepository {
10 | fun getNews(): LiveData>
11 | }
12 |
13 | class NewsRepositoryImpl(val service: NewsService) : NewsRepository {
14 |
15 | var items = MutableLiveData>()
16 |
17 | override fun getNews(): LiveData> {
18 | service.getTopNewsList("us").enqueueWithProcessing(
19 | preProcessing = {
20 | it?.articles.convertToNewsItem()
21 | },
22 | success = {
23 | items.value = it
24 | },
25 | fail = { _, _ -> }
26 | )
27 |
28 | return items
29 | }
30 | }
31 |
32 | fun List?.convertToNewsItem(): List {
33 | if (this == null) return emptyList()
34 | return this.asSequence().map { item ->
35 | NewsItem(id = item.hashCode(),
36 | thumbnail = item.urlToImage ?: "",
37 | headline = item.title ?: "--",
38 | description = item.description ?: "--",
39 | link = item.url ?: "",
40 | source = item.source?.name ?: "--")
41 | }.toList()
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/model/ArticleDTO.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | class ArticleDTO {
7 |
8 | @SerializedName("source")
9 | @Expose
10 | var source: SourceDTO? = null
11 | @SerializedName("author")
12 | @Expose
13 | var author: String? = null
14 | @SerializedName("title")
15 | @Expose
16 | var title: String? = null
17 | @SerializedName("description")
18 | @Expose
19 | var description: String? = null
20 | @SerializedName("url")
21 | @Expose
22 | var url: String? = null
23 | @SerializedName("urlToImage")
24 | @Expose
25 | var urlToImage: String? = null
26 | @SerializedName("publishedAt")
27 | @Expose
28 | var publishedAt: String? = null
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/model/NewsDTO.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | class NewsDTO {
7 |
8 | @SerializedName("status")
9 | @Expose
10 | var status: String? = null
11 | @SerializedName("totalResults")
12 | @Expose
13 | var totalResults: Int? = null
14 | @SerializedName("articles")
15 | @Expose
16 | var articles: List? = null
17 | }
18 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/model/SourceDTO.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.model
2 |
3 | import com.google.gson.annotations.Expose
4 | import com.google.gson.annotations.SerializedName
5 |
6 | class SourceDTO {
7 |
8 | @SerializedName("id")
9 | @Expose
10 | var id: Any? = null
11 | @SerializedName("name")
12 | @Expose
13 | var name: String? = null
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/services/DataTransferCallback.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.services
2 |
3 | import com.google.gson.FieldNamingPolicy
4 | import com.google.gson.Gson
5 | import com.google.gson.GsonBuilder
6 | import com.google.gson.JsonSyntaxException
7 | import com.google.gson.annotations.SerializedName
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.Deferred
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.async
12 | import kotlinx.coroutines.launch
13 | import news.ta.com.news.services.DataTransferCallback.Companion.ioScope
14 | import news.ta.com.news.services.DataTransferCallback.Companion.mainScope
15 | import okhttp3.Headers
16 | import org.koin.core.KoinComponent
17 | import retrofit2.Call
18 | import retrofit2.Callback
19 | import retrofit2.Response
20 | import java.io.IOException
21 | import java.io.InterruptedIOException
22 | import java.lang.Exception
23 | import java.net.SocketTimeoutException
24 | import java.net.UnknownHostException
25 |
26 | data class ErrorBody(
27 | @SerializedName("error")
28 | val error: String
29 | )
30 |
31 | data class ErrorItem(
32 | @SerializedName("code")
33 | val code: String,
34 | @SerializedName("text")
35 | val message: String
36 | )
37 |
38 | class DataTransferCallback(
39 | private val success: (T?) -> Unit,
40 | private val headers: ((Headers?) -> Unit)? = null,
41 | private val fail: ((ResponseType, Throwable?) -> Unit)?
42 | ) : Callback, KoinComponent {
43 |
44 | companion object {
45 | val ioScope = CoroutineScope(Dispatchers.IO)
46 | val mainScope = CoroutineScope(Dispatchers.Main)
47 | }
48 |
49 | private val gson: Gson = GsonBuilder()
50 | .setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
51 | .setDateFormat("dd-MM-yyyy")
52 | .create()
53 |
54 | override fun onResponse(call: Call?, response: Response?) {
55 | headers?.invoke(response?.headers())
56 |
57 | when (response?.code()) {
58 | in 200..399 -> success.invoke(response?.body())
59 | 401 -> {
60 | val errorBody = getErrorBody(response)
61 | val throwable = getThrowable(errorBody)
62 | fail?.invoke(ResponseType.UNAUTHORIZED, throwable)
63 | }
64 | 503 -> fail?.invoke(ResponseType.EMPTY, null)
65 | 502, 504 -> fail?.invoke(ResponseType.TIMEOUT, null)
66 | 404 -> {
67 | val errorBody = getErrorBody(response)
68 | val throwable = getThrowable(errorBody)
69 | fail?.invoke(ResponseType.NOT_FOUND, throwable)
70 | }
71 | else -> {
72 | handleError(response)
73 | fail?.invoke(ResponseType.GENERAL_ERROR, null)
74 | }
75 | }
76 | }
77 |
78 | private fun handleError(response: Response?) {
79 | try {
80 | val errorBody = getErrorBody(response)
81 | val throwable = getThrowable(errorBody)
82 | fail?.invoke(ResponseType.GENERAL_ERROR, throwable)
83 | } catch (e: JsonSyntaxException) {
84 | e.printStackTrace()
85 | fail?.invoke(ResponseType.NOT_FOUND, null)
86 | }
87 | }
88 |
89 | private fun getErrorBody(response: Response?): ErrorBody? {
90 | return try {
91 | val errorResponse = response?.errorBody()?.string()
92 | gson.fromJson(errorResponse, ErrorBody::class.java)
93 | } catch (e: Exception) {
94 | null
95 | }
96 | }
97 |
98 | private fun getThrowable(errorBody: ErrorBody?): Throwable? {
99 | val message = errorBody?.error
100 | return Throwable(message)
101 | }
102 |
103 | override fun onFailure(call: Call?, throwable: Throwable?) {
104 | if (throwable == null) {
105 | fail?.invoke(ResponseType.GENERAL_ERROR, throwable)
106 | return
107 | }
108 | when (throwable::class.java) {
109 | UnknownHostException::class.java -> fail?.invoke(ResponseType.NO_INTERNET, throwable)
110 | IOException::class.java -> fail?.invoke(ResponseType.NO_INTERNET, throwable)
111 | InterruptedIOException::class.java -> fail?.invoke(ResponseType.NO_INTERNET, throwable)
112 | SocketTimeoutException::class.java -> fail?.invoke(ResponseType.TIMEOUT, throwable)
113 | else -> fail?.invoke(ResponseType.GENERAL_ERROR, throwable)
114 | }
115 | }
116 | }
117 |
118 | fun Call.processEnqueue(
119 | success: (T?) -> Unit,
120 | headers: ((Headers?) -> Unit)? = null,
121 | fail: ((ResponseType, Throwable?) -> Unit)? = null
122 | ) {
123 | try {
124 | enqueue(DataTransferCallback(success, headers, fail))
125 | } catch (e: IOException) {
126 | }
127 | }
128 |
129 | fun Call.enqueueNow() {
130 | this.enqueueWithProcessing({}, {})
131 | }
132 |
133 | fun Call.enqueueWithProcessing(
134 | preProcessing: (T?) -> O,
135 | success: (O?) -> Unit,
136 | headers: ((Headers?) -> Unit)? = null,
137 | fail: ((ResponseType, Throwable?) -> Unit)? = null
138 | ) {
139 |
140 | fun backgroundProcessingAsync(obj: T?): Deferred {
141 | return ioScope.async {
142 | return@async preProcessing(obj)
143 | }
144 | }
145 |
146 | val wrappedSuccess: (T?) -> Unit = {
147 | mainScope.launch {
148 | val obj = backgroundProcessingAsync(it).await()
149 | success.invoke(obj)
150 | }
151 | }
152 |
153 | processEnqueue(wrappedSuccess, headers, fail)
154 | }
155 |
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/services/NewsService.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.services
2 |
3 | import news.ta.com.news.model.NewsDTO
4 | import retrofit2.Call
5 | import retrofit2.http.GET
6 | import retrofit2.http.Query
7 |
8 | interface NewsService {
9 | @GET("v2/top-headlines")
10 | fun getTopNewsList(@Query("country") country: String? = "th"): Call
11 | }
--------------------------------------------------------------------------------
/app/src/main/kotlin/news/ta/com/news/services/ResponseType.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.services
2 |
3 | enum class ResponseType {
4 | SUCCESS, EMPTY, NO_INTERNET, GENERAL_ERROR, UNAUTHORIZED, TIMEOUT, NOT_FOUND
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_divider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/drawable-xxhdpi/ic_divider.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_globe_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/drawable-xxhdpi/ic_globe_16.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/drawable-xxhdpi/ic_home.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_image_default.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/drawable-xxhdpi/ic_image_default.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_news.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/drawable-xxhdpi/ic_news.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/ic_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/drawable-xxhdpi/ic_up.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_selection.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/decor_m.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/headline_bg.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
16 |
21 |
26 |
31 |
36 |
41 |
46 |
51 |
56 |
61 |
66 |
71 |
76 |
81 |
86 |
91 |
96 |
101 |
106 |
111 |
116 |
121 |
126 |
131 |
136 |
141 |
146 |
151 |
156 |
161 |
166 |
171 |
172 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/title_bg.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/font/averia_serif_libre_light.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
17 |
18 |
21 |
22 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/fragment_news_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
16 |
17 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/item_news.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
29 |
30 |
39 |
40 |
44 |
45 |
54 |
55 |
63 |
64 |
72 |
73 |
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
13 |
14 |
21 |
22 |
23 |
24 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_news_details.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
10 |
11 |
12 |
15 |
16 |
19 |
20 |
28 |
29 |
30 |
31 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_news_details.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
16 |
17 |
22 |
23 |
28 |
29 |
32 |
33 |
40 |
41 |
42 |
56 |
57 |
58 |
59 |
69 |
70 |
77 |
78 |
85 |
86 |
96 |
97 |
98 |
99 |
100 |
107 |
108 |
112 |
113 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_news_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
10 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_news.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
15 |
16 |
17 |
30 |
31 |
40 |
41 |
46 |
47 |
55 |
56 |
64 |
65 |
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-land/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 80dp
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #1b61ff
4 | #2452b9
5 | #ff518d
6 | #c2c6d1
7 | #FFFFFF
8 | #c5000000
9 | #b923ff
10 | @color/colorPrimary
11 | #7d1b61ff
12 | #f6f4fa
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 128dp
4 | 64dp
5 | 32dp
6 | 16dp
7 | 5dp
8 | 4dp
9 | 100dp
10 | 0dp
11 | 1dp
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/font_certs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @array/com_google_android_gms_fonts_certs_dev
5 | - @array/com_google_android_gms_fonts_certs_prod
6 |
7 |
8 | -
9 | MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
10 |
11 |
12 |
13 | -
14 | MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/preloaded_fonts.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - @font/averia_serif_libre_light
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Hot News
3 | Read full content
4 | Please select news form the list
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
19 |
20 |
23 |
24 |
27 |
28 |
31 |
32 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/test/java/news/ta/com/news/feature/newsdetail/NewsDetailsViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newsdetail
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import news.ta.com.news.feature.newslist.NewsItem
5 | import org.amshove.kluent.mock
6 | import org.amshove.kluent.shouldBe
7 | import org.amshove.kluent.shouldEqual
8 | import org.amshove.kluent.shouldNotBe
9 | import org.junit.Rule
10 | import org.junit.Test
11 | import org.junit.rules.TestRule
12 |
13 | class NewsDetailsViewModelTest {
14 |
15 | @get:Rule
16 | var rule: TestRule = InstantTaskExecutorRule()
17 |
18 | @Test
19 | fun `case-01 when init NewsDetailsViewModel blankContent = true`() {
20 | val viewModel = NewsDetailsViewModel()
21 | viewModel.blankContent.get() shouldBe true
22 | }
23 |
24 | @Test
25 | fun `case-02 when init NewsDetailsViewModel onClickReadMore not null`() {
26 | val viewModel = NewsDetailsViewModel()
27 | viewModel.onClickReadMore shouldNotBe null
28 | }
29 |
30 | @Test
31 | fun `case-03 init NewsDetailsViewModel the set item blankContent != true and selectedEvent = true`() {
32 | val viewModel = NewsDetailsViewModel()
33 | viewModel.selectedEvent.observeForever { it shouldBe true }
34 | viewModel.item.set(NewsItem())
35 | viewModel.blankContent.get() shouldBe false
36 | }
37 |
38 | @Test
39 | fun `case-04 init NewsDetailsViewModel the set item perform onClick clickReadMoreEvent should change data`() {
40 | val viewModel = NewsDetailsViewModel()
41 | val newsItem = NewsItem(link = Math.random().toString())
42 | viewModel.clickReadMoreEvent.observeForever { it shouldEqual newsItem.link }
43 | viewModel.item.set(newsItem)
44 | viewModel.onClickReadMore.onClick(mock())
45 | }
46 |
47 | @Test
48 | fun `case-05 init NewsDetailsViewModel the not set item and perform onClick clickReadMoreEvent = empty`() {
49 | val viewModel = NewsDetailsViewModel()
50 | viewModel.clickReadMoreEvent.observeForever { it shouldEqual "" }
51 | viewModel.onClickReadMore.onClick(mock())
52 | }
53 | }
--------------------------------------------------------------------------------
/app/src/test/java/news/ta/com/news/feature/newslist/NewsListViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import org.amshove.kluent.shouldBe
4 | import org.amshove.kluent.shouldEqual
5 | import org.amshove.kluent.shouldNotBe
6 | import org.junit.Test
7 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
8 | import org.amshove.kluent.mock
9 | import org.junit.rules.TestRule
10 | import org.junit.Rule
11 |
12 | class NewsListViewModelTest {
13 |
14 | @get:Rule
15 | var rule: TestRule = InstantTaskExecutorRule()
16 |
17 | private val repository: NewsRepository = mock()
18 |
19 | @Test
20 | fun `case-01 init viewModel - repository should be auto injected`() {
21 | val viewModel = NewsListViewModel(repository)
22 | viewModel.repository shouldNotBe null
23 | }
24 |
25 | @Test
26 | fun `case-02 init viewModel - default state of hasDetail should be false`() {
27 | val viewModel = NewsListViewModel(repository)
28 | viewModel.hasViewDetail shouldBe false
29 | }
30 |
31 | @Test
32 | fun `case-03 init viewModel - default items should equal to null`() {
33 | val viewModel = NewsListViewModel(repository)
34 | viewModel.items shouldEqual null
35 | }
36 |
37 | @Test
38 | fun `case-04 init viewModel - items showDetailMediator not has observe`() {
39 | val viewModel = NewsListViewModel(repository)
40 | viewModel.showDetailMediator.hasObservers() shouldBe false
41 | }
42 |
43 | @Test
44 | fun `case-05 init viewModel - items gotoDetailMediator not has observe`() {
45 | val viewModel = NewsListViewModel(repository)
46 | viewModel.gotoDetailMediator.hasObservers() shouldBe false
47 | }
48 |
49 | @Test(expected = NullPointerException::class)
50 | fun `case-06 init viewModel - items has null observe`() {
51 | val viewModel = NewsListViewModel(repository)
52 | viewModel.items.hasObservers() shouldBe false
53 | }
54 |
55 | @Test
56 | fun `case-07 viewModel observe for itemClickEvent when hasDetailView = false, gotoDetailMediator should = item`() {
57 | val viewModel = NewsListViewModel(repository)
58 | val newsItem = NewsItem()
59 | viewModel.hasViewDetail = false
60 | viewModel.gotoDetailMediator.observeForever {
61 | it shouldEqual newsItem
62 | }
63 | }
64 |
65 | @Test
66 | fun `case-08 viewModel observe for itemClickEvent when hasDetailView = true, showDetailMediator should = item`() {
67 | val viewModel = NewsListViewModel(repository)
68 | val newsItem = NewsItem()
69 | viewModel.hasViewDetail = true
70 | viewModel.showDetailMediator.observeForever {
71 | it shouldEqual newsItem
72 | }
73 | }
74 |
75 | @Test
76 | fun `case-09 viewModel call afterGotoDetail, gotoDetailMediator should = null`() {
77 | val viewModel = NewsListViewModel(repository)
78 | viewModel.afterGotoDetail()
79 | viewModel.gotoDetailMediator.observeForever {
80 | it shouldBe null
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/app/src/test/java/news/ta/com/news/feature/newslist/NewsRepositoryTest.kt:
--------------------------------------------------------------------------------
1 | package news.ta.com.news.feature.newslist
2 |
3 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
4 | import com.nhaarman.mockito_kotlin.verify
5 | import news.ta.com.news.model.ArticleDTO
6 | import news.ta.com.news.model.NewsDTO
7 | import news.ta.com.news.services.NewsService
8 | import org.amshove.kluent.any
9 | import org.amshove.kluent.mock
10 | import org.amshove.kluent.shouldBe
11 | import org.amshove.kluent.shouldEqual
12 | import org.amshove.kluent.shouldEqualTo
13 | import org.amshove.kluent.shouldNotBe
14 | import org.junit.Rule
15 | import org.junit.Test
16 | import org.junit.rules.TestRule
17 | import org.mockito.Mockito
18 |
19 | class NewsRepositoryTest {
20 | @get:Rule
21 | var rule: TestRule = InstantTaskExecutorRule()
22 |
23 | var service: NewsService = mock()
24 |
25 | @Test
26 | fun `case-01 init repository with service`() {
27 | val repository = NewsRepositoryImpl(service)
28 | repository.service shouldNotBe null
29 | }
30 |
31 | @Test
32 | fun `case-02 when repository call getNews, service should call getTopNews with 'us'`() {
33 | val repository = NewsRepositoryImpl(service)
34 | Mockito.`when`(service.getTopNewsList(any())).thenReturn(mock())
35 | repository.getNews()
36 | verify(repository.service.getTopNewsList("us"))
37 | }
38 |
39 | @Test
40 | fun `case-03 convert NewsDTO with article to get List of newsItem`() {
41 | val newsDTO = NewsDTO()
42 | val list = ArrayList()
43 | list.add(ArticleDTO())
44 | list.add(ArticleDTO())
45 | newsDTO.articles = list
46 | val item = newsDTO.articles.convertToNewsItem()
47 |
48 | item.size shouldEqualTo list.size
49 | }
50 |
51 | @Test
52 | fun `case-04 convert NewsDTO with null article to get List of newsItem`() {
53 | val newsDTO = NewsDTO()
54 | newsDTO.articles = null
55 | val item = newsDTO.articles?.convertToNewsItem()
56 |
57 | item shouldBe null
58 | }
59 |
60 | @Test
61 | fun `case-5 create NewsItem with input nothing get default data`() {
62 | val item = NewsItem()
63 |
64 | with(item) {
65 | id shouldEqualTo 0
66 | thumbnail shouldEqual ""
67 | link shouldEqual ""
68 | headline shouldEqual "--"
69 | description shouldEqual "--"
70 | source shouldEqual "--"
71 | }
72 | }
73 |
74 | @Test
75 | fun `case-6 create NewsItem with input constructor get same data`() {
76 | val id = Math.random().toInt()
77 | val string = Math.random().toString()
78 |
79 | val item = NewsItem(id = id,
80 | thumbnail = string,
81 | link = string,
82 | headline = string,
83 | description = string,
84 | source = string)
85 |
86 | with(item) {
87 | id shouldEqualTo id
88 | thumbnail shouldEqual string
89 | link shouldEqual string
90 | headline shouldEqual string
91 | description shouldEqual string
92 | source shouldEqual string
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | apply plugin: 'kotlin'
3 |
4 | buildscript {
5 |
6 | ext {
7 | kotlin_version = '1.3.50'
8 | acc_extensions_version = '1.1.1'
9 | support_version = '27.1.1'
10 | retrofit_version = '2.3.0'
11 | okhttp_version = '3.8.1'
12 | dagger_version = '2.10'
13 | multidex_version = '1.0.3'
14 | coroutines_version = '1.3.3'
15 | kotlin_test_runner_version = '0.3.1'
16 | gradle_plugin_version = '3.1.3'
17 | koin_version = '2.0.1'
18 | }
19 |
20 | repositories {
21 | google()
22 | jcenter()
23 | }
24 | dependencies {
25 | classpath 'com.android.tools.build:gradle:3.5.3'
26 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
27 |
28 | // NOTE: Do not place your application dependencies here; they belong
29 | // in the individual module build.gradle files
30 | }
31 | }
32 |
33 | allprojects {
34 | repositories {
35 | google()
36 | jcenter()
37 | }
38 | }
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/api-versions-0-29.0.5.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/build/intermediates/lint-cache/api-versions-0-29.0.5.bin
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/activity/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/annotation/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/appcompat/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/arch/core/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/cardview/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/constraintlayout/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/core/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/lifecycle/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/multidex/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/recyclerview/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/slice/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/test/espresso/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/androidx/test/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/com/google/android/material/group-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/maven.google/master-index.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/sdk-registry.xml/sdk-registry.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/build/intermediates/lint-cache/typos-en.txt-2.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/build/intermediates/lint-cache/typos-en.txt-2.bin
--------------------------------------------------------------------------------
/build/kotlin-build/version.txt:
--------------------------------------------------------------------------------
1 | 11001
--------------------------------------------------------------------------------
/build/kotlin/AndroidArchitecturejar-classes.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/build/kotlin/AndroidArchitecturejar-classes.txt
--------------------------------------------------------------------------------
/build/libs/AndroidArchitecture.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/build/libs/AndroidArchitecture.jar
--------------------------------------------------------------------------------
/build/tmp/jar/MANIFEST.MF:
--------------------------------------------------------------------------------
1 | Manifest-Version: 1.0
2 |
3 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | android.enableJetifier=true
13 | android.useAndroidX=true
14 | org.gradle.jvmargs=-Xmx1536m
15 |
16 | # When configured, Gradle will run in incubating parallel mode.
17 | # This option should only be used with decoupled projects. More details, visit
18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
19 | # org.gradle.parallel=true
20 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theerasan/AndroidArchitecture/2d046a8f0ac142e1a220686b2bea50edbf2deeb4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Feb 13 11:13:28 ICT 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/ktlint/ktlint.gradle:
--------------------------------------------------------------------------------
1 | configurations {
2 | ktlint
3 | }
4 |
5 | dependencies {
6 | ktlint 'com.github.shyiko:ktlint:0.12.1'
7 | }
8 |
9 | task ktlint(type: JavaExec) {
10 | main = "com.github.shyiko.ktlint.Main"
11 | classpath = configurations.ktlint
12 | args "src/**/*.kt"
13 | }
14 |
15 | check.dependsOn ktlint
16 |
17 | task ktlintFormat(type: JavaExec) {
18 | main = "com.github.shyiko.ktlint.Main"
19 | classpath = configurations.ktlint
20 | args "-F", "src/**/*.kt"
21 | }
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | ## This file must *NOT* be checked into Version Control Systems,
2 | # as it contains information specific to your local configuration.
3 | #
4 | # Location of the SDK. This is only used by Gradle.
5 | # For customization when using a Version Control System, please read the
6 | # header note.
7 | #Thu Feb 13 11:13:23 ICT 2020
8 | sdk.dir=C\:\\Users\\Ta\\AppData\\Local\\Android\\Sdk
9 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------