How to store images and .mp3 file locally from API flutter? - flutter

I have displayed images and played audio from GET request. Now I need to store the images and .mp3 audio files locally. Is there any way to achieve this. I need to store List of images and audio files. Thank You.
This is the way I get response from API.
"data": [
{
"id": 1052,
"title": "1",
"audio_file": "",
"desc": null,
"display_title": 1,
"audio_src": null,
"image_src": "https://topik.com/storage/uploads/topik/images/20220620092536.jpg"
},
{
"id": 1053,
"title": "2",
"audio_file": "",
"desc": null,
"display_title": 1,
"audio_src": null,
"image_src": "https://topik.com/storage/uploads/topik/images/20220620092545.jpg"
},
]

You can use the path and path_provider to implement this. Sample code for the Api response you shared:
import 'dart:io';
import 'package:http/http.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
/// Returns the response from the api in a Map
Map getResponseMap(String apiUrl) {
var response = http.get(apiUrl);
Map responseMap = jsonDecode(response) as Map<String, dynamic>;
return responseMap;
}
/// Returns the list of files from the api response.
List<Map> getItemsListFromResponseMap(Map responseMap) {
var list = responseMap['data'];
return list.cast<Map>(); // cast the list to a list of Maps.
}
/// Returns the file name with extension.
String getFileNameFromUrl(String url) {
String fileName = url.split('/').toList().last;
return fileName;
}
Future<void> downloadAudioFromAPIResponse(Map singleResponseItem) async {
String audioUrl = singleResponseItem['audio_src'];
await saveNetworkFileToLocalDirectory(audioUrl); // Your file will be saved to the specified directoty in the [saveNetworkFileToLocalDirectory] function below.
}
Future<void> downloadImageFromAPIResponse(Map singleResponseItem) async {
String imgUrl = singleResponseItem['image_src'];
await saveNetworkFileToLocalDirectory(imgUrl); // Your file will be saved to the specified directoty in the [saveNetworkFileToLocalDirectory] function below.
}
Future<void> saveNetworkFileToLocalDirectory(String fileSrcUrl) async {
var response = await http.get(fileSrcUrl);
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String filePath = join(documentDirectory.path, getFileNameFromUrl(fileSrcUrl));
File file = new File(filePath);
await file.writeAsBytes(response.bodyBytes);
// The file has been written at the filePath specified, in this case,
// The app's document directory.
}
You can change the path to which the file is written by changing the getApplicationDocumentsDirectory to something else. For available paths, check out path_provider.
You can use the sample code to download all the files in your response as:
Map mainResponse = getResponseMap("YourApiRequestUrlHere");
// Get list of all item in the response by API.
List<Map> individualItems = getItemsListFromResponseMap(mainResponse);
// pre-evalute the count to improve performance by not having to call .length every time the loop finishes execution.
int count = individualItems.length;
for (int i = 0; i < count; i++) {
Map item = individualItems[i]; // get a single item.
if(item['audio_src'] != null && item['image_src'] != null) {
await downloadAudioFromAPIResponse(item);
await downloadImageFromAPIResponse(item);
} else if (item['audio_src'] != null) {
await downloadAudioFromAPIResponse(item);
} else if (item['image_src'] != null) {
await downloadImageFromAPIResponse(item);
} else {
continue;
}
}
If my answer was helpful, please mark my answer as Correct. Thank you.

After converting json object to dart object
use
Internet_file package
Uint8List bytes = await InternetFile.get(
data.image_src,
headers: headers, /* in case you want to set auth otherwise remove it*/
process: (percentage) {
print('downloadPercentage: $percentage');
},
);
Now use PathProvider
Directory tempDir = await getTemporaryDirectory();
String tempPath = tempDir.path;
print("temp path : " + tempDir.toString());
await writeToFile(bytes, '${tempPath}/filename.png');
print("written");

Related

Cannot retain value inside finalList variable? Firstly, fetchTracksMetaData and store result into variable finalList, so fetchLibraryTracks can use

For building a music player library, I am firstly picking the directories from where we get audio files. Then, provide the chosen directory as string to fetchTracksMetaData function, which lists all the files inside the picked directory and gets all the tags.
Now, I want to store these fetched tracks into the variable finalList, so that other methods like fetchLibraryTracks can use it to fetch tracks added when fetchTracksMetaData was called.
The finalList inside fetchLibraryTracks has 0 item, when called. Why?
class Library {
List<Map<String, dynamic>> finalList=[];
Future<String> chooseFolder() async
{
var pickedDirectory = await FilePicker.platform.getDirectoryPath();
debugPrint("Picked Directory = $pickedDirectory");
return pickedDirectory.toString();
}
Future<List<Map<String, dynamic>>?> fetchTracksMetaData({required String directory}) async
{
final OnAudioEdit audioEdit = OnAudioEdit();
/// List all the files inside the picked directory.
final List<FileSystemEntity> list = await Directory(directory).list().toList();
// For every file in the picked directory
for (var item in list) {
// If item is a File inside picked Directory.
if (item is File) {
// Get the plain file name
final String filePath = item
.toString()
.replaceRange(0, 7, "")
.replaceAll(RegExp(r".mp3'"), ".mp3");
debugPrint("FilePath = $filePath");
// If the File is MP3
if (filePath.contains(".mp3")) {
AudioModel song = (await audioEdit.readAudio(filePath).catchError((error) {
debugPrint('🤭 Error: $error');
}));
debugPrint("âž• to finalList");
finalList.add({
'fileLocation': filePath,
'tags': [song]
});
//debugPrint(song.toString());
debugPrint(finalList.last.toString());
} else {
debugPrint("Not a MP3 file:- $filePath");
}
} else if (item is Directory) {
debugPrint("Directory = $item");
} else {
debugPrint("Neither file nor directory");
}
}
debugPrint("Completed Fetching ${finalList.length}");
return finalList;
}
Future<List<Map<String, dynamic>>> fetchLibraryTracks() async {
await Future.delayed(const Duration(
seconds: 4,
));
List<Map<String, dynamic>> tracksList = [];
debugPrint("Loading ${finalList.length} Tracks ");
for (var index = 0; index < finalList.length; index++) {
tracksList.add(finalList[index]);
debugPrint("Last file location:- ${tracksList.last['fileLocation']}");
}
return tracksList;
}
}

How to save image correctly that's being uploaded to Flutter Android app?

First, I created a sample HttpServer console app using shelf-related packages to make sure I have the correct code to handle the image upload URI handler.
The console app was able to receive and save the image correctly then I tried the same code to Flutter Android app with only one minor difference which is the location of the saved image file.
Here's the code:
import 'package:shelf_router/shelf_router.dart' as srouter;
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:mime/mime.dart';
import 'package:path_provider/path_provider.dart' as prod;
var app = srouter.Router();
final directory = await prod.getExternalStorageDirectory();
app.post('/upload', (Request request) async {
String? boundary = request.headers['content-type'];
final payload = await request.read();
final boundWord = 'boundary=';
if (boundary != null) {
var boundIndex = boundary.indexOf(boundWord);
boundary = boundary.substring(boundIndex + boundWord.length);
final transformer = MimeMultipartTransformer(boundary);
final parts = await transformer.bind(payload).toList();
for (var part in parts) {
final content = await part.toList();
await File('${directory?.path}/newImgFile.png').writeAsBytes(content[0]);
}
}
return Response.ok('Upload done');
});
await io.serve(app, '0.0.0.0', 8080);
When I ran this and I send an image file using curl, it seems that it does everything correctly but when I checked the saved newImgFile, it's incorrect.
The file size doesn't match at all. I had my test image is in 900 KB but it was saved with much less than that (I think it was 30 KB).
I did notice one thing that's different on the content after this code line:
final content = await part.toList();
With the same image upload, content is a list of only 1 item for HttpServer console app but for HttpServer Flutter Android app, content is a list with 2 items.
I don't know if that's going to help in solving this issue but that's a difference that I am noticing.
I think the most useful conceptual model of MIME multipart uploads is:
any number of parts, in any order, each comprised of
any number of chunks, in consecutive order
Based on what you've described, you are receiving multiple chunks for at least one of the parts, which you will need to deal with. There is another issue with your code that may crop up, and that is the assumption that the part you care about is last (eg, you may have multiple parts, but you are overwriting the file with the last one anyway).
Putting them both together, you could do something like the following:
import 'package:http_parser/http_parser.dart';
...
app.post('/upload', (Request request) async {
final contentType = request.headers['content-type'];
if (contentType == null) {
return Response(400, body: 'Missing content-type');
}
final mediaType = MediaType.parse(contentType);
if (mediaType.mimeType != 'multipart/form-data') {
return Response(400, body: 'Invalid content-type');
}
final boundary = mediaType.parameters['boundary'];
if (boundary == null) {
return Response(400, body: 'Missing boundary');
}
final payload = request.read();
final parts = await MimeMultipartTransformer(boundary).bind(payload).toList();
for (final part in parts) {
if (part.headers['content-type'] != 'image/png') {
continue;
}
final file = File('${directory?.path}/newImgFile.png');
if (await file.exists()) {
await file.delete();
}
final chunks = await part.toList();
for (final chunk in chunks) {
await file.writeAsBytes(chunk, mode: FileMode.append);
}
return Response.ok('Upload done');
}
return Response(400, body: 'No good parts');
});
which will:
check whether the request Content-Type is valid (using MediaType)
find the first part that has the appropriate image/png content type
delete the destination file if it already exists
append each chunk to the newly created destination file
Going further, you could take advantage of the fact that both MimeMultipartTransformer.bind() and MimeMultipart implement Stream and do something like the following:
app.post('/upload', (Request request) async {
final contentType = request.headers['content-type'];
if (contentType == null) {
return Response(400, body: 'Missing content-type');
}
final mediaType = MediaType.parse(contentType);
if (mediaType.mimeType != 'multipart/form-data') {
return Response(400, body: 'Invalid content-type');
}
final boundary = mediaType.parameters['boundary'];
if (boundary == null) {
return Response(400, body: 'Missing boundary');
}
final payload = request.read();
final parts = MimeMultipartTransformer(boundary).bind(payload).where((part) {
return part.headers['content-type'] == 'image/png';
});
final partsIterator = StreamIterator(parts);
while (await partsIterator.moveNext()) {
final part = partsIterator.current;
final file = File('${directory?.path}/newImgFile.png');
if (await file.exists()) {
await file.delete();
}
final chunksIterator = StreamIterator(part);
while (await chunksIterator.moveNext()) {
final chunk = chunksIterator.current;
await file.writeAsBytes(chunk, mode: FileMode.append);
}
return Response.ok('Upload done');
}
return Response(400, body: 'No good parts');
});
which:
does some filtering upfront to ensure only image/png parts are considered, and
uses StreamIterator to allow iterating over the stream with await

Flutter just_audio package how play audio from bytes

I'm using just_audio plugin and it has on description a feature: Read from byte stream.
Basically when I put a file (from url) to play, I'm saving the bytes from file so after this step I want to play it locally.
I have a question about how play from byte stream. Can anyone provide an example how to do this? I need to put this on my playlist so it has to be a child of ConcatanatingAudioSource.
The only Audio Source that I found was using it from Uri.
final _playlist = ConcatenatingAudioSource(
children: [
AudioSource.uri(
Uri.parse(
"https://s3.amazonaws.com/scifri-episodes/scifri20181123-episode.mp3"),
tag: AudioMetadata(
album: "Science Friday",
title: "ddddd",
artwork:
"https://media.wnyc.org/i/1400/1400/l/80/1/ScienceFriday_WNYCStudios_1400.jpg",
),
)
]
)
This is how I save the bytes:
void getBytes() async {
Uri uri = Uri.parse(url);
var rng = new Random();
// get temporary directory of device.
Directory tempDir = await getTemporaryDirectory();
// get temporary path from temporary directory.
String tempPath = tempDir.path;
// create a new file in temporary path with random file name.
File file = new File('$tempPath' + (rng.nextInt(100)).toString() + '.mp3');
// call http.get method and pass imageUrl into it to get response.
http.Response response = await http.get(uri);
// write bodyBytes received in response to file.
await file.writeAsBytes(response.bodyBytes);
}
Thanks in advance
So it seems that you need to create your own class as an extension of StreamAudioSource.
import 'dart:typed_data';
import 'package:just_audio/just_audio.dart';
class MyJABytesSource extends StreamAudioSource {
final Uint8List _buffer;
MyJABytesSource(this._buffer) : super(tag: 'MyAudioSource');
#override
Future<StreamAudioResponse> request([int? start, int? end]) async {
// Returning the stream audio response with the parameters
return StreamAudioResponse(
sourceLength: _buffer.length,
contentLength: (start ?? 0) - (end ?? _buffer.length),
offset: start ?? 0,
stream: Stream.fromIterable([_buffer.sublist(start ?? 0, end)]),
contentType: 'audio/wav',
);
}
}
And then invoke it like so
await thePlayer.setAudioSource(MyJABytesSource(bytes));
You can call thePlayer.play(). after, but I prefer to use this as a listener.
thePlayer.processingStateStream.listen((ja.ProcessingState state) {
if (state == ja.ProcessingState.ready) {
// I'm using flutter_cache_manager, and it serves all the file
// under the same name, which is fine, but I think this is why
// I need to pause before I can play again.
// (For tracks after the first, the first plays fine.)
// You probably won't need to pause, but I'm not sure.
thePlayer.pause();
thePlayer.play();
} else if (state == ja.ProcessingState.completed) {
// What to do when it completes.
}
});
The nice part about doing this way, is that you don't actually need await keyword, which can be situationally useful. I have it there just to make clear that this is an async function.

Reading from text files until String max length

I'm new to coding in Dart so please bear with me. I searched up how to read files with the readAsString() function from the flutter API. It says that it will read the entire content of the file and return it as a String. However, is there some sort of String max size that it can only read? I could not find the max size of a String in Dart online. Thanks.
Here's the code in case you want a look:
import 'dart:async';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class Storage {
Future<String> get localPath async {
final directory = await getApplicationDocumentsDirectory();
return directory.path;
}
Future<File> get localFile async {
final path = await localPath;
return File('$path/data.txt');
}
Future<List<String>> read() async {
try {
final file = await localFile;
String contents = await file.readAsString(); //the important part
return contents.split(";");
} catch (exception) {
return null;
}
}
void write(List data) async {
final file = await localFile;
String toWrite = "";
for (int i = 0; i < data.length; i++) {
toWrite += data.elementAt(i) + ";";
}
file.writeAsString(toWrite);
}
}
Maybe you want something like:
var myFileStream = File('path/to/file').openRead();
var firstChars = myFileStream.take(1024);
This will limit the memory part of the file to the first 1024 characters.
(I think. :)

Document access during initialisation issue on Android

I am having trouble accessing files saved in getApplicationDocumentsDirectory on android only, I keep locale, theme and assets files stored there and when booting up the app those files are always inaccessible and as a result the app does not load, I tried using shared_prefs to track a first load scenario to use the files in assets/ and then later use the ones download but that seems to be an unreliable fix as at 2nd run they also inaccessible.
The only thing that seems to work reliable is the theme.json, but both copy functions run through the same helpers so what copies the theme over is what copies the locale over. Theme is loaded up in main.dart but the locale is loaded up with EasylocaLizationDelegate and if the files are in Document storage it can never load them up.
None of these issues are present on iOS, does anyone know of any special tricks to make this work for android, I have tried google but not really sure what to search for.
class FileHelpers {
static Future<Directory> getDownloadDirectory() async {
Directory dir = await getApplicationDocumentsDirectory();
globals.documentDirectory = dir;
return dir;
}
static verifyOrCreateDirectory(String folderName) async {
String path = globals.documentDirectory.path;
final Directory _folder = Directory('$path/$folderName/');
if (await _folder.exists()) {
//if folder already exists return path
return _folder.path;
} else {
//if folder not exists create folder and then return its path
final Directory _createdFolder = await _folder.create(recursive: true);
return _createdFolder.path;
}
}
static void writeStringToFile(String data, String path) {
new File(path).writeAsString(jsonEncode(data));
}
}
class ThemeHelpers {
static final _folderName = 'theme';
static final _fileName = 'theme.json';
static Future<void> loadTheme() async {
String documentPath = globals.documentDirectory.path;
try {
String assetTheme = 'assets/$_fileName';
String documentTheme = "$documentPath/$_folderName/$_fileName";
String loadPath = documentTheme;
/// We need to check if we can use the local assets as Android has some
/// issues using document assets this early on in the app run
if (!globals.canUseLocalAssets) {
loadPath = assetTheme;
}
Box box = await Hive.openBox(globals.environment.entryCode);
String data = await rootBundle.loadString(loadPath);
box.put(HIVE_COLLECTION_THEME, data);
} catch (e) {
print(e);
}
}
/// In order to support offline themes we ship the current version as at build
/// time with the app.
/// This method is to copy the files from the assets folder into the documents
/// directory. This gives us the option to then update when needed.
static copyTheme() async {
String path = await FileHelpers.verifyOrCreateDirectory(_folderName);
String themePath = '$path/$_fileName';
/// Check if the file already exists, if not copy it over
if (FileSystemEntity.typeSync(themePath) == FileSystemEntityType.notFound) {
String assetPath = "assets/$_fileName";
String data = await rootBundle.loadString(assetPath);
await new File(themePath).writeAsString(data);
print('theme copied');
}
}
static updateTheme() async {
String path = await FileHelpers.verifyOrCreateDirectory(_folderName);
var url = ApiEndpoint.uri('/theme').toString();
try {
bool shouldUpdate = await _checkIfUpdated();
if (shouldUpdate) {
/// If theme has been updated download the latest
/// version and store it.
///
/// Currently this happens as a non-blocking action
/// so updates will take effect the next time the user opens the app.
var response = await Session.apiGet(url);
String localePath = '$path/$_fileName';
await new File(localePath).writeAsString(jsonEncode(response));
/// Once updated reload the theme into [Hive]
loadTheme();
}
} catch (error) {
print('Unable to update theme');
}
}
/// This method makes alight call comparing the [__lastUpdated] in our local
/// copy of the Locale to the API version.
static Future<bool> _checkIfUpdated() async {
String assetPath =
'${globals.documentDirectory.path}/$_folderName/$_fileName';
if (FileSystemEntity.typeSync(assetPath) ==
FileSystemEntityType.notFound) throw "File not found";
try {
String data = await rootBundle.loadString(assetPath);
String currentThemeDate = json.decode(data)['__lastUpdated'];
var url = ApiEndpoint.uri(
'/theme/check',
queryParameters: {
"date": currentThemeDate,
},
).toString();
bool response = await Session.apiGet(url);
if (response.runtimeType == ErrorModel) throw response;
return response;
} catch (error) {
print("Unable to check for updated theme: $error");
return Future.value(false);
}
}
}
class LocaleHelpers {
static final _directory = DIRECTORY_LOCALE;
/// In order to support offline locale we ship the current version as at build
/// time with the app.
/// This method is to copy the files from the assets folder into the documents
/// directory. This gives us the option to then update when needed.
static copyLocaleFiles() async {
String path = await FileHelpers.verifyOrCreateDirectory(_directory);
for (var locale in globals.supportedLocale) {
String localeKey = locale.toString();
String localePath = '$path$localeKey.json';
/// Check if the file already exists, if not copy it over
if (FileSystemEntity.typeSync(localePath) ==
FileSystemEntityType.notFound) {
String assetPath = "assets/$_directory/$localeKey.json";
String data = await rootBundle.loadString(assetPath);
FileHelpers.writeStringToFile(data, localePath);
}
}
}
static updateLocale() async {
String path = await FileHelpers.verifyOrCreateDirectory(_directory);
/// Iterate through locale and check each supported
/// language if there is an updated version on the API
for (Locale locale in globals.supportedLocale) {
String localeString = locale.toString();
var url = ApiEndpoint.uri('/locale/$localeString.json').toString();
try {
bool shouldUpdate = await _checkIfUpdated(localeString);
if (shouldUpdate) {
/// If Locale has been updated download the latest
/// version and store it.
///
/// Currently this happens as a non-blocking action
/// so updates will take effect the next time the user opens the app.
var response = await Session.apiGet(url);
String localePath = '$path/$localeString.json';
FileHelpers.writeStringToFile(localePath, jsonEncode(response));
}
} catch (error) {
print('Unable to update locale: $localeString');
}
}
}
/// This method makes alight call comparing the [_sheetToFbDate] in our local
/// copy of the Locale to the API version.
static Future<bool> _checkIfUpdated(String langKey) async {
try {
String assetPath = '${globals.documentDirectory.path}/locale/$langKey.json';
String data = await rootBundle.loadString(assetPath);
var currentLocaleDate = json.decode(data)['_sheetToFbDate'];
var url = ApiEndpoint.uri(
'/locale/check/$langKey',
queryParameters: {
"date": currentLocaleDate,
},
).toString();
bool response = await Session.apiGet(url);
if (response.runtimeType == ErrorModel) throw response;
return response;
} catch (error) {
print("Unable to check for update: $langKey");
return Future.value(false);
}
}
}
I eventually realised that while not an issue on iOS, using rootBundle.loadString does not work for document storage on Android.
I needed to replace that bit with
File file = File('PATH_TO_FILE');
data = await file.readAsString();