I want to Update Profile Picture in my app. Every Time User updates his profile picture. But the Problem is Profile picture is only updating when i restart the app after clearing the app from the memory.
The code i want to implement automatically is placed in onCreate method.
And the uploaded pic will always be from the internal Storage. Code is Attached Bellow.
mImageView = findViewById(R.id.profile_Pic);
ContextWrapper cw = new ContextWrapper(getApplicationContext());
File directory = cw.getDir("imageDir", Context.MODE_PRIVATE);
File myImageFile = new File(directory, "my_image.jpeg");
Picasso.get().load(myImageFile).into(mImageView);
I want that the IMageView for the Profile pic is Always updated automatically when the user updates his/her dp.What Should i do or Where Should i place that code?
You can use Livedata to observe data changes and then update the view in the livedata's observer. If the internal storage was managed by Room, you could make it return a Livedata and go from there.
At a glance, I would do something like this (using Kotlin here, but I'm sure you'll understand):
Create a view model for the Activity, that would manage the Activity state. The view model would have a Livedata of type File:
class MyViewModel : ViewModel() {
private val dataAccess: SomeClassThatControlsDataAccess = ...
private val _myImageFile: MutableLiveData<File> = MutableLiveData()
val myImageFile: LiveData<File>
get() = _myImageFile
fun updateUserProfilePic() {
val image: File = dataAccess.getProfilePicFromStorage() // This method would encapsulate that file retrieval code you have on OnCreate
_myImageFile.value = image
}
// other stuff
}
Observe this in the Activity. Whenever the Activity is resumed and there's a change, the view will be updated:
class MyActivity : AppCompatActivity() {
private lateinit var mModel: MyViewModel
// ...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Other code to setup the activity...
// Get the ViewModel
mModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
// Observe the LiveData
mModel.myImageFile.observe(this, Observer {
Picasso.get().load(it).into(mImageView)
})
}
override fun OnResume() {
super.OnResume()
mModel.updateUserProfilePic()
}
}
If you don't want to use any Android framework stuff, you can use something like RxJava and follow a similar approach.
You can also probably just place that code on your OnResume method and be done with it, but that has a tendency of creating a coupled design that’s bug prone and hard to change and/or maintain.
How is the user updating their profile picture?. You could use Picasso each time the user updates their profile picture.
You could use Picasso in onActivityResult
*edit: added code sample
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Check which request we're responding to
if (requestCode == TAKE_PICTURE || requestCode == PICK_FROM_GALLERY) {
// Make sure the request was successful
if (resultCode == RESULT_OK) {
// DO SOMETHING
Picasso.with(this).load(myImageFile).into(
}
}
}
Related
I have an repository that contains an in-memory cache list inside a StateFlow. The problem is that whenever the user logs out and logs into another account, the old data from the previous user is still there.
object Repository {
private lateinit var remoteDataSource: RemoteDataSource
operator fun invoke(remoteDataSource: remoteDataSource) {
this.remoteDataSource = remoteDataSource
return this
}
private val myList = MutableStateFlow(listOf<myData>())
suspend fun getData(): Flow<List<myData>> =
withContext(Dispatchers.IO) {
if (myList.value.isEmpty()) {
val response = remoteDataSource.getData()
if (response != null) {
myList.value = response.map { it.toMyData() }
}
}
myList
}
suspend fun addData(newData: MyData) =
withContext(Dispatchers.IO) {
myList.value = myList.value.plus(newData)
remoteDataSource.addData(myData.toMyDataRequest())
}
}
This repository is used by multiple ViewModels. The list itself is only observed by one screen (let's call it myFragment), but other screens can add new elements to it. I've tried to clear the repository on myFragment's onDestroyView, but it clears the list whenever the user navigates away from myFragment (even when it's not a logout).
We could observe whenever the user logs out in an userRepository, but i don't know how to observe data in one repository from another repository (there's nothing like viewModelScope.launch to collect flows or something like that).
What approach can be used to solve this? And how would it clear the list?
i don't know how to observe data in one repository from another repository
I'd argue you shouldn't in this case.
You have a use-case: Logout.
When you invoke this use-case, you should perform al the necessary operations that your app requires. In this case, you should call your repository to let it know.
suspend fun clearData() =
withContext(Dispatchers.IO) {
// clear your data
}
I'd argue that you shouldn't hardcode the Dispatcher, since you'll likely test this at some point; in your tests you're going to use TestDispatcher or similar, and if you hardcode it, it will be harder to test. You write tests, right?
So now your use case..
class LogoutUseCase(repo: YourRepo) {
operator fun invoke() {
repo.clearData()
//do the logout
}
}
That's how I would think about this.
Your scope for all this is the UI that initiated the logout...
Is this good to put the collect latest inside observe?
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!)
.observe(viewLifecycleOwner) {
if (it != null) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.referralDetailsResponse.collect { referralResponseState ->
when (referralResponseState) {
State.Empty -> {
}
is State.Failed -> {
Timber.e("${referralResponseState.message}")
}
State.Loading -> {
Timber.i("LOADING")
}
is State.Success<*> -> {
// ACCESS LIVEDATA RESULT HERE??
}}}}
I'm sure it isn't, my API is called thrice too as the local DB changes, what is the right way to do this?
My ViewModel looks like this where I'm getting user information from local Room DB and referral details response is the API response
private val _referralDetailsResponse = Channel<State>(Channel.BUFFERED)
val referralDetailsResponse = _referralDetailsResponse.receiveAsFlow()
init {
val inviteSlug: String? = savedStateHandle["inviteSlug"]
// Fire invite link
if (inviteSlug != null) {
referralDetail(inviteSlug)
}
}
fun referralDetail(referral: String?) = viewModelScope.launch {
_referralDetailsResponse.send(State.Loading)
when (
val response =
groupsRepositoryImpl.referralDetails(referral)
) {
is ResultWrapper.GenericError -> {
_referralDetailsResponse.send(State.Failed(response.error?.error))
}
ResultWrapper.NetworkError -> {
_referralDetailsResponse.send(State.Failed("Network Error"))
}
is ResultWrapper.Success<*> -> {
_referralDetailsResponse.send(State.Success(response.value))
}
}
}
fun fetchUserProfileLocal(username: String) =
userRepository.getUserLocal(username).asLiveData()
You can combine both streams of data into one stream and use their results. For example we can convert LiveData to Flow, using LiveData.asFlow() extension function, and combine both flows:
combine(
viewModel.fetchUserProfileLocal(PreferencesManager(requireContext()).userName!!).asFlow(),
viewModel.referralDetailsResponse
) { userProfile, referralResponseState ->
...
}.launchIn(viewLifecycleOwner.lifecycleScope)
But it is better to move combining logic to ViewModel class and observe the overall result.
Dependency to use LiveData.asFlow() extension function:
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
it certainly is not a good practice to put a collect inside the observe.
I think what you should do is collect your livedata/flows inside your viewmodel and expose the 'state' of your UI from it with different values or a combined state object using either Flows or Livedata
for example in your first code block I would change it like this
get rid of "userProfile" from your viewmodel
create and expose from your viewmodel to your activity three LiveData/StateFlow objects for your communityFeedPageData, errorMessage, refreshingState
then in your viewmodel, where you would update the "userProfile" update the three new state objects instead
this way you will take the business logic of "what to do in each state" outside from your activity and inside your viewmodel, and your Activity's job will become to only update your UI based on values from your viewmodel
For the specific case of your errorMessage and because you want to show it only once and not re-show it on Activity rotation, consider exposing a hot flow like this:
private val errorMessageChannel = Channel<CharSequence>()
val errorMessageFlow = errorMessageChannel.receiveAsFlow()
What "receiveAsFlow()" does really nicely, is that something emitted to the channel will be collected by one collector only, so a new collector (eg if your activity recreates on a rotation) will not receive the message and your user will not see it again
I want to run the code only once when the composable is loaded. So I am using LaunchedEffect with key as true to achieve this.
LaunchedEffect(true) {
// do API call
}
This code is working fine but whenever there is any configuration change like screen rotation this code is executed again. How can I prevent it from running again in case of configuration change?
The simplest solution is to store information about whether you made an API call with rememberSaveable: it will live when the configuration changes.
var initialApiCalled by rememberSaveable { mutableStateOf(false) }
if (!initialApiCalled) {
LaunchedEffect(Unit) {
// do API call
initialApiCalled = false
}
}
The disadvantage of this solution is that if the configuration changes before the API call is completed, the LaunchedEffect coroutine will be cancelled, as will your API call.
The cleanest solution is to use a view model, and execute the API call inside init:
class ScreenViewModel: ViewModel() {
init {
viewModelScope.launch {
// do API call
}
}
}
#Composable
fun Screen(viewModel: ScreenViewModel = viewModel()) {
}
Passing view model like this, as a parameter, is recommended by official documentation. In the prod code you don't need to pass any parameter to this view, just call it like Screen(): the view model will be created by default viewModel() parameter. It is moved to the parameter for test/preview capability as shown in this answer.
I assume the best way is to use the .also on the livedata/stateflow lazy creation so that you do guarantee as long as the view model is alive, the loadState is called only one time, and also guarantee the service itself is not called unless someone is listening to it. Then you listen to the state from the viewmodel, and no need to call anything api call from launched effect, also your code will be reacting to specic state.
Here is a code example
class MyViewModel : ViewModel() {
private val uiScreenState: : MutableStateFlow<WhatEverState> =
MutableStateFlow(WhatEverIntialState).also {
loadState()
}
fun loadState(): StateFlow<WhatEverState>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
When using this code, you do not have to call loadstate at all in the activity, you just listen to the observer.
You may check the below code for the listening
class MyFragment : Fragment {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setContent {
StartingComposeTheme {
Box(modifier = Modifier.fillMaxSize()) {
val state by viewModel.uiScreenState.collectAsState()
when (state) {
//do something
}
}
}
}
}
}
}}
#Islam Mansour answer work good for dedicated viewModel to UI but my case is shared ViewModel by many UIs fragments
In my case above answers does not solve my problem for calling API for just only first time call when user navigate to the concerned UI section.
Because I have multiple composable UIs in NavHost as Fragment
And my ViewModel through all fragments
so, the API should only call when user navigate to the desired fragment
so, the below lazy property initialiser solve my problem;
val myDataList by lazy {
Log.d("test","call only once when called from UI used inside)")
loadDatatoThisList()
mutableStateListOf<MyModel>()
}
mutableStateListOf<LIST_TYPE> automatically recompose UI when data added to this
variable appeded by by lazy intialized only once when explicilty called
I have 2 apps, one a very simple toy app that exists to call the other:
const val AUTHENTICATE_CODE = 42
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
fab.setOnClickListener {
Intent(Intent.ACTION_VIEW, Uri.parse("testapp://hello.world/")) //2nd app has intent filter to intercept this.
.also { intent -> startActivityForResult(intent, AUTHENTICATE_CODE) }
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val textView = findViewById<TextView>(R.id.hello_text)
if (requestCode == AUTHENTICATE_CODE && resultCode == Activity.RESULT_OK) {
requireNotNull(data) {
textView.text = "Error: Intent was null. Data lost."
return#onActivityResult
}
val dataExtra = data.getStringExtra("com.example.app.DATA")
requireNotNull(dataExtra){
textView.text = "Error: Intent did not contain data."
return#onActivityResult
}
Log.d("TestAppPlsIgnore", "Result Intent received")
textView.text = "Success! $dataExtra"
} else {
textView.text = "Something went wrong. Request = $requestCode; Result = $resultCode"
}
}
//...
}
The other app is a little more involved:
The activity in app 2 that the toy app launches implements the navigation library from Jetpack.
Most of the fragments that are in that activity's nav graph implement the same ViewModel. i.e. private val mainViewModel by activityViewModels<MainActivityViewModel>()
Inside the MainActivityViewModel is a LiveData<String> that we'll call data. The MainActivity of app 2 has an observer watching data similar to this:
val dataObserver = Observer<String> { data ->
val result = Intent()
result.putExtra("com.example.app.DATA", data)
Log.d("MainActivity.DataObserver", "Sending data $data")
setResult(Activity.RESULT_OK, result)
finish()
}
mainViewModel.data.observe(this, dataObserver)
In the general flow to get to a point where a string is put into data, the navigation view of the main activity will likely navigate between one or more fragments.
The expected result: When a string is added to data in app 2, the observer will create the result intent, set it as the result, and finish app 2. App 1 will receive the result and call onActivityResult, and we should display "Success!" plus some data.
What I get: The observer does work. The log statement shows the correct data was received by the observer. App 2 finishes. And app 1's onActivityResult displays the fail case, showing the correct request code, but a response code == Activity.RESULT_CANCELLED. If the requireNotNull(data) statement is moved outside the if statement, app 1 will instead show that the intent returned was null.
My questions:
RESULT_CANCELLED is not being explicitly returned, and I am attempting to return an intent with data. So that should only leave the activity crashing as a reason why RESULT_CANCELLED is being returned. Navigating across a nav graph will inevitably cause some fragments to reach the end of their lifecycle. Would Android confuse that for an activity crashing?
Why is there a null intent when onActivityResult is being called? For the most part, I'm just following what's outlined in the documentation, if a bit more verbosely.
Is this not the right way to send a simple string between two specific apps? I don't want to use share intents, because this is meant to be a more direct communication between specific apps rather than a broad communication between my app and a category of apps.
It turns out that you should not call finishAfterTransition() elsewhere in an app if you plan to use an Observer-based setup like mine to send data through startActivityForResult(). finishAfterTransition() causes a conflict with any calls to finish(), and you'll send a null result and a ResultCode of RESULT_CANCELLED.
using PagedList, and here it does not have database back, but a data list (call it CachedDataList) in the memory which could be filled in by fetchMore() function.
Having the PositionalDataSource, DataSource.Factory and PagedList.BoundaryCallback, it works but one issue here.
The flow is the PositionalDataSource's loadInitial() will be called at beginning to start to load data from the CachedDataList, and call loadRange() after that to continue loading data from the CachedDataList by page size.
When all data from the CachedDataList are paged off, the BoundaryCallback::onItemAtEndLoaded() will be called (if there is no backing data at beginning then the BoundaryCallback::onZeroItemsLoaded() is called),
In there it will start to asking fetchMore to append more data into the CachedDataList, and when the new data is appended to it then call the DataSource's invalidate() to restart the new PagedList and DataSource pair, and starting from PositionalDataSource's loadInitial() again.
It is done by
observableCachedData.observe(owner, refreshHandler!!)
//??? TODO: how to only listen to newly posted data after
//starting the observe?
//DOC: 'If LiveData already has data set, it will be delivered to the observer.'
// fetchMore
val didFetch = dataRequester.fetchMore() //asyc call
here, it observes the observableCachedData's change, and if there is change then the onChanged() of the
class RefreshHandler(val observableCachedData: MutableLiveData<List<Data>>) : Observer<List<Data>> {
override fun onChanged(datalist: List<Data>?)
will be called, and in which to call the DataSource's invalidate()
but the subscription of observableCachedData.observe() causes the refreshHandler called immediately (it's by design as stated in the DOC), this behavior is not desired here since we want the handler is called when the new data is append to the CachedDataList.
i.e. the CachedDataList had 30 data, when do fetchMore() there will be another 30 data appended to it, become 60. But this onChange() is called with data still at 30 (the append has not been coming yet).
Is there a way to subscribe to a live data but only get notified for update that happened after it is subscribed to it?
class DataBoundaryCallback(
private val owner: LifecycleOwner,
private val dataRequester: FetchMoreRequester,
private val dataSourceFactory: DataSourceFactory?
) : PagedList.BoundaryCallback<IData>() {
private var hasRequestInProgress = false
override fun onZeroItemsLoaded() {
requestAndSaveData()
}
override fun onItemAtEndLoaded(itemAtEnd: IData) {
requestAndSaveData()
}
private fun requestAndSaveData() {
if (hasRequestInProgress) return
hasRequestInProgress = true
// ask dataRequester to fetchMore
// setup observer
val cachedDataList = dataRequester.getCachedLiveData()
val observableCachedData = cachedDataList.getLiveData()
refreshHandler = RefreshHandler(observableCachedData)
observableCachedData.observe(owner, refreshHandler!!) //??? TODO: how to only listen to newly posted data after starting the observe? DOC: 'If LiveData already has data set, it will be delivered to the observer.'
// fetchMore
val didFetch = dataRequester.fetchMore()
if (!didFetch) { //not stated fetch
hasRequestInProgress = false
observableCachedData.removeObserver(refreshHandler!!)
}
}
var refreshHandler : RefreshHandler? = null
inner class RefreshHandler(val observableCachedData: MutableLiveData<List<Data>>) : Observer<List<Data>> {
override fun onChanged(datalist: List<Data>?) {
observableCachedData.removeObserver(refreshHandler!!)
val dataSource = dataSourceFactory.getPositionalDataSource()
// to start a new PagedList and DataSource pair flow
// and trigger the DataSource's loadInitial()
dataSource.invalidate()
}
}
}
You can use SingleLiveEvent
SingleLiveEvent
made it work with checking the emitted data list in the RefreshHandler(val observableCachedData: MutableLiveData<List<Data>>), only invalidate if there is more added to the list.
(it should look for a better way to determine the datalist has truly increased).
inner class RefreshHandler(val observableCachedData: MutableLiveData<List<Data>>) : Observer<List<Data>> {
val oldData = observableCachedData.value
override fun onChanged(datalist: List<Data>?) {
if (datalist.size == oldData.size) {
// is from previous cached data in the liveData
return
}
observableCachedData.removeObserver(refreshHandler!!)
val dataSource = dataSourceFactory.getPositionalDataSource()
// to start a new PagedList and DataSource pair flow
// and trigger the DataSource's loadInitial()
dataSource.invalidate()
}
}