From c596499381373723c1f73e0ec23dc49607d455ec Mon Sep 17 00:00:00 2001 From: Prem Nirmal Date: Wed, 26 Jan 2022 14:39:09 +0000 Subject: [PATCH] Minor UI tweaks, lib updates, and fetch fixes --- app/build.gradle | 22 +++---- .../premnirmal/ticker/home/HomeFragment.kt | 28 ++++++++- .../premnirmal/ticker/model/StocksProvider.kt | 41 ++++++------- .../ticker/news/QuoteDetailActivity.kt | 39 ++++++------ .../ticker/portfolio/PortfolioFragment.kt | 14 +++-- .../ticker/widget/WidgetDataProvider.kt | 8 ++- .../res/layout-sw600dp-land/fragment_home.xml | 58 ++++++++---------- app/src/main/res/layout/fragment_home.xml | 59 ++++++++----------- app/version.properties | 4 +- 9 files changed, 147 insertions(+), 126 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9c3b31ce..d51394d4 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -130,7 +130,7 @@ android { debuggable true versionNameSuffix "-DEBUG" ext.enableCrashlytics = false - minifyEnabled true + minifyEnabled false proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } @@ -159,14 +159,14 @@ android { ext { LIFECYCLE_VERSION = "2.4.0" - DAGGER_VERSION = "2.37" + DAGGER_VERSION = "2.40.5" MOCKITO_VERSION = "3.4.6" JUNIT_VERSION = "4.13.1" RETROFIT_VERSION = "2.9.0" OKHTTP_VERSION = "4.9.0" ROBOLECTRIC_VERSION = "4.3.1" - COROUTINES_VERSION = "1.5.2" - ROOM_VERSION = "2.4.0" + COROUTINES_VERSION = "1.6.0" + ROOM_VERSION = "2.4.1" WORK_VERSION = "2.7.1" } @@ -176,14 +176,14 @@ dependencies { implementation "androidx.core:core-ktx:1.7.0" implementation "androidx.multidex:multidex:2.0.1" - implementation "androidx.appcompat:appcompat:1.3.1" + implementation "androidx.appcompat:appcompat:1.4.1" implementation "androidx.legacy:legacy-support-v4:1.0.0" - implementation "com.google.android.material:material:1.4.0" + implementation "com.google.android.material:material:1.5.0" implementation "androidx.preference:preference-ktx:1.1.1" - implementation "androidx.browser:browser:1.3.0" - implementation "androidx.constraintlayout:constraintlayout:2.1.1" + implementation "androidx.browser:browser:1.4.0" + implementation "androidx.constraintlayout:constraintlayout:2.1.3" implementation "androidx.viewpager2:viewpager2:1.0.0" - implementation 'androidx.core:core-splashscreen:1.0.0-alpha02' + implementation 'androidx.core:core-splashscreen:1.0.0-beta01' implementation "javax.inject:javax.inject:1" implementation "javax.annotation:javax.annotation-api:1.3.2" @@ -216,8 +216,8 @@ dependencies { kapt "androidx.room:room-compiler:$ROOM_VERSION" implementation "androidx.room:room-ktx:$ROOM_VERSION" - prodImplementation "com.google.firebase:firebase-crashlytics:18.2.3" - prodImplementation "com.google.firebase:firebase-analytics:19.0.2" + prodImplementation "com.google.firebase:firebase-crashlytics:18.2.7" + prodImplementation "com.google.firebase:firebase-analytics:20.0.2" // debugImplementation "com.squareup.leakcanary:leakcanary-android:2.4" diff --git a/app/src/main/kotlin/com/github/premnirmal/ticker/home/HomeFragment.kt b/app/src/main/kotlin/com/github/premnirmal/ticker/home/HomeFragment.kt index ba441e17..7e9bde40 100644 --- a/app/src/main/kotlin/com/github/premnirmal/ticker/home/HomeFragment.kt +++ b/app/src/main/kotlin/com/github/premnirmal/ticker/home/HomeFragment.kt @@ -14,6 +14,7 @@ import com.github.premnirmal.ticker.isNetworkOnline import com.github.premnirmal.ticker.portfolio.PortfolioFragment import com.github.premnirmal.ticker.widget.WidgetDataProvider import com.github.premnirmal.tickerwidget.R +import com.google.android.material.appbar.AppBarLayout import com.google.android.material.tabs.TabLayoutMediator import kotlinx.android.synthetic.main.fragment_home.app_bar_layout import kotlinx.android.synthetic.main.fragment_home.subtitle @@ -75,12 +76,18 @@ class HomeFragment : BaseFragment(), ChildFragment, PortfolioFragment.Parent { TabLayoutMediator(tabs, view_pager) { tab, position -> tab.text = widgetDataProvider.widgetDataList()[position].widgetName() }.attach() + app_bar_layout.addOnOffsetChangedListener(offsetChangedListener) subtitle.text = subtitleText viewModel.fetchState.observe(viewLifecycleOwner) { updateHeader() } } + override fun onDestroyView() { + app_bar_layout.removeOnOffsetChangedListener(offsetChangedListener) + super.onDestroyView() + } + override fun onHiddenChanged(hidden: Boolean) { super.onHiddenChanged(hidden) if (!hidden) updateHeader() @@ -99,13 +106,13 @@ class HomeFragment : BaseFragment(), ChildFragment, PortfolioFragment.Parent { // Don't attempt to make many requests in a row if the stocks don't fetch. if (fetchCount <= MAX_FETCH_COUNT) { attemptingFetch = true - viewModel.fetch().observe(viewLifecycleOwner, { success -> + viewModel.fetch().observe(viewLifecycleOwner) { success -> attemptingFetch = false swipe_container?.isRefreshing = false if (success) { update() } - }) + } } else { attemptingFetch = false InAppMessage.showMessage(requireActivity(), R.string.refresh_failed, error = true) @@ -135,6 +142,23 @@ class HomeFragment : BaseFragment(), ChildFragment, PortfolioFragment.Parent { swipe_container.isEnabled = true } + private val offsetChangedListener = object : AppBarLayout.OnOffsetChangedListener { + private var isTitleShowing = true + + override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) { + val show = verticalOffset > -20 + if (show && !isTitleShowing) { + subtitle.animate().alpha(1f).start() + tabs.animate().alpha(1f).start() + isTitleShowing = true + } else if (!show && isTitleShowing) { + subtitle.animate().alpha(0f).start() + tabs.animate().alpha(0f).start() + isTitleShowing = false + } + } + } + // ChildFragment override fun scrollToTop() { diff --git a/app/src/main/kotlin/com/github/premnirmal/ticker/model/StocksProvider.kt b/app/src/main/kotlin/com/github/premnirmal/ticker/model/StocksProvider.kt index 91adb764..825cb0ea 100644 --- a/app/src/main/kotlin/com/github/premnirmal/ticker/model/StocksProvider.kt +++ b/app/src/main/kotlin/com/github/premnirmal/ticker/model/StocksProvider.kt @@ -71,16 +71,16 @@ class StocksProvider : IStocksProvider, CoroutineScope { this.tickerSet.addAll(DEFAULT_STOCKS) } val lastFetched = preferences.getLong(LAST_FETCHED, 0L) - _lastFetched.value = lastFetched + _lastFetched.tryEmit(lastFetched) val nextFetch = preferences.getLong(NEXT_FETCH, 0L) - _nextFetch.value = nextFetch + _nextFetch.tryEmit(nextFetch) alarmScheduler.enqueuePeriodicRefresh(context) if (lastFetched == 0L) { launch { - fetch() + fetch().collect() } } else { - _fetchState.value = FetchState.Success(lastFetched) + _fetchState.tryEmit(FetchState.Success(lastFetched)) runBlocking { fetchLocal() } } } @@ -114,7 +114,7 @@ class StocksProvider : IStocksProvider, CoroutineScope { msToNextAlarm: Long ) { val updateTime = alarmScheduler.scheduleUpdate(msToNextAlarm, context) - _nextFetch.value = updateTime.toInstant().toEpochMilli() + _nextFetch.tryEmit(updateTime.toInstant().toEpochMilli()) preferences.edit() .putLong(NEXT_FETCH, _nextFetch.value) .apply() @@ -125,7 +125,7 @@ class StocksProvider : IStocksProvider, CoroutineScope { private fun fetchStockInternal(ticker: String, allowCache: Boolean): Flow> = flow { val quote = if (allowCache) quoteMap[ticker] else null - quote?.let { FetchResult.success(quote) } ?: run { + quote?.let { emit(FetchResult.success(quote)) } ?: run { try { emit(api.getStock(ticker)) } catch (ex: Exception) { @@ -198,14 +198,15 @@ class StocksProvider : IStocksProvider, CoroutineScope { quote.symbol = ticker quoteMap[ticker] = quote saveTickers() - _tickers.value = tickerSet.toList() - _portfolio.value = quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value } + _tickers.tryEmit(tickerSet.toList()) + _portfolio.tryEmit(quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value }) launch { fetchStockInternal(ticker, false).collect { result -> if (result.wasSuccessful) { val data = result.data quoteMap[ticker] = data storage.saveQuote(result.data) + _portfolio.tryEmit(quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value }) } } } @@ -233,7 +234,7 @@ class StocksProvider : IStocksProvider, CoroutineScope { } if (!tickerSet.contains(ticker)) { tickerSet.add(ticker) - _tickers.value = tickerSet.toList() + _tickers.tryEmit(tickerSet.toList()) saveTickers() } val holding = Holding(ticker, shares, price) @@ -243,7 +244,7 @@ class StocksProvider : IStocksProvider, CoroutineScope { val id = storage.addHolding(holding) holding.id = id } - _portfolio.value = quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value } + _portfolio.tryEmit(quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value }) return holding } } @@ -260,8 +261,8 @@ class StocksProvider : IStocksProvider, CoroutineScope { launch { storage.removeHolding(ticker, holding) } - _tickers.value = tickerSet.toList() - _portfolio.value = quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value } + _tickers.tryEmit(tickerSet.toList()) + _portfolio.tryEmit(quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value }) } } @@ -272,11 +273,11 @@ class StocksProvider : IStocksProvider, CoroutineScope { saveTickers() if (filterNot.isNotEmpty()) { launch { - fetch() + fetch().collect() } } - _tickers.value = tickerSet.toList() - _portfolio.value = quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value } + _tickers.tryEmit(tickerSet.toList()) + _portfolio.tryEmit(quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value }) } return this.tickerSet } @@ -286,8 +287,8 @@ class StocksProvider : IStocksProvider, CoroutineScope { tickerSet.remove(ticker) saveTickers() quoteMap.remove(ticker) - _tickers.value = tickerSet.toList() - _portfolio.value = quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value } + _tickers.tryEmit(tickerSet.toList()) + _portfolio.tryEmit(quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value }) } launch { storage.removeQuoteBySymbol(ticker) @@ -301,8 +302,8 @@ class StocksProvider : IStocksProvider, CoroutineScope { tickerSet.remove(it) quoteMap.remove(it) } - _tickers.value = tickerSet.toList() - _portfolio.value = quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value } + _tickers.tryEmit(tickerSet.toList()) + _portfolio.tryEmit(quoteMap.filter { widgetDataProvider.containsTicker(it.key) }.map { it.value }) } saveTickers() launch { @@ -335,7 +336,7 @@ class StocksProvider : IStocksProvider, CoroutineScope { launch { storage.saveQuotes(portfolio) fetchLocal() - fetch() + fetch().collect() } } diff --git a/app/src/main/kotlin/com/github/premnirmal/ticker/news/QuoteDetailActivity.kt b/app/src/main/kotlin/com/github/premnirmal/ticker/news/QuoteDetailActivity.kt index e93fbf4d..4bf80a3a 100644 --- a/app/src/main/kotlin/com/github/premnirmal/ticker/news/QuoteDetailActivity.kt +++ b/app/src/main/kotlin/com/github/premnirmal/ticker/news/QuoteDetailActivity.kt @@ -8,7 +8,6 @@ import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import com.github.mikephil.charting.charts.LineChart @@ -32,7 +31,7 @@ import com.github.premnirmal.ticker.widget.WidgetDataProvider import com.github.premnirmal.tickerwidget.R import com.github.premnirmal.tickerwidget.R.color import com.github.premnirmal.tickerwidget.R.dimen -import kotlinx.android.synthetic.main.activity_graph.* +import kotlinx.android.synthetic.main.activity_graph.one_day import kotlinx.android.synthetic.main.activity_quote_detail.alert_above import kotlinx.android.synthetic.main.activity_quote_detail.alert_below import kotlinx.android.synthetic.main.activity_quote_detail.alert_header @@ -108,7 +107,7 @@ class QuoteDetailActivity : BaseGraphActivity(), NewsFeedAdapter.NewsClickListen ticker = checkNotNull(intent.getStringExtra(TICKER)) viewModel = ViewModelProvider(this).get(QuoteDetailViewModel::class.java) - viewModel.quote.observe(this, Observer { result -> + viewModel.quote.observe(this) { result -> if (result.wasSuccessful) { quote = result.data fetch() @@ -121,40 +120,40 @@ class QuoteDetailActivity : BaseGraphActivity(), NewsFeedAdapter.NewsClickListen graphView.setNoDataText(getString(R.string.error_fetching_stock)) news_container.displayedChild = INDEX_ERROR } - }) - viewModel.data.observe(this, Observer { data -> + } + viewModel.data.observe(this) { data -> dataPoints = data loadGraph(ticker) - }) - viewModel.dataFetchError.observe(this, Observer { + } + viewModel.dataFetchError.observe(this) { progress.visibility = View.GONE graphView.setNoDataText(getString(R.string.graph_fetch_failed)) InAppMessage.showMessage(this@QuoteDetailActivity, R.string.graph_fetch_failed, error = true) - }) - viewModel.newsData.observe(this, Observer { data -> + } + viewModel.newsData.observe(this) { data -> analytics.trackGeneralEvent( GeneralEvent("FetchNews") .addProperty("Success", "True") ) setUpArticles(data) - }) - viewModel.newsError.observe(this, Observer { + } + viewModel.newsError.observe(this) { news_container.displayedChild = INDEX_ERROR InAppMessage.showMessage(this@QuoteDetailActivity, R.string.news_fetch_failed, error = true) analytics.trackGeneralEvent( GeneralEvent("FetchNews") .addProperty("Success", "False") ) - }) + } viewModel.fetchQuote(ticker) - var view: View? = null - when (range) { - Range.ONE_DAY -> view = one_day - Range.TWO_WEEKS -> view = two_weeks - Range.ONE_MONTH -> view = one_month - Range.THREE_MONTH -> view = three_month - Range.ONE_YEAR -> view = one_year - Range.MAX -> view = max + val view = when (range) { + Range.ONE_DAY -> one_day + Range.TWO_WEEKS -> two_weeks + Range.ONE_MONTH -> one_month + Range.THREE_MONTH -> three_month + Range.ONE_YEAR -> one_year + Range.MAX -> max + else -> null } view?.isEnabled = false } diff --git a/app/src/main/kotlin/com/github/premnirmal/ticker/portfolio/PortfolioFragment.kt b/app/src/main/kotlin/com/github/premnirmal/ticker/portfolio/PortfolioFragment.kt index 413b84cc..d1b95cef 100644 --- a/app/src/main/kotlin/com/github/premnirmal/ticker/portfolio/PortfolioFragment.kt +++ b/app/src/main/kotlin/com/github/premnirmal/ticker/portfolio/PortfolioFragment.kt @@ -9,12 +9,14 @@ import android.view.View import android.view.ViewGroup import android.widget.PopupMenu import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ItemTouchHelper import com.github.premnirmal.ticker.analytics.ClickEvent import com.github.premnirmal.ticker.base.BaseFragment import com.github.premnirmal.ticker.components.InAppMessage import com.github.premnirmal.ticker.components.Injector import com.github.premnirmal.ticker.home.ChildFragment +import com.github.premnirmal.ticker.model.IStocksProvider import com.github.premnirmal.ticker.network.data.Quote import com.github.premnirmal.ticker.news.QuoteDetailActivity import com.github.premnirmal.ticker.portfolio.StocksAdapter.QuoteClickListener @@ -25,6 +27,7 @@ import com.github.premnirmal.ticker.widget.WidgetDataProvider import com.github.premnirmal.tickerwidget.R import kotlinx.android.synthetic.main.fragment_portfolio.stockList import kotlinx.android.synthetic.main.fragment_portfolio.view_flipper +import kotlinx.coroutines.launch import javax.inject.Inject /** @@ -65,6 +68,7 @@ class PortfolioFragment : BaseFragment(), ChildFragment, QuoteClickListener, OnS class InjectionHolder { @Inject internal lateinit var widgetDataProvider: WidgetDataProvider + @Inject internal lateinit var stocksProvider: IStocksProvider init { Injector.appComponent.inject(this) @@ -117,11 +121,6 @@ class PortfolioFragment : BaseFragment(), ChildFragment, QuoteClickListener, OnS widgetId = requireArguments().getInt(KEY_WIDGET_ID) } - override fun onResume() { - super.onResume() - update() - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -155,6 +154,11 @@ class PortfolioFragment : BaseFragment(), ChildFragment, QuoteClickListener, OnS } else { view_flipper.displayedChild = 1 } + lifecycleScope.launch { + holder.stocksProvider.portfolio.collect { + update() + } + } } private fun update() { diff --git a/app/src/main/kotlin/com/github/premnirmal/ticker/widget/WidgetDataProvider.kt b/app/src/main/kotlin/com/github/premnirmal/ticker/widget/WidgetDataProvider.kt index f19b3b48..fda4ff8a 100644 --- a/app/src/main/kotlin/com/github/premnirmal/ticker/widget/WidgetDataProvider.kt +++ b/app/src/main/kotlin/com/github/premnirmal/ticker/widget/WidgetDataProvider.kt @@ -68,7 +68,13 @@ class WidgetDataProvider { fun removeWidget(widgetId: Int): WidgetData? { return synchronized(widgets) { val removed = widgets.remove(widgetId) - removed?.onWidgetRemoved() + removed?.let { + if (widgetCount == 0) { + val widget = dataForWidgetId(AppWidgetManager.INVALID_APPWIDGET_ID) + widget.addTickers(it.getTickers()) + } + it.onWidgetRemoved() + } return@synchronized removed } } diff --git a/app/src/main/res/layout-sw600dp-land/fragment_home.xml b/app/src/main/res/layout-sw600dp-land/fragment_home.xml index 2f043c6c..2bf5f1fd 100644 --- a/app/src/main/res/layout-sw600dp-land/fragment_home.xml +++ b/app/src/main/res/layout-sw600dp-land/fragment_home.xml @@ -39,47 +39,41 @@ - + + - - - - - + /> - + + - - - - - + />