20220127のAndroidに関する記事は3件です。

[Flutter]初心者でもできる無限スクロールの実装方法

一覧表示画面で、スクロールすると次々にデータを読み込んでいく、無限スクロールを簡単に作る方法を紹介します。 無限スクロールを作る方法がいろいろあるのですが、初心者の自分でもできたのでこれが一番簡単かと思います。 データを取得するAPIを準備する 今回はAPI経由でデータを取得し、一覧表示するのでまずAPIを準備します。 今回は、以下のswaggerで記載したtitleのみが一覧で返されるAPIを用意しました。次のデータがあれば、レスポンスのoffsetに次の位置が返り、なければ返りません。 sample.yml openapi: 3.0.0 info: title: sample version: '1.0' description: sample contact: name: murapon servers: - url: 'http://localhost:31180' paths: /list: get: operationId: get-list summary: 一覧取得 description: 一覧取得 parameters: - schema: type: integer default: '10' in: query name: limit description: limit - schema: type: integer default: '0' in: query name: offset description: offset responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/response_list_get' components: schemas: response_list_get: title: reponse_list_get description: 一覧取得APIのレスポンスモデル type: object properties: list: type: array items: type: object properties: title: type: string description: タイトル required: - title total_count: type: integer description: 総件数 next_offset: type: integer description: 次のoffset required: - list - total_count flutterでswaggerを読み込んで、APIからデータを取得できるようにする FlutterでSwagger(openapi-generator)を使う方法を参考にしてください。 一覧表示を作る 以下が、flutter側の全ソースです。 sample_page.dart import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:openapi/api.dart'; class SamplePage extends StatefulWidget { const SamplePage({ Key key, }) : super(key: key); @override _SamplePageState createState() => _SamplePageState(); } class _SamplePageState extends State<SamplePage> { List<Map> list = null; bool loading = false; int offset = 0; @override Widget build(BuildContext context) { var length = list?.length ?? 0; return Scaffold( appBar: AppBar(title: Text("一覧")), body: new ListView.builder( itemBuilder: (context, index) { if (index == length && offset!=null) { _load(); return new Center( child: new Container( margin: const EdgeInsets.only(top: 8.0), width: 32.0, height: 32.0, child: const CircularProgressIndicator(), ), ); } else if (index > length) { return null; } if(list.length > index) { var event = list[index]; return Container( child: ListTile(title: Text(event['title'])), ); } else { return null; } }, ), ); } Future<void> _load() async { if (loading || offset == null) { return null; } loading = true; try { var client = ApiClient(basePath: "http://10.0.2.2:31180"); var defaultApi = DefaultApi(client); int limit = 3; // 一度に取得する件数 var response = await defaultApi.getListWithHttpInfo( limit:limit, offset: offset); ResponseListGet results = ResponseListGet.fromJson(jsonDecode(response.body)); setState(() { offset = results.nextOffset; if (list == null) { list = <Map>[]; } results.list.forEach((dynamic item) { list .add({'title': item.title as String}); }); }); } catch (e) { print(e.toString()); } finally { loading = false; } } } 最初の、 sample_page.dart(抜粋) List<Map> list = null; bool loading = false; int offset = 0; で、初期化し、_load();を実行します。 _load();の中で、APIからデータを取得し、setStateを使って、取得したデータをlistに入れつつ、再描画処理を実行し、初期表示を行います。 2回目は、取得したoffsetの値があれば、再度APIを実行しなければ、実行しません。 API経由で全データを取得し終わったら、それ以上スクロールしても何もしないという実装にしたかったので、loadingとoffsetで制御しましたが、これを二つを消せば、何度も_load()を実行し続けることが可能です。_load()の中身を変えることで、全部表示したらまた最初から表示し直すなどの表示も可能です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]忙しい人のためのアーキテクチャガイド~Domain Layer編~

はじめに この記事は、シリーズ作です。 今回のDomain Layer編は、元々の分量が少ないです。 スクロール数は元と変わらないかもですが、文字数は 1 / 3 くらいにまで縮められたと思います。 目次 Domain Layerとは? Domain Layerのメリット 他の責務との関係 呼び出し ライフサイクル スレッド周り Domain Layerの使われ方 Domain Layerとは? Domain Layerのメリット Domain Layerに責務を切り分けるメリットは コードの重複を回避 ViewModelの肥大化を回避 アプリのテスタビリティが向上 大規模なクラスを回避 他の責務との関係 Domain Layerは、ViewModelとRepositoryの間に入ります。 Domainクラスは、単一の機能に対して責務を持ます。 また、再利用可能なので、あるユースケースが他のユースケースに依存することもあります。 document class GetLatestNewsWithAuthorsUseCase( private val newsRepository: NewsRepository, private val authorsRepository: AuthorsRepository ) { /* ... */ } document class GetLatestNewsWithAuthorsUseCase( private val newsRepository: NewsRepository, private val authorsRepository: AuthorsRepository, private val formatDateUseCase: FormatDateUseCase ) { /* ... */ } 呼び出し ユースケースは、オーバーロードを用いることで関数であるかのように呼び出すことができます。 Kotlinでオーバーロードを定義するには、operatorキーワードを用います。 document class FormatDateUseCase(userRepository: UserRepository) { private val formatter = SimpleDateFormat( userRepository.getPreferredDateFormat(), userRepository.getPreferredLocale() ) operator fun invoke(date: Date): String { return formatter.format(date) } } class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() { init { val today = Calendar.getInstance() val todaysDate = formatDateUseCase(today) /* ... */ } } ライフサイクル ユースケースは、自身のライフサイクルを持たず、呼び出し元のスコープで生存します。 また、可変データを取る設計をしないので(すべきではない)、毎回新しいインスタンスを呼び出します。 スレッド周り ユースケースのロジックは、長時間操作の可能性があるので、メインスレッドから安全に呼び出せるようにしないといけません。 キャッシュなど多くのリソースを消費する計算処理は、Data Layerの責務なので混在しないようにしましょう。 document class MyUseCase( private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default ) { suspend operator fun invoke(...) = withContext(defaultDispatcher) { // Long-running blocking operations happen on a background thread. } } Domain Layerの使われ方 同じことの繰り返しになりますが、念の為。 再利用性 ViewModelで同じようなロジックが定義されている場合、ユースケースに集約させることで再利用性と個別のテストを実現します。 また、集約されているので、変更時は一箇所のみで済みます。 複数のRepositoryが絡むロジック 図のGetLatestNewsWithAuthorsUseCaseは、二つのRepositoryを用いたロジックになっています。 下記のユースケースが入ると思うと、かなりViewModelがスッキリしますね。 /** * This use case fetches the latest news and the associated author. */ class GetLatestNewsWithAuthorsUseCase( private val newsRepository: NewsRepository, private val authorsRepository: AuthorsRepository, private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default ) { suspend operator fun invoke(): List<ArticleWithAuthor> = withContext(defaultDispatcher) { val news = newsRepository.fetchLatestNews() val result: MutableList<ArticleWithAuthor> = mutableListOf() // This is not parallelized, the use case is linearly slow. for (article in news) { // The repository exposes suspend functions val author = authorsRepository.getAuthor(article.authorId) result.add(ArticleWithAuthor(article, author)) } result } } おわりに いかがだったでしょうか。 これにて、全ての項目を読了いたしました。 お疲れ様でした。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]忙しい人のためのアーキテクチャガイド~Data Layer編~

はじめに この記事は、シリーズ作です。 今回は、概要編で少しだけ触れたData Layerについての項目を要約していきます。 Data Layer編はかなり長いので、要約の意義が出てきますね。 分量を 1 / 3 くらいにすぼめています。 目次 Data Layerとは? データの公開 ビジネスロジックの複雑化 信頼できる情報源 スレッド インスタンスのライフサイクル 良いビジネルモデル いろいろなビジネルロジック エラーの公開 実例 Data Layerとは? 概要編にまとめてあります。 データの公開 データやデータ操作の機能を公開する時は suspend関数を用いる 時間経過に伴うデータ変更に関しては、Flow(推奨)で購読する class ExampleRepository( private val exampleRemoteDataSource: ExampleRemoteDataSource, // network private val exampleLocalDataSource: ExampleLocalDataSource // database ) { val data: Flow<Example> = ... suspend fun modifyData(example: Example) { ... } } ビジネスロジックの複雑化 下記図式では、データ操作において様々なソースが必要なものは、情報源の一元化させてもの達をさらに一元化させていく例です。 また、ログインやユーザー登録などのそれぞれの責務においてカプセル化も実現しています。 信頼できるデータソースの条件 UI LayerがRepositoryから引っ張ってくるデータは、アプリの唯一の情報源であるため、信頼のおけるものである必要があります。 信頼できるデータソースがローカルDB等のキャッシュや通信処理によるデータなど、異なる場合もありますが、重要なのは、 データの不変性 出来る限り最新であること スレッド メインスレッドは、適切なスレッドに移動させる責務があります。 そのため、Repositoryの呼び出しはsuspend関数にてメインスレッドで行うのがベストです。 インスタンスのライフサイクル ローカルDBクラスが保持しているキャッシュをどのスコープで、生存させるかは重要です。 良いビジネスモデル ビジネスモデルとは、データ操作で使われる型ですね。 Google的、理想のビジネスモデルは「サブセット(一部)」であるとのことです。 the data layer might be a subset of the information that you get from the different data sources document data class ArticleApiModel( val id: Long, val title: String, val content: String, val publicationDate: Date, val modifications: Array<ArticleApiModel>, val comments: Array<CommentApiModel>, val lastModificationDate: Date, val authorId: Long, val authorName: String, val authorDateOfBirth: Date, val readTimeMin: Int ) 例えば、こちらはニュースアプリで必要なデータ。 しかし、これは網羅的なもので「サブセット」ではありません。 ニュースアプリにおいて、削除したい場面ではこのようなビジネスモデルが好ましいです。 document data class Article( val id: Long, val title: String, val content: String, val publicationDate: Date, val authorName: String, val readTimeMin: Int ) メリット メモリの節約につながる アプリで使用される形に適合できる(?) 関心の分離が向上 いろいろなデータ操作 Data Layerで行われるデータ操作のパターンを具体的に見ていきます。 UI指向 特定の画面を表示している間に行われるもの。 ライフサイクルをViewModelに合わせます。 アプリ指向 アプリを開いている間に行われるもの。 例)キャッシュデータ ライフサイクルをApplicatoinクラスやData Layerのクラス(Reposiotoryなど)に合わせます。 ビジネス指向 保存や更新などのデータ操作。 例)アップロード処理 この処理は、必ず遂行する必要があります。 WorkManager の使用を推奨しています。 エラーも公開する Data Layerは純粋にデータを他の責務に公開する役割がありますが、エラーを公開するという責務も持ちます。 Kotlinがサポートしているエラー処理機構を使用します。 例)Coroutine exception handling / Flow catch 実践してみる データソースの定義 ポイント ディスク操作やネットワーク IOに最適化されたスレッドで処理する(メインセーフ) interface を用いることで、可変性高める document class NewsRemoteDataSource( private val newsApi: NewsApi, private val ioDispatcher: CoroutineDispatcher ) { /** * Fetches the latest news from the network and returns the result. * This executes on an IO-optimized thread pool, the function is main-safe. */ suspend fun fetchLatestNews(): List<ArticleHeadline> = // Move the execution to an IO-optimized thread since the ApiService // doesn't support coroutines and makes synchronous requests. withContext(ioDispatcher) { newsApi.fetchLatestNews() } } } // Makes news-related network synchronous requests. interface NewsApi { fun fetchLatestNews(): List<ArticleHeadline> } Repositoryの定義 ポイント 追加ロジックなし データソースとViewModelの仲介役として機能する document // NewsRepository is consumed from other layers of the hierarchy. class NewsRepository( private val newsRemoteDataSource: NewsRemoteDataSource ) { suspend fun fetchLatestNews(): List<ArticleHeadline> = newsRemoteDataSource.fetchLatestNews() } キャッシュの実装 先程のアプリ指向による実装です。 ポイント 2回目以降のリクエストは、キャッシュデータを用いる キャッシュデータの保存期間や方法はさまざま Mutex で他のスレッドから保護 class NewsRepository( private val newsRemoteDataSource: NewsRemoteDataSource ) { // Mutex to make writes to cached values thread-safe. private val latestNewsMutex = Mutex() // Cache of the latest news got from the network. private var latestNews: List<ArticleHeadline> = emptyList() suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> { if (refresh || latestNews.isEmpty()) { val networkResult = newsRemoteDataSource.fetchLatestNews() // Thread-safe write to latestNews latestNewsMutex.withLock { this.latestNews = networkResult } } return latestNewsMutex.withLock { this.latestNews } } } 画面 < 操作!?(アプリ指向) Repositoryのロジックが、呼び出し元のCoroutineScopeを使用している場合、ネットワークリクエスト中にアプリが閉じられたり、他のアプリに移ったりすると中断されてしまいます。 そのため、CoroutineScopeは、呼び出し先のRepositoryクラスのライフサイクルと同期させる必要があります。 document class NewsRepository( ..., // This could be CoroutineScope(SupervisorJob() + Dispatchers.Default). private val externalScope: CoroutineScope ) { ... } ディスク操作 ニュースアプリの例だと、ブックマークされた記事やユーザー設定のデータを保存するとき、プロセスの停止を乗り越えて、オフライン実行できるためにディスクに保存する必要が出てきます。 大規模なデータセットには、Room 更新不要な小規模なデータセットには、DataStore JSONのようなデータの集合体には、ファイル その際に、データソースはデータがどのように保存されているのかの内部事情を知ってはいけません(関心の分離) ビジネス指向のデータ操作 前述した通り、ビジネス指向のデータ操作は、必ず遂行される必要があるのでWorkManagerを使用します。 下記は、最新のニュース記事を取得する操作をスケジューリングしています。 document class RefreshLatestNewsWorker( private val newsRepository: NewsRepository, context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result = try { newsRepository.refreshLatestNews() Result.success() } catch (error: Throwable) { Result.failure() } } ポイント 個別のデータソースに切り分ける 条件が満たされた時に実行が始まるようにする document private const val REFRESH_RATE_HOURS = 4L private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask" private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag" class NewsTasksDataSource( private val workManager: WorkManager ) { fun fetchNewsPeriodically() { val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>( REFRESH_RATE_HOURS, TimeUnit.HOURS ).setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED) .setRequiresCharging(true) .build() ) .addTag(TAG_FETCH_LATEST_NEWS) workManager.enqueueUniquePeriodicWork( FETCH_LATEST_NEWS_TASK, ExistingPeriodicWorkPolicy.KEEP, fetchNewsRequest.build() ) } fun cancelFetchingNewsPeriodically() { workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS) } } おわりに いかがだったでしょうか? 次回は、Domain Layerについて要約します。 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む