From 60ad6ce23189c43ec7e586a35ffc92ae3a3ec513 Mon Sep 17 00:00:00 2001 From: Michael Lien Date: Sun, 17 Jan 2021 16:41:00 +0800 Subject: [PATCH] Refactor HotBoardsFragment.kt to Kotlin and Koin --- app/build.gradle | 5 + .../java/tw/y_studio/ptt/PttApplication.kt | 17 ++ .../ptt/adapter/HotBoardsListAdapter.kt | 48 ++-- .../y_studio/ptt/api/PopularBoardListAPI.kt | 24 +- .../java/tw/y_studio/ptt/di/ApiModules.kt | 12 + .../java/tw/y_studio/ptt/di/AppModules.kt | 10 + .../tw/y_studio/ptt/di/DataSourceModules.kt | 15 ++ .../main/java/tw/y_studio/ptt/di/Injection.kt | 9 - .../tw/y_studio/ptt/di/ViewModelModules.kt | 10 + .../y_studio/ptt/fragment/HomeFragment.java | 1 + .../ptt/fragment/HotBoardsFragment.java | 218 ------------------ .../java/tw/y_studio/ptt/model/HotBoard.kt | 10 + .../tw/y_studio/ptt/model/HotBoardsItem.kt | 8 + .../popular/IPopularRemoteDataSource.kt | 6 +- .../popular/PopularRemoteDataSourceImpl.kt | 7 +- .../ptt/ui/hot_board/HotBoardsFragment.kt | 124 ++++++++++ .../ptt/ui/hot_board/HotBoardsViewModel.kt | 54 +++++ .../popular/PopularRemoteDataSourceTest.kt | 3 +- versions.gradle | 7 + 19 files changed, 311 insertions(+), 277 deletions(-) create mode 100644 app/src/main/java/tw/y_studio/ptt/di/ApiModules.kt create mode 100644 app/src/main/java/tw/y_studio/ptt/di/AppModules.kt create mode 100644 app/src/main/java/tw/y_studio/ptt/di/DataSourceModules.kt create mode 100644 app/src/main/java/tw/y_studio/ptt/di/ViewModelModules.kt delete mode 100644 app/src/main/java/tw/y_studio/ptt/fragment/HotBoardsFragment.java create mode 100644 app/src/main/java/tw/y_studio/ptt/model/HotBoard.kt create mode 100644 app/src/main/java/tw/y_studio/ptt/model/HotBoardsItem.kt create mode 100644 app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsFragment.kt create mode 100644 app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsViewModel.kt diff --git a/app/build.gradle b/app/build.gradle index 3ee8bcc..f4caef3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -94,6 +94,11 @@ dependencies { // Jsoup implementation deps.jsoup + implementation deps.koin.android + implementation deps.koin.scope + implementation deps.koin.viewModel + implementation deps.koin.fragment + // Kotlin implementation deps.kotlin.stdlib.jdk8 implementation deps.kotlin.coroutines.core diff --git a/app/src/main/java/tw/y_studio/ptt/PttApplication.kt b/app/src/main/java/tw/y_studio/ptt/PttApplication.kt index 329cca1..ee23bed 100644 --- a/app/src/main/java/tw/y_studio/ptt/PttApplication.kt +++ b/app/src/main/java/tw/y_studio/ptt/PttApplication.kt @@ -10,6 +10,12 @@ import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFact import com.facebook.imagepipeline.core.ImagePipelineConfig import com.facebook.imagepipeline.core.ImagePipelineFactory import okhttp3.OkHttpClient +import org.koin.android.ext.koin.androidContext +import org.koin.core.context.startKoin +import tw.y_studio.ptt.di.apiModules +import tw.y_studio.ptt.di.appModules +import tw.y_studio.ptt.di.dataSourceModules +import tw.y_studio.ptt.di.viewModelModules import tw.y_studio.ptt.fresco.BitmapMemoryCacheSupplier import tw.y_studio.ptt.fresco.OkHttpNetworkFetcher import tw.y_studio.ptt.utils.OkHttpUtils @@ -20,6 +26,17 @@ class PttApplication : MultiDexApplication() { override fun onCreate() { super.onCreate() + startKoin { + androidContext(this@PttApplication) + modules( + listOf( + appModules, + viewModelModules, + apiModules, + dataSourceModules + ) + ) + } if (mOkHttpClient == null) { try { mOkHttpClient = OkHttpUtils().getCacheClient(this) diff --git a/app/src/main/java/tw/y_studio/ptt/adapter/HotBoardsListAdapter.kt b/app/src/main/java/tw/y_studio/ptt/adapter/HotBoardsListAdapter.kt index 466e31c..4588afc 100644 --- a/app/src/main/java/tw/y_studio/ptt/adapter/HotBoardsListAdapter.kt +++ b/app/src/main/java/tw/y_studio/ptt/adapter/HotBoardsListAdapter.kt @@ -7,15 +7,13 @@ import android.widget.TextView import androidx.appcompat.widget.AppCompatImageButton import androidx.recyclerview.widget.RecyclerView import tw.y_studio.ptt.R +import tw.y_studio.ptt.model.HotBoardsItem import tw.y_studio.ptt.ptt.PttColor -import tw.y_studio.ptt.utils.StringUtils.notNullString class HotBoardsListAdapter( - private val data: List> + private val data: List, + private val onItemClickListener: OnItemClickListener, ) : RecyclerView.Adapter() { - - private var mOnItemClickListener: OnItemClickListener? = null - private var mOnItemLongClickListener: OnItemLongClickListener? = null private val editMode = false override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -38,31 +36,23 @@ class HotBoardsListAdapter( when (getItemViewType(position)) { TYPE_NORMAL -> { (holder as? ViewHolder)?.apply { - textViewTitle.text = notNullString(data[position]["title"]) - textViewSubtitle.text = notNullString(data[position]["subtitle"]) - textViewOnlinePeople.text = notNullString(data[position]["online"]) + textViewTitle.text = data[position].title + textViewSubtitle.text = data[position].subtitle + textViewOnlinePeople.text = data[position].online person.setColorFilter( PttColor.ColorTrans( - notNullString(data[position]["onlineColor"]) + data[position].onlineColor ) ) - itemView.setOnClickListener { mOnItemClickListener?.onItemClick(it, adapterPosition) } - itemView.setOnLongClickListener { - mOnItemLongClickListener?.onItemClick(it, adapterPosition) - true - } + itemView.setOnClickListener { onItemClickListener.onItemClick(data[position]) } } } TYPE_EDIT -> { (holder as? ViewHolderEdit)?.apply { - textViewTitle.text = notNullString(data[position]["title"]) - textViewSubtitle.text = notNullString(data[position]["subtitle"]) - textViewOnlinePeople.text = notNullString(data[position]["online"]) - itemView.setOnClickListener { mOnItemClickListener?.onItemClick(it, adapterPosition) } - itemView.setOnLongClickListener { - mOnItemLongClickListener?.onItemClick(it, adapterPosition) - true - } + textViewTitle.text = data[position].title + textViewSubtitle.text = data[position].subtitle + textViewOnlinePeople.text = data[position].online + itemView.setOnClickListener { onItemClickListener.onItemClick(data[position]) } } } } @@ -72,14 +62,6 @@ class HotBoardsListAdapter( return data.size } - fun setOnItemClickListener(listener: OnItemClickListener?) { - mOnItemClickListener = listener - } - - fun setOnItemLongClickListener(listener: OnItemLongClickListener?) { - mOnItemLongClickListener = listener - } - private inner class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { val textViewTitle: TextView = v.findViewById(R.id.textView_hot_boards_title) val textViewSubtitle: TextView = v.findViewById(R.id.textView_hot_boards_subtitle) @@ -95,11 +77,7 @@ class HotBoardsListAdapter( // define interface interface OnItemClickListener { - fun onItemClick(view: View?, position: Int) - } - - interface OnItemLongClickListener { - fun onItemClick(view: View?, position: Int) + fun onItemClick(item: HotBoardsItem) } companion object { diff --git a/app/src/main/java/tw/y_studio/ptt/api/PopularBoardListAPI.kt b/app/src/main/java/tw/y_studio/ptt/api/PopularBoardListAPI.kt index f2ffcf6..8662b50 100644 --- a/app/src/main/java/tw/y_studio/ptt/api/PopularBoardListAPI.kt +++ b/app/src/main/java/tw/y_studio/ptt/api/PopularBoardListAPI.kt @@ -2,13 +2,14 @@ package tw.y_studio.ptt.api import okhttp3.Request import org.json.JSONArray +import tw.y_studio.ptt.model.HotBoard import java.util.* class PopularBoardListAPI : BaseAPIHelper(), IBaseAPI { - private val _data: MutableList> = mutableListOf() + private val _data: MutableList = mutableListOf() @Throws(Exception::class) - fun refresh(page: Int, count: Int): MutableList> { + fun refresh(page: Int, count: Int): MutableList { _data.clear() val request = Request.Builder() .url("$hostUrl/api/Board/Popular?page=$page&count=$count") @@ -28,16 +29,15 @@ class PopularBoardListAPI : BaseAPIHelper(), IBaseAPI { while (!list.isNull(i)) { val m3 = list.getJSONObject(i) i++ - val item: MutableMap = HashMap() - item["number"] = m3.getInt("sn") - item["title"] = m3.getString("name") - item["subtitle"] = m3.getString("title") - item["boardType"] = m3.getInt("boardType") - item["moderators"] = "" - item["class"] = "" - item["online"] = m3.getInt("onlineCount") - item["onlineColor"] = 7 - _data.add(item) + val hotboard = HotBoard( + number = m3.getInt("sn"), + title = m3.getString("name"), + subtitle = m3.getString("title"), + boardType = m3.getInt("boardType"), + online = m3.getInt("onlineCount"), + onlineColor = "7" + ) + _data.add(hotboard) } } return _data diff --git a/app/src/main/java/tw/y_studio/ptt/di/ApiModules.kt b/app/src/main/java/tw/y_studio/ptt/di/ApiModules.kt new file mode 100644 index 0000000..1a55ae8 --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/di/ApiModules.kt @@ -0,0 +1,12 @@ +package tw.y_studio.ptt.di + +import org.koin.dsl.module +import tw.y_studio.ptt.api.PopularBoardListAPI +import tw.y_studio.ptt.api.PostAPI +import tw.y_studio.ptt.api.SearchBoardAPI + +val apiModules = module { + factory { PopularBoardListAPI() } + factory { SearchBoardAPI() } + factory { PostAPI() } +} diff --git a/app/src/main/java/tw/y_studio/ptt/di/AppModules.kt b/app/src/main/java/tw/y_studio/ptt/di/AppModules.kt new file mode 100644 index 0000000..02ab673 --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/di/AppModules.kt @@ -0,0 +1,10 @@ +package tw.y_studio.ptt.di + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val appModules = module { + single(named("IO")) { Dispatchers.IO } +} diff --git a/app/src/main/java/tw/y_studio/ptt/di/DataSourceModules.kt b/app/src/main/java/tw/y_studio/ptt/di/DataSourceModules.kt new file mode 100644 index 0000000..46c9005 --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/di/DataSourceModules.kt @@ -0,0 +1,15 @@ +package tw.y_studio.ptt.di + +import org.koin.dsl.module +import tw.y_studio.ptt.source.remote.popular.IPopularRemoteDataSource +import tw.y_studio.ptt.source.remote.popular.PopularRemoteDataSourceImpl +import tw.y_studio.ptt.source.remote.post.IPostRemoteDataSource +import tw.y_studio.ptt.source.remote.post.PostRemoteDataSourceImpl +import tw.y_studio.ptt.source.remote.search.ISearchBoardRemoteDataSource +import tw.y_studio.ptt.source.remote.search.SearchBoardRemoteDataSourceImpl + +val dataSourceModules = module { + factory { PopularRemoteDataSourceImpl(get()) } + factory { SearchBoardRemoteDataSourceImpl(get()) } + factory { PostRemoteDataSourceImpl(get()) } +} diff --git a/app/src/main/java/tw/y_studio/ptt/di/Injection.kt b/app/src/main/java/tw/y_studio/ptt/di/Injection.kt index 02eda1f..641f194 100644 --- a/app/src/main/java/tw/y_studio/ptt/di/Injection.kt +++ b/app/src/main/java/tw/y_studio/ptt/di/Injection.kt @@ -1,27 +1,18 @@ package tw.y_studio.ptt.di -import tw.y_studio.ptt.api.PopularBoardListAPI import tw.y_studio.ptt.api.PostAPI import tw.y_studio.ptt.api.SearchBoardAPI -import tw.y_studio.ptt.source.remote.popular.IPopularRemoteDataSource -import tw.y_studio.ptt.source.remote.popular.PopularRemoteDataSourceImpl import tw.y_studio.ptt.source.remote.post.PostRemoteDataSourceImpl import tw.y_studio.ptt.source.remote.search.ISearchBoardRemoteDataSource import tw.y_studio.ptt.source.remote.search.SearchBoardRemoteDataSourceImpl object Injection { object API { - val popularBoardListAPI by lazy { - PopularBoardListAPI() - } val searchBoardAPI by lazy { SearchBoardAPI() } val postAPI by lazy { PostAPI() } } object RemoteDataSource { - val popularRemoteDataSource: IPopularRemoteDataSource by lazy { - PopularRemoteDataSourceImpl(API.popularBoardListAPI) - } val searchBoardRemoteDataSource: ISearchBoardRemoteDataSource by lazy { SearchBoardRemoteDataSourceImpl(API.searchBoardAPI) } diff --git a/app/src/main/java/tw/y_studio/ptt/di/ViewModelModules.kt b/app/src/main/java/tw/y_studio/ptt/di/ViewModelModules.kt new file mode 100644 index 0000000..a05c8a7 --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/di/ViewModelModules.kt @@ -0,0 +1,10 @@ +package tw.y_studio.ptt.di + +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.qualifier.named +import org.koin.dsl.module +import tw.y_studio.ptt.ui.hot_board.HotBoardsViewModel + +val viewModelModules = module { + viewModel { HotBoardsViewModel(get(), get(named("IO"))) } +} diff --git a/app/src/main/java/tw/y_studio/ptt/fragment/HomeFragment.java b/app/src/main/java/tw/y_studio/ptt/fragment/HomeFragment.java index 6cf9fd8..5e55399 100644 --- a/app/src/main/java/tw/y_studio/ptt/fragment/HomeFragment.java +++ b/app/src/main/java/tw/y_studio/ptt/fragment/HomeFragment.java @@ -16,6 +16,7 @@ import tw.y_studio.ptt.R; import tw.y_studio.ptt.ui.BaseFragment; +import tw.y_studio.ptt.ui.hot_board.HotBoardsFragment; import tw.y_studio.ptt.ui.setting.SettingFragment; import java.util.HashMap; diff --git a/app/src/main/java/tw/y_studio/ptt/fragment/HotBoardsFragment.java b/app/src/main/java/tw/y_studio/ptt/fragment/HotBoardsFragment.java deleted file mode 100644 index 88d3489..0000000 --- a/app/src/main/java/tw/y_studio/ptt/fragment/HotBoardsFragment.java +++ /dev/null @@ -1,218 +0,0 @@ -package tw.y_studio.ptt.fragment; - -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.Toast; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import tw.y_studio.ptt.R; -import tw.y_studio.ptt.adapter.HotBoardsListAdapter; -import tw.y_studio.ptt.di.Injection; -import tw.y_studio.ptt.source.remote.popular.IPopularRemoteDataSource; -import tw.y_studio.ptt.ui.BaseFragment; -import tw.y_studio.ptt.ui.ClickFix; -import tw.y_studio.ptt.ui.CustomLinearLayoutManager; -import tw.y_studio.ptt.ui.article.list.ArticleListFragment; -import tw.y_studio.ptt.utils.DebugUtils; -import tw.y_studio.ptt.utils.StringUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class HotBoardsFragment extends BaseFragment { - - public static HotBoardsFragment newInstance() { - Bundle args = new Bundle(); - HotBoardsFragment fragment = new HotBoardsFragment(); - fragment.setArguments(args); - return fragment; - } - - public static HotBoardsFragment newInstance(Bundle args) { - HotBoardsFragment fragment = new HotBoardsFragment(); - fragment.setArguments(args); - return fragment; - } - - private RecyclerView mRecyclerView; - private SwipeRefreshLayout mSwipeRefreshLayout; - private HotBoardsListAdapter mHotBoardsListAdapter; - - private List> data = new ArrayList<>(); - private ClickFix mClickFix = new ClickFix(); - - private LinearLayout search_bar; - - @Nullable - @Override - public View onCreateView( - LayoutInflater inflater, - @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.hot_boards_fragment_layout, container, false); - - setMainView(view); - - Bundle bundle = getArguments(); - - search_bar = getMainView().findViewById(R.id.hot_boards_fragment_search); - mRecyclerView = getMainView().findViewById(R.id.hot_boards_fragment_recyclerView); - mSwipeRefreshLayout = getMainView().findViewById(R.id.hot_boards_fragment_refresh_layout); - - search_bar.setOnClickListener( - new View.OnClickListener() { - - @Override - public void onClick(View v) { - loadFragmentNoAnim( - SearchBoardsFragment.newInstance(), getCurrentFragment()); - } - }); - - mHotBoardsListAdapter = new HotBoardsListAdapter(data); - - final CustomLinearLayoutManager layoutManager = new CustomLinearLayoutManager(getContext()); - layoutManager.setOrientation(RecyclerView.VERTICAL); - mRecyclerView.setHasFixedSize(true); - mRecyclerView.setLayoutManager(layoutManager); - mRecyclerView.setAdapter(mHotBoardsListAdapter); - - mSwipeRefreshLayout.setColorSchemeResources( - android.R.color.holo_red_light, - android.R.color.holo_blue_light, - android.R.color.holo_green_light, - android.R.color.holo_orange_light); - - mSwipeRefreshLayout.setOnRefreshListener( - new SwipeRefreshLayout.OnRefreshListener() { - - @Override - public void onRefresh() { - loadData(); - } - }); - - mHotBoardsListAdapter.setOnItemClickListener( - new HotBoardsListAdapter.OnItemClickListener() { - - @Override - public void onItemClick(View view, int position) { - if (mClickFix.isFastDoubleClick()) return; - Bundle bundle = new Bundle(); - bundle.putString( - "title", - StringUtils.notNullString(data.get(position).get("title"))); - bundle.putString( - "subtitle", - StringUtils.notNullString(data.get(position).get("subtitle"))); - loadFragment(ArticleListFragment.newInstance(bundle), getCurrentFragment()); - } - }); - - return view; - } - - protected void onAnimOver() { - loadData(); - } - - private Handler mThreadHandler; - private HandlerThread mThread; - private Runnable r1; - - private List> data_temp = new ArrayList<>(); - - private IPopularRemoteDataSource popularRemoteDataSource = - Injection.RemoteDataSource.INSTANCE.getPopularRemoteDataSource(); - - public void scrollToTop() { - try { - if (mRecyclerView != null) { - mRecyclerView.scrollToPosition(0); - } - } catch (Exception e) { - } - } - - private void getDataFromApi() { - r1 = - new Runnable() { - - public void run() { - runOnUI( - () -> { - mSwipeRefreshLayout.setRefreshing(true); - }); - - GattingData = true; - data_temp.clear(); - - try { - data_temp.addAll(popularRemoteDataSource.getPopularBoardData(1, 128)); - runOnUI( - () -> { - data.addAll(data_temp); - mHotBoardsListAdapter.notifyDataSetChanged(); - data_temp.clear(); - mSwipeRefreshLayout.setRefreshing(false); - }); - - DebugUtils.Log("HotBoardsFragment", "get data from web success"); - } catch (final Exception e) { - DebugUtils.Log("HotBoardsFragment", "Error : " + e.toString()); - runOnUI( - () -> { - Toast.makeText( - getActivity(), - "Error : " + e.toString(), - Toast.LENGTH_SHORT) - .show(); - mSwipeRefreshLayout.setRefreshing(false); - }); - } - GattingData = false; - } - }; - - mThread = new HandlerThread("name"); - mThread.start(); - mThreadHandler = new Handler(mThread.getLooper()); - mThreadHandler.post(r1); - } - - private boolean GattingData = false; - - private void loadData() { - if (GattingData) return; - GattingData = true; - data.clear(); - mHotBoardsListAdapter.notifyDataSetChanged(); - getDataFromApi(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - if (data != null) { - data.clear(); - } - // 移除工作 - if (mThreadHandler != null) { - mThreadHandler.removeCallbacks(r1); - } - // (關閉Thread) - if (mThread != null) { - mThread.quit(); - } - } -} diff --git a/app/src/main/java/tw/y_studio/ptt/model/HotBoard.kt b/app/src/main/java/tw/y_studio/ptt/model/HotBoard.kt new file mode 100644 index 0000000..e109693 --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/model/HotBoard.kt @@ -0,0 +1,10 @@ +package tw.y_studio.ptt.model + +data class HotBoard( + val number: Int, + val title: String, + val subtitle: String, + val boardType: Int, + val online: Int, + val onlineColor: String +) diff --git a/app/src/main/java/tw/y_studio/ptt/model/HotBoardsItem.kt b/app/src/main/java/tw/y_studio/ptt/model/HotBoardsItem.kt new file mode 100644 index 0000000..1ac027a --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/model/HotBoardsItem.kt @@ -0,0 +1,8 @@ +package tw.y_studio.ptt.model + +data class HotBoardsItem( + val title: String = "", + val subtitle: String = "", + val online: String = "", + val onlineColor: String = "" +) diff --git a/app/src/main/java/tw/y_studio/ptt/source/remote/popular/IPopularRemoteDataSource.kt b/app/src/main/java/tw/y_studio/ptt/source/remote/popular/IPopularRemoteDataSource.kt index 1606c97..30681e8 100644 --- a/app/src/main/java/tw/y_studio/ptt/source/remote/popular/IPopularRemoteDataSource.kt +++ b/app/src/main/java/tw/y_studio/ptt/source/remote/popular/IPopularRemoteDataSource.kt @@ -1,6 +1,10 @@ package tw.y_studio.ptt.source.remote.popular +import tw.y_studio.ptt.model.HotBoard + interface IPopularRemoteDataSource { - fun getPopularBoardData(page: Int, count: Int): MutableList> + fun getPopularBoardData(page: Int, count: Int): MutableList + + fun disposeAll() } diff --git a/app/src/main/java/tw/y_studio/ptt/source/remote/popular/PopularRemoteDataSourceImpl.kt b/app/src/main/java/tw/y_studio/ptt/source/remote/popular/PopularRemoteDataSourceImpl.kt index f698768..d122a7f 100644 --- a/app/src/main/java/tw/y_studio/ptt/source/remote/popular/PopularRemoteDataSourceImpl.kt +++ b/app/src/main/java/tw/y_studio/ptt/source/remote/popular/PopularRemoteDataSourceImpl.kt @@ -1,13 +1,18 @@ package tw.y_studio.ptt.source.remote.popular import tw.y_studio.ptt.api.PopularBoardListAPI +import tw.y_studio.ptt.model.HotBoard class PopularRemoteDataSourceImpl( private val popularBoardListAPI: PopularBoardListAPI ) : IPopularRemoteDataSource { @Throws(Exception::class) - override fun getPopularBoardData(page: Int, count: Int): MutableList> { + override fun getPopularBoardData(page: Int, count: Int): MutableList { return popularBoardListAPI.refresh(page, count) } + + override fun disposeAll() { + popularBoardListAPI.close() + } } diff --git a/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsFragment.kt b/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsFragment.kt new file mode 100644 index 0000000..d47c884 --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsFragment.kt @@ -0,0 +1,124 @@ +package tw.y_studio.ptt.ui.hot_board + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.koin.androidx.viewmodel.ext.android.viewModel +import tw.y_studio.ptt.adapter.HotBoardsListAdapter +import tw.y_studio.ptt.databinding.HotBoardsFragmentLayoutBinding +import tw.y_studio.ptt.fragment.SearchBoardsFragment +import tw.y_studio.ptt.model.HotBoardsItem +import tw.y_studio.ptt.ui.BaseFragment +import tw.y_studio.ptt.ui.ClickFix +import tw.y_studio.ptt.ui.CustomLinearLayoutManager +import tw.y_studio.ptt.ui.article.list.ArticleListFragment +import tw.y_studio.ptt.utils.observeNotNull + +class HotBoardsFragment : BaseFragment() { + private var _binding: HotBoardsFragmentLayoutBinding? = null + private val binding get() = _binding + private val mClickFix = ClickFix() + + private val viewModel: HotBoardsViewModel by viewModel() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return HotBoardsFragmentLayoutBinding.inflate(inflater, container, false).apply { + _binding = this + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding?.apply { + + hotBoardsFragmentSearch.setOnClickListener( + View.OnClickListener { + loadFragmentNoAnim( + SearchBoardsFragment.newInstance(), currentFragment + ) + } + ) + + hotBoardsFragmentRecyclerView.apply { + val layoutManager = CustomLinearLayoutManager(context) + layoutManager.orientation = RecyclerView.VERTICAL + setHasFixedSize(true) + this.layoutManager = layoutManager + adapter = HotBoardsListAdapter( + viewModel.data, + object : HotBoardsListAdapter.OnItemClickListener { + override fun onItemClick(item: HotBoardsItem) { + if (mClickFix.isFastDoubleClick) return + loadFragment( + ArticleListFragment.newInstance( + Bundle().apply { + putString("title", item.title) + putString("subtitle", item.subtitle) + } + ), + currentFragment + ) + } + } + ) + } + hotBoardsFragmentRefreshLayout.apply { + setColorSchemeResources( + android.R.color.holo_red_light, + android.R.color.holo_blue_light, + android.R.color.holo_green_light, + android.R.color.holo_orange_light + ) + setOnRefreshListener { + viewModel.loadData() + } + } + } + viewModel.apply { + observeNotNull(loadingState) { + binding?.hotBoardsFragmentRefreshLayout?.isRefreshing = it + binding?.hotBoardsFragmentRecyclerView?.adapter?.notifyDataSetChanged() + } + observeNotNull(errorMessage) { + Log.e("errorMessage", it) + } + } + } + + override fun onAnimOver() { + viewModel.loadData() + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + fun scrollToTop() { + Log.d("scrollToTop", "scrollToTop: hotBoardsFragmentRecyclerView") + binding?.hotBoardsFragmentRecyclerView?.scrollToPosition(0) + } + + companion object { + fun newInstance(): HotBoardsFragment { + val args = Bundle() + val fragment = HotBoardsFragment() + fragment.arguments = args + return fragment + } + + @JvmStatic + fun newInstance(args: Bundle?): HotBoardsFragment { + val fragment = HotBoardsFragment() + fragment.arguments = args + return fragment + } + } +} diff --git a/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsViewModel.kt b/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsViewModel.kt new file mode 100644 index 0000000..14de954 --- /dev/null +++ b/app/src/main/java/tw/y_studio/ptt/ui/hot_board/HotBoardsViewModel.kt @@ -0,0 +1,54 @@ +package tw.y_studio.ptt.ui.hot_board + +import android.util.Log +import androidx.lifecycle.* +import kotlinx.coroutines.* +import tw.y_studio.ptt.model.HotBoardsItem +import tw.y_studio.ptt.source.remote.popular.IPopularRemoteDataSource + +class HotBoardsViewModel( + private val popularRemoteDataSource: IPopularRemoteDataSource, + private val ioDispatcher: CoroutineDispatcher +) : ViewModel() { + val data: MutableList = mutableListOf() + + private val _loadingState = MutableLiveData() + val loadingState: LiveData = _loadingState + + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData = _errorMessage + + fun loadData() { + viewModelScope.launch { + if (_loadingState.value == true) return@launch + data.clear() + _loadingState.value = true + getDataFromApi() + _loadingState.value = false + } + } + + private suspend fun getDataFromApi() = withContext(ioDispatcher) { + try { + val boardData = popularRemoteDataSource.getPopularBoardData(1, 128) + .map { + HotBoardsItem( + it.title, + it.subtitle, + it.online.toString(), + it.onlineColor + ) + } + + data.addAll(boardData) + if (data.isNotEmpty()) Log.d("getDataFromApi", data[0].toString()) + } catch (e: Exception) { + _errorMessage.value = "Error: $e" + } + } + + override fun onCleared() { + super.onCleared() + popularRemoteDataSource.disposeAll() + } +} diff --git a/app/src/test/java/tw/y_studio/ptt/source/remote/popular/PopularRemoteDataSourceTest.kt b/app/src/test/java/tw/y_studio/ptt/source/remote/popular/PopularRemoteDataSourceTest.kt index 13ba126..535e2c4 100644 --- a/app/src/test/java/tw/y_studio/ptt/source/remote/popular/PopularRemoteDataSourceTest.kt +++ b/app/src/test/java/tw/y_studio/ptt/source/remote/popular/PopularRemoteDataSourceTest.kt @@ -9,6 +9,7 @@ import org.junit.After import org.junit.Before import org.junit.Test import tw.y_studio.ptt.api.PopularBoardListAPI +import tw.y_studio.ptt.model.HotBoard class PopularRemoteDataSourceTest { private lateinit var popularRemoteDataSource: IPopularRemoteDataSource @@ -25,7 +26,7 @@ class PopularRemoteDataSourceTest { @Test fun get_popular_board_data_then_return_data() { // GIVEN - val data = mapOf("foo" to "bar") + val data = HotBoard(1, "foo", "bar", 1, 1, "") every { popularBoardListAPI.refresh(any(), any()) } returns mutableListOf(data) // WHEN diff --git a/versions.gradle b/versions.gradle index a759287..73c9e7c 100644 --- a/versions.gradle +++ b/versions.gradle @@ -12,6 +12,7 @@ ext.buildConfig = [ ext.versions = [ 'androidGradle' : '4.1.0', + 'koin' : '2.2.0', 'kotlin' : '1.4.10', 'androidXAppCompat' : '1.2.0', 'androidXBrowser' : '1.2.0', @@ -54,6 +55,12 @@ ext.deps = [ 'android': "org.jetbrains.kotlinx:kotlinx-coroutines-android:${versions.coroutines}" ] ], + 'koin': [ + 'android': "org.koin:koin-android:${versions.koin}", + 'scope': "org.koin:koin-androidx-scope:${versions.koin}", + 'viewModel': "org.koin:koin-androidx-viewmodel:${versions.koin}", + 'fragment': "org.koin:koin-androidx-fragment:${versions.koin}" + ], 'androidX': [ 'appcompat' : "androidx.appcompat:appcompat:${versions.androidXAppCompat}", 'browser': "androidx.browser:browser:${versions.androidXBrowser}",